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

101 lines
3.1 KiB
Python

from __future__ import annotations
import hashlib
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING
from cachetools import TTLCache
if TYPE_CHECKING:
from yuxi.channels.models import FetchOptions, HistoricalMessage
class HistoryFetcher(ABC):
@abstractmethod
async def fetch_thread_history(
self,
thread_id: str,
options: FetchOptions,
) -> list[HistoricalMessage]:
"""获取线程历史消息,按时间升序"""
@abstractmethod
async def fetch_parent_message(
self,
thread_id: str,
) -> HistoricalMessage | None:
"""获取线程根消息(父消息)"""
class HistoryFormatter:
FORMAT_TEMPLATES: dict[str, dict[str, str]] = {
"xml": {
"header": "[Thread history]\n",
"message": " [{sender}] {content}\n",
"footer": "[/Thread history]\n",
},
"markdown": {
"header": "**Thread History**\n\n",
"message": "> **{sender}**: {content}\n\n",
"footer": "",
},
"compact": {
"header": "",
"message": "{sender}: {content}\n",
"footer": "",
},
}
def __init__(self, format_type: str = "xml"):
self.template = self.FORMAT_TEMPLATES.get(format_type, self.FORMAT_TEMPLATES["xml"])
def format_history(
self,
messages: list[HistoricalMessage],
max_chars: int = 4000,
) -> str:
if not messages:
return ""
result: list[str] = [self.template["header"]]
current_chars = len(result[0])
for msg in reversed(messages):
content = msg.content[:200] if len(msg.content) > 200 else msg.content
line = self.template["message"].format(sender=msg.sender_name, content=content)
if current_chars + len(line) > max_chars:
break
result.insert(1, line)
current_chars += len(line)
result.append(self.template["footer"])
return "".join(result)
class HistoryCache:
def __init__(self, max_size: int = 100, ttl_seconds: int = 300):
self._cache: TTLCache[str, list[HistoricalMessage]] = TTLCache(maxsize=max_size, ttl=ttl_seconds)
self._thread_keys: dict[str, set[str]] = {}
def _make_key(self, thread_id: str, options: FetchOptions) -> str:
key_data = f"{thread_id}:{options.max_messages}:{options.before_message_id}"
return hashlib.md5(key_data.encode()).hexdigest()
def get(self, thread_id: str, options: FetchOptions) -> list[HistoricalMessage] | None:
key = self._make_key(thread_id, options)
return self._cache.get(key)
def set(self, thread_id: str, options: FetchOptions, messages: list[HistoricalMessage]) -> None:
key = self._make_key(thread_id, options)
self._cache[key] = messages
if thread_id not in self._thread_keys:
self._thread_keys[thread_id] = set()
self._thread_keys[thread_id].add(key)
def invalidate(self, thread_id: str) -> None:
keys = self._thread_keys.pop(thread_id, set())
for key in keys:
self._cache.pop(key, None)