ForcePilot/backend/package/yuxi/channels/adapters/telegram/threading.py
Kris 71fec609bb feat(telegram-adapter): 实现完整的Telegram适配器基础代码
新增了Telegram适配器的全套基础模块,包括:
1.  核心适配器入口与会话工具
2.  账号管理、认证与配置系统
3.  连接相关的轮询、Webhook、更新偏移管理
4.  话题路由、管理与缓存系统
5.  消息反抖动、超时配置与工具类
6.  响应式UI与命令交互系统
7.  反应表情与通知系统
8.  审批与安全审计模块
9.  健康检查与状态监控
10. 贴纸缓存与视觉工具
11. 流式响应与协作功能
12. 群组迁移与目标归一化处理
2026-05-12 00:49:52 +08:00

126 lines
4.1 KiB
Python

from __future__ import annotations
import asyncio
import time
from typing import Any
from yuxi.utils.logging_config import logger
class ThreadBinding:
def __init__(
self,
thread_key: str,
subagent_id: str | None = None,
acp_binding: str | None = None,
idle_timeout_ms: int = 86_400_000,
):
self.thread_key = thread_key
self.subagent_id = subagent_id
self.acp_binding = acp_binding
self.idle_timeout_ms = idle_timeout_ms
self.last_activity: float = time.monotonic()
self.created_at: float = time.monotonic()
def touch(self) -> None:
self.last_activity = time.monotonic()
def is_expired(self) -> bool:
elapsed = (time.monotonic() - self.last_activity) * 1000
return elapsed > self.idle_timeout_ms
class ThreadBindingManager:
def __init__(self, config: dict[str, Any] | None = None):
cfg = config or {}
bindings_cfg = cfg.get("thread_bindings", {})
self._enabled = bindings_cfg.get("enabled", False)
self._default_idle_timeout_ms = bindings_cfg.get("idle_timeout_ms", 86_400_000)
self._cleanup_interval = bindings_cfg.get("cleanup_interval", 300)
self._bindings: dict[str, ThreadBinding] = {}
self._cleanup_task: asyncio.Task | None = None
@property
def enabled(self) -> bool:
return self._enabled
def bind(
self,
thread_key: str,
subagent_id: str | None = None,
acp_binding: str | None = None,
idle_timeout_ms: int | None = None,
) -> ThreadBinding:
timeout = idle_timeout_ms or self._default_idle_timeout_ms
binding = ThreadBinding(
thread_key=thread_key,
subagent_id=subagent_id,
acp_binding=acp_binding,
idle_timeout_ms=timeout,
)
self._bindings[thread_key] = binding
logger.debug(f"[Telegram] Thread bound: {thread_key} -> subagent={subagent_id}")
return binding
def unbind(self, thread_key: str) -> bool:
if thread_key in self._bindings:
del self._bindings[thread_key]
logger.debug(f"[Telegram] Thread unbound: {thread_key}")
return True
return False
def get_binding(self, thread_key: str) -> ThreadBinding | None:
binding = self._bindings.get(thread_key)
if binding and binding.is_expired():
self.unbind(thread_key)
return None
return binding
def get_subagent_id(self, thread_key: str) -> str | None:
binding = self.get_binding(thread_key)
return binding.subagent_id if binding else None
def touch(self, thread_key: str) -> None:
binding = self._bindings.get(thread_key)
if binding:
binding.touch()
def cleanup_expired(self) -> int:
expired = [k for k, v in self._bindings.items() if v.is_expired()]
for k in expired:
self.unbind(k)
if expired:
logger.info(f"[Telegram] Cleaned up {len(expired)} expired thread bindings")
return len(expired)
async def start_cleanup_loop(self) -> None:
if not self._enabled:
return
async def _loop():
while True:
await asyncio.sleep(self._cleanup_interval)
self.cleanup_expired()
self._cleanup_task = asyncio.create_task(_loop())
async def stop_cleanup_loop(self) -> None:
if self._cleanup_task and not self._cleanup_task.done():
self._cleanup_task.cancel()
try:
await self._cleanup_task
except asyncio.CancelledError:
pass
self._cleanup_task = None
def get_all_bindings(self) -> dict[str, dict[str, Any]]:
return {
k: {
"thread_key": v.thread_key,
"subagent_id": v.subagent_id,
"acp_binding": v.acp_binding,
"idle_remaining_ms": max(0, int(v.idle_timeout_ms - (time.monotonic() - v.last_activity) * 1000)),
}
for k, v in self._bindings.items()
}