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.
- {fr_cli-2.2.6/fr_cli.egg-info → fr_cli-2.2.7}/PKG-INFO +2 -2
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/__init__.py +1 -1
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/agent/executor.py +22 -4
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/agent/generator.py +2 -1
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/agent/manager.py +27 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/agent/workflow.py +59 -49
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/command/executor.py +32 -6
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/core/core.py +40 -1
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/core/llm.py +51 -14
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/lang/i18n.py +21 -5
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/main.py +2 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/repl/commands.py +172 -15
- {fr_cli-2.2.6 → fr_cli-2.2.7/fr_cli.egg-info}/PKG-INFO +2 -2
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli.egg-info/SOURCES.txt +3 -1
- {fr_cli-2.2.6 → fr_cli-2.2.7}/pyproject.toml +2 -2
- fr_cli-2.2.7/tests/test_integration_real.py +745 -0
- fr_cli-2.2.7/tests/test_model_config.py +391 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/LICENSE +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/MANIFEST.in +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/README.md +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/README.md +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/WEAPON.MD +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/addon/plugin.py +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/agent/__init__.py +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/agent/builtins/__init__.py +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/agent/builtins/_utils.py +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/agent/builtins/db.py +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/agent/builtins/local.py +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/agent/builtins/rag.py +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/agent/builtins/rag_watcher_daemon.py +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/agent/builtins/remote.py +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/agent/builtins/spider.py +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/agent/client.py +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/agent/master.py +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/agent/master_prompt.py +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/agent/remote.py +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/agent/server.py +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/breakthrough/update.py +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/command/__init__.py +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/command/registry.py +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/command/security.py +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/conf/config.py +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/conf/wizard.py +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/core/chat.py +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/core/intent.py +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/core/recommender.py +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/core/stream.py +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/core/sysmon.py +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/core/thinking.py +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/gatekeeper/__init__.py +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/gatekeeper/daemon.py +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/gatekeeper/manager.py +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/memory/context.py +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/memory/history.py +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/memory/session.py +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/repl/__init__.py +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/security/security.py +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/ui/ui.py +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/weapon/cron.py +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/weapon/dataframe.py +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/weapon/disk.py +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/weapon/fs.py +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/weapon/launcher.py +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/weapon/loader.py +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/weapon/mail.py +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/weapon/mcp.py +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/weapon/vision.py +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli/weapon/web.py +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli.egg-info/dependency_links.txt +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli.egg-info/entry_points.txt +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli.egg-info/requires.txt +0 -0
- {fr_cli-2.2.6 → fr_cli-2.2.7}/fr_cli.egg-info/top_level.txt +0 -0
- {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.
|
|
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
|
|
@@ -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":
|
|
27
|
-
"
|
|
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":
|
|
61
|
-
"
|
|
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':
|
|
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":
|
|
109
|
-
"
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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": "
|
|
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": "!命令
|
|
46
|
-
"pipe_tip": "!命令 | 提示
|
|
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,
|
|
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,
|
|
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,
|