本次提交对频道模块进行了全面重构并新增多项核心功能: 1. 优化适配器状态获取逻辑,修复状态返回空值问题 2. 新增4种频道异常类型,完善错误处理体系 3. 大幅精简Mixin类,移除冗余的抽象方法定义 4. 重构适配器注册系统,统一注册入口并新增内置适配器加载方法 5. 扩展插件系统,新增更多元数据配置项支持 6. 新增线程类型、会话范围等模型定义,扩展事件类型枚举 7. 优化用户映射逻辑,使用PostgreSQL upsert避免重复创建 8. 新增历史消息注入模块,支持多格式历史格式化与缓存管理 9. 新增线程能力配置与各平台预置适配配置 10. 新增线程绑定管理器,支持多类型线程绑定生命周期管理 11. 重构__init__.py,整理导出模块与类型 12. 扩展基础适配器类,新增凭证解析、状态存储等核心方法 13. 重写消息路由器,支持按频道加载策略、安全校验与多命令处理 14. 新增/history、/context、/summary等交互命令实现 15. 优化消息记录与统计逻辑,完善路由调度链路
101 lines
3.1 KiB
Python
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)
|