ForcePilot/backend/package/yuxi/channel/extensions/qqbot/gateway.py
Kris 2ab65f153f feat(channel): 添加 QQ Bot 渠道扩展
新增 QQ Bot 渠道扩展,支持在 Yuxi 平台中集成 QQ 机器人渠道。

包含以下功能模块:
- api_client: QQ API 客户端封装
- api_routes: API 路由管理
- config: 渠道配置管理
- gateway: SSE/WebSocket 网关接入
- websocket: WebSocket 实时连接
- credentials: 凭证管理
- token: Token 管理
- outbound: 外发消息管理
- outbound_media: 媒体外发
- streaming: 流式消息处理
- streaming_media: 媒体流处理
- pairing: 用户配对与绑定
- security: 安全校验
- dedupe: 消息去重
- monitor: 渠道状态监控
- status: 会话状态管理
- session: 会话管理
- pipeline: 消息管道
- pipeline_stages: 管道阶段
- commands: 指令处理
- commands_builtin: 内置指令
- interaction: 交互处理
- approval: 审批流程
- ark: ARK 消息
- audio: 音频处理
- media: 媒体资源
- media_chunked: 分块媒体
- media_tags: 媒体标签
- message_queue: 消息队列
- delivery: 消息送达确认
- reconnect: 重连机制
- typing_keepalive: 输入状态保活
- group_activation: 群激活
- group_gating: 群门控
- group_history: 群历史
- known_users: 已知用户
- ref_index: 引用索引
- tools: Agent 工具集成
- types: 类型定义
2026-05-21 11:35:12 +08:00

112 lines
4.2 KiB
Python

from __future__ import annotations
import asyncio
import logging
from typing import Any
from yuxi.channel.context import ChannelContext
from yuxi.channel.extensions.qqbot.api_client import QQBotApiClient
from yuxi.channel.extensions.qqbot.config import QQBotConfigAdapter
from yuxi.channel.extensions.qqbot.credentials import CredentialBackup
from yuxi.channel.extensions.qqbot.session import SessionStore
from yuxi.channel.extensions.qqbot.status import QQBotStatusAdapter
from yuxi.channel.extensions.qqbot.token import TokenManager
from yuxi.channel.extensions.qqbot.types import GatewayEvent, QQBotAccountConfig
from yuxi.channel.extensions.qqbot.websocket import QQBotGatewayConnection
logger = logging.getLogger(__name__)
class QQBotGateway:
def __init__(
self,
account: QQBotAccountConfig,
config_adapter: QQBotConfigAdapter,
status_adapter: QQBotStatusAdapter,
message_handler: Any | None = None,
):
self._account = account
self._config_adapter = config_adapter
self._status = status_adapter
self._message_handler = message_handler
self._token_manager: TokenManager | None = None
self._api_client: QQBotApiClient | None = None
self._session_store = SessionStore()
self._credential_backup = CredentialBackup()
self._connection: QQBotGatewayConnection | None = None
self._gateway_task: asyncio.Task | None = None
self._running = False
async def start(self, ctx: ChannelContext) -> None:
self._status.running = True
if not self._account.app_id or not self._account.client_secret:
backup = self._credential_backup.load(self._account.account_id)
if backup:
self._account.app_id = backup.get("app_id", "")
self._account.client_secret = backup.get("client_secret", "")
if not self._account.app_id or not self._account.client_secret:
raise RuntimeError("QQBot account not configured: missing App ID or Client Secret")
self._token_manager = TokenManager(self._account.app_id, self._account.client_secret)
await self._token_manager.start_background_refresh()
self._api_client = QQBotApiClient(self._token_manager)
self._connection = QQBotGatewayConnection(
api_client=self._api_client,
session_store=self._session_store,
account_id=self._account.account_id,
dispatch_handler=self._handle_dispatch,
reconnect_handler=self._handle_reconnect,
)
self._status.token_source = self._account.secret_source
await self._connection.start()
self._status.connected = True
self._running = True
self._credential_backup.save(
self._account.account_id,
self._account.app_id,
self._account.client_secret or "",
)
async def stop(self, ctx: ChannelContext) -> None:
self._running = False
if self._connection:
await self._connection.stop()
self._connection = None
if self._token_manager:
await self._token_manager.close()
self._token_manager = None
if self._api_client:
await self._api_client.close()
self._api_client = None
self._status.connected = False
self._status.running = False
async def _handle_dispatch(self, event: GatewayEvent) -> None:
if self._message_handler:
try:
await self._message_handler(event, self._api_client, self._account)
except Exception:
logger.exception("[qqbot:%s] Message handler error", self._account.account_id)
async def _handle_reconnect(self) -> None:
logger.warning("[qqbot:%s] Handling reconnect request", self._account.account_id)
raise RuntimeError("Gateway connection lost, triggering reconnect")
@property
def api_client(self) -> QQBotApiClient | None:
return self._api_client
@property
def bot_openid(self) -> str | None:
return self._connection.bot_openid if self._connection else None
@property
def connected(self) -> bool:
return self._status.connected