fr-cli 2.2.6__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 (73) hide show
  1. {fr_cli-2.2.6/fr_cli.egg-info → fr_cli-2.2.7}/PKG-INFO +2 -2
  2. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/__init__.py +1 -1
  3. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/agent/executor.py +22 -4
  4. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/agent/generator.py +2 -1
  5. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/agent/manager.py +27 -0
  6. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/agent/workflow.py +59 -49
  7. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/command/executor.py +32 -6
  8. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/core/core.py +40 -1
  9. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/core/llm.py +51 -14
  10. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/lang/i18n.py +21 -5
  11. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/main.py +2 -0
  12. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/repl/commands.py +172 -15
  13. {fr_cli-2.2.6 → fr_cli-2.2.7/fr_cli.egg-info}/PKG-INFO +2 -2
  14. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli.egg-info/SOURCES.txt +3 -1
  15. {fr_cli-2.2.6 → fr_cli-2.2.7}/pyproject.toml +2 -2
  16. fr_cli-2.2.7/tests/test_integration_real.py +745 -0
  17. fr_cli-2.2.7/tests/test_model_config.py +391 -0
  18. {fr_cli-2.2.6 → fr_cli-2.2.7}/LICENSE +0 -0
  19. {fr_cli-2.2.6 → fr_cli-2.2.7}/MANIFEST.in +0 -0
  20. {fr_cli-2.2.6 → fr_cli-2.2.7}/README.md +0 -0
  21. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/README.md +0 -0
  22. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/WEAPON.MD +0 -0
  23. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/addon/plugin.py +0 -0
  24. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/agent/__init__.py +0 -0
  25. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/agent/builtins/__init__.py +0 -0
  26. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/agent/builtins/_utils.py +0 -0
  27. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/agent/builtins/db.py +0 -0
  28. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/agent/builtins/local.py +0 -0
  29. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/agent/builtins/rag.py +0 -0
  30. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/agent/builtins/rag_watcher_daemon.py +0 -0
  31. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/agent/builtins/remote.py +0 -0
  32. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/agent/builtins/spider.py +0 -0
  33. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/agent/client.py +0 -0
  34. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/agent/master.py +0 -0
  35. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/agent/master_prompt.py +0 -0
  36. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/agent/remote.py +0 -0
  37. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/agent/server.py +0 -0
  38. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/breakthrough/update.py +0 -0
  39. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/command/__init__.py +0 -0
  40. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/command/registry.py +0 -0
  41. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/command/security.py +0 -0
  42. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/conf/config.py +0 -0
  43. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/conf/wizard.py +0 -0
  44. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/core/chat.py +0 -0
  45. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/core/intent.py +0 -0
  46. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/core/recommender.py +0 -0
  47. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/core/stream.py +0 -0
  48. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/core/sysmon.py +0 -0
  49. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/core/thinking.py +0 -0
  50. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/gatekeeper/__init__.py +0 -0
  51. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/gatekeeper/daemon.py +0 -0
  52. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/gatekeeper/manager.py +0 -0
  53. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/memory/context.py +0 -0
  54. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/memory/history.py +0 -0
  55. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/memory/session.py +0 -0
  56. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/repl/__init__.py +0 -0
  57. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/security/security.py +0 -0
  58. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/ui/ui.py +0 -0
  59. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/weapon/cron.py +0 -0
  60. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/weapon/dataframe.py +0 -0
  61. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/weapon/disk.py +0 -0
  62. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/weapon/fs.py +0 -0
  63. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/weapon/launcher.py +0 -0
  64. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/weapon/loader.py +0 -0
  65. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/weapon/mail.py +0 -0
  66. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/weapon/mcp.py +0 -0
  67. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/weapon/vision.py +0 -0
  68. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/weapon/web.py +0 -0
  69. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli.egg-info/dependency_links.txt +0 -0
  70. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli.egg-info/entry_points.txt +0 -0
  71. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli.egg-info/requires.txt +0 -0
  72. {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli.egg-info/top_level.txt +0 -0
  73. {fr_cli-2.2.6 → 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.6
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
@@ -1,4 +1,4 @@
1
1
  """
2
2
  凡人打字机 - 基于智谱AI的终极全能终端工具
3
3
  """
4
- __version__ = "2.2.6"
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
  解析用户输入的模型参数
@@ -28,7 +28,7 @@ I18N = {
28
28
  "sec_title": "⚠️ 检测到高危神通,请选择因果:", "sec_opt_y": "[Y]仅此", "sec_opt_a": "[A]本轮", "sec_opt_f": "[F]永世", "sec_opt_n": "[N]拒绝", "sec_denied": "🛑 终止。",
29
29
  "sec_read": "读取卷轴", "sec_write": "写入法宝", "sec_exec": "执行法宝", "sec_mount": "开辟洞府", "sec_gen_img": "祭炼画卷", "sec_send_mail": "发送邮件", "sec_fetch_web": "抓取互联网", "sec_upload_disk": "上传至云端", "sec_download_disk": "下载自云端", "sec_shell": "执行系统命令",
30
30
  "gen_ing": "🎨 祭炼…", "gen_ok": "✅ 画卷成: {}", "gen_fail": "❌ 破碎: ", "see_warn": "⚠️ 需法器 glm-4v-plus", "see_ing": "👁️ 天眼…",
31
- "help_title": "📜 修仙指南:", "help_cfg": "【配置】", "help_fs": "【洞府】", "help_sess": "【轮回】", "help_plugin": "【法宝】", "help_extra": "【神通】", "help_shell": "【破壁】",
31
+ "help_title": "📜 修仙指南:", "help_cfg": "【天道】", "help_fs": "【洞府】", "help_sess": "【轮回】", "help_plugin": "【法宝】", "help_extra": "【神通】", "help_shell": "【破界】",
32
32
  "help_usage": "💡 用法: /help [主题] 可用主题: config, fs, session, plugin, mail, cron, web, disk, vision, shell, tools, security, app, agent, builtin, dataframe, gatekeeper, mcp, all",
33
33
  "help_not_found": "❌ 未知主题: {} 可用: config, fs, session, plugin, mail, cron, web, disk, vision, shell, tools, security, app, agent, builtin, dataframe, gatekeeper, mcp, all",
34
34
  "empty": "空空如也…", "none": "无", "no_sess": "无记忆。", "no_plugins": "无技能。",
@@ -42,8 +42,8 @@ I18N = {
42
42
  "web_err": "❌ 迷路:", "web_no_res": "无果。", "web_title": "📜 搜魂:",
43
43
  "disk_setup": "/disk_setup", "disk_ls": "/disk_ls <盘>", "disk_up": "/disk_up <盘> <路>", "disk_down": "/disk_down <盘> <云> [本]",
44
44
  "disk_ok_up": "✅ 飞升: {}", "disk_ok_down": "✅ 降落: {}", "disk_err": "❌ 御剑: ", "disk_no_cfg": "❌ 未配盘", "disk_miss_dep": "❌ 缺库: {} (pip install {})",
45
- "shell_tip": "!命令 执行本地Shell(如 !ls)",
46
- "pipe_tip": "!命令 | 提示 管道喂给AI(如 !ps aux | 找出占用CPU最高的进程)",
45
+ "shell_tip": "!命令 调用天地法则(如 !ls)",
46
+ "pipe_tip": "!命令 | 提示 周天推演喂给仙人(如 !ps aux | 找出占用灵力最高的修士)",
47
47
  "pipe_prefix": "[系统管道数据]:\n",
48
48
  "artifact_detect": "⚡ 检测到法宝结构,赐名 (回车放弃): ",
49
49
  "recommend_title": "💡 推荐功能:",
@@ -72,7 +72,7 @@ I18N = {
72
72
  # ---- 详细帮助文本 ----
73
73
  "help_detail_config": """📜 【配置】
74
74
 
75
- /model <name> 切换AI模型 (glm-4-flash, glm-4-plus, glm-4v-plus)
75
+ /model <name> 切换AI模型 (glm-4-flash, deepseek-chat, moonshot-v1-8k, doubao-1-5-pro-32k-250115, mimo-v2-flash 等)
76
76
  /key <key> 修改智谱AI API Key
77
77
  /limit <n> 设置Token上限 (最小1000)
78
78
  /lang <zh/en> 切换界面语言
@@ -305,6 +305,8 @@ AI自动输出调用标记, 程序解析并执行:
305
305
  /agent_edit <名称> <类型> 编辑 Agent 设定(persona/memory/skills/agent/workflow)
306
306
  /agent_run <名称> [参数] 运行指定 Agent
307
307
  /agent_delete <名称> 删除 Agent
308
+ /agent_model <名称> [provider:model|clear|--key <key>]
309
+ 查看/设置 Agent 专属模型(独立 config.json 持久化)
308
310
 
309
311
  Agent 目录: ~/.fr_cli_agents/<名称>/
310
312
  • persona.md — 角色设定
@@ -312,6 +314,12 @@ Agent 目录: ~/.fr_cli_agents/<名称>/
312
314
  • skills.md — 技能说明
313
315
  • agent.py — 可选自定义执行逻辑(必须包含 run(context, **kwargs))
314
316
  • workflow.md — 可选工作流定义
317
+ • config.json — 专属模型配置(provider / model / key,可选)
318
+
319
+ 模型绑定示例:
320
+ /agent_model my_agent deepseek:deepseek-chat — 绑定专属模型
321
+ /agent_model my_agent --key sk-own-key — 设置独立 API Key
322
+ /agent_model my_agent clear — 清除专属配置,恢复全局默认
315
323
 
316
324
  将已有代码转为 Agent 的方法:
317
325
  1. 在对话中让 AI 生成包含 def run(context, **kwargs) 的代码
@@ -496,7 +504,7 @@ Example:
496
504
  "rec_pipe": "Pipe command output to AI",
497
505
  "help_detail_config": """📜 [Config]
498
506
 
499
- /model <name> Switch AI model (glm-4-flash, glm-4-plus, glm-4v-plus)
507
+ /model <name> Switch AI model (glm-4-flash, deepseek-chat, moonshot-v1-8k, doubao-1-5-pro-32k-250115, mimo-v2-flash, etc.)
500
508
  /key <key> Change ZhipuAI API Key
501
509
  /limit <n> Set token limit (min 1000)
502
510
  /lang <zh/en> Switch UI language
@@ -729,6 +737,8 @@ Common app aliases:
729
737
  /agent_edit <name> <type> Edit Agent settings (persona/memory/skills/agent/workflow)
730
738
  /agent_run <name> [args] Run specified Agent
731
739
  /agent_delete <name> Delete Agent
740
+ /agent_model <name> [provider:model|clear|--key <key>]
741
+ View/set Agent-specific model (persisted in config.json)
732
742
 
733
743
  Agent directory: ~/.fr_cli_agents/<name>/
734
744
  • persona.md — Character setting
@@ -736,6 +746,12 @@ Agent directory: ~/.fr_cli_agents/<name>/
736
746
  • skills.md — Skill descriptions
737
747
  • agent.py — Optional custom execution logic (must contain run(context, **kwargs))
738
748
  • workflow.md — Optional workflow definition
749
+ • config.json — Model binding config (provider / model / key, optional)
750
+
751
+ Model binding examples:
752
+ /agent_model my_agent deepseek:deepseek-chat — Bind exclusive model
753
+ /agent_model my_agent --key sk-own-key — Set independent API Key
754
+ /agent_model my_agent clear — Clear config, fallback to global default
739
755
 
740
756
  How to turn existing code into an Agent:
741
757
  1. Ask AI to generate code containing def run(context, **kwargs)
@@ -71,6 +71,7 @@ from fr_cli.repl.commands import (
71
71
  _cmd_agent_run,
72
72
  _cmd_agent_edit,
73
73
  _cmd_agent_forge,
74
+ _cmd_agent_model,
74
75
  _cmd_remote_agent_add,
75
76
  _cmd_remote_agent_list,
76
77
  _cmd_remote_agent_del,
@@ -130,6 +131,7 @@ _COMMAND_ROUTES = {
130
131
  "/agent_run": _cmd_agent_run,
131
132
  "/agent_edit": _cmd_agent_edit,
132
133
  "/agent_forge": _cmd_agent_forge,
134
+ "/agent_model": _cmd_agent_model,
133
135
  "/remote_agent_add": _cmd_remote_agent_add,
134
136
  "/remote_agent_list": _cmd_remote_agent_list,
135
137
  "/remote_agent_del": _cmd_remote_agent_del,