fr-cli 2.1.0__py3-none-any.whl

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 (64) hide show
  1. fr_cli/README.md +148 -0
  2. fr_cli/WEAPON.MD +186 -0
  3. fr_cli/__init__.py +4 -0
  4. fr_cli/addon/plugin.py +69 -0
  5. fr_cli/agent/__init__.py +9 -0
  6. fr_cli/agent/builtins/__init__.py +4 -0
  7. fr_cli/agent/builtins/_utils.py +48 -0
  8. fr_cli/agent/builtins/db.py +269 -0
  9. fr_cli/agent/builtins/local.py +105 -0
  10. fr_cli/agent/builtins/rag.py +652 -0
  11. fr_cli/agent/builtins/rag_watcher_daemon.py +156 -0
  12. fr_cli/agent/builtins/remote.py +214 -0
  13. fr_cli/agent/builtins/spider.py +247 -0
  14. fr_cli/agent/client.py +164 -0
  15. fr_cli/agent/executor.py +86 -0
  16. fr_cli/agent/generator.py +104 -0
  17. fr_cli/agent/manager.py +193 -0
  18. fr_cli/agent/master.py +604 -0
  19. fr_cli/agent/master_prompt.py +118 -0
  20. fr_cli/agent/remote.py +70 -0
  21. fr_cli/agent/server.py +279 -0
  22. fr_cli/agent/workflow.py +164 -0
  23. fr_cli/breakthrough/update.py +154 -0
  24. fr_cli/command/__init__.py +4 -0
  25. fr_cli/command/executor.py +276 -0
  26. fr_cli/command/registry.py +1034 -0
  27. fr_cli/command/security.py +30 -0
  28. fr_cli/conf/config.py +126 -0
  29. fr_cli/conf/wizard.py +172 -0
  30. fr_cli/core/chat.py +280 -0
  31. fr_cli/core/core.py +111 -0
  32. fr_cli/core/intent.py +129 -0
  33. fr_cli/core/recommender.py +71 -0
  34. fr_cli/core/stream.py +83 -0
  35. fr_cli/core/sysmon.py +117 -0
  36. fr_cli/core/thinking.py +215 -0
  37. fr_cli/gatekeeper/__init__.py +7 -0
  38. fr_cli/gatekeeper/daemon.py +216 -0
  39. fr_cli/gatekeeper/manager.py +218 -0
  40. fr_cli/lang/i18n.py +827 -0
  41. fr_cli/main.py +329 -0
  42. fr_cli/memory/context.py +119 -0
  43. fr_cli/memory/history.py +96 -0
  44. fr_cli/memory/session.py +134 -0
  45. fr_cli/repl/__init__.py +0 -0
  46. fr_cli/repl/commands.py +1098 -0
  47. fr_cli/security/security.py +46 -0
  48. fr_cli/ui/ui.py +116 -0
  49. fr_cli/weapon/cron.py +217 -0
  50. fr_cli/weapon/dataframe.py +97 -0
  51. fr_cli/weapon/disk.py +141 -0
  52. fr_cli/weapon/fs.py +206 -0
  53. fr_cli/weapon/launcher.py +249 -0
  54. fr_cli/weapon/loader.py +98 -0
  55. fr_cli/weapon/mail.py +227 -0
  56. fr_cli/weapon/mcp.py +204 -0
  57. fr_cli/weapon/vision.py +74 -0
  58. fr_cli/weapon/web.py +88 -0
  59. fr_cli-2.1.0.dist-info/METADATA +227 -0
  60. fr_cli-2.1.0.dist-info/RECORD +64 -0
  61. fr_cli-2.1.0.dist-info/WHEEL +5 -0
  62. fr_cli-2.1.0.dist-info/entry_points.txt +2 -0
  63. fr_cli-2.1.0.dist-info/licenses/LICENSE +21 -0
  64. fr_cli-2.1.0.dist-info/top_level.txt +1 -0
fr_cli/weapon/fs.py ADDED
@@ -0,0 +1,206 @@
1
+ """
2
+ 虚拟文件系统 (VFS) - 安全沙盒引擎
3
+ 限制AI和用户只能在允许的目录内操作
4
+ """
5
+ import os
6
+ from pathlib import Path
7
+ from fr_cli.lang.i18n import T
8
+ from fr_cli.ui.ui import GREEN, RED, CYAN, RESET
9
+
10
+ class VFS:
11
+ def __init__(self, allowed_dirs):
12
+ self.ds = [str(Path(d).resolve()) for d in allowed_dirs]
13
+ self.cwd = self.ds[0] if self.ds else None
14
+
15
+ def _resolve(self, p):
16
+ """安全解析路径,防止../逃逸"""
17
+ if not self.cwd: return None
18
+ base = Path(self.cwd)
19
+ target = (base / p).resolve()
20
+ # 检查解析后的路径是否仍在允许的目录树内
21
+ for d in self.ds:
22
+ base_path = Path(d).resolve()
23
+ try:
24
+ # 使用 relative_to 精确判断是否为目标目录的子路径
25
+ # 可正确处理根目录(/)及避免 /foo 错误匹配 /foo-bar 的前缀问题
26
+ target.relative_to(base_path)
27
+ return target
28
+ except ValueError:
29
+ continue
30
+ return None
31
+
32
+ def add(self, p, l):
33
+ try:
34
+ rp = str(Path(p).resolve())
35
+ if not os.path.isdir(rp): return False, f"{RED}{T('err_dir_no', l)}{RESET}"
36
+ if rp not in self.ds:
37
+ self.ds.append(rp)
38
+ if not self.cwd: self.cwd = rp
39
+ return True, f"{GREEN}{T('ok_dir_add', l, rp)}{RESET}"
40
+ except Exception as e: return False, f"{RED}{e}{RESET}"
41
+
42
+ def cd(self, p, l):
43
+ if not p: return True, f"{GREEN}{self.cwd}{RESET}"
44
+ # 支持直接切换到已挂载的根目录
45
+ for d in self.ds:
46
+ if Path(p).resolve() == Path(d).resolve():
47
+ self.cwd = d; return True, f"{GREEN}{T('ok_cd', l, self.cwd)}{RESET}"
48
+
49
+ target = self._resolve(p)
50
+ if not target: return False, f"{RED}{T('err_bound', l)}{RESET}"
51
+ if not target.is_dir(): return False, f"{RED}{T('err_no_file', l)}{RESET}"
52
+ self.cwd = str(target)
53
+ return True, f"{GREEN}{T('ok_cd', l, self.cwd)}{RESET}"
54
+
55
+ def ls(self, l):
56
+ if not self.cwd: return None, f"{RED}{T('no_dir', l)}{RESET}"
57
+ try:
58
+ p = Path(self.cwd)
59
+ items = []
60
+ for f in p.iterdir():
61
+ if f.name.startswith('.'): continue
62
+ items.append(f"{CYAN}{f.name}/" if f.is_dir() else f.name)
63
+ return sorted(items), None
64
+ except Exception as e: return None, f"{RED}{e}{RESET}"
65
+
66
+ def read(self, fn, l):
67
+ target = self._resolve(fn)
68
+ if not target: return None, f"{RED}{T('err_bound', l)}{RESET}"
69
+ if not target.is_file(): return None, f"{RED}{T('err_no_file', l)}{RESET}"
70
+ try:
71
+ # 尝试多种编码读取
72
+ for enc in ['utf-8', 'gbk', 'latin-1']:
73
+ try: return target.read_text(encoding=enc), None
74
+ except UnicodeDecodeError: continue
75
+ return None, f"{RED}Decode fail{RESET}"
76
+ except Exception as e: return None, f"{RED}{e}{RESET}"
77
+
78
+ def write(self, fn, content, l, mode='w', encoding='utf-8'):
79
+ """安全写入文件
80
+
81
+ Args:
82
+ fn: 文件名
83
+ content: 文件内容
84
+ l: 语言
85
+ mode: 写入模式 ('w'=覆盖, 'a'=追加)
86
+ encoding: 文件编码
87
+
88
+ Returns:
89
+ (success, message)
90
+ """
91
+ target = self._resolve(fn)
92
+ if not target: return False, f"{RED}{T('err_bound', l)}{RESET}"
93
+
94
+ try:
95
+ # 确保父目录存在(覆盖和追加模式都需要)
96
+ parent = target.parent
97
+ if not parent.exists():
98
+ parent.mkdir(parents=True, exist_ok=True)
99
+
100
+ # 写入文件
101
+ with open(target, mode, encoding=encoding) as f:
102
+ f.write(content)
103
+
104
+ return True, f"{GREEN}{T('ok_write', l, str(target))}{RESET}"
105
+ except PermissionError:
106
+ return False, f"{RED}{T('err_write_perm', l)}{RESET}"
107
+ except Exception as e:
108
+ return False, f"{RED}{e}{RESET}"
109
+
110
+ def append(self, fn, content, l, encoding='utf-8'):
111
+ """追加内容到文件
112
+
113
+ Args:
114
+ fn: 文件名
115
+ content: 要追加的内容
116
+ l: 语言
117
+ encoding: 文件编码
118
+
119
+ Returns:
120
+ (success, message)
121
+ """
122
+ return self.write(fn, content, l, mode='a', encoding=encoding)
123
+
124
+ def exists(self, fn):
125
+ """检查文件是否存在
126
+
127
+ Args:
128
+ fn: 文件名
129
+
130
+ Returns:
131
+ bool: 文件是否存在
132
+ """
133
+ target = self._resolve(fn)
134
+ return target is not None and target.exists()
135
+
136
+ def delete(self, fn, l):
137
+ """删除文件
138
+
139
+ Args:
140
+ fn: 文件名
141
+ l: 语言
142
+
143
+ Returns:
144
+ (success, message)
145
+ """
146
+ target = self._resolve(fn)
147
+ if not target: return False, f"{RED}{T('err_bound', l)}{RESET}"
148
+ if not target.exists(): return False, f"{RED}{T('err_no_file', l)}{RESET}"
149
+
150
+ try:
151
+ target.unlink()
152
+ return True, f"{GREEN}{T('ok_delete', l, str(target))}{RESET}"
153
+ except PermissionError:
154
+ return False, f"{RED}{T('err_write_perm', l)}{RESET}"
155
+ except Exception as e:
156
+ return False, f"{RED}{e}{RESET}"
157
+
158
+ def list_dirs(self, l):
159
+ """列出所有已挂载的工作目录(洞府)
160
+
161
+ Returns:
162
+ (列表, None) 或 (None, 错误信息)
163
+ """
164
+ if not self.ds:
165
+ return None, f"{RED}{T('no_dir', l)}{RESET}"
166
+ items = []
167
+ for i, d in enumerate(self.ds):
168
+ marker = f" {GREEN}[{T('cur_dir', l)}]{RESET}" if d == self.cwd else ""
169
+ items.append(f" [{i}] {CYAN}{d}{RESET}{marker}")
170
+ return items, None
171
+
172
+ def remove_dir(self, p, l):
173
+ """从允许列表中移除指定工作目录
174
+
175
+ 支持按索引或绝对/相对路径删除。
176
+ 若移除的是当前 cwd,自动切换到剩余目录中的第一个。
177
+
178
+ Args:
179
+ p: 索引字符串或路径
180
+ l: 语言
181
+
182
+ Returns:
183
+ (success, message)
184
+ """
185
+ if not self.ds:
186
+ return False, f"{RED}{T('no_dir', l)}{RESET}"
187
+
188
+ # 尝试按索引解析
189
+ try:
190
+ idx = int(p)
191
+ if idx < 0 or idx >= len(self.ds):
192
+ return False, f"{RED}{T('err_dir_idx', l)}{RESET}"
193
+ removed = self.ds.pop(idx)
194
+ except ValueError:
195
+ # 按路径解析
196
+ rp = str(Path(p).resolve())
197
+ if rp not in self.ds:
198
+ return False, f"{RED}{T('err_dir_not_mounted', l, rp)}{RESET}"
199
+ self.ds.remove(rp)
200
+ removed = rp
201
+
202
+ # 若删除的是当前 cwd,自动切换
203
+ if self.cwd == removed:
204
+ self.cwd = self.ds[0] if self.ds else None
205
+
206
+ return True, f"{GREEN}{T('ok_dir_remove', l, removed)}{RESET}"
@@ -0,0 +1,249 @@
1
+ """
2
+ 本地应用启动器 —— 驭器神通
3
+ 支持跨平台调用浏览器、办公软件、通讯工具等本机程序。
4
+ """
5
+ import platform
6
+ import subprocess
7
+
8
+ SYSTEM = platform.system()
9
+
10
+ # 常用应用别名映射表 —— 按平台映射到可执行命令或应用包名
11
+ _APP_ALIASES = {
12
+ "Darwin": {
13
+ # 浏览器
14
+ "chrome": "Google Chrome",
15
+ "googlechrome": "Google Chrome",
16
+ "safari": "Safari",
17
+ "firefox": "Firefox",
18
+ "edge": "Microsoft Edge",
19
+ "browser": "Safari",
20
+ "浏览器": "Safari",
21
+ # 办公
22
+ "word": "Microsoft Word",
23
+ "msword": "Microsoft Word",
24
+ "excel": "Microsoft Excel",
25
+ "msexcel": "Microsoft Excel",
26
+ "powerpoint": "Microsoft PowerPoint",
27
+ "ppt": "Microsoft PowerPoint",
28
+ "mspowerpoint": "Microsoft PowerPoint",
29
+ "wps": "WPS Office",
30
+ # 通讯
31
+ "wechat": "WeChat",
32
+ "微信": "WeChat",
33
+ "qq": "QQ",
34
+ "tim": "Tencent TIM",
35
+ "dingtalk": "DingTalk",
36
+ "钉钉": "DingTalk",
37
+ "飞书": "Lark",
38
+ "lark": "Lark",
39
+ # 编辑器 / 工具
40
+ "notepad": "TextEdit",
41
+ "textedit": "TextEdit",
42
+ "记事本": "TextEdit",
43
+ "notes": "Notes",
44
+ "备忘录": "Notes",
45
+ "vscode": "Visual Studio Code",
46
+ "code": "Visual Studio Code",
47
+ "terminal": "Terminal",
48
+ "iterm": "iTerm",
49
+ "终端": "Terminal",
50
+ "计算器": "Calculator",
51
+ "calc": "Calculator",
52
+ "calculator": "Calculator",
53
+ # 媒体
54
+ "music": "Music",
55
+ "itunes": "Music",
56
+ "播放器": "Music",
57
+ "spotify": "Spotify",
58
+ "vlc": "VLC",
59
+ "quicktime": "QuickTime Player",
60
+ "photos": "Photos",
61
+ "相册": "Photos",
62
+ # 系统
63
+ "finder": "Finder",
64
+ "访达": "Finder",
65
+ "systempreferences": "System Preferences",
66
+ "系统偏好设置": "System Preferences",
67
+ "appstore": "App Store",
68
+ "app store": "App Store",
69
+ "地图": "Maps",
70
+ "maps": "Maps",
71
+ },
72
+ "Windows": {
73
+ # 浏览器
74
+ "chrome": "chrome",
75
+ "googlechrome": "chrome",
76
+ "edge": "msedge",
77
+ "firefox": "firefox",
78
+ "browser": "start microsoft-edge:",
79
+ "浏览器": "start microsoft-edge:",
80
+ # 办公
81
+ "word": "winword",
82
+ "msword": "winword",
83
+ "excel": "excel",
84
+ "msexcel": "excel",
85
+ "powerpoint": "powerpnt",
86
+ "ppt": "powerpnt",
87
+ "mspowerpoint": "powerpnt",
88
+ "wps": "wps",
89
+ # 通讯
90
+ "wechat": "WeChat",
91
+ "微信": "WeChat",
92
+ "qq": "QQ",
93
+ "tim": "TIM",
94
+ "dingtalk": "DingTalk",
95
+ "钉钉": "DingTalk",
96
+ "飞书": "Lark",
97
+ "lark": "Lark",
98
+ # 编辑器 / 工具
99
+ "notepad": "notepad",
100
+ "记事本": "notepad",
101
+ "vscode": "code",
102
+ "code": "code",
103
+ "terminal": "wt",
104
+ "终端": "wt",
105
+ "计算器": "calc",
106
+ "calc": "calc",
107
+ "calculator": "calc",
108
+ # 媒体
109
+ "music": "mswindowsmusic:",
110
+ "itunes": "itunes",
111
+ "播放器": "mswindowsmusic:",
112
+ "spotify": "spotify",
113
+ "vlc": "vlc",
114
+ "photos": "ms-photos:",
115
+ "相册": "ms-photos:",
116
+ # 系统
117
+ "explorer": "explorer",
118
+ "文件资源管理器": "explorer",
119
+ "settings": "ms-settings:",
120
+ "系统设置": "ms-settings:",
121
+ "appstore": "ms-windows-store:",
122
+ "app store": "ms-windows-store:",
123
+ },
124
+ "Linux": {
125
+ # 浏览器
126
+ "chrome": "google-chrome",
127
+ "googlechrome": "google-chrome",
128
+ "chromium": "chromium-browser",
129
+ "firefox": "firefox",
130
+ "edge": "microsoft-edge",
131
+ "browser": "xdg-open",
132
+ "浏览器": "xdg-open",
133
+ # 办公
134
+ "word": "libreoffice --writer",
135
+ "excel": "libreoffice --calc",
136
+ "powerpoint": "libreoffice --impress",
137
+ "ppt": "libreoffice --impress",
138
+ "wps": "wps",
139
+ # 通讯
140
+ "wechat": "wechat",
141
+ "微信": "wechat",
142
+ "qq": "qq",
143
+ "lark": "lark",
144
+ "飞书": "lark",
145
+ # 编辑器 / 工具
146
+ "notepad": "gedit",
147
+ "记事本": "gedit",
148
+ "vscode": "code",
149
+ "code": "code",
150
+ "terminal": "gnome-terminal",
151
+ "终端": "gnome-terminal",
152
+ "计算器": "gnome-calculator",
153
+ "calc": "gnome-calculator",
154
+ "calculator": "gnome-calculator",
155
+ # 媒体
156
+ "vlc": "vlc",
157
+ "播放器": "vlc",
158
+ "spotify": "spotify",
159
+ # 系统
160
+ "files": "nautilus",
161
+ "文件管理器": "nautilus",
162
+ "settings": "gnome-control-center",
163
+ "系统设置": "gnome-control-center",
164
+ },
165
+ }
166
+
167
+
168
+ def _resolve_app(name):
169
+ """将别名解析为平台对应的应用标识"""
170
+ key = name.lower().strip()
171
+ plat = SYSTEM
172
+ aliases = _APP_ALIASES.get(plat, {})
173
+ return aliases.get(key, name)
174
+
175
+
176
+ def open_file(path, lang="zh"):
177
+ """用系统默认程序打开文件或 URL(跨平台)"""
178
+ if not path:
179
+ return False, "路径为空" if lang == "zh" else "Empty path"
180
+ try:
181
+ if SYSTEM == "Darwin":
182
+ subprocess.Popen(["open", path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
183
+ elif SYSTEM == "Windows":
184
+ subprocess.Popen(["start", "", path], shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
185
+ else:
186
+ subprocess.Popen(["xdg-open", path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
187
+ return True, f"已打开: {path}" if lang == "zh" else f"Opened: {path}"
188
+ except Exception as e:
189
+ return False, str(e)
190
+
191
+
192
+ def launch_app(name, target=None, lang="zh"):
193
+ """启动指定应用程序,可选传入文件/URL 作为目标"""
194
+ if not name:
195
+ return False, "应用名称为空" if lang == "zh" else "Empty app name"
196
+
197
+ app = _resolve_app(name)
198
+
199
+ try:
200
+ if SYSTEM == "Darwin":
201
+ cmd = ["open", "-a", app]
202
+ if target:
203
+ cmd.append(target)
204
+ subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
205
+ elif SYSTEM == "Windows":
206
+ # Windows 某些应用支持直接传目标,某些用 start
207
+ if target:
208
+ # 如果是 URL,直接用 start;如果是文件,用应用打开
209
+ if target.startswith("http"):
210
+ subprocess.Popen(["start", "", target], shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
211
+ else:
212
+ subprocess.Popen(["start", "", "", app, target], shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
213
+ else:
214
+ # 某些 Windows 命令本身就是 URI 协议或 shell 命令
215
+ if app.endswith(":") or " " in app:
216
+ subprocess.Popen([app], shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
217
+ else:
218
+ subprocess.Popen(["start", "", app], shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
219
+ else:
220
+ # Linux
221
+ parts = app.split()
222
+ cmd = parts.copy()
223
+ if target:
224
+ cmd.append(target)
225
+ subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
226
+
227
+ msg = f"已启动: {name}" if lang == "zh" else f"Launched: {name}"
228
+ if target:
229
+ msg += f" ({target})"
230
+ return True, msg
231
+ except Exception as e:
232
+ return False, str(e)
233
+
234
+
235
+ def list_apps(lang="zh"):
236
+ """列出当前平台支持的应用别名"""
237
+ plat = SYSTEM
238
+ aliases = _APP_ALIASES.get(plat, {})
239
+ if not aliases:
240
+ return None, "暂无预置应用列表" if lang == "zh" else "No app list available"
241
+
242
+ items = []
243
+ seen = set()
244
+ for alias, real in sorted(aliases.items()):
245
+ if real not in seen:
246
+ seen.add(real)
247
+ items.append(f" {real:<30} ← {alias}")
248
+ header = "本机可用应用映射:" if lang == "zh" else "Available app mappings:"
249
+ return header + "\n" + "\n".join(items), None
@@ -0,0 +1,98 @@
1
+ """
2
+ 法宝图谱加载器
3
+ 从统一注册表获取工具信息,替代 WEAPON.MD 的文本解析。
4
+ 保留 WEAPON.MD 作为人类可读文档,程序逻辑不再依赖其解析。
5
+ """
6
+ from fr_cli.command.registry import get_registry
7
+
8
+
9
+ # 旧类别到注册表工具名的映射(用于兼容旧接口)
10
+ _LEGACY_CATEGORIES = {
11
+ "file_operations": (["write_file", "read_file", "list_files", "change_dir", "append_file", "delete_file"], "fr_cli/weapon/fs.py"),
12
+ "image_analysis": (["analyze_image", "generate_image"], "fr_cli/weapon/vision.py"),
13
+ "email_management": (["mail_inbox", "mail_read", "mail_send"], "fr_cli/weapon/mail.py"),
14
+ "web_search": (["search_web", "fetch_web"], "fr_cli/weapon/web.py"),
15
+ "scheduled_tasks": (["cron_add", "cron_list", "cron_del"], "fr_cli/weapon/cron.py"),
16
+ "cloud_storage": (["disk_ls", "disk_up", "disk_down"], "fr_cli/weapon/disk.py"),
17
+ "session_management": (["save_session", "list_sessions", "export_session"], "fr_cli/memory/history.py"),
18
+ "configuration": (["set_model", "set_key", "set_limit", "set_lang"], "fr_cli/conf/config.py"),
19
+ "launcher_system": (["open_file", "launch_app", "list_apps"], "fr_cli/weapon/launcher.py"),
20
+ "agent_system": (["agent_create", "agent_run"], "fr_cli/agent/"),
21
+ "data_scroll": (["read_excel", "read_csv"], "fr_cli/weapon/dataframe.py"),
22
+ }
23
+
24
+
25
+ def load_weapon_md(mcp_tools=None):
26
+ """
27
+ 从统一注册表获取法宝图谱。
28
+ 保持返回格式兼容旧接口:(tools:list, trigger_map:dict)
29
+ :param mcp_tools: MCP 外部神通列表,可选
30
+ """
31
+ reg = get_registry()
32
+ reg_tools = {t["name"]: t for t in reg.get_available_tools(plugins={})}
33
+
34
+ tools = []
35
+ trigger_map = {}
36
+
37
+ for cat_name, (tool_names, path) in _LEGACY_CATEGORIES.items():
38
+ commands = []
39
+ triggers = []
40
+ descriptions = []
41
+ for tn in tool_names:
42
+ if tn in reg_tools:
43
+ commands.append(tn)
44
+ info = reg_tools[tn]
45
+ if info.get("description"):
46
+ descriptions.append(info["description"])
47
+ if info.get("triggers"):
48
+ triggers.extend(info["triggers"])
49
+ if commands:
50
+ tools.append({
51
+ "name": cat_name,
52
+ "description": ", ".join(descriptions) if descriptions else cat_name,
53
+ "commands": commands,
54
+ "path": path,
55
+ })
56
+ # 去重 triggers
57
+ seen = set()
58
+ unique_triggers = []
59
+ for t in triggers:
60
+ if t not in seen:
61
+ seen.add(t)
62
+ unique_triggers.append(t)
63
+ trigger_map[cat_name] = unique_triggers
64
+
65
+ # 注入 MCP 外部神通
66
+ if mcp_tools:
67
+ mcp_commands = [t["name"] for t in mcp_tools]
68
+ tools.append({
69
+ "name": "mcp_tools",
70
+ "description": "MCP 外部神通: " + ", ".join([t["name"] for t in mcp_tools]),
71
+ "commands": mcp_commands,
72
+ "path": "fr_cli/weapon/mcp.py",
73
+ })
74
+ trigger_map["mcp_tools"] = ["mcp", "外部工具"]
75
+
76
+ return tools, trigger_map
77
+
78
+
79
+ def get_available_tools(weapon_tools, plugins):
80
+ """
81
+ 获取当前可用的工具列表(从传入的 weapon_tools 追加插件)
82
+ 保持与旧接口完全兼容。
83
+ """
84
+ tools = [t.copy() for t in weapon_tools]
85
+ if plugins:
86
+ for plugin_name in plugins.keys():
87
+ tools.append({
88
+ "name": f"plugin_{plugin_name}",
89
+ "description": f"自定义插件: {plugin_name}",
90
+ "commands": [f"/{plugin_name}"],
91
+ })
92
+ return tools
93
+
94
+
95
+ # should_inject_tools 已移除:
96
+ # 主程序逻辑中从未调用此函数(main.py 使用 _should_force_tool + _classify_intent 做意图判定)。
97
+ # MasterAgent 模式下直接注入全部工具列表,无需触发词匹配。
98
+ # 保留 load_weapon_md 和 get_available_tools 作为兼容层。