{"repo": "chasen-liao/lanobot", "files": {"config.json": {"5ab2c91813e035a444f9a51d9778402f9d6ed946": {"t": 1785746769.0461369, "n": 401, "h": "56e38729bd56d0bc", "c": "{\n \"llm\": {\n \"provider\": \"siliconflow\",\n \"model\": \"deepseek-ai/DeepSeek-V3.2\",\n \"api_key\": \"sk-wlyxwslviscdgdgsfqelmsyvhjbsjloxdvvoritvjnremboi\",\n \"base_url\": \"https://api.siliconflow.cn/v1\",\n \"temperature\": 0.1,\n \"max_tokens\": 4096\n },\n \"channels\": {\n \"feishu_enabled\": false,\n \"feishu_app_id\": \"\",\n \"feishu_app_secret\": \"\"\n },\n \"logging\": {\n \"level\": \"INFO\"\n }\n}\n"}, "5256bc5495634f4de1a9f0cdb886ba412b5012b8": {"t": 1785767628.2883065, "n": 401, "h": "56e38729bd56d0bc", "c": "{\n \"llm\": {\n \"provider\": \"siliconflow\",\n \"model\": \"deepseek-ai/DeepSeek-V3.2\",\n \"api_key\": \"sk-wlyxwslviscdgdgsfqelmsyvhjbsjloxdvvoritvjnremboi\",\n \"base_url\": \"https://api.siliconflow.cn/v1\",\n \"temperature\": 0.1,\n \"max_tokens\": 4096\n },\n \"channels\": {\n \"feishu_enabled\": false,\n \"feishu_app_id\": \"\",\n \"feishu_app_secret\": \"\"\n },\n \"logging\": {\n \"level\": \"INFO\"\n }\n}\n"}, "9fd5b3cf984fd747e434f2e271b666e5a8476cb6": {"t": 1785767629.5318918, "n": 405, "h": "6ce613680ed1d585", "c": "{\n \"llm\": {\n \"provider\": \"siliconflow\",\n \"model\": \"Pro/deepseek-ai/DeepSeek-V3.2\",\n \"api_key\": \"sk-wlyxwslviscdgdgsfqelmsyvhjbsjloxdvvoritvjnremboi\",\n \"base_url\": \"https://api.siliconflow.cn/v1\",\n \"temperature\": 0.1,\n \"max_tokens\": 4096\n },\n \"channels\": {\n \"feishu_enabled\": false,\n \"feishu_app_id\": \"\",\n \"feishu_app_secret\": \"\"\n },\n \"logging\": {\n \"level\": \"INFO\"\n }\n}\n"}}, "framework/README.md": {"5ab2c91813e035a444f9a51d9778402f9d6ed946": {"t": 1785747232.4867244, "n": 11902, "h": "84b0062b2a605be4", "c": "# Lanobot 架构设计文档\n\n## 1. 整体架构\n\n```mermaid\nflowchart TB\n subgraph Channels[\"渠道层 (channels/)\"]\n TG[Telegram]\n SL[Slack]\n DD[钉钉]\n FS[飞书]\n DC[Discord]\n WA[WhatsApp]\n QQ[QQ]\n EM[Email]\n end\n\n subgraph Bus[\"消息总线 (bus/)\"]\n MB[MessageBus]\n BE[事件定义]\n end\n\n subgraph Core[\"核心Agent层 (agent/)\"]\n AG[AgentGraph
LangGraph StateGraph]\n SN[State Nodes]\n ED[Edges]\n end\n\n subgraph Tools[\"工具层 (tools/)\"]\n FS[文件系统]\n SH[Shell执行]\n WB[Web搜索/抓取]\n MS[Message发送]\n SP[Spawn子代理]\n CR[Cron定时]\n MC[MCP工具]\n end\n\n subgraph Memory[\"记忆层 (memory/)\"]\n CP[Checkpointer
短期记忆]\n LS[Long-term Store
长期记忆]\n HM[History Manager]\n end\n\n subgraph Providers[\"LLM提供商 (providers/)\"]\n OP[OpenAI]\n AN[Anthropic]\n AZ[Azure OpenAI]\n DS[DeepSeek]\n OR[OpenRouter]\n LM[本地模型
Ollama/vLLM]\n end\n\n subgraph Config[\"配置层 (config/)\"]\n SC[Schema
Pydantic]\n LD[Loader]\n end\n\n subgraph Session[\"会话管理 (session/)\"]\n SM[SessionManager]\n SH[Session历史]\n end\n\n Channels --> MB\n MB --> AG\n AG --> Tools\n AG --> AN\n Tools -->|结果| AG\n AG <--> CP\n AG <--> LS\n AG <--> HM\n AG <--> MB\n MB --> Channels\n```\n\n## 2. 模块职责\n\n### 2.1 渠道层 (channels/)\n\n**职责**:对接各种IM/通讯平台的API,统一消息格式\n\n**保留原因**:与业务无关,纯粹适配外部接口\n\n```python\n# channels/base.py\nclass ChannelAdapter(ABC):\n \"\"\"渠道适配器基类\"\"\"\n\n @abstractmethod\n async def start(self):\n \"\"\"启动渠道监听\"\"\"\n\n @abstractmethod\n async def send(self, chat_id: str, content: str, metadata: dict = None):\n \"\"\"发送消息\"\"\"\n\n @abstractmethod\n async def stop(self):\n \"\"\"停止渠道\"\"\"\n```\n\n**支持的渠道**:\n- Telegram, Slack, Discord\n- 钉钉, 飞书, 企业微信\n- WhatsApp, QQ, Email, Matrix\n\n### 2.2 消息总线 (bus/)\n\n**职责**:异步消息队列,解耦各模块\n\n**保留原因**:架构清晰,状态无关\n\n```python\n# bus/queue.py\nclass MessageBus:\n \"\"\"Async message queue for decoupled channel-agent communication\"\"\"\n\n def __init__(self):\n self.inbound: asyncio.Queue[InboundMessage] = asyncio.Queue()\n self.outbound: asyncio.Queue[OutboundMessage] = asyncio.Queue()\n\n async def publish_inbound(self, msg: InboundMessage):\n await self.inbound.put(msg)\n\n async def consume_inbound(self) -> InboundMessage:\n return await self.inbound.get()\n\n async def publish_outbound(self, msg: OutboundMessage):\n await self.outbound.put(msg)\n```\n\n### 2.3 Agent核心层 (agent/) - LangChain 1.x重构\n\n**职责**:Agent推理循环,工具调用协调\n\n**重构方案**:使用LangChain 1.x的 `create_agent` API(基于LangGraph)\n\n> ⚠️ **重要更新**: LangGraph v1 废弃了 `create_react_agent`,推荐使用 LangChain 1.x 的 `create_agent`\n\n```python\n# agent/graph.py\nfrom langchain.agents import create_agent\nfrom langgraph.checkpoint.memory import InMemorySaver\n\nclass AgentGraph:\n \"\"\"LangChain 1.x Agent构建器\"\"\"\n\n def __init__(self, model, tools: list, checkpointer=None, middleware=None):\n self.graph = create_agent(\n model, # 模型实例或模型名\n tools, # 工具列表\n system_prompt=\"你是lanobot,一个超轻量级AI助手。\", # 注意:不再是prompt\n checkpointer=checkpointer,\n middleware=middleware, # 可选:中间件列表\n )\n\n async def invoke(self, message: str, thread_id: str):\n config = {\"configurable\": {\"thread_id\": thread_id}}\n return await self.graph.ainvoke(\n {\"messages\": [{\"role\": \"user\", \"content\": message}]},\n config=config\n )\n```\n\n#### Middleware系统\n\n```python\n# agent/middleware.py\nfrom langchain.agents.middleware import HumanInTheLoopMiddleware\n\n# 人机协作中间件 - 敏感操作需要审批\nhuman_middleware = HumanInTheLoopMiddleware(\n interrupt_on={\n \"send_message\": True, # 发送消息需要审批\n \"exec_command\": True, # 执行命令需要审批\n \"write_file\": True, # 写文件需要审批\n }\n)\n\n# 创建带中间件的Agent\nagent = create_agent(\n model,\n tools,\n system_prompt=\"...\",\n middleware=[human_middleware],\n checkpointer=InMemorySaver(),\n)\n```\n\n### 2.4 工具层 (tools/) - LangChain化\n\n**职责**:Agent可调用的外部能力\n\n**重构方案**:使用`@tool`装饰器或`StructuredTool`\n\n```python\n# tools/filesystem.py\nfrom langchain_core.tools import tool, StructuredTool\nfrom pydantic import BaseModel, Field\n\n# 方式1:使用@tool装饰器(简单场景)\n@tool\ndef read_file(path: str) -> str:\n \"\"\"读取文件内容\n\n Args:\n path: 文件路径(绝对路径或相对于workspace的路径)\n\n Returns:\n 文件内容字符串\n \"\"\"\n # 实现逻辑\n pass\n\n@tool\ndef write_file(path: str, content: str) -> str:\n \"\"\"写入文件内容\n\n Args:\n path: 文件路径\n content: 要写入的内容\n \"\"\"\n pass\n\n@tool\ndef list_directory(path: str = \".\") -> str:\n \"\"\"列出目录内容\"\"\"\n pass\n\n@tool\ndef edit_file(path: str, old: str, new: str) -> str:\n \"\"\"编辑文件内容(替换指定文本)\"\"\"\n pass\n```\n\n```python\n# tools/shell.py\n@tool\ndef exec_command(command: str, timeout: int = 60) -> str:\n \"\"\"执行Shell命令\n\n Args:\n command: 要执行的命令\n timeout: 超时时间(秒),默认60秒\n \"\"\"\n pass\n```\n\n```python\n# tools/web.py\n@tool\ndef web_search(query: str, max_results: int = 5) -> str:\n \"\"\"Web搜索\"\"\"\n pass\n\n@tool\ndef web_fetch(url: str) -> str:\n \"\"\"抓取网页内容\"\"\"\n pass\n\n@tool\ndef message_send(channel: str, chat_id: str, content: str) -> str:\n \"\"\"发送消息到指定渠道\"\"\"\n pass\n\n@tool\ndef spawn_subagent(task: str, model: str = None) -> str:\n \"\"\"Spawn a sub-agent to handle subtasks\"\"\"\n pass\n```\n\n### 2.5 记忆层 (memory/) - LangChain化\n\n**职责**:短期会话记忆 + 长期记忆\n\n**重构方案**:LangGraph Checkpointer + LangChain Store\n\n> ⚠️ **注意**: LangChain 1.x 使用 `InMemorySaver` 替代了 `MemorySaver`\n\n```python\n# memory/checkpointer.py\nfrom langgraph.checkpoint.memory import InMemorySaver\n\n# 开发环境\ncheckpointer = InMemorySaver()\n\n# 生产环境\n# from langgraph.checkpoint.postgres import PostgresSaver\n# checkpointer = PostgresSaver.from_conn_string(conn_string)\n```\n\n```python\n# memory/store.py\nfrom langchain.storage import InMemoryStore\nfrom langgraph.store.postgres import PostgresStore\n\nclass MemoryStore:\n \"\"\"长期记忆存储\"\"\"\n\n def __init__(self, use_postgres: bool = False):\n if use_postgres:\n self.store = PostgresStore.from_conn_string(conn_string)\n else:\n self.store = InMemoryStore()\n\n async def search_memory(self, query: str, k: int = 5) -> list:\n \"\"\"语义检索相关记忆\"\"\"\n pass\n\n async def save_memory(self, key: str, value: str, metadata: dict = None):\n \"\"\"保存记忆\"\"\"\n pass\n```\n\n### 2.6 Providers层 (providers/)\n\n**职责**:LLM接口适配\n\n**保留原因**:对接各种API,但包装为LangChain接口\n\n```python\n# providers/langchain_wrapper.py\nfrom langchain_anthropic import ChatAnthropic\nfrom langchain_openai import ChatOpenAI\nfrom langchain_community.chat_models import ChatLiteLLM\n\ndef create_llm(provider: str, model: str, api_key: str = None, api_base: str = None):\n \"\"\"创建LangChain LLM实例\"\"\"\n\n if provider == \"anthropic\":\n return ChatAnthropic(\n model=model,\n api_key=api_key,\n base_url=api_base\n )\n elif provider == \"openai\":\n return ChatOpenAI(\n model=model,\n api_key=api_key,\n base_url=api_base\n )\n # LiteLLM支持所有OpenAI兼容的API\n elif provider == \"litellm\":\n return ChatLiteLLM(\n model=model,\n api_key=api_key,\n base_url=api_base\n )\n```\n\n### 2.7 配置层 (config/)\n\n**职责**:YAML/Env配置解析\n\n**保留**:Pydantic配置系统已经很完善\n\n```python\n# config/schema.py\nclass Config(BaseSettings):\n agents: AgentsConfig\n channels: ChannelsConfig\n providers: ProvidersConfig\n gateway: GatewayConfig\n tools: ToolsConfig\n```\n\n### 2.8 会话管理 (session/)\n\n**职责**:会话历史持久化\n\n**保留**:Session管理逻辑与具体Agent实现解耦\n\n```python\n# session/manager.py\nclass SessionManager:\n \"\"\"会话管理器\"\"\"\n\n def __init__(self, workspace: Path):\n self.sessions_dir = workspace / \"sessions\"\n\n def get_or_create(self, session_key: str) -> Session:\n \"\"\"获取或创建会话\"\"\"\n\n def save(self, session: Session):\n \"\"\"保存会话\"\"\"\n\n def get_history(self, session_key: str, max_messages: int = None) -> list:\n \"\"\"获取会话历史\"\"\"\n```\n\n## 3. 数据流\n\n### 3.1 消息处理流程\n\n```mermaid\nsequenceDiagram\n participant C as Channel\n participant B as MessageBus\n participant G as AgentGraph\n participant T as Tools\n participant P as Provider\n participant M as Memory\n participant O as Outbound\n\n C->>B: publish_inbound(msg)\n B->>G: 消费消息\n G->>P: LLM调用(带工具定义)\n P-->>G: LLM响应(含tool_calls)\n G->>T: 执行工具\n T-->>G: 工具结果\n G->>M: 保存checkpointer状态\n G->>P: 继续LLM调用\n P-->>G: 最终响应\n G->>B: publish_outbound(response)\n B->>O: 推送响应\n O->>C: 发送消息\n```\n\n### 3.2 记忆系统流程\n\n```mermaid\nflowchart LR\n A[用户消息] --> B{检查token限制}\n B -->|超限| C[consolidate_messages]\n B -->|正常| D[直接处理]\n C --> E[LLM摘要]\n E --> F[保存到MEMORY.md]\n F --> D\n D --> G[返回响应]\n```\n\n## 4. 目录结构\n\n```\nlanobot/\n├── main.py # 入口点\n├── pyproject.toml # 项目配置\n├── CLAUDE.md # Claude Code指导\n│\n├── config/ # 配置层\n│ ├── schema.py # Pydantic配置模型\n│ ├── loader.py # 配置加载\n│ └── paths.py # 路径工具\n│\n├── bus/ # 消息总线\n│ ├── queue.py # MessageBus实现\n│ └── events.py # 事件定义\n│\n├── channels/ # 渠道层(保留)\n│ ├── base.py # 渠道基类\n│ ├── manager.py # 渠道管理\n│ ├── telegram.py\n│ ├── slack.py\n│ ├── feishu.py\n│ ├── dingtalk.py\n│ ├── discord.py\n│ ├── whatsapp.py\n│ ├── qq.py\n│ ├── wecom.py\n│ ├── email.py\n│ └── matrix.py\n│\n├── providers/ # LLM提供商\n│ ├── base.py # 基础接口(可保留)\n│ ├── registry.py # 提供商注册\n│ ├── langchain_wrapper.py # LangChain包装器\n│ ├── openai_provider.py\n│ ├── anthropic_provider.py\n│ ├── azure_openai_provider.py\n│ ├── litellm_provider.py\n│ └── custom_provider.py\n│\n├── agent/ # Agent核心层(LangChain化)\n│ ├── __init__.py\n│ ├── state.py # AgentState定义\n│ ├── graph.py # LangGraph构建\n│ ├── nodes.py # 节点函数\n│ ├── edges.py # 边定义\n│ └── middleware.py # 中间件\n│\n├── tools/ # 工具层(LangChain化)\n│ ├── __init__.py\n│ ├── registry.py # 工具注册\n│ ├── filesystem.py # 文件操作\n│ ├── shell.py # Shell执行\n│ ├── web.py # Web工具\n│ ├── message.py # 消息发送\n│ ├── spawn.py # 子代理\n│ ├── cron.py # 定时任务\n│ └── mcp.py # MCP集成\n│\n├── memory/ # 记忆层(LangChain化)\n│ ├── checkpointer.py # LangGraph Checkpointer\n│ ├── store.py # 长期记忆\n│ └── history.py # 历史记录管理\n│\n├── session/ # 会话管理\n│ ├── __init__.py\n│ ├── manager.py # 会话管理器\n│ └── types.py # 会话类型\n│\n├── cron/ # 定时任务\n│ ├── service.py\n│ └── types.py\n│\n├── heartbeat/ # 心跳服务\n│ └── service.py\n│\n├── cli/ # CLI命令\n│ ├── commands.py\n│ └── __init__.py\n│\n└── utils/ # 工具函数\n └── helpers.py\n```\n\n## 5. 重构计划\n\n### Phase 1: 基础架构\n- [ ] 创建agent/state.py - AgentState定义\n- [ ] 创建agent/graph.py - LangGraph构建\n- [ ] 创建providers/langchain_wrapper.py - LLM包装\n- [ ] 集成现有MessageBus\n\n### Phase 2: 工具迁移\n- [ ] 将tools/filesystem.py转为@tool\n- [ ] 将tools/shell.py转为@tool\n- [ ] 将tools/web.py转为@tool\n- [ ] 集成MCP工具\n\n### Phase 3: 记忆系统\n- [ ] 集成LangGraph Checkpointer\n- [ ] 实现长期记忆Store\n- [ ] 迁移会话历史\n\n### Phase 4: 渠道适配\n- [ ] 适配现有渠道到新Agent接口\n- [ ] 测试各渠道连通性\n\n### Phase 5: CLI和配置\n- [ ] 更新CLI命令\n- [ ] 验证配置系统\n\n## 6. 关键设计决策\n\n### 6.1 为什么用混合架构?\n- **保留**:渠道、配置、业务无关的模块已经稳定\n- **重构**:Agent核心需要LangChain的标准化和生态\n\n### 6.2 LangGraph vs 纯LangChain\n- 使用`create_agent`快速构建\n- 后续可扩展为自定义StateGraph\n\n### 6.3 记忆策略\n- **短期**:LangGraph Checkpointer (thread_id级别)\n- **长期**:LangChain Store (跨会话)\n- **历史**:文件存储 (可grep搜索)\n\n### 6.4 Provider策略\n- 保留原有provider注册机制\n- 新增LangChain包装层\n- 渐进式迁移"}, "771d048f6878219b0a134244862568223f15fd53": {"t": 1785767627.4940884, "n": 11902, "h": "84b0062b2a605be4", "c": "# Lanobot 架构设计文档\n\n## 1. 整体架构\n\n```mermaid\nflowchart TB\n subgraph Channels[\"渠道层 (channels/)\"]\n TG[Telegram]\n SL[Slack]\n DD[钉钉]\n FS[飞书]\n DC[Discord]\n WA[WhatsApp]\n QQ[QQ]\n EM[Email]\n end\n\n subgraph Bus[\"消息总线 (bus/)\"]\n MB[MessageBus]\n BE[事件定义]\n end\n\n subgraph Core[\"核心Agent层 (agent/)\"]\n AG[AgentGraph
LangGraph StateGraph]\n SN[State Nodes]\n ED[Edges]\n end\n\n subgraph Tools[\"工具层 (tools/)\"]\n FS[文件系统]\n SH[Shell执行]\n WB[Web搜索/抓取]\n MS[Message发送]\n SP[Spawn子代理]\n CR[Cron定时]\n MC[MCP工具]\n end\n\n subgraph Memory[\"记忆层 (memory/)\"]\n CP[Checkpointer
短期记忆]\n LS[Long-term Store
长期记忆]\n HM[History Manager]\n end\n\n subgraph Providers[\"LLM提供商 (providers/)\"]\n OP[OpenAI]\n AN[Anthropic]\n AZ[Azure OpenAI]\n DS[DeepSeek]\n OR[OpenRouter]\n LM[本地模型
Ollama/vLLM]\n end\n\n subgraph Config[\"配置层 (config/)\"]\n SC[Schema
Pydantic]\n LD[Loader]\n end\n\n subgraph Session[\"会话管理 (session/)\"]\n SM[SessionManager]\n SH[Session历史]\n end\n\n Channels --> MB\n MB --> AG\n AG --> Tools\n AG --> AN\n Tools -->|结果| AG\n AG <--> CP\n AG <--> LS\n AG <--> HM\n AG <--> MB\n MB --> Channels\n```\n\n## 2. 模块职责\n\n### 2.1 渠道层 (channels/)\n\n**职责**:对接各种IM/通讯平台的API,统一消息格式\n\n**保留原因**:与业务无关,纯粹适配外部接口\n\n```python\n# channels/base.py\nclass ChannelAdapter(ABC):\n \"\"\"渠道适配器基类\"\"\"\n\n @abstractmethod\n async def start(self):\n \"\"\"启动渠道监听\"\"\"\n\n @abstractmethod\n async def send(self, chat_id: str, content: str, metadata: dict = None):\n \"\"\"发送消息\"\"\"\n\n @abstractmethod\n async def stop(self):\n \"\"\"停止渠道\"\"\"\n```\n\n**支持的渠道**:\n- Telegram, Slack, Discord\n- 钉钉, 飞书, 企业微信\n- WhatsApp, QQ, Email, Matrix\n\n### 2.2 消息总线 (bus/)\n\n**职责**:异步消息队列,解耦各模块\n\n**保留原因**:架构清晰,状态无关\n\n```python\n# bus/queue.py\nclass MessageBus:\n \"\"\"Async message queue for decoupled channel-agent communication\"\"\"\n\n def __init__(self):\n self.inbound: asyncio.Queue[InboundMessage] = asyncio.Queue()\n self.outbound: asyncio.Queue[OutboundMessage] = asyncio.Queue()\n\n async def publish_inbound(self, msg: InboundMessage):\n await self.inbound.put(msg)\n\n async def consume_inbound(self) -> InboundMessage:\n return await self.inbound.get()\n\n async def publish_outbound(self, msg: OutboundMessage):\n await self.outbound.put(msg)\n```\n\n### 2.3 Agent核心层 (agent/) - LangChain 1.x重构\n\n**职责**:Agent推理循环,工具调用协调\n\n**重构方案**:使用LangChain 1.x的 `create_agent` API(基于LangGraph)\n\n> ⚠️ **重要更新**: LangGraph v1 废弃了 `create_react_agent`,推荐使用 LangChain 1.x 的 `create_agent`\n\n```python\n# agent/graph.py\nfrom langchain.agents import create_agent\nfrom langgraph.checkpoint.memory import InMemorySaver\n\nclass AgentGraph:\n \"\"\"LangChain 1.x Agent构建器\"\"\"\n\n def __init__(self, model, tools: list, checkpointer=None, middleware=None):\n self.graph = create_agent(\n model, # 模型实例或模型名\n tools, # 工具列表\n system_prompt=\"你是lanobot,一个超轻量级AI助手。\", # 注意:不再是prompt\n checkpointer=checkpointer,\n middleware=middleware, # 可选:中间件列表\n )\n\n async def invoke(self, message: str, thread_id: str):\n config = {\"configurable\": {\"thread_id\": thread_id}}\n return await self.graph.ainvoke(\n {\"messages\": [{\"role\": \"user\", \"content\": message}]},\n config=config\n )\n```\n\n#### Middleware系统\n\n```python\n# agent/middleware.py\nfrom langchain.agents.middleware import HumanInTheLoopMiddleware\n\n# 人机协作中间件 - 敏感操作需要审批\nhuman_middleware = HumanInTheLoopMiddleware(\n interrupt_on={\n \"send_message\": True, # 发送消息需要审批\n \"exec_command\": True, # 执行命令需要审批\n \"write_file\": True, # 写文件需要审批\n }\n)\n\n# 创建带中间件的Agent\nagent = create_agent(\n model,\n tools,\n system_prompt=\"...\",\n middleware=[human_middleware],\n checkpointer=InMemorySaver(),\n)\n```\n\n### 2.4 工具层 (tools/) - LangChain化\n\n**职责**:Agent可调用的外部能力\n\n**重构方案**:使用`@tool`装饰器或`StructuredTool`\n\n```python\n# tools/filesystem.py\nfrom langchain_core.tools import tool, StructuredTool\nfrom pydantic import BaseModel, Field\n\n# 方式1:使用@tool装饰器(简单场景)\n@tool\ndef read_file(path: str) -> str:\n \"\"\"读取文件内容\n\n Args:\n path: 文件路径(绝对路径或相对于workspace的路径)\n\n Returns:\n 文件内容字符串\n \"\"\"\n # 实现逻辑\n pass\n\n@tool\ndef write_file(path: str, content: str) -> str:\n \"\"\"写入文件内容\n\n Args:\n path: 文件路径\n content: 要写入的内容\n \"\"\"\n pass\n\n@tool\ndef list_directory(path: str = \".\") -> str:\n \"\"\"列出目录内容\"\"\"\n pass\n\n@tool\ndef edit_file(path: str, old: str, new: str) -> str:\n \"\"\"编辑文件内容(替换指定文本)\"\"\"\n pass\n```\n\n```python\n# tools/shell.py\n@tool\ndef exec_command(command: str, timeout: int = 60) -> str:\n \"\"\"执行Shell命令\n\n Args:\n command: 要执行的命令\n timeout: 超时时间(秒),默认60秒\n \"\"\"\n pass\n```\n\n```python\n# tools/web.py\n@tool\ndef web_search(query: str, max_results: int = 5) -> str:\n \"\"\"Web搜索\"\"\"\n pass\n\n@tool\ndef web_fetch(url: str) -> str:\n \"\"\"抓取网页内容\"\"\"\n pass\n\n@tool\ndef message_send(channel: str, chat_id: str, content: str) -> str:\n \"\"\"发送消息到指定渠道\"\"\"\n pass\n\n@tool\ndef spawn_subagent(task: str, model: str = None) -> str:\n \"\"\"Spawn a sub-agent to handle subtasks\"\"\"\n pass\n```\n\n### 2.5 记忆层 (memory/) - LangChain化\n\n**职责**:短期会话记忆 + 长期记忆\n\n**重构方案**:LangGraph Checkpointer + LangChain Store\n\n> ⚠️ **注意**: LangChain 1.x 使用 `InMemorySaver` 替代了 `MemorySaver`\n\n```python\n# memory/checkpointer.py\nfrom langgraph.checkpoint.memory import InMemorySaver\n\n# 开发环境\ncheckpointer = InMemorySaver()\n\n# 生产环境\n# from langgraph.checkpoint.postgres import PostgresSaver\n# checkpointer = PostgresSaver.from_conn_string(conn_string)\n```\n\n```python\n# memory/store.py\nfrom langchain.storage import InMemoryStore\nfrom langgraph.store.postgres import PostgresStore\n\nclass MemoryStore:\n \"\"\"长期记忆存储\"\"\"\n\n def __init__(self, use_postgres: bool = False):\n if use_postgres:\n self.store = PostgresStore.from_conn_string(conn_string)\n else:\n self.store = InMemoryStore()\n\n async def search_memory(self, query: str, k: int = 5) -> list:\n \"\"\"语义检索相关记忆\"\"\"\n pass\n\n async def save_memory(self, key: str, value: str, metadata: dict = None):\n \"\"\"保存记忆\"\"\"\n pass\n```\n\n### 2.6 Providers层 (providers/)\n\n**职责**:LLM接口适配\n\n**保留原因**:对接各种API,但包装为LangChain接口\n\n```python\n# providers/langchain_wrapper.py\nfrom langchain_anthropic import ChatAnthropic\nfrom langchain_openai import ChatOpenAI\nfrom langchain_community.chat_models import ChatLiteLLM\n\ndef create_llm(provider: str, model: str, api_key: str = None, api_base: str = None):\n \"\"\"创建LangChain LLM实例\"\"\"\n\n if provider == \"anthropic\":\n return ChatAnthropic(\n model=model,\n api_key=api_key,\n base_url=api_base\n )\n elif provider == \"openai\":\n return ChatOpenAI(\n model=model,\n api_key=api_key,\n base_url=api_base\n )\n # LiteLLM支持所有OpenAI兼容的API\n elif provider == \"litellm\":\n return ChatLiteLLM(\n model=model,\n api_key=api_key,\n base_url=api_base\n )\n```\n\n### 2.7 配置层 (config/)\n\n**职责**:YAML/Env配置解析\n\n**保留**:Pydantic配置系统已经很完善\n\n```python\n# config/schema.py\nclass Config(BaseSettings):\n agents: AgentsConfig\n channels: ChannelsConfig\n providers: ProvidersConfig\n gateway: GatewayConfig\n tools: ToolsConfig\n```\n\n### 2.8 会话管理 (session/)\n\n**职责**:会话历史持久化\n\n**保留**:Session管理逻辑与具体Agent实现解耦\n\n```python\n# session/manager.py\nclass SessionManager:\n \"\"\"会话管理器\"\"\"\n\n def __init__(self, workspace: Path):\n self.sessions_dir = workspace / \"sessions\"\n\n def get_or_create(self, session_key: str) -> Session:\n \"\"\"获取或创建会话\"\"\"\n\n def save(self, session: Session):\n \"\"\"保存会话\"\"\"\n\n def get_history(self, session_key: str, max_messages: int = None) -> list:\n \"\"\"获取会话历史\"\"\"\n```\n\n## 3. 数据流\n\n### 3.1 消息处理流程\n\n```mermaid\nsequenceDiagram\n participant C as Channel\n participant B as MessageBus\n participant G as AgentGraph\n participant T as Tools\n participant P as Provider\n participant M as Memory\n participant O as Outbound\n\n C->>B: publish_inbound(msg)\n B->>G: 消费消息\n G->>P: LLM调用(带工具定义)\n P-->>G: LLM响应(含tool_calls)\n G->>T: 执行工具\n T-->>G: 工具结果\n G->>M: 保存checkpointer状态\n G->>P: 继续LLM调用\n P-->>G: 最终响应\n G->>B: publish_outbound(response)\n B->>O: 推送响应\n O->>C: 发送消息\n```\n\n### 3.2 记忆系统流程\n\n```mermaid\nflowchart LR\n A[用户消息] --> B{检查token限制}\n B -->|超限| C[consolidate_messages]\n B -->|正常| D[直接处理]\n C --> E[LLM摘要]\n E --> F[保存到MEMORY.md]\n F --> D\n D --> G[返回响应]\n```\n\n## 4. 目录结构\n\n```\nlanobot/\n├── main.py # 入口点\n├── pyproject.toml # 项目配置\n├── CLAUDE.md # Claude Code指导\n│\n├── config/ # 配置层\n│ ├── schema.py # Pydantic配置模型\n│ ├── loader.py # 配置加载\n│ └── paths.py # 路径工具\n│\n├── bus/ # 消息总线\n│ ├── queue.py # MessageBus实现\n│ └── events.py # 事件定义\n│\n├── channels/ # 渠道层(保留)\n│ ├── base.py # 渠道基类\n│ ├── manager.py # 渠道管理\n│ ├── telegram.py\n│ ├── slack.py\n│ ├── feishu.py\n│ ├── dingtalk.py\n│ ├── discord.py\n│ ├── whatsapp.py\n│ ├── qq.py\n│ ├── wecom.py\n│ ├── email.py\n│ └── matrix.py\n│\n├── providers/ # LLM提供商\n│ ├── base.py # 基础接口(可保留)\n│ ├── registry.py # 提供商注册\n│ ├── langchain_wrapper.py # LangChain包装器\n│ ├── openai_provider.py\n│ ├── anthropic_provider.py\n│ ├── azure_openai_provider.py\n│ ├── litellm_provider.py\n│ └── custom_provider.py\n│\n├── agent/ # Agent核心层(LangChain化)\n│ ├── __init__.py\n│ ├── state.py # AgentState定义\n│ ├── graph.py # LangGraph构建\n│ ├── nodes.py # 节点函数\n│ ├── edges.py # 边定义\n│ └── middleware.py # 中间件\n│\n├── tools/ # 工具层(LangChain化)\n│ ├── __init__.py\n│ ├── registry.py # 工具注册\n│ ├── filesystem.py # 文件操作\n│ ├── shell.py # Shell执行\n│ ├── web.py # Web工具\n│ ├── message.py # 消息发送\n│ ├── spawn.py # 子代理\n│ ├── cron.py # 定时任务\n│ └── mcp.py # MCP集成\n│\n├── memory/ # 记忆层(LangChain化)\n│ ├── checkpointer.py # LangGraph Checkpointer\n│ ├── store.py # 长期记忆\n│ └── history.py # 历史记录管理\n│\n├── session/ # 会话管理\n│ ├── __init__.py\n│ ├── manager.py # 会话管理器\n│ └── types.py # 会话类型\n│\n├── cron/ # 定时任务\n│ ├── service.py\n│ └── types.py\n│\n├── heartbeat/ # 心跳服务\n│ └── service.py\n│\n├── cli/ # CLI命令\n│ ├── commands.py\n│ └── __init__.py\n│\n└── utils/ # 工具函数\n └── helpers.py\n```\n\n## 5. 重构计划\n\n### Phase 1: 基础架构\n- [ ] 创建agent/state.py - AgentState定义\n- [ ] 创建agent/graph.py - LangGraph构建\n- [ ] 创建providers/langchain_wrapper.py - LLM包装\n- [ ] 集成现有MessageBus\n\n### Phase 2: 工具迁移\n- [ ] 将tools/filesystem.py转为@tool\n- [ ] 将tools/shell.py转为@tool\n- [ ] 将tools/web.py转为@tool\n- [ ] 集成MCP工具\n\n### Phase 3: 记忆系统\n- [ ] 集成LangGraph Checkpointer\n- [ ] 实现长期记忆Store\n- [ ] 迁移会话历史\n\n### Phase 4: 渠道适配\n- [ ] 适配现有渠道到新Agent接口\n- [ ] 测试各渠道连通性\n\n### Phase 5: CLI和配置\n- [ ] 更新CLI命令\n- [ ] 验证配置系统\n\n## 6. 关键设计决策\n\n### 6.1 为什么用混合架构?\n- **保留**:渠道、配置、业务无关的模块已经稳定\n- **重构**:Agent核心需要LangChain的标准化和生态\n\n### 6.2 LangGraph vs 纯LangChain\n- 使用`create_agent`快速构建\n- 后续可扩展为自定义StateGraph\n\n### 6.3 记忆策略\n- **短期**:LangGraph Checkpointer (thread_id级别)\n- **长期**:LangChain Store (跨会话)\n- **历史**:文件存储 (可grep搜索)\n\n### 6.4 Provider策略\n- 保留原有provider注册机制\n- 新增LangChain包装层\n- 渐进式迁移"}}, "tests/test_providers/test_registry.py": {"5ab2c91813e035a444f9a51d9778402f9d6ed946": {"t": 1785747510.7227652, "n": 6737, "h": "808c7305dbb7dd54", "c": "\"\"\"Provider 注册表测试.\"\"\"\n\nimport pytest\n\nfrom lanobot.providers.registry import (\n PROVIDERS,\n find_by_model,\n find_by_name,\n find_gateway,\n get_default_provider,\n get_provider,\n list_providers,\n)\n\n\nclass TestProviderSpec:\n \"\"\"ProviderSpec 数据类测试.\"\"\"\n\n def test_create_spec(self):\n \"\"\"测试创建 Provider 规范.\"\"\"\n from lanobot.providers.registry import ProviderSpec\n\n spec = ProviderSpec(\n name=\"test\",\n keywords=(\"test\",),\n env_key=\"TEST_API_KEY\",\n display_name=\"Test Provider\",\n litellm_prefix=\"test\",\n )\n assert spec.name == \"test\"\n assert spec.litellm_prefix == \"test\"\n assert spec.label == \"Test Provider\"\n\n def test_label_property(self):\n \"\"\"测试 label 属性.\"\"\"\n spec = get_provider(\"siliconflow\")\n assert spec.label == \"SiliconFlow\"\n\n\nclass TestProviderRegistry:\n \"\"\"Provider 注册表测试.\"\"\"\n\n def test_providers_not_empty(self):\n \"\"\"测试注册表不为空.\"\"\"\n assert len(PROVIDERS) > 0\n\n def test_list_providers(self):\n \"\"\"测试列出所有 Provider.\"\"\"\n providers = list_providers()\n assert isinstance(providers, list)\n assert len(providers) > 0\n\n def test_gateway_providers(self):\n \"\"\"测试网关 Provider.\"\"\"\n # OpenRouter - 通过 API Key 前缀检测\n spec = get_provider(\"openrouter\")\n assert spec.name == \"openrouter\"\n assert spec.is_gateway is True\n assert spec.detect_by_key_prefix == \"sk-or-\"\n assert spec.default_api_base == \"https://openrouter.ai/api/v1\"\n\n # SiliconFlow - 通过 URL 关键字检测\n spec = get_provider(\"siliconflow\")\n assert spec.name == \"siliconflow\"\n assert spec.is_gateway is True\n assert \"siliconflow\" in spec.detect_by_base_keyword\n\n # VolcEngine\n spec = get_provider(\"volcengine\")\n assert spec.name == \"volcengine\"\n assert spec.is_gateway is True\n assert \"volces\" in spec.detect_by_base_keyword\n\n def test_standard_providers(self):\n \"\"\"测试标准 Provider.\"\"\"\n # DeepSeek\n spec = get_provider(\"deepseek\")\n assert spec.name == \"deepseek\"\n assert spec.is_gateway is False\n assert spec.litellm_prefix == \"deepseek\"\n assert spec.env_key == \"DEEPSEEK_API_KEY\"\n\n # OpenAI\n spec = get_provider(\"openai\")\n assert spec.name == \"openai\"\n assert spec.litellm_prefix == \"\"\n\n # Anthropic\n spec = get_provider(\"anthropic\")\n assert spec.name == \"anthropic\"\n assert spec.supports_prompt_caching is True\n\n def test_local_providers(self):\n \"\"\"测试本地 Provider.\"\"\"\n # Ollama\n spec = get_provider(\"ollama\")\n assert spec.name == \"ollama\"\n assert spec.is_local is True\n assert spec.default_api_base == \"http://localhost:11434\"\n\n # vLLM\n spec = get_provider(\"vllm\")\n assert spec.name == \"vllm\"\n assert spec.is_local is True\n\n def test_direct_providers(self):\n \"\"\"测试直接调用 Provider.\"\"\"\n spec = get_provider(\"custom\")\n assert spec.name == \"custom\"\n assert spec.is_direct is True\n\n spec = get_provider(\"azure_openai\")\n assert spec.name == \"azure_openai\"\n assert spec.is_direct is True\n\n def test_oauth_providers(self):\n \"\"\"测试 OAuth Provider.\"\"\"\n spec = get_provider(\"openai_codex\")\n assert spec.name == \"openai_codex\"\n assert spec.is_oauth is True\n assert spec.env_key == \"\"\n\n def test_chinese_providers(self):\n \"\"\"测试中文 Provider.\"\"\"\n # Zhipu (智谱)\n spec = get_provider(\"zhipu\")\n assert spec.name == \"zhipu\"\n assert \"glm\" in spec.keywords\n assert \"zhi\" in spec.display_name.lower()\n\n # DashScope (阿里云)\n spec = get_provider(\"dashscope\")\n assert spec.name == \"dashscope\"\n assert \"qwen\" in spec.keywords\n\n # Moonshot (月之暗面)\n spec = get_provider(\"moonshot\")\n assert spec.name == \"moonshot\"\n assert \"kimi\" in spec.keywords\n\n # MiniMax\n spec = get_provider(\"minimax\")\n assert spec.name == \"minimax\"\n assert spec.default_api_base == \"https://api.minimax.io/v1\"\n\n def test_unknown_provider_raises_error(self):\n \"\"\"测试未知 Provider 抛出错误.\"\"\"\n with pytest.raises(ValueError) as exc:\n get_provider(\"unknown_provider\")\n assert \"Unknown provider\" in str(exc.value)\n\n def test_default_provider(self):\n \"\"\"测试获取默认 Provider (第一个非 Direct).\"\"\"\n default = get_default_provider()\n # 第一个非 Direct 应该是 OpenRouter (Gateway)\n assert default.is_gateway is True\n\n def test_find_by_name(self):\n \"\"\"测试按名称查找.\"\"\"\n spec = find_by_name(\"deepseek\")\n assert spec is not None\n assert spec.name == \"deepseek\"\n\n # 不存在的名称返回 None\n assert find_by_name(\"nonexistent\") is None\n\n def test_find_gateway_by_key_prefix(self):\n \"\"\"测试通过 API Key 前缀检测网关.\"\"\"\n # sk-or- -> OpenRouter\n spec = find_gateway(\n provider_name=None,\n api_key=\"sk-or-v1xxxxx\",\n api_base=None,\n )\n assert spec is not None\n assert spec.name == \"openrouter\"\n\n def test_find_gateway_by_base_keyword(self):\n \"\"\"测试通过 URL 关键字检测网关.\"\"\"\n # siliconflow in URL -> SiliconFlow\n spec = find_gateway(\n provider_name=None,\n api_key=\"sk-xxxx\",\n api_base=\"https://api.siliconflow.cn/v1\",\n )\n assert spec is not None\n assert spec.name == \"siliconflow\"\n\n def test_find_by_model(self):\n \"\"\"测试通过模型名称查找 Provider.\"\"\"\n # gpt-* -> OpenAI\n spec = find_by_model(\"gpt-4o\")\n assert spec is not None\n assert spec.name == \"openai\"\n\n # claude-* -> Anthropic\n spec = find_by_model(\"claude-3-opus\")\n assert spec is not None\n assert spec.name == \"anthropic\"\n\n # deepseek-chat -> DeepSeek\n spec = find_by_model(\"deepseek-chat\")\n assert spec is not None\n assert spec.name == \"deepseek\"\n\n # qwen-max -> DashScope\n spec = find_by_model(\"qwen-max\")\n assert spec is not None\n assert spec.name == \"dashscope\"\n\n # kimi-k2.5 -> Moonshot\n spec = find_by_model(\"kimi-k2.5\")\n assert spec is not None\n assert spec.name == \"moonshot\"\n\n def test_model_overrides(self):\n \"\"\"测试模型参数覆盖.\"\"\"\n spec = get_provider(\"moonshot\")\n assert spec.model_overrides is not None\n # kimi-k2.5 有 temperature 覆盖\n has_override = any(m == \"kimi-k2.5\" for m, _ in spec.model_overrides)\n assert has_override is True\n\n\nif __name__ == \"__main__\":\n pytest.main([__file__, \"-v\"])"}, "9fd5b3cf984fd747e434f2e271b666e5a8476cb6": {"t": 1785767628.1130886, "n": 6737, "h": "808c7305dbb7dd54", "c": "\"\"\"Provider 注册表测试.\"\"\"\n\nimport pytest\n\nfrom lanobot.providers.registry import (\n PROVIDERS,\n find_by_model,\n find_by_name,\n find_gateway,\n get_default_provider,\n get_provider,\n list_providers,\n)\n\n\nclass TestProviderSpec:\n \"\"\"ProviderSpec 数据类测试.\"\"\"\n\n def test_create_spec(self):\n \"\"\"测试创建 Provider 规范.\"\"\"\n from lanobot.providers.registry import ProviderSpec\n\n spec = ProviderSpec(\n name=\"test\",\n keywords=(\"test\",),\n env_key=\"TEST_API_KEY\",\n display_name=\"Test Provider\",\n litellm_prefix=\"test\",\n )\n assert spec.name == \"test\"\n assert spec.litellm_prefix == \"test\"\n assert spec.label == \"Test Provider\"\n\n def test_label_property(self):\n \"\"\"测试 label 属性.\"\"\"\n spec = get_provider(\"siliconflow\")\n assert spec.label == \"SiliconFlow\"\n\n\nclass TestProviderRegistry:\n \"\"\"Provider 注册表测试.\"\"\"\n\n def test_providers_not_empty(self):\n \"\"\"测试注册表不为空.\"\"\"\n assert len(PROVIDERS) > 0\n\n def test_list_providers(self):\n \"\"\"测试列出所有 Provider.\"\"\"\n providers = list_providers()\n assert isinstance(providers, list)\n assert len(providers) > 0\n\n def test_gateway_providers(self):\n \"\"\"测试网关 Provider.\"\"\"\n # OpenRouter - 通过 API Key 前缀检测\n spec = get_provider(\"openrouter\")\n assert spec.name == \"openrouter\"\n assert spec.is_gateway is True\n assert spec.detect_by_key_prefix == \"sk-or-\"\n assert spec.default_api_base == \"https://openrouter.ai/api/v1\"\n\n # SiliconFlow - 通过 URL 关键字检测\n spec = get_provider(\"siliconflow\")\n assert spec.name == \"siliconflow\"\n assert spec.is_gateway is True\n assert \"siliconflow\" in spec.detect_by_base_keyword\n\n # VolcEngine\n spec = get_provider(\"volcengine\")\n assert spec.name == \"volcengine\"\n assert spec.is_gateway is True\n assert \"volces\" in spec.detect_by_base_keyword\n\n def test_standard_providers(self):\n \"\"\"测试标准 Provider.\"\"\"\n # DeepSeek\n spec = get_provider(\"deepseek\")\n assert spec.name == \"deepseek\"\n assert spec.is_gateway is False\n assert spec.litellm_prefix == \"deepseek\"\n assert spec.env_key == \"DEEPSEEK_API_KEY\"\n\n # OpenAI\n spec = get_provider(\"openai\")\n assert spec.name == \"openai\"\n assert spec.litellm_prefix == \"\"\n\n # Anthropic\n spec = get_provider(\"anthropic\")\n assert spec.name == \"anthropic\"\n assert spec.supports_prompt_caching is True\n\n def test_local_providers(self):\n \"\"\"测试本地 Provider.\"\"\"\n # Ollama\n spec = get_provider(\"ollama\")\n assert spec.name == \"ollama\"\n assert spec.is_local is True\n assert spec.default_api_base == \"http://localhost:11434\"\n\n # vLLM\n spec = get_provider(\"vllm\")\n assert spec.name == \"vllm\"\n assert spec.is_local is True\n\n def test_direct_providers(self):\n \"\"\"测试直接调用 Provider.\"\"\"\n spec = get_provider(\"custom\")\n assert spec.name == \"custom\"\n assert spec.is_direct is True\n\n spec = get_provider(\"azure_openai\")\n assert spec.name == \"azure_openai\"\n assert spec.is_direct is True\n\n def test_oauth_providers(self):\n \"\"\"测试 OAuth Provider.\"\"\"\n spec = get_provider(\"openai_codex\")\n assert spec.name == \"openai_codex\"\n assert spec.is_oauth is True\n assert spec.env_key == \"\"\n\n def test_chinese_providers(self):\n \"\"\"测试中文 Provider.\"\"\"\n # Zhipu (智谱)\n spec = get_provider(\"zhipu\")\n assert spec.name == \"zhipu\"\n assert \"glm\" in spec.keywords\n assert \"zhi\" in spec.display_name.lower()\n\n # DashScope (阿里云)\n spec = get_provider(\"dashscope\")\n assert spec.name == \"dashscope\"\n assert \"qwen\" in spec.keywords\n\n # Moonshot (月之暗面)\n spec = get_provider(\"moonshot\")\n assert spec.name == \"moonshot\"\n assert \"kimi\" in spec.keywords\n\n # MiniMax\n spec = get_provider(\"minimax\")\n assert spec.name == \"minimax\"\n assert spec.default_api_base == \"https://api.minimax.io/v1\"\n\n def test_unknown_provider_raises_error(self):\n \"\"\"测试未知 Provider 抛出错误.\"\"\"\n with pytest.raises(ValueError) as exc:\n get_provider(\"unknown_provider\")\n assert \"Unknown provider\" in str(exc.value)\n\n def test_default_provider(self):\n \"\"\"测试获取默认 Provider (第一个非 Direct).\"\"\"\n default = get_default_provider()\n # 第一个非 Direct 应该是 OpenRouter (Gateway)\n assert default.is_gateway is True\n\n def test_find_by_name(self):\n \"\"\"测试按名称查找.\"\"\"\n spec = find_by_name(\"deepseek\")\n assert spec is not None\n assert spec.name == \"deepseek\"\n\n # 不存在的名称返回 None\n assert find_by_name(\"nonexistent\") is None\n\n def test_find_gateway_by_key_prefix(self):\n \"\"\"测试通过 API Key 前缀检测网关.\"\"\"\n # sk-or- -> OpenRouter\n spec = find_gateway(\n provider_name=None,\n api_key=\"sk-or-v1xxxxx\",\n api_base=None,\n )\n assert spec is not None\n assert spec.name == \"openrouter\"\n\n def test_find_gateway_by_base_keyword(self):\n \"\"\"测试通过 URL 关键字检测网关.\"\"\"\n # siliconflow in URL -> SiliconFlow\n spec = find_gateway(\n provider_name=None,\n api_key=\"sk-xxxx\",\n api_base=\"https://api.siliconflow.cn/v1\",\n )\n assert spec is not None\n assert spec.name == \"siliconflow\"\n\n def test_find_by_model(self):\n \"\"\"测试通过模型名称查找 Provider.\"\"\"\n # gpt-* -> OpenAI\n spec = find_by_model(\"gpt-4o\")\n assert spec is not None\n assert spec.name == \"openai\"\n\n # claude-* -> Anthropic\n spec = find_by_model(\"claude-3-opus\")\n assert spec is not None\n assert spec.name == \"anthropic\"\n\n # deepseek-chat -> DeepSeek\n spec = find_by_model(\"deepseek-chat\")\n assert spec is not None\n assert spec.name == \"deepseek\"\n\n # qwen-max -> DashScope\n spec = find_by_model(\"qwen-max\")\n assert spec is not None\n assert spec.name == \"dashscope\"\n\n # kimi-k2.5 -> Moonshot\n spec = find_by_model(\"kimi-k2.5\")\n assert spec is not None\n assert spec.name == \"moonshot\"\n\n def test_model_overrides(self):\n \"\"\"测试模型参数覆盖.\"\"\"\n spec = get_provider(\"moonshot\")\n assert spec.model_overrides is not None\n # kimi-k2.5 有 temperature 覆盖\n has_override = any(m == \"kimi-k2.5\" for m, _ in spec.model_overrides)\n assert has_override is True\n\n\nif __name__ == \"__main__\":\n pytest.main([__file__, \"-v\"])"}}}}