新增 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: 类型定义
124 lines
4.3 KiB
Python
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") |