ForcePilot/backend/package/yuxi/channel/extensions/tlon/gateway.py
Kris 0babb1ca8e feat(channel): 添加 Tlon 渠道扩展
新增 Tlon 渠道扩展,支持在 Yuxi 平台中集成 Tlon/Urbit 去中心化通讯平台。

包含以下功能模块:
- tlon_api: Tlon API 客户端封装
- config: 渠道配置管理
- gateway: SSE/WebSocket 网关接入
- sse_client: SSE 客户端
- outbound: 外发消息管理
- send: 消息发送
- security: 安全校验
- auth: 认证管理
- monitor: 渠道状态监控
- status: 会话状态管理
- session: 会话管理
- approval: 审批流程
- channel_mgmt: 频道管理
- channel_ops: 频道操作
- contacts: 联系人管理
- discovery: 服务发现
- doctor: 健康诊断
- expose: 服务暴露
- gallery: 图库管理
- history: 历史记录
- hooks: 钩子管理
- media: 媒体资源处理
- notebook: 笔记本功能
- settings_store: 设置存储
- setup: 初始化设置
- story: 故事功能
- targets: 目标管理
- cite_parser: 引用解析
- utils: 工具函数
- types: 类型定义
2026-05-21 11:55:25 +08:00

124 lines
4.3 KiB
Python

import asyncio
import logging
from yuxi.channel.extensions.tlon.auth import authenticate
from yuxi.channel.extensions.tlon.sse_client import UrbitSSEClient, SSEConfig
from yuxi.channel.extensions.tlon.config import TlonConfigAdapter
from yuxi.channel.extensions.tlon.errors import UrbitAuthError
from yuxi.channel.extensions.tlon.monitor import monitor_tlon_provider
from yuxi.channel.extensions.tlon.doctor import TlonDoctor
logger = logging.getLogger(__name__)
_config_adapter = TlonConfigAdapter()
async def start_tlon_gateway(ctx) -> asyncio.Task | None:
account_id = ctx.account_id
config = ctx.config
account = _config_adapter.resolve_account(account_id, config)
if not _config_adapter.is_configured(account):
logger.warning("[tlon] Account %s not configured, skipping", account_id)
return None
doctor = TlonDoctor()
if doctor.check_migration_needed(config):
ctx.config = doctor.normalize_compatibility_config(config)
account = _config_adapter.resolve_account(account_id, ctx.config)
ship = account["ship"]
url = account["url"]
code = account["code"]
cookie = await _authenticate_with_retry(url, code)
async def on_reconnect():
return await authenticate(url, code)
sse_config = SSEConfig(
url=url,
cookie=cookie,
ship=ship,
on_reconnect=on_reconnect,
)
client = UrbitSSEClient(sse_config)
try:
await client.connect()
logger.info("[tlon] SSE client connected for %s (%s)", ship, account_id)
except Exception as e:
logger.error("[tlon] Failed to connect SSE client: %s", e)
return None
try:
blocked = await client.scry("/chat/blocked.json")
client._blocked_ships = blocked.get("blocked", [])
logger.info("[tlon] Loaded %d blocked ships", len(client._blocked_ships))
except Exception:
client._blocked_ships = []
try:
from yuxi.channel.extensions.tlon.settings_store import (
TlonSettingsStore, migrate_config_to_settings,
)
settings = TlonSettingsStore()
store_data = await settings.load_from_scry(client)
if not store_data:
migrate_config_to_settings(config, client)
logger.info("[tlon] Config migrated to Settings Store")
store_data = await settings.load_from_scry(client)
if store_data:
from yuxi.channel.extensions.tlon.monitor import apply_settings_to_account
await apply_settings_to_account(account, settings)
except Exception as e:
logger.warning("[tlon] Settings store init skipped: %s", e)
try:
from yuxi.channel.extensions.tlon.discovery import discover_and_track
auto_discover = account.get("auto_discover_channels", False)
if auto_discover or not account.get("group_channels"):
watched = set(account.get("group_channels", []))
discovered = await discover_and_track(client, watched)
if discovered:
logger.info("[tlon] Startup discovery found %d channels", len(discovered))
account["group_channels"] = list(watched)
except Exception as e:
logger.warning("[tlon] Channel discovery skipped: %s", e)
ctx._tlon_client = client
ctx._tlon_account = account
async def run():
try:
while not ctx.cancel_event.is_set():
try:
await monitor_tlon_provider(ctx, client, account)
except Exception as e:
logger.error("[tlon] Monitor crashed: %s", e)
await client.attempt_reconnect()
finally:
await client.close()
task = asyncio.create_task(run())
return task
async def stop_tlon_gateway(ctx) -> None:
client = getattr(ctx, "_tlon_client", None)
if client:
await client.close()
async def _authenticate_with_retry(url: str, code: str, max_attempts: int = 10) -> str:
for attempt in range(1, max_attempts + 1):
try:
return await authenticate(url, code)
except UrbitAuthError:
if attempt == max_attempts:
raise
delay = min(1000 * (2 ** (attempt - 1)), 30000)
await asyncio.sleep(delay / 1000)
raise UrbitAuthError("Max retries exceeded")