ForcePilot/backend/package/yuxi/channels/thread_binding_manager.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

124 lines
3.6 KiB
Python

from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from enum import StrEnum
from typing import Any
from yuxi.utils.datetime_utils import utc_now_naive
class BindingType(StrEnum):
AGENT = "agent"
SUBAGENT = "subagent"
ACP = "acp"
CONVERSATION = "conversation"
@dataclass
class ThreadBinding:
thread_id: str
binding_type: BindingType
target_id: str
created_at: datetime = field(default_factory=utc_now_naive)
expires_at: datetime | None = None
metadata: dict[str, Any] = field(default_factory=dict)
@property
def is_expired(self) -> bool:
if self.expires_at is None:
return False
return utc_now_naive() > self.expires_at
class ThreadBindingManager:
def __init__(
self,
default_ttl_hours: int = 24,
):
self._bindings: dict[str, ThreadBinding] = {}
self._default_ttl = timedelta(hours=default_ttl_hours) if default_ttl_hours else None
self._listeners: list[Callable] = []
def bind(
self,
thread_id: str,
binding_type: BindingType,
target_id: str,
ttl_hours: int | None = None,
metadata: dict[str, Any] | None = None,
) -> ThreadBinding:
expires = None
if ttl_hours is not None:
expires = utc_now_naive() + timedelta(hours=ttl_hours)
elif self._default_ttl:
expires = utc_now_naive() + self._default_ttl
binding = ThreadBinding(
thread_id=thread_id,
binding_type=binding_type,
target_id=target_id,
expires_at=expires,
metadata=metadata or {},
)
self._bindings[f"{thread_id}:{binding_type.value}"] = binding
self._notify("bind", binding)
return binding
def unbind(self, thread_id: str, binding_type: BindingType) -> bool:
key = f"{thread_id}:{binding_type.value}"
if key in self._bindings:
binding = self._bindings.pop(key)
self._notify("unbind", binding)
return True
return False
def get_binding(self, thread_id: str, binding_type: BindingType) -> ThreadBinding | None:
key = f"{thread_id}:{binding_type.value}"
binding = self._bindings.get(key)
if binding and binding.is_expired:
self.unbind(thread_id, binding_type)
return None
return binding
def list_bindings(
self,
thread_id: str | None = None,
binding_type: BindingType | None = None,
) -> list[ThreadBinding]:
results: list[ThreadBinding] = []
expired_keys: list[str] = []
for key, binding in self._bindings.items():
if binding.is_expired:
expired_keys.append(key)
continue
if thread_id and binding.thread_id != thread_id:
continue
if binding_type and binding.binding_type != binding_type:
continue
results.append(binding)
for key in expired_keys:
self._bindings.pop(key, None)
return results
def add_listener(self, listener: Callable[[str, ThreadBinding], None]) -> None:
self._listeners.append(listener)
def remove_listener(self, listener: Callable[[str, ThreadBinding], None]) -> None:
if listener in self._listeners:
self._listeners.remove(listener)
def _notify(self, event: str, binding: ThreadBinding) -> None:
for listener in self._listeners:
try:
listener(event, binding)
except Exception:
pass