ForcePilot/backend/package/yuxi/channels/bridge.py
Kris ede29b1809 refactor(channel): 完成频道模块大重构与功能扩展
本次提交对频道模块进行了全面重构并新增多项核心功能:
1.  优化适配器状态获取逻辑,修复状态返回空值问题
2.  新增4种频道异常类型,完善错误处理体系
3.  大幅精简Mixin类,移除冗余的抽象方法定义
4.  重构适配器注册系统,统一注册入口并新增内置适配器加载方法
5.  扩展插件系统,新增更多元数据配置项支持
6.  新增线程类型、会话范围等模型定义,扩展事件类型枚举
7.  优化用户映射逻辑,使用PostgreSQL upsert避免重复创建
8.  新增历史消息注入模块,支持多格式历史格式化与缓存管理
9.  新增线程能力配置与各平台预置适配配置
10. 新增线程绑定管理器,支持多类型线程绑定生命周期管理
11. 重构__init__.py,整理导出模块与类型
12. 扩展基础适配器类,新增凭证解析、状态存储等核心方法
13. 重写消息路由器,支持按频道加载策略、安全校验与多命令处理
14. 新增/history、/context、/summary等交互命令实现
15. 优化消息记录与统计逻辑,完善路由调度链路
2026-05-13 16:41:11 +08:00

238 lines
7.9 KiB
Python

from __future__ import annotations
from collections.abc import AsyncIterator
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from yuxi.channels.base import BaseChannelAdapter
from yuxi.channels.models import ChannelMessage, ChannelResponse, DeliveryResult, HealthStatus
class BridgeAdapter:
def __init__(self, legacy: BaseChannelAdapter):
self._legacy = legacy
@property
def channel_id(self) -> str:
return self._legacy.channel_id
@property
def channel_type(self) -> str:
return self._legacy.channel_type.value
def is_enabled(self) -> bool:
return bool(self._legacy.config.get("enabled", False))
def is_configured(self) -> bool:
return bool(self._legacy.config)
def list_account_ids(self) -> list[str]:
return [self.channel_id]
def resolve_account(self, account_id: str) -> dict | None:
return self._legacy.config if account_id == self.channel_id else None
async def connect(self) -> None:
await self._legacy.connect()
async def disconnect(self) -> None:
await self._legacy.disconnect()
async def health_check(self) -> HealthStatus:
return await self._legacy.health_check()
async def pre_connect(self) -> dict:
return await self._legacy.pre_connect()
async def send(self, response: ChannelResponse) -> DeliveryResult:
return await self._legacy.send(response)
async def send_media(self, chat_id: str, media_type: str, data: Any) -> DeliveryResult:
return await self._legacy.send_media(chat_id, media_type, data)
async def edit_message(self, chat_id: str, msg_id: str, content: str) -> DeliveryResult:
return await self._legacy.edit_message(chat_id, msg_id, content)
async def delete_message(self, chat_id: str, msg_id: str) -> DeliveryResult:
return await self._legacy.delete_message(chat_id, msg_id)
async def send_reaction(self, chat_id: str, msg_id: str, emoji: str) -> DeliveryResult:
return await self._legacy.send_reaction(chat_id, msg_id, emoji)
def on_message(self, handler) -> None:
self._legacy.on_message(handler)
def normalize_inbound(self, raw: Any) -> ChannelMessage:
return self._legacy.normalize_inbound(raw)
def format_outbound(self, response: ChannelResponse) -> Any:
return self._legacy.format_outbound(response)
async def receive(self) -> AsyncIterator[ChannelMessage]:
async for msg in self._legacy.receive():
yield msg
async def download_media(self, file_id: str) -> bytes:
return await self._legacy.download_media(file_id)
async def get_user_info(self, channel_user_id: str) -> dict:
return await self._legacy.get_user_info(channel_user_id)
def resolve_reply_mode(self, config: dict[str, Any], chat_type: str) -> str:
return "off"
def resolve_thread_id(self, message: ChannelMessage) -> str | None:
return None
async def send_typing_indicator(self, chat_id: str) -> None:
pass
async def send_stream_chunk(self, chat_id: str, msg_id: str, chunk: str, finished: bool) -> DeliveryResult:
return await self._legacy.send_stream_chunk(chat_id, msg_id, chunk, finished)
@property
def status(self) -> str:
return getattr(self._legacy, "_status", None) or "unknown"
def snapshot(self) -> dict[str, Any]:
from yuxi.channels.models import build_snapshot_from_adapter
snap = build_snapshot_from_adapter(self._legacy)
return snap.model_dump()
def default_account_id(self) -> str:
return self.channel_id
def disabled_reason(self) -> str:
return "disabled" if not self.is_enabled() else ""
def unconfigured_reason(self) -> str:
return "not_configured" if not self.is_configured() else ""
def describe_account(self) -> dict:
return {"channel_id": self.channel_id, "channel_type": self.channel_type}
def resolve_allow_from(self) -> list[str]:
return ["*"]
def has_configured_state(self) -> bool:
return self.is_configured()
async def logout_account(self, ctx) -> None:
await self._legacy.disconnect()
async def login_with_qr_start(self, force: bool, timeout_ms: int) -> str:
return ""
def chunker(self, text: str, limit: int) -> list[str]:
return [text] if len(text) <= limit else [text[i : i + limit] for i in range(0, len(text), limit)]
@property
def chunker_mode(self) -> str:
return "text"
@property
def text_chunk_limit(self) -> int:
return getattr(self._legacy, "text_chunk_limit", 4096)
@property
def delivery_mode(self) -> str:
return "direct"
def sanitize_text(self, text: str) -> str:
return text
async def send_text(self, ctx) -> DeliveryResult:
return await self._legacy.send(ctx)
async def send_media_url(self, ctx) -> DeliveryResult:
return await self._legacy.send(ctx)
async def send_poll(self, ctx) -> DeliveryResult:
return await self._legacy.send(ctx)
async def pin_message(self, chat_id: str, msg_id: str) -> DeliveryResult:
raise NotImplementedError
async def unpin_message(self, chat_id: str, msg_id: str) -> DeliveryResult:
raise NotImplementedError
async def probe_account(self, account: dict, timeout_ms: int) -> dict:
try:
hs = await self.health_check()
return {"status": hs.status}
except Exception:
return {"status": "error"}
async def build_account_snapshot(self, account: dict, probe: dict):
from yuxi.channels.models import ChannelAccountSnapshot
return ChannelAccountSnapshot(
account_id=account.get("account_id", self.channel_id),
channel_id=self.channel_id,
channel_type=self._legacy.channel_type,
)
def build_channel_summary(self, account: dict, snapshot: dict) -> dict:
return {"channel_id": self.channel_id, "status": self.status}
async def audit_account(self, account: dict, timeout_ms: int) -> dict:
return {"channel_id": self.channel_id, "audited": True}
def resolve_account_state(self, configured: bool, enabled: bool) -> str:
if not configured:
return "not_configured"
if not enabled:
return "disabled"
return "active"
def collect_status_issues(self, accounts: list) -> list[str]:
issues = []
if not self.is_configured():
issues.append("not_configured")
if not self.is_enabled():
issues.append("disabled")
return issues
def on_config_changed(self, prev_cfg: dict, next_cfg: dict) -> None:
pass
async def run_startup_maintenance(self) -> None:
await self._legacy._refresh_token_if_needed()
def normalize_target(self, raw: Any) -> str:
return str(raw)
def resolve_inbound_conversation(self, from_: str, to: str, thread_id: str) -> dict:
return {"from": from_, "to": to, "thread_id": thread_id}
def resolve_delivery_target(self, conversation_id: str) -> dict:
return {"chat_id": conversation_id}
def infer_target_chat_type(self, to: str) -> str:
return "direct"
def parse_explicit_target(self, raw: str) -> dict:
return {"target": raw}
def transform_reply_payload(self, payload: dict) -> dict:
return payload
def resolve_outbound_session_route(self, target: dict) -> dict:
return {"channel_id": self.channel_id}
def build_tool_context(self, context: dict) -> dict:
return context
def resolve_auto_thread_id(self, to: str, reply_to_id: str) -> str | None:
return None
def resolve_reply_transport(self, thread_id: str, reply_to_id: str) -> dict:
return {"thread_id": thread_id, "reply_to_id": reply_to_id}
async def check_ready(self) -> bool:
return self.is_enabled() and self.is_configured()
async def clear_typing_indicator(self, chat_id: str) -> None:
pass