ForcePilot/backend/package/yuxi/channels/bridge.py
Kris 7a972055b7 feat: 新增渠道服务基础框架与核心工具类
新增大量渠道适配器相关的协议、策略、工具类与基础设施代码,包括:
1.  多协议定义:认证、消息、配置、网关等核心接口
2.  策略模块:上下文、群聊、去重、防抖等业务策略
3.  工具集:重试、去重、文本分块、消息格式化等SDK工具
4.  基础设施:外部进程管理、事件广播、熔断机制等
5.  账户与管道系统:账户管理、消息处理管道实现
6.  运行时服务:状态收集、维护任务、日志等后台服务
2026-05-12 00:53:57 +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", "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