ForcePilot/backend/package/yuxi/channel/extensions/googlechat/gateway.py
Kris c4e301a5e0 feat(googlechat): 新增Google Chat渠道完整实现
新增Google Chat插件的所有核心功能模块,包括账户配置、消息收发、权限控制、媒体处理、会话管理等完整能力
2026-05-21 10:49:33 +08:00

109 lines
4.1 KiB
Python

from __future__ import annotations
import asyncio
import logging
import time
from yuxi.channel.extensions.googlechat.auth import probe_account
from yuxi.channel.extensions.googlechat.config import GoogleChatConfigAdapter
from yuxi.channel.extensions.googlechat.types import ResolvedGoogleChatAccount
from yuxi.channel.extensions.googlechat.webhook import GoogleChatWebhookHandler
from yuxi.channel.gateway.routes import webhook_registry
logger = logging.getLogger(__name__)
_STATUS_OK = "ok"
_STATUS_ERROR = "error"
class GoogleChatGatewayAdapter:
def __init__(self):
self._config_adapter = GoogleChatConfigAdapter()
self._webhook_handler = GoogleChatWebhookHandler()
self._running: dict[str, bool] = {}
self._start_times: dict[str, float] = {}
self._status_snapshots: dict[str, dict] = {}
async def start(self, ctx) -> object:
account_dict = getattr(ctx, "account", {}) or {}
account = account_dict.get("resolved")
if not isinstance(account, ResolvedGoogleChatAccount):
account_id = account_dict.get("account_id", "default")
resolved_dict = await self._config_adapter.resolve_account(account_id)
account = resolved_dict.get("resolved")
if not account or not account.is_configured:
logger.warning("Google Chat account not configured: %s", account.account_id if account else "unknown")
return asyncio.Queue()
account_id = account.account_id
webhook_path = account.webhook_path
self._webhook_handler.register_target(webhook_path, account)
webhook_registry.register(
"googlechat",
self._webhook_handler.handle_webhook,
guard_config=None,
)
self._running[account_id] = True
self._start_times[account_id] = time.time()
self._status_snapshots[account_id] = {
"running": True,
"webhook_path": webhook_path,
"audience_type": account.audience_type,
"audience": account.audience,
"credential_source": account.credential_source,
"start_time": time.time(),
}
self._warn_app_principal_misconfiguration(account)
logger.info("Google Chat gateway started for account: %s, webhook: %s", account_id, webhook_path)
queue = getattr(ctx, "queue", asyncio.Queue())
return queue
async def stop(self, ctx) -> None:
account_dict = getattr(ctx, "account", {}) or {}
account_id = account_dict.get("account_id", "default")
account = account_dict.get("resolved")
if isinstance(account, ResolvedGoogleChatAccount):
self._webhook_handler.unregister_target(account.webhook_path)
self._running.pop(account_id, None)
self._status_snapshots[account_id] = {
"running": False,
"stop_time": time.time(),
}
logger.info("Google Chat gateway stopped for account: %s", account_id)
def get_status(self, account_id: str) -> dict:
return self._status_snapshots.get(account_id, {})
async def probe(self, account_dict: dict) -> dict:
account = account_dict.get("resolved")
if not isinstance(account, ResolvedGoogleChatAccount):
return {"ok": False, "status": "no_account"}
return await probe_account(account)
@staticmethod
def _warn_app_principal_misconfiguration(account: ResolvedGoogleChatAccount) -> None:
if account.audience_type != "app-url":
return
if not account.app_principal:
logger.error(
'appPrincipal is missing for audienceType "app-url"; '
"add-on token verification will fail. "
"Set appPrincipal to the numeric OAuth 2.0 client ID (uniqueId, 21 digits), not an email."
)
elif "@" in account.app_principal:
logger.error(
'appPrincipal "%s" looks like an email address. '
"Set appPrincipal to the numeric OAuth 2.0 client ID (uniqueId, 21 digits), not an email.",
account.app_principal,
)