fr-cli 2.2.5__tar.gz → 2.2.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. {fr_cli-2.2.5/fr_cli.egg-info → fr_cli-2.2.7}/PKG-INFO +7 -7
  2. {fr_cli-2.2.5 → fr_cli-2.2.7}/README.md +3 -3
  3. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/README.md +11 -4
  4. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/__init__.py +1 -1
  5. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/agent/executor.py +22 -4
  6. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/agent/generator.py +2 -1
  7. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/agent/manager.py +27 -0
  8. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/agent/workflow.py +59 -49
  9. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/command/executor.py +32 -6
  10. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/core/core.py +40 -1
  11. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/core/llm.py +51 -14
  12. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/lang/i18n.py +21 -5
  13. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/main.py +2 -0
  14. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/repl/commands.py +172 -15
  15. {fr_cli-2.2.5 → fr_cli-2.2.7/fr_cli.egg-info}/PKG-INFO +7 -7
  16. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli.egg-info/SOURCES.txt +2 -13
  17. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli.egg-info/requires.txt +2 -2
  18. {fr_cli-2.2.5 → fr_cli-2.2.7}/pyproject.toml +4 -4
  19. fr_cli-2.2.7/tests/test_integration_real.py +745 -0
  20. fr_cli-2.2.7/tests/test_model_config.py +391 -0
  21. fr_cli-2.2.5/tests/test_agent_client.py +0 -199
  22. fr_cli-2.2.5/tests/test_agent_server.py +0 -199
  23. fr_cli-2.2.5/tests/test_ai_save_file_with_verify.py +0 -228
  24. fr_cli-2.2.5/tests/test_all.py +0 -917
  25. fr_cli-2.2.5/tests/test_auto_session.py +0 -116
  26. fr_cli-2.2.5/tests/test_builtins.py +0 -67
  27. fr_cli-2.2.5/tests/test_dataframe.py +0 -42
  28. fr_cli-2.2.5/tests/test_gatekeeper.py +0 -118
  29. fr_cli-2.2.5/tests/test_integration.py +0 -224
  30. fr_cli-2.2.5/tests/test_intent_classification.py +0 -268
  31. fr_cli-2.2.5/tests/test_launcher.py +0 -98
  32. fr_cli-2.2.5/tests/test_master_agent.py +0 -162
  33. fr_cli-2.2.5/tests/test_structured_tools.py +0 -245
  34. {fr_cli-2.2.5 → fr_cli-2.2.7}/LICENSE +0 -0
  35. {fr_cli-2.2.5 → fr_cli-2.2.7}/MANIFEST.in +0 -0
  36. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/WEAPON.MD +0 -0
  37. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/addon/plugin.py +0 -0
  38. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/agent/__init__.py +0 -0
  39. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/agent/builtins/__init__.py +0 -0
  40. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/agent/builtins/_utils.py +0 -0
  41. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/agent/builtins/db.py +0 -0
  42. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/agent/builtins/local.py +0 -0
  43. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/agent/builtins/rag.py +0 -0
  44. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/agent/builtins/rag_watcher_daemon.py +0 -0
  45. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/agent/builtins/remote.py +0 -0
  46. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/agent/builtins/spider.py +0 -0
  47. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/agent/client.py +0 -0
  48. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/agent/master.py +0 -0
  49. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/agent/master_prompt.py +0 -0
  50. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/agent/remote.py +0 -0
  51. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/agent/server.py +0 -0
  52. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/breakthrough/update.py +0 -0
  53. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/command/__init__.py +0 -0
  54. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/command/registry.py +0 -0
  55. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/command/security.py +0 -0
  56. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/conf/config.py +0 -0
  57. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/conf/wizard.py +0 -0
  58. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/core/chat.py +0 -0
  59. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/core/intent.py +0 -0
  60. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/core/recommender.py +0 -0
  61. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/core/stream.py +0 -0
  62. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/core/sysmon.py +0 -0
  63. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/core/thinking.py +0 -0
  64. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/gatekeeper/__init__.py +0 -0
  65. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/gatekeeper/daemon.py +0 -0
  66. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/gatekeeper/manager.py +0 -0
  67. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/memory/context.py +0 -0
  68. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/memory/history.py +0 -0
  69. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/memory/session.py +0 -0
  70. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/repl/__init__.py +0 -0
  71. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/security/security.py +0 -0
  72. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/ui/ui.py +0 -0
  73. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/weapon/cron.py +0 -0
  74. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/weapon/dataframe.py +0 -0
  75. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/weapon/disk.py +0 -0
  76. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/weapon/fs.py +0 -0
  77. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/weapon/launcher.py +0 -0
  78. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/weapon/loader.py +0 -0
  79. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/weapon/mail.py +0 -0
  80. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/weapon/mcp.py +0 -0
  81. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/weapon/vision.py +0 -0
  82. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli/weapon/web.py +0 -0
  83. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli.egg-info/dependency_links.txt +0 -0
  84. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli.egg-info/entry_points.txt +0 -0
  85. {fr_cli-2.2.5 → fr_cli-2.2.7}/fr_cli.egg-info/top_level.txt +0 -0
  86. {fr_cli-2.2.5 → fr_cli-2.2.7}/setup.cfg +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fr-cli
3
- Version: 2.2.5
4
- Summary: 凡人打字机 - 支持多模型(Zhipu/DeepSeek/Kimi/Qwen/StepFun/MiniMax/Spark)的终极全能终端工具
3
+ Version: 2.2.7
4
+ Summary: 凡人打字机 - 支持多模型(Zhipu/DeepSeek/Kimi/Qwen/StepFun/MiniMax/Spark/Doubao/MiMo)的终极全能终端工具
5
5
  Author: FANREN CLI Author
6
6
  License-Expression: MIT
7
7
  Project-URL: Homepage, https://github.com/yourname/fr-cli
@@ -37,7 +37,7 @@ Requires-Dist: pyodbc>=4.0.0; extra == "db"
37
37
  Requires-Dist: oracledb>=1.3.0; extra == "db"
38
38
  Provides-Extra: rag
39
39
  Requires-Dist: chromadb>=0.4.0; extra == "rag"
40
- Requires-Dist: sentence-transformers>=2.2.5; extra == "rag"
40
+ Requires-Dist: sentence-transformers>=2.2.0; extra == "rag"
41
41
  Provides-Extra: remote
42
42
  Requires-Dist: paramiko>=3.0.0; extra == "remote"
43
43
  Provides-Extra: spider
@@ -56,7 +56,7 @@ Requires-Dist: psycopg2-binary>=2.9.0; extra == "all"
56
56
  Requires-Dist: pyodbc>=4.0.0; extra == "all"
57
57
  Requires-Dist: oracledb>=1.3.0; extra == "all"
58
58
  Requires-Dist: chromadb>=0.4.0; extra == "all"
59
- Requires-Dist: sentence-transformers>=2.2.5; extra == "all"
59
+ Requires-Dist: sentence-transformers>=2.2.0; extra == "all"
60
60
  Requires-Dist: paramiko>=3.0.0; extra == "all"
61
61
  Requires-Dist: selenium>=4.10.0; extra == "all"
62
62
  Requires-Dist: bypy; extra == "all"
@@ -71,11 +71,11 @@ Dynamic: license-file
71
71
 
72
72
  # 凡人打字机 (fr-cli)
73
73
 
74
- 基于智谱 AI (ZhipuAI/GLM) 的终极全能终端工具。
74
+ 支持多模型(智谱/DeepSeek/Kimi/Qwen/StepFun/MiniMax/讯飞星火)的终极全能终端工具。
75
75
 
76
76
  ## ✨ 功能特性
77
77
 
78
- - 🤖 **AI 对话**:基于 GLM-4 系列模型的智能对话
78
+ - 🤖 **AI 对话**:支持多模型(智谱 GLM / DeepSeek / Kimi / 通义千问 / StepFun / MiniMax / 讯飞星火)
79
79
  - 🧠 **MasterAgent 主控**:自我进化的 ReAct 主控 Agent,自动规划、调用工具、反思进化
80
80
  - 🧩 **思维模式**:direct / CoT / ToT / ReAct 四种推理模式切换
81
81
  - 📁 **文件沙盒**:安全的虚拟文件系统,支持读写/目录操作
@@ -105,7 +105,7 @@ pip install fr-cli
105
105
  fr-cli
106
106
  ```
107
107
 
108
- 首次运行会引导输入智谱 API Key。
108
+ 首次运行会引导输入当前道统的 API Key。
109
109
 
110
110
  ## 📝 使用示例
111
111
 
@@ -1,10 +1,10 @@
1
1
  # 凡人打字机 (fr-cli)
2
2
 
3
- 基于智谱 AI (ZhipuAI/GLM) 的终极全能终端工具。
3
+ 支持多模型(智谱/DeepSeek/Kimi/Qwen/StepFun/MiniMax/讯飞星火)的终极全能终端工具。
4
4
 
5
5
  ## ✨ 功能特性
6
6
 
7
- - 🤖 **AI 对话**:基于 GLM-4 系列模型的智能对话
7
+ - 🤖 **AI 对话**:支持多模型(智谱 GLM / DeepSeek / Kimi / 通义千问 / StepFun / MiniMax / 讯飞星火)
8
8
  - 🧠 **MasterAgent 主控**:自我进化的 ReAct 主控 Agent,自动规划、调用工具、反思进化
9
9
  - 🧩 **思维模式**:direct / CoT / ToT / ReAct 四种推理模式切换
10
10
  - 📁 **文件沙盒**:安全的虚拟文件系统,支持读写/目录操作
@@ -34,7 +34,7 @@ pip install fr-cli
34
34
  fr-cli
35
35
  ```
36
36
 
37
- 首次运行会引导输入智谱 API Key。
37
+ 首次运行会引导输入当前道统的 API Key。
38
38
 
39
39
  ## 📝 使用示例
40
40
 
@@ -1,14 +1,14 @@
1
1
  # 凡人打字机 (fr-cli)
2
2
 
3
- 基于智谱 AI (ZhipuAI / GLM) 的终极全能终端工具。
3
+ 支持多模型(智谱/DeepSeek/Kimi/Qwen/StepFun/MiniMax/讯飞星火)的终极全能终端工具。
4
4
 
5
5
  **🇨🇳 中文简介**
6
6
 
7
- 支持:AI 智能对话、MasterAgent 自我进化主控、思维模式切换(direct/CoT/ToT/ReAct)、文件沙盒操作、联网搜索(SSRF 防护)、图片生成与识别、邮件收发、定时任务(shlex 安全解析)、云盘集成、会话记忆、按日期自动存档、插件进化(子进程隔离)、四阶安全拦截、Shell 管道直通 AI。
7
+ 支持:多模型 AI 对话(智谱/DeepSeek/Kimi/Qwen/StepFun/MiniMax/讯飞星火)、MasterAgent 自我进化主控、思维模式切换(direct/CoT/ToT/ReAct)、文件沙盒操作、联网搜索(SSRF 防护)、图片生成与识别、邮件收发、定时任务(shlex 安全解析)、云盘集成、会话记忆、按日期自动存档、插件进化(子进程隔离)、四阶安全拦截、Shell 管道直通 AI。
8
8
 
9
9
  **🇺🇸 English Intro**
10
10
 
11
- The ultimate all-knowing terminal tool based on Zhipu AI. Supports AI chat, MasterAgent self-evolving controller, thinking modes (direct/CoT/ToT/ReAct), virtual filesystem, web search (SSRF-protected), image generation & vision, email, cron jobs (shlex-safe), cloud drive, session memory, auto date-based archiving, self-evolving plugins (subprocess-isolated), and powerful Shell piping.
11
+ The ultimate all-knowing terminal tool supporting multiple LLM providers (Zhipu/DeepSeek/Kimi/Qwen/StepFun/MiniMax/Spark). Supports AI chat, MasterAgent self-evolving controller, thinking modes (direct/CoT/ToT/ReAct), virtual filesystem, web search (SSRF-protected), image generation & vision, email, cron jobs (shlex-safe), cloud drive, session memory, auto date-based archiving, self-evolving plugins (subprocess-isolated), and powerful Shell piping.
12
12
 
13
13
  ---
14
14
 
@@ -19,7 +19,7 @@ pip install fr-cli
19
19
  fr-cli
20
20
  ```
21
21
 
22
- 首次运行会引导输入智谱 API Key。
22
+ 首次运行会引导输入当前道统的 API Key。
23
23
 
24
24
  ## 🎮 使用方式
25
25
 
@@ -41,6 +41,13 @@ fr-cli
41
41
  /session_load <idx> 加载存档会话
42
42
  /mode direct|cot|tot|react 切换思维模式
43
43
  /master on|off|status MasterAgent 主控
44
+ /model <模型名> 切换当前道统模型
45
+ /model <道统>:<模型名> 同时切换道统和模型
46
+ /key <key> 修改当前道统 API Key
47
+ /key <道统> <key> 为指定道统设置 Key
48
+ /providers 查看所有道统配置
49
+ /providers add <p> <k> [m] 添加/更新道统配置
50
+ /providers use <p> 切换到指定道统
44
51
  /mcp_list 列出 MCP 服务器及工具
45
52
  /mcp_add <名> <命令> [参数] 添加 MCP 服务器
46
53
  /mcp_del <名> 删除 MCP 服务器
@@ -1,4 +1,4 @@
1
1
  """
2
2
  凡人打字机 - 基于智谱AI的终极全能终端工具
3
3
  """
4
- __version__ = "2.2.5"
4
+ __version__ = "2.2.7"
@@ -19,12 +19,17 @@ def run_agent(name, state, **kwargs):
19
19
  return None, "agent.py missing run(context, **kwargs)"
20
20
  progress = load_progress(name)
21
21
  latest = progress.get("latest", {})
22
+
23
+ # 解析 Agent 专属 LLM 配置
24
+ client, provider, model = state.resolve_agent_llm(name)
25
+
22
26
  context = {
23
27
  "persona": persona,
24
28
  "memory": memory,
25
29
  "skills": skills,
26
- "client": state.client,
27
- "model": state.model_name,
30
+ "client": client,
31
+ "provider": provider,
32
+ "model": model,
28
33
  "lang": state.lang,
29
34
  "executor": state.executor,
30
35
  "state": state,
@@ -34,11 +39,15 @@ def run_agent(name, state, **kwargs):
34
39
  "latest_status": latest.get("status", ""),
35
40
  "execution_count": progress.get("counter", 0),
36
41
  }
42
+ # 将工具调用的 LLM 上下文切换为 Agent 专属配置
43
+ state.executor.push_agent_context(client, model)
37
44
  try:
38
45
  result = mod.run(context, **kwargs)
39
46
  return result, None
40
47
  except Exception as e:
41
48
  return None, str(e)
49
+ finally:
50
+ state.executor.pop_agent_context()
42
51
 
43
52
 
44
53
  def delegate_to_agent(name, state, pipeline_input=None, **kwargs):
@@ -53,23 +62,32 @@ def delegate_to_agent(name, state, pipeline_input=None, **kwargs):
53
62
  return None, "agent.py not found or load failed"
54
63
  if not hasattr(mod, "run"):
55
64
  return None, "agent.py missing run(context, **kwargs)"
65
+
66
+ # 解析 Agent 专属 LLM 配置
67
+ client, provider, model = state.resolve_agent_llm(name)
68
+
56
69
  context = {
57
70
  "persona": persona,
58
71
  "memory": memory,
59
72
  "skills": skills,
60
- "client": state.client,
61
- "model": state.model_name,
73
+ "client": client,
74
+ "provider": provider,
75
+ "model": model,
62
76
  "lang": state.lang,
63
77
  "executor": state.executor,
64
78
  "state": state,
65
79
  "agent_name": name,
66
80
  "pipeline_input": pipeline_input,
67
81
  }
82
+ # 将工具调用的 LLM 上下文切换为 Agent 专属配置
83
+ state.executor.push_agent_context(client, model)
68
84
  try:
69
85
  result = mod.run(context, **kwargs)
70
86
  return result, None
71
87
  except Exception as e:
72
88
  return None, str(e)
89
+ finally:
90
+ state.executor.pop_agent_context()
73
91
 
74
92
 
75
93
  def run_multi_agent(names, state, initial_input=None, **kwargs):
@@ -33,7 +33,8 @@ Agent 名称: {name}
33
33
  - 'persona': str — 人设文本
34
34
  - 'memory': str — 记忆文本
35
35
  - 'skills': str — 技能文本
36
- - 'client': ZhipuAI 实例
36
+ - 'client': LLM 客户端实例(已根据 Agent 专属配置或全局默认初始化)
37
+ - 'provider': str — 当前使用的道统/提供商 ID(如 'zhipu', 'deepseek')
37
38
  - 'model': str — 模型名称
38
39
  - 'lang': str — 语言代码('zh' 或 'en')
39
40
  - 'executor': CommandExecutor 实例(可使用 invoke_tool/execute 调用工具)
@@ -12,6 +12,7 @@ MEMORY_FILE = "memory.md"
12
12
  SKILLS_FILE = "skills.md"
13
13
  AGENT_CODE_FILE = "agent.py"
14
14
  PROGRESS_FILE = "progress.json"
15
+ AGENT_CONFIG_FILE = "config.json"
15
16
 
16
17
 
17
18
  def _agent_dir(name: str) -> Path:
@@ -53,6 +54,7 @@ def list_agents() -> list:
53
54
  "has_persona": (d / PERSONA_FILE).exists(),
54
55
  "has_memory": (d / MEMORY_FILE).exists(),
55
56
  "has_skills": (d / SKILLS_FILE).exists(),
57
+ "has_config": (d / AGENT_CONFIG_FILE).exists(),
56
58
  })
57
59
  return agents
58
60
 
@@ -191,3 +193,28 @@ def get_progress_history(name: str, limit: int = 10) -> list:
191
193
  progress = load_progress(name)
192
194
  history = progress.get("history", [])
193
195
  return history[-limit:] if history else []
196
+
197
+
198
+ # ---------- Agent 专属配置读写 ----------
199
+
200
+ def load_agent_config(name: str) -> dict:
201
+ """读取 Agent 的专属模型配置(config.json)"""
202
+ f = _agent_dir(name) / AGENT_CONFIG_FILE
203
+ if not f.exists():
204
+ return {}
205
+ try:
206
+ return json.loads(f.read_text(encoding="utf-8"))
207
+ except Exception:
208
+ return {}
209
+
210
+
211
+ def save_agent_config(name: str, data: dict):
212
+ """保存 Agent 的专属模型配置到 config.json"""
213
+ d = _agent_dir(name)
214
+ # 确保 Agent 洞府存在,避免在无效目录创建孤立文件
215
+ d.mkdir(parents=True, exist_ok=True)
216
+ f = d / AGENT_CONFIG_FILE
217
+ try:
218
+ f.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
219
+ except Exception:
220
+ pass
@@ -4,6 +4,7 @@ Agent workflow engine
4
4
  """
5
5
 
6
6
  import re
7
+ from fr_cli.agent.manager import load_persona, load_memory, load_skills, save_memory
7
8
 
8
9
  WORKFLOW_FILE = "workflow.md"
9
10
 
@@ -101,64 +102,73 @@ def run_workflow(name, state, user_input=None, **kwargs):
101
102
  memory = load_memory(name)
102
103
  skills = load_skills(name)
103
104
 
105
+ # 解析 Agent 专属 LLM 配置
106
+ client, provider, model = state.resolve_agent_llm(name)
107
+
104
108
  context = {
105
109
  "persona": persona,
106
110
  "memory": memory,
107
111
  "skills": skills,
108
- "client": state.client,
109
- "model": state.model_name,
112
+ "client": client,
113
+ "provider": provider,
114
+ "model": model,
110
115
  "lang": state.lang,
111
116
  "executor": state.executor,
112
117
  "state": state,
113
118
  "agent_name": name,
114
119
  }
115
120
 
121
+ # 将工具调用的 LLM 上下文切换为 Agent 专属配置
122
+ state.executor.push_agent_context(client, model)
116
123
  step_results = []
117
- for step in steps:
118
- action = step["action"]
119
- params = {k: _substitute_vars(v, context, step_results, user_input) for k, v in step["params"].items()}
120
- result = None
121
- error = None
122
-
123
- try:
124
- if action in ("invoke_tool", "tool"):
125
- tool_name = params.pop("tool", list(params.keys())[0] if params else "")
126
- tool_params = params
127
- result, error = state.executor.invoke_tool(tool_name, tool_params)
128
- elif action in ("execute_cmd", "cmd", "command"):
129
- cmd_str = params.get("cmd", "")
130
- result, error = state.executor.execute(cmd_str)
131
- elif action in ("agent_call", "agent", "call_agent"):
132
- target = params.get("target") or params.get("agent") or params.get("to")
133
- message = params.get("message", "")
134
- result, error = run_agent(target, state, pipeline_input=message, **kwargs)
135
- elif action in ("ai_generate", "ai", "generate", "ask"):
136
- prompt = params.get("prompt", "")
137
- from fr_cli.core.stream import stream_cnt
138
- msgs = [{"role": "user", "content": prompt}]
139
- result, _, _ = stream_cnt(state.client, state.model_name, msgs, state.lang)
140
- elif action in ("save_memory", "memory_append"):
141
- mem = params.get("content", "")
142
- from fr_cli.agent.manager import save_memory, load_memory
143
- old = load_memory(name)
144
- save_memory(name, old + "\n" + mem if old else mem)
145
- result = "记忆已更新"
146
- else:
147
- error = f"未知动作: {action}"
148
- except Exception as e:
149
- error = str(e)
150
-
151
- step_results.append({
152
- "step": step["num"],
153
- "title": step["title"],
154
- "action": action,
155
- "result": result,
156
- "error": error,
157
- })
158
-
159
- if error:
160
- return None, f"步骤 {step['num']} ({step['title']}) 失败: {error}", step_results
161
-
162
- final_result = step_results[-1]["result"] if step_results else None
163
- return final_result, None, step_results
124
+ try:
125
+ for step in steps:
126
+ action = step["action"]
127
+ params = {k: _substitute_vars(v, context, step_results, user_input) for k, v in step["params"].items()}
128
+ result = None
129
+ error = None
130
+
131
+ try:
132
+ if action in ("invoke_tool", "tool"):
133
+ tool_name = params.pop("tool", list(params.keys())[0] if params else "")
134
+ tool_params = params
135
+ result, error = state.executor.invoke_tool(tool_name, tool_params)
136
+ elif action in ("execute_cmd", "cmd", "command"):
137
+ cmd_str = params.get("cmd", "")
138
+ result, error = state.executor.execute(cmd_str)
139
+ elif action in ("agent_call", "agent", "call_agent"):
140
+ target = params.get("target") or params.get("agent") or params.get("to")
141
+ message = params.get("message", "")
142
+ result, error = run_agent(target, state, pipeline_input=message, **kwargs)
143
+ elif action in ("ai_generate", "ai", "generate", "ask"):
144
+ prompt = params.get("prompt", "")
145
+ from fr_cli.core.stream import stream_cnt
146
+ msgs = [{"role": "user", "content": prompt}]
147
+ # 使用 context 中的 client/model,已根据 Agent 专属配置解析
148
+ result, _, _ = stream_cnt(context["client"], context["model"], msgs, state.lang)
149
+ elif action in ("save_memory", "memory_append"):
150
+ mem = params.get("content", "")
151
+ old = load_memory(name)
152
+ save_memory(name, old + "\n" + mem if old else mem)
153
+ result = "记忆已更新"
154
+ else:
155
+ error = f"未知动作: {action}"
156
+ except Exception as e:
157
+ error = str(e)
158
+
159
+ step_results.append({
160
+ "step": step["num"],
161
+ "title": step["title"],
162
+ "action": action,
163
+ "result": result,
164
+ "error": error,
165
+ })
166
+
167
+ if error:
168
+ return None, f"步骤 {step['num']} ({step['title']}) 失败: {error}", step_results
169
+
170
+ final_result = step_results[-1]["result"] if step_results else None
171
+ return final_result, None, step_results
172
+ finally:
173
+ state.executor.pop_agent_context()
164
174
 
@@ -10,8 +10,13 @@ from fr_cli.command.registry import get_registry
10
10
  from fr_cli.addon.plugin import exec_plugin
11
11
 
12
12
 
13
- def _build_deps(state):
14
- """根据 AppState 动态构建依赖命名空间(每次调用实时反射,避免快照过时)"""
13
+ def _build_deps(state, client=None, model_name=None):
14
+ """根据 AppState 动态构建依赖命名空间(每次调用实时反射,避免快照过时)
15
+
16
+ Args:
17
+ client: 可选的覆盖 client(如 Agent 专属模型)
18
+ model_name: 可选的覆盖模型名
19
+ """
15
20
  return SimpleNamespace(
16
21
  vfs=state.vfs,
17
22
  mail_c=state.mail_c,
@@ -21,8 +26,8 @@ def _build_deps(state):
21
26
  lang=state.lang,
22
27
  security=state.security,
23
28
  cfg=state.cfg,
24
- client=state.client,
25
- model_name=state.model_name,
29
+ client=client or state.client,
30
+ model_name=model_name or state.model_name,
26
31
  mcp=getattr(state, "mcp", None),
27
32
  )
28
33
 
@@ -41,13 +46,34 @@ class CommandExecutor:
41
46
  def __init__(self, state):
42
47
  self.state = state
43
48
  self._reg = get_registry()
49
+ # Agent 专属模型上下文覆盖(栈结构,支持嵌套 Agent 调用)
50
+ self._agent_ctx_stack = []
51
+
52
+ # ------------------------------------------------------------------
53
+ # Agent 上下文覆盖管理
54
+ # ------------------------------------------------------------------
55
+ def push_agent_context(self, client, model_name):
56
+ """临时将工具调用的 LLM 上下文切换为 Agent 专属配置"""
57
+ self._agent_ctx_stack.append((client, model_name))
58
+
59
+ def pop_agent_context(self):
60
+ """恢复工具调用的 LLM 上下文为全局默认"""
61
+ if self._agent_ctx_stack:
62
+ self._agent_ctx_stack.pop()
63
+
64
+ def _get_deps(self):
65
+ """构建依赖命名空间,优先使用 Agent 专属覆盖"""
66
+ if self._agent_ctx_stack:
67
+ client, model_name = self._agent_ctx_stack[-1]
68
+ return _build_deps(self.state, client, model_name)
69
+ return _build_deps(self.state)
44
70
 
45
71
  # ------------------------------------------------------------------
46
72
  # 第一层:结构化工具调用
47
73
  # ------------------------------------------------------------------
48
74
  def invoke_tool(self, tool_name, kwargs, msgs=None):
49
75
  """根据工具名和结构化参数,通过注册表调度执行。返回 (result, error)"""
50
- return self._reg.dispatch(_build_deps(self.state), tool_name, msgs=msgs, **kwargs)
76
+ return self._reg.dispatch(self._get_deps(), tool_name, msgs=msgs, **kwargs)
51
77
 
52
78
  # ------------------------------------------------------------------
53
79
  # 第二层:传统命令解析(用户输入 / 插件调用)
@@ -65,7 +91,7 @@ class CommandExecutor:
65
91
  exec_plugin(cmd, self.state.plugins[cmd], p_args, self.state.lang)
66
92
  return f"Plugin {cmd} executed", None
67
93
  # 其余命令通过注册表内部接口直接调度,避免 dispatch_cmd 再次 split
68
- return self._reg._dispatch_cmd_parts(_build_deps(self.state), parts, msgs=msgs)
94
+ return self._reg._dispatch_cmd_parts(self._get_deps(), parts, msgs=msgs)
69
95
 
70
96
  # ------------------------------------------------------------------
71
97
  # 第三层:AI 回复解析
@@ -11,7 +11,7 @@ from fr_cli.command.security import SecurityManager
11
11
  from fr_cli.command.executor import CommandExecutor
12
12
  from fr_cli.weapon.loader import load_weapon_md
13
13
  from fr_cli.weapon.mcp import MCPManager
14
- from fr_cli.core.llm import create_llm_client, list_providers, get_provider_info, resolve_provider_model
14
+ from fr_cli.core.llm import create_llm_client, create_llm_client_for, list_providers, get_provider_info, resolve_provider_model
15
15
 
16
16
 
17
17
  class AppState:
@@ -49,6 +49,9 @@ class AppState:
49
49
  # 自动会话存档路径(按日期编号)
50
50
  self.auto_session_path = None
51
51
 
52
+ # LLM 客户端缓存(供 Agent 专属模型复用)
53
+ self._client_cache = {}
54
+
52
55
  # 命令执行引擎
53
56
  self.executor = CommandExecutor(self)
54
57
 
@@ -146,3 +149,39 @@ class AppState:
146
149
  self.cfg["thinking_mode"] = mode
147
150
  self.thinking_mode = mode
148
151
  self.save_cfg()
152
+
153
+ def get_client_for(self, provider: str, model: str, override_key: str = None):
154
+ """
155
+ 获取指定 provider + model 的 LLM 客户端,带缓存避免重复初始化
156
+ 若提供了 override_key,则优先使用(如 Agent 专属 key)
157
+ """
158
+ cache_key = (provider, model, override_key)
159
+ cached = self._client_cache.get(cache_key)
160
+ if cached is not None:
161
+ return cached
162
+
163
+ client, _, _ = create_llm_client_for(provider, model, self.cfg, override_key)
164
+ self._client_cache[cache_key] = client
165
+ return client
166
+
167
+ def resolve_agent_llm(self, agent_name: str):
168
+ """
169
+ 解析 Agent 的 LLM 配置:优先读取 Agent 的 config.json,
170
+ 若无专属配置则回退到全局默认。
171
+
172
+ 返回: (client, provider, model)
173
+ """
174
+ from fr_cli.agent.manager import load_agent_config
175
+ agent_cfg = load_agent_config(agent_name)
176
+
177
+ provider = agent_cfg.get("provider")
178
+ model = agent_cfg.get("model")
179
+ override_key = agent_cfg.get("key") or None
180
+
181
+ # 防御性校验:provider 和 model 必须均为非空字符串才生效
182
+ if provider and model and isinstance(provider, str) and isinstance(model, str):
183
+ client = self.get_client_for(provider, model, override_key)
184
+ return client, provider, model
185
+
186
+ # 回退到全局默认
187
+ return self.client, self.provider, self.model_name
@@ -118,9 +118,43 @@ _PROVIDERS: Dict[str, Dict[str, Any]] = {
118
118
  "client_class": OpenAICompatibleClient,
119
119
  "base_url": "https://spark-api-open.xf-yun.com/v1",
120
120
  },
121
+ "doubao": {
122
+ "name": "豆包 (Doubao)",
123
+ "default_model": "doubao-1-5-pro-32k-250115",
124
+ "client_class": OpenAICompatibleClient,
125
+ "base_url": "https://ark.cn-beijing.volces.com/api/v3",
126
+ },
127
+ "mimo": {
128
+ "name": "小米 MiMo",
129
+ "default_model": "mimo-v2-flash",
130
+ "client_class": OpenAICompatibleClient,
131
+ "base_url": "https://api.xiaomimimo.com/v1",
132
+ },
121
133
  }
122
134
 
123
135
 
136
+ def _resolve_llm_kwargs(provider: str, cfg: dict, override_key: str = None):
137
+ """
138
+ 根据配置解析创建 LLM 客户端所需的参数。
139
+ 返回: (client_class, kwargs_dict)
140
+ """
141
+ providers_cfg = cfg.get("providers", {})
142
+ pcfg = providers_cfg.get(provider, {})
143
+
144
+ # 解析 key:override_key > provider 专属 > 顶层 key(zhipu 向后兼容)
145
+ api_key = override_key or pcfg.get("key") or cfg.get("key", "")
146
+
147
+ info = _PROVIDERS.get(provider, _PROVIDERS["zhipu"])
148
+ client_class = info["client_class"]
149
+ base_url = pcfg.get("base_url") or info.get("base_url")
150
+
151
+ kwargs = {"api_key": api_key}
152
+ if base_url:
153
+ kwargs["base_url"] = base_url
154
+
155
+ return client_class, kwargs
156
+
157
+
124
158
  def create_llm_client(cfg: dict):
125
159
  """
126
160
  根据配置创建对应的 LLM 客户端
@@ -133,25 +167,12 @@ def create_llm_client(cfg: dict):
133
167
  """
134
168
  provider = cfg.get("provider", "zhipu")
135
169
  providers_cfg = cfg.get("providers", {})
136
-
137
- # 获取当前提供商配置
138
170
  pcfg = providers_cfg.get(provider, {})
139
171
 
140
- # 向后兼容:如果 providers 中没有当前 provider,从顶层读取 key/model
141
- # 使用 'or' 确保空字符串也能正确回退到顶层 key
142
- api_key = pcfg.get("key") or cfg.get("key", "")
143
172
  default_model = _PROVIDERS.get(provider, _PROVIDERS["zhipu"])["default_model"]
144
173
  model = pcfg.get("model") or cfg.get("model", default_model)
145
174
 
146
- info = _PROVIDERS.get(provider, _PROVIDERS["zhipu"])
147
- client_class = info["client_class"]
148
- # 优先使用用户自定义的 base_url,其次使用内置默认
149
- base_url = pcfg.get("base_url") or info.get("base_url")
150
-
151
- kwargs = {"api_key": api_key}
152
- if base_url:
153
- kwargs["base_url"] = base_url
154
-
175
+ client_class, kwargs = _resolve_llm_kwargs(provider, cfg)
155
176
  return client_class(**kwargs), provider, model
156
177
 
157
178
 
@@ -168,6 +189,22 @@ def get_provider_info(provider_id: str):
168
189
  return _PROVIDERS.get(provider_id)
169
190
 
170
191
 
192
+ def create_llm_client_for(provider: str, model: str, cfg: dict, override_key: str = None):
193
+ """
194
+ 根据全局配置创建指定 provider + model 的 LLM 客户端
195
+
196
+ Args:
197
+ provider: 提供商 ID
198
+ model: 模型名称
199
+ cfg: 全局配置字典
200
+ override_key: 可选的覆盖 key(如 Agent 专属 key)
201
+
202
+ 返回: (client_instance, provider_id, model_name)
203
+ """
204
+ client_class, kwargs = _resolve_llm_kwargs(provider, cfg, override_key)
205
+ return client_class(**kwargs), provider, model
206
+
207
+
171
208
  def resolve_provider_model(arg: str) -> tuple:
172
209
  """
173
210
  解析用户输入的模型参数