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.
- fr_cli/README.md +148 -0
- fr_cli/WEAPON.MD +186 -0
- fr_cli/__init__.py +4 -0
- fr_cli/addon/plugin.py +69 -0
- fr_cli/agent/__init__.py +9 -0
- fr_cli/agent/builtins/__init__.py +4 -0
- fr_cli/agent/builtins/_utils.py +48 -0
- fr_cli/agent/builtins/db.py +269 -0
- fr_cli/agent/builtins/local.py +105 -0
- fr_cli/agent/builtins/rag.py +652 -0
- fr_cli/agent/builtins/rag_watcher_daemon.py +156 -0
- fr_cli/agent/builtins/remote.py +214 -0
- fr_cli/agent/builtins/spider.py +247 -0
- fr_cli/agent/client.py +164 -0
- fr_cli/agent/executor.py +86 -0
- fr_cli/agent/generator.py +104 -0
- fr_cli/agent/manager.py +193 -0
- fr_cli/agent/master.py +604 -0
- fr_cli/agent/master_prompt.py +118 -0
- fr_cli/agent/remote.py +70 -0
- fr_cli/agent/server.py +279 -0
- fr_cli/agent/workflow.py +164 -0
- fr_cli/breakthrough/update.py +154 -0
- fr_cli/command/__init__.py +4 -0
- fr_cli/command/executor.py +276 -0
- fr_cli/command/registry.py +1034 -0
- fr_cli/command/security.py +30 -0
- fr_cli/conf/config.py +126 -0
- fr_cli/conf/wizard.py +172 -0
- fr_cli/core/chat.py +280 -0
- fr_cli/core/core.py +111 -0
- fr_cli/core/intent.py +129 -0
- fr_cli/core/recommender.py +71 -0
- fr_cli/core/stream.py +83 -0
- fr_cli/core/sysmon.py +117 -0
- fr_cli/core/thinking.py +215 -0
- fr_cli/gatekeeper/__init__.py +7 -0
- fr_cli/gatekeeper/daemon.py +216 -0
- fr_cli/gatekeeper/manager.py +218 -0
- fr_cli/lang/i18n.py +827 -0
- fr_cli/main.py +329 -0
- fr_cli/memory/context.py +119 -0
- fr_cli/memory/history.py +96 -0
- fr_cli/memory/session.py +134 -0
- fr_cli/repl/__init__.py +0 -0
- fr_cli/repl/commands.py +1098 -0
- fr_cli/security/security.py +46 -0
- fr_cli/ui/ui.py +116 -0
- fr_cli/weapon/cron.py +217 -0
- fr_cli/weapon/dataframe.py +97 -0
- fr_cli/weapon/disk.py +141 -0
- fr_cli/weapon/fs.py +206 -0
- fr_cli/weapon/launcher.py +249 -0
- fr_cli/weapon/loader.py +98 -0
- fr_cli/weapon/mail.py +227 -0
- fr_cli/weapon/mcp.py +204 -0
- fr_cli/weapon/vision.py +74 -0
- fr_cli/weapon/web.py +88 -0
- fr_cli-2.1.0.dist-info/METADATA +227 -0
- fr_cli-2.1.0.dist-info/RECORD +64 -0
- fr_cli-2.1.0.dist-info/WHEEL +5 -0
- fr_cli-2.1.0.dist-info/entry_points.txt +2 -0
- fr_cli-2.1.0.dist-info/licenses/LICENSE +21 -0
- fr_cli-2.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1034 @@
|
|
|
1
|
+
"""
|
|
2
|
+
统一工具注册表
|
|
3
|
+
所有内置命令与AI工具通过装饰器注册,实现单一入口、自动安全确认、参数校验。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# ---- 触发关键词常量(避免同类工具重复定义)----
|
|
8
|
+
_TRIGGERS_FILE = ["文件", "目录", "folder", "读取", "read", "保存到", "save to", "写入文件", "创建文件", "生成文件", "ls", "cat", "cd", "write", "append", "delete"]
|
|
9
|
+
_TRIGGERS_WEB = ["搜索", "search", "查一下", "查询", "look up", "最新新闻", "今天天气", "股价", "汇率", "查百度", "查谷歌"]
|
|
10
|
+
_TRIGGERS_MAIL = ["邮件", "mail", "email", "发邮件", "收件箱", "inbox", "发送邮件", "查看邮件"]
|
|
11
|
+
_TRIGGERS_CRON = ["定时任务", "定时执行", "周期性执行", "cron job", "scheduled task", "定时器"]
|
|
12
|
+
_TRIGGERS_DISK = ["云盘", "上传文件", "下载文件", "cloud disk", "upload file", "download file", "云端"]
|
|
13
|
+
_TRIGGERS_SESSION = ["保存会话", "加载会话", "导出会话", "save session", "load session", "export session"]
|
|
14
|
+
_TRIGGERS_CONFIG = ["切换模型", "换模型", "改模型", "set model", "api key", "api密钥", "切换语言", "设置上限"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ToolRegistry:
|
|
18
|
+
"""工具注册表 —— 单一真相源"""
|
|
19
|
+
|
|
20
|
+
def __init__(self):
|
|
21
|
+
self._tools = {} # name -> tool_info
|
|
22
|
+
self._aliases = {} # alias(without /) -> name
|
|
23
|
+
|
|
24
|
+
def register(self, name, description="", params=None, security=None, aliases=None, needs_msgs=False, triggers=None):
|
|
25
|
+
"""装饰器:注册一个工具/命令"""
|
|
26
|
+
def decorator(func):
|
|
27
|
+
self._tools[name] = {
|
|
28
|
+
"name": name,
|
|
29
|
+
"description": description,
|
|
30
|
+
"params": params or {},
|
|
31
|
+
"security": security,
|
|
32
|
+
"aliases": aliases or [],
|
|
33
|
+
"needs_msgs": needs_msgs,
|
|
34
|
+
"triggers": triggers or [],
|
|
35
|
+
"handler": func,
|
|
36
|
+
}
|
|
37
|
+
for alias in (aliases or []):
|
|
38
|
+
key = alias.lstrip("/")
|
|
39
|
+
self._aliases[key] = name
|
|
40
|
+
return func
|
|
41
|
+
return decorator
|
|
42
|
+
|
|
43
|
+
def _check_security(self, deps, security_key, target):
|
|
44
|
+
if not security_key:
|
|
45
|
+
return True
|
|
46
|
+
if deps.security is None:
|
|
47
|
+
return True # 测试/非交互环境中无 security 时放行,由调用方保障安全
|
|
48
|
+
return deps.security.check(security_key, target)
|
|
49
|
+
|
|
50
|
+
def dispatch(self, deps, tool_name, msgs=None, skip_security=False, **kwargs):
|
|
51
|
+
"""结构化调用:tool_name + kwargs"""
|
|
52
|
+
tool = self._tools.get(tool_name)
|
|
53
|
+
if not tool:
|
|
54
|
+
return None, f"Unknown tool: {tool_name}"
|
|
55
|
+
|
|
56
|
+
# 参数校验
|
|
57
|
+
for param, ptype in tool["params"].items():
|
|
58
|
+
if param not in kwargs:
|
|
59
|
+
return None, f"Missing required parameter: {param}"
|
|
60
|
+
|
|
61
|
+
# 安全确认(仅当未显式跳过且声明了安全级别时)
|
|
62
|
+
if not skip_security and tool["security"]:
|
|
63
|
+
target = kwargs.get("path", tool_name)
|
|
64
|
+
if not self._check_security(deps, tool["security"], target):
|
|
65
|
+
return None, "Denied"
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
if tool["needs_msgs"]:
|
|
69
|
+
return tool["handler"](deps, msgs=msgs, **kwargs)
|
|
70
|
+
return tool["handler"](deps, **kwargs)
|
|
71
|
+
except Exception as e:
|
|
72
|
+
return None, f"Error: {e}"
|
|
73
|
+
|
|
74
|
+
def _dispatch_cmd_parts(self, deps, parts, msgs=None):
|
|
75
|
+
"""内部:根据已分词的 parts 调度命令(复用逻辑,避免重复 split)"""
|
|
76
|
+
if not parts:
|
|
77
|
+
return None, "Empty command"
|
|
78
|
+
|
|
79
|
+
cmd = parts[0].lstrip("/")
|
|
80
|
+
tool_name = self._aliases.get(cmd, cmd)
|
|
81
|
+
tool = self._tools.get(tool_name)
|
|
82
|
+
|
|
83
|
+
if not tool:
|
|
84
|
+
return None, f"Unknown command: {cmd}"
|
|
85
|
+
|
|
86
|
+
kwargs = self._parse_cmd_args(parts, tool, deps)
|
|
87
|
+
if isinstance(kwargs, tuple) and len(kwargs) == 2 and kwargs[0] is None:
|
|
88
|
+
return kwargs
|
|
89
|
+
return self.dispatch(deps, tool_name, msgs=msgs, skip_security=True, **kwargs)
|
|
90
|
+
|
|
91
|
+
def dispatch_cmd(self, deps, cmd_str, msgs=None):
|
|
92
|
+
"""命令字符串调用:/cmd args(跳过安全确认,由调用方负责)"""
|
|
93
|
+
parts = cmd_str.strip().split()
|
|
94
|
+
return self._dispatch_cmd_parts(deps, parts, msgs=msgs)
|
|
95
|
+
|
|
96
|
+
def _parse_cmd_args(self, parts, tool, deps):
|
|
97
|
+
"""将命令行参数解析为 kwargs"""
|
|
98
|
+
cmd = parts[0]
|
|
99
|
+
arg1 = parts[1] if len(parts) > 1 else ""
|
|
100
|
+
arg2 = parts[2] if len(parts) > 2 else ""
|
|
101
|
+
name = tool["name"]
|
|
102
|
+
|
|
103
|
+
# 文件操作
|
|
104
|
+
if name in ("write_file", "append_file"):
|
|
105
|
+
return {"path": arg1, "content": ' '.join(parts[2:]) if len(parts) > 2 else ""}
|
|
106
|
+
if name == "read_file":
|
|
107
|
+
return {"path": arg1}
|
|
108
|
+
if name == "list_files":
|
|
109
|
+
return {}
|
|
110
|
+
if name == "change_dir":
|
|
111
|
+
return {"path": arg1}
|
|
112
|
+
if name == "delete_file":
|
|
113
|
+
return {"path": arg1}
|
|
114
|
+
|
|
115
|
+
# 图片
|
|
116
|
+
if name == "analyze_image":
|
|
117
|
+
return {"path": arg1, "text": arg2}
|
|
118
|
+
if name == "generate_image":
|
|
119
|
+
return {"prompt": arg1}
|
|
120
|
+
|
|
121
|
+
# 网络
|
|
122
|
+
if name == "search_web":
|
|
123
|
+
return {"query": arg1}
|
|
124
|
+
if name == "fetch_web":
|
|
125
|
+
return {"url": arg1}
|
|
126
|
+
|
|
127
|
+
# 邮件
|
|
128
|
+
if name == "mail_inbox":
|
|
129
|
+
return {}
|
|
130
|
+
if name == "mail_read":
|
|
131
|
+
return {"id": arg1}
|
|
132
|
+
if name == "mail_send":
|
|
133
|
+
body = ' '.join(parts[3:]) if len(parts) > 3 else ""
|
|
134
|
+
return {"to": arg1, "subject": arg2, "body": body}
|
|
135
|
+
if name == "mail_setup":
|
|
136
|
+
return {}
|
|
137
|
+
|
|
138
|
+
# 定时任务
|
|
139
|
+
if name == "cron_add":
|
|
140
|
+
return {"command": arg2, "interval": int(arg1) if arg1.isdigit() else 0}
|
|
141
|
+
if name == "cron_list":
|
|
142
|
+
return {}
|
|
143
|
+
if name == "cron_del":
|
|
144
|
+
return {"id": arg1}
|
|
145
|
+
|
|
146
|
+
# 云盘
|
|
147
|
+
if name == "disk_ls":
|
|
148
|
+
return {}
|
|
149
|
+
if name == "disk_up":
|
|
150
|
+
return {"local": arg1, "remote": arg2}
|
|
151
|
+
if name == "disk_down":
|
|
152
|
+
return {"remote": arg1, "local": arg2}
|
|
153
|
+
if name == "disk_setup":
|
|
154
|
+
return {}
|
|
155
|
+
|
|
156
|
+
# 会话
|
|
157
|
+
if name == "save_session":
|
|
158
|
+
return {"name": arg1}
|
|
159
|
+
if name in ("list_sessions", "load_session"):
|
|
160
|
+
return {}
|
|
161
|
+
if name == "export_session":
|
|
162
|
+
return {}
|
|
163
|
+
if name == "delete_session":
|
|
164
|
+
return {}
|
|
165
|
+
|
|
166
|
+
# 配置
|
|
167
|
+
if name == "set_model":
|
|
168
|
+
return {"name": arg1}
|
|
169
|
+
if name == "set_key":
|
|
170
|
+
return {"key": arg1}
|
|
171
|
+
if name == "set_limit":
|
|
172
|
+
return {"limit": arg1}
|
|
173
|
+
if name == "set_lang":
|
|
174
|
+
return {"code": arg1}
|
|
175
|
+
|
|
176
|
+
# 别名
|
|
177
|
+
if name == "set_alias":
|
|
178
|
+
return {"key": arg1, "value": arg2}
|
|
179
|
+
|
|
180
|
+
# 撤销
|
|
181
|
+
if name == "undo":
|
|
182
|
+
return {}
|
|
183
|
+
|
|
184
|
+
# 插件列表
|
|
185
|
+
if name == "list_plugins":
|
|
186
|
+
return {}
|
|
187
|
+
|
|
188
|
+
# 更新
|
|
189
|
+
if name == "update_check":
|
|
190
|
+
return {}
|
|
191
|
+
if name == "update_run":
|
|
192
|
+
return {}
|
|
193
|
+
|
|
194
|
+
# 本地应用启动
|
|
195
|
+
if name == "open_file":
|
|
196
|
+
return {"path": arg1}
|
|
197
|
+
if name == "launch_app":
|
|
198
|
+
return {"name": arg1, "target": ' '.join(parts[2:]) if len(parts) > 2 else None}
|
|
199
|
+
if name == "list_apps":
|
|
200
|
+
return {}
|
|
201
|
+
|
|
202
|
+
# Agent 分身
|
|
203
|
+
if name == "agent_create":
|
|
204
|
+
return {"name": arg1, "description": ' '.join(parts[2:]) if len(parts) > 2 else ""}
|
|
205
|
+
if name == "agent_run":
|
|
206
|
+
return {"name": arg1}
|
|
207
|
+
|
|
208
|
+
# 数据卷轴
|
|
209
|
+
if name == "read_excel":
|
|
210
|
+
return {"path": arg1}
|
|
211
|
+
if name == "read_csv":
|
|
212
|
+
return {"path": arg1}
|
|
213
|
+
|
|
214
|
+
return {}
|
|
215
|
+
|
|
216
|
+
def get_tools(self):
|
|
217
|
+
return list(self._tools.values())
|
|
218
|
+
|
|
219
|
+
def get_trigger_map(self):
|
|
220
|
+
"""获取工具触发关键词映射"""
|
|
221
|
+
return {name: info["triggers"] for name, info in self._tools.items() if info.get("triggers")}
|
|
222
|
+
|
|
223
|
+
def get_available_tools(self, plugins):
|
|
224
|
+
"""获取 AI 可用的工具列表(含插件)"""
|
|
225
|
+
tools = []
|
|
226
|
+
for t in self._tools.values():
|
|
227
|
+
tools.append({
|
|
228
|
+
"name": t["name"],
|
|
229
|
+
"description": t["description"],
|
|
230
|
+
"commands": [f"/{t['name']}"] + t["aliases"],
|
|
231
|
+
"triggers": t.get("triggers", []),
|
|
232
|
+
})
|
|
233
|
+
for plugin_name in (plugins or {}):
|
|
234
|
+
tools.append({
|
|
235
|
+
"name": f"plugin_{plugin_name}",
|
|
236
|
+
"description": f"自定义插件: {plugin_name}",
|
|
237
|
+
"commands": [f"/{plugin_name}"],
|
|
238
|
+
"triggers": [],
|
|
239
|
+
})
|
|
240
|
+
return tools
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# ------------------------------------------------------------------
|
|
244
|
+
# 全局注册表实例
|
|
245
|
+
# ------------------------------------------------------------------
|
|
246
|
+
_registry = ToolRegistry()
|
|
247
|
+
register = _registry.register
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def get_registry():
|
|
251
|
+
return _registry
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
# ------------------------------------------------------------------
|
|
255
|
+
# Helper:确保子系统已配置
|
|
256
|
+
# ------------------------------------------------------------------
|
|
257
|
+
def _ensure_mail(deps):
|
|
258
|
+
if deps.mail_c and getattr(deps.mail_c, "email", None) and getattr(deps.mail_c, "password", None) and getattr(deps.mail_c, "imap_server", None):
|
|
259
|
+
return True
|
|
260
|
+
from fr_cli.conf.wizard import mail_wizard
|
|
261
|
+
ok, deps.cfg = mail_wizard(deps.cfg, deps.lang)
|
|
262
|
+
if ok:
|
|
263
|
+
from fr_cli.weapon.mail import MailClient
|
|
264
|
+
deps.mail_c = MailClient(deps.cfg.get("mail", {}))
|
|
265
|
+
return ok
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _ensure_disk(deps):
|
|
269
|
+
if deps.disk_c and getattr(deps.disk_c, "type", None):
|
|
270
|
+
return True
|
|
271
|
+
from fr_cli.conf.wizard import disk_wizard
|
|
272
|
+
ok, deps.cfg = disk_wizard(deps.cfg, deps.lang)
|
|
273
|
+
if ok:
|
|
274
|
+
from fr_cli.weapon.disk import CloudDisk
|
|
275
|
+
deps.disk_c = CloudDisk(deps.cfg.get("disk", {}))
|
|
276
|
+
return ok
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
# ------------------------------------------------------------------
|
|
280
|
+
# ========== 文件操作 ==========
|
|
281
|
+
# ------------------------------------------------------------------
|
|
282
|
+
@register(
|
|
283
|
+
name="write_file",
|
|
284
|
+
triggers=_TRIGGERS_FILE,
|
|
285
|
+
description="写入文件",
|
|
286
|
+
params={"path": str, "content": str},
|
|
287
|
+
security="sec_write",
|
|
288
|
+
aliases=["/write"],
|
|
289
|
+
)
|
|
290
|
+
def _write_file(deps, **kwargs):
|
|
291
|
+
ok, msg = deps.vfs.write(kwargs["path"], kwargs["content"], deps.lang)
|
|
292
|
+
return (msg, None) if ok else (None, msg)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
@register(
|
|
296
|
+
name="read_file",
|
|
297
|
+
triggers=_TRIGGERS_FILE,
|
|
298
|
+
description="读取文件",
|
|
299
|
+
params={"path": str},
|
|
300
|
+
security="sec_read",
|
|
301
|
+
aliases=["/cat"],
|
|
302
|
+
)
|
|
303
|
+
def _read_file(deps, **kwargs):
|
|
304
|
+
txt, err = deps.vfs.read(kwargs["path"], deps.lang)
|
|
305
|
+
return (txt, None) if not err else (None, err)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
@register(
|
|
309
|
+
name="list_files",
|
|
310
|
+
triggers=_TRIGGERS_FILE,
|
|
311
|
+
description="列出文件",
|
|
312
|
+
params={},
|
|
313
|
+
aliases=["/ls"],
|
|
314
|
+
)
|
|
315
|
+
def _list_files(deps, **kwargs):
|
|
316
|
+
items, err = deps.vfs.ls(deps.lang)
|
|
317
|
+
return ("\n".join(items), None) if not err else (None, err)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
@register(
|
|
321
|
+
name="change_dir",
|
|
322
|
+
triggers=_TRIGGERS_FILE,
|
|
323
|
+
description="切换目录",
|
|
324
|
+
params={"path": str},
|
|
325
|
+
aliases=["/cd"],
|
|
326
|
+
)
|
|
327
|
+
def _change_dir(deps, **kwargs):
|
|
328
|
+
ok, msg = deps.vfs.cd(kwargs["path"], deps.lang)
|
|
329
|
+
return (msg, None) if ok else (None, msg)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
@register(
|
|
333
|
+
name="append_file",
|
|
334
|
+
triggers=_TRIGGERS_FILE,
|
|
335
|
+
description="追加文件",
|
|
336
|
+
params={"path": str, "content": str},
|
|
337
|
+
security="sec_write",
|
|
338
|
+
aliases=["/append"],
|
|
339
|
+
)
|
|
340
|
+
def _append_file(deps, **kwargs):
|
|
341
|
+
ok, msg = deps.vfs.append(kwargs["path"], kwargs["content"], deps.lang)
|
|
342
|
+
return (msg, None) if ok else (None, msg)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
@register(
|
|
346
|
+
name="delete_file",
|
|
347
|
+
triggers=_TRIGGERS_FILE,
|
|
348
|
+
description="删除文件",
|
|
349
|
+
params={"path": str},
|
|
350
|
+
security="sec_write",
|
|
351
|
+
aliases=["/delete"],
|
|
352
|
+
)
|
|
353
|
+
def _delete_file(deps, **kwargs):
|
|
354
|
+
ok, msg = deps.vfs.delete(kwargs["path"], deps.lang)
|
|
355
|
+
return (msg, None) if ok else (None, msg)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
# ------------------------------------------------------------------
|
|
359
|
+
# ========== 图片 ==========
|
|
360
|
+
# ------------------------------------------------------------------
|
|
361
|
+
@register(
|
|
362
|
+
name="analyze_image",
|
|
363
|
+
triggers=["分析图片", "识图", "看图", "describe image", "图片内容", "识别图片", "生成图片", "画图", "画一张"],
|
|
364
|
+
description="图片分析",
|
|
365
|
+
params={"path": str, "text": str},
|
|
366
|
+
security="sec_read",
|
|
367
|
+
aliases=["/see"],
|
|
368
|
+
needs_msgs=True,
|
|
369
|
+
)
|
|
370
|
+
def _analyze_image(deps, msgs=None, **kwargs):
|
|
371
|
+
from fr_cli.weapon.vision import prep_see_msg
|
|
372
|
+
from fr_cli.core.stream import stream_cnt
|
|
373
|
+
if not msgs:
|
|
374
|
+
return None, "No message history available"
|
|
375
|
+
prep_see_msg(msgs, kwargs["path"], kwargs.get("text", ""), vfs=deps.vfs)
|
|
376
|
+
txt, _, response_time = stream_cnt(deps.client, deps.model_name, msgs, deps.lang)
|
|
377
|
+
return f"图片分析结果:\n{txt}\n耗时: {response_time:.2f}秒", None
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
@register(
|
|
381
|
+
name="generate_image",
|
|
382
|
+
description="生成图片",
|
|
383
|
+
params={"prompt": str},
|
|
384
|
+
security="sec_gen_img",
|
|
385
|
+
)
|
|
386
|
+
def _generate_image(deps, **kwargs):
|
|
387
|
+
from fr_cli.weapon.vision import gen_img
|
|
388
|
+
out_dir = deps.vfs.cwd if deps.vfs else "."
|
|
389
|
+
ok, res = gen_img(deps.client, kwargs["prompt"], out_dir, deps.lang)
|
|
390
|
+
return (res, None) if ok else (None, res)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
# ------------------------------------------------------------------
|
|
394
|
+
# ========== 网络 ==========
|
|
395
|
+
# ------------------------------------------------------------------
|
|
396
|
+
@register(
|
|
397
|
+
name="search_web",
|
|
398
|
+
triggers=_TRIGGERS_WEB,
|
|
399
|
+
description="网络搜索",
|
|
400
|
+
params={"query": str},
|
|
401
|
+
security="sec_fetch_web",
|
|
402
|
+
aliases=["/web"],
|
|
403
|
+
)
|
|
404
|
+
def _search_web(deps, **kwargs):
|
|
405
|
+
res, err = deps.web_c.search(kwargs["query"], deps.lang)
|
|
406
|
+
if err:
|
|
407
|
+
return None, err
|
|
408
|
+
return "\n".join([f"- {r['title']}\n {r['url']}\n {r['snippet'][:50]}..." for r in res]), None
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
@register(
|
|
412
|
+
name="fetch_web",
|
|
413
|
+
triggers=_TRIGGERS_WEB,
|
|
414
|
+
description="抓取网页",
|
|
415
|
+
params={"url": str},
|
|
416
|
+
security="sec_fetch_web",
|
|
417
|
+
aliases=["/fetch"],
|
|
418
|
+
)
|
|
419
|
+
def _fetch_web(deps, **kwargs):
|
|
420
|
+
txt, err = deps.web_c.fetch(kwargs["url"], deps.lang)
|
|
421
|
+
return (txt, None) if not err else (None, err)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
# ------------------------------------------------------------------
|
|
425
|
+
# ========== 邮件 ==========
|
|
426
|
+
# ------------------------------------------------------------------
|
|
427
|
+
@register(
|
|
428
|
+
name="mail_inbox",
|
|
429
|
+
triggers=_TRIGGERS_MAIL,
|
|
430
|
+
description="查看收件箱",
|
|
431
|
+
params={},
|
|
432
|
+
aliases=["/mail_inbox"],
|
|
433
|
+
)
|
|
434
|
+
def _mail_inbox(deps, **kwargs):
|
|
435
|
+
from fr_cli.lang.i18n import T
|
|
436
|
+
if not _ensure_mail(deps):
|
|
437
|
+
return None, T("mail_no_cfg", deps.lang)
|
|
438
|
+
mails, err = deps.mail_c.inbox(deps.lang)
|
|
439
|
+
if err:
|
|
440
|
+
return None, err
|
|
441
|
+
return "\n".join([f"{m['id']} {m['sub'][:30]} ({m['from']})" for m in mails]), None
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
@register(
|
|
445
|
+
name="mail_read",
|
|
446
|
+
triggers=_TRIGGERS_MAIL,
|
|
447
|
+
description="读取邮件",
|
|
448
|
+
params={"id": str},
|
|
449
|
+
aliases=["/mail_read"],
|
|
450
|
+
)
|
|
451
|
+
def _mail_read(deps, **kwargs):
|
|
452
|
+
from fr_cli.lang.i18n import T
|
|
453
|
+
if not _ensure_mail(deps):
|
|
454
|
+
return None, T("mail_no_cfg", deps.lang)
|
|
455
|
+
m, err = deps.mail_c.read(kwargs["id"], deps.lang)
|
|
456
|
+
if err:
|
|
457
|
+
return None, err
|
|
458
|
+
return f"{m['sub']}\n{m['from']} | {m['date']}\n\n{m['body']}", None
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
@register(
|
|
462
|
+
name="mail_send",
|
|
463
|
+
triggers=_TRIGGERS_MAIL,
|
|
464
|
+
description="发送邮件",
|
|
465
|
+
params={"to": str, "subject": str, "body": str},
|
|
466
|
+
security="sec_send_mail",
|
|
467
|
+
aliases=["/mail_send"],
|
|
468
|
+
)
|
|
469
|
+
def _mail_send(deps, **kwargs):
|
|
470
|
+
from fr_cli.lang.i18n import T
|
|
471
|
+
if not _ensure_mail(deps):
|
|
472
|
+
return None, T("mail_no_cfg", deps.lang)
|
|
473
|
+
ok, err = deps.mail_c.send(kwargs["to"], kwargs["subject"], kwargs["body"], deps.lang)
|
|
474
|
+
return (T("mail_ok", deps.lang), None) if ok else (None, err or "Send failed")
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
@register(
|
|
478
|
+
name="mail_setup",
|
|
479
|
+
description="邮件配置向导",
|
|
480
|
+
params={},
|
|
481
|
+
aliases=["/mail_setup"],
|
|
482
|
+
)
|
|
483
|
+
def _mail_setup(deps, **kwargs):
|
|
484
|
+
from fr_cli.conf.wizard import mail_wizard
|
|
485
|
+
ok, deps.cfg = mail_wizard(deps.cfg, deps.lang)
|
|
486
|
+
if ok:
|
|
487
|
+
from fr_cli.weapon.mail import MailClient
|
|
488
|
+
deps.mail_c = MailClient(deps.cfg.get("mail", {}))
|
|
489
|
+
return ("OK", None) if ok else (None, "Cancelled")
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
# ------------------------------------------------------------------
|
|
493
|
+
# ========== 定时任务 ==========
|
|
494
|
+
# ------------------------------------------------------------------
|
|
495
|
+
@register(
|
|
496
|
+
name="cron_add",
|
|
497
|
+
triggers=_TRIGGERS_CRON,
|
|
498
|
+
description="添加定时任务",
|
|
499
|
+
params={"command": str, "interval": int},
|
|
500
|
+
security="sec_exec",
|
|
501
|
+
aliases=["/cron_add"],
|
|
502
|
+
)
|
|
503
|
+
def _cron_add(deps, **kwargs):
|
|
504
|
+
from fr_cli.weapon.cron import add_job, _default_manager
|
|
505
|
+
from fr_cli.gatekeeper.manager import sync_gatekeeper_cron_jobs
|
|
506
|
+
jid, m = add_job(kwargs["command"], kwargs["interval"], deps.lang)
|
|
507
|
+
if jid is not None:
|
|
508
|
+
# 自动同步到 gatekeeper 配置
|
|
509
|
+
sync_gatekeeper_cron_jobs(cron_jobs=_default_manager.export_jobs())
|
|
510
|
+
return m, None
|
|
511
|
+
return None, m
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
@register(
|
|
515
|
+
name="cron_list",
|
|
516
|
+
triggers=_TRIGGERS_CRON,
|
|
517
|
+
description="列出定时任务",
|
|
518
|
+
params={},
|
|
519
|
+
aliases=["/cron_list"],
|
|
520
|
+
)
|
|
521
|
+
def _cron_list(deps, **kwargs):
|
|
522
|
+
from fr_cli.weapon.cron import list_jobs
|
|
523
|
+
res, err = list_jobs(deps.lang)
|
|
524
|
+
return ("\n".join(res), None) if not err else (None, err)
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
@register(
|
|
528
|
+
name="cron_del",
|
|
529
|
+
triggers=_TRIGGERS_CRON,
|
|
530
|
+
description="删除定时任务",
|
|
531
|
+
params={"id": str},
|
|
532
|
+
aliases=["/cron_del"],
|
|
533
|
+
)
|
|
534
|
+
def _cron_del(deps, **kwargs):
|
|
535
|
+
from fr_cli.weapon.cron import del_job, _default_manager
|
|
536
|
+
from fr_cli.gatekeeper.manager import sync_gatekeeper_cron_jobs
|
|
537
|
+
ok, m = del_job(int(kwargs["id"]), deps.lang)
|
|
538
|
+
if ok:
|
|
539
|
+
# 自动同步到 gatekeeper 配置
|
|
540
|
+
sync_gatekeeper_cron_jobs(cron_jobs=_default_manager.export_jobs())
|
|
541
|
+
return m, None
|
|
542
|
+
return None, m
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
# ------------------------------------------------------------------
|
|
546
|
+
# ========== 云盘 ==========
|
|
547
|
+
# ------------------------------------------------------------------
|
|
548
|
+
@register(
|
|
549
|
+
name="disk_ls",
|
|
550
|
+
triggers=_TRIGGERS_DISK,
|
|
551
|
+
description="列出云盘文件",
|
|
552
|
+
params={},
|
|
553
|
+
aliases=["/disk_ls"],
|
|
554
|
+
)
|
|
555
|
+
def _disk_ls(deps, **kwargs):
|
|
556
|
+
from fr_cli.lang.i18n import T
|
|
557
|
+
if not _ensure_disk(deps):
|
|
558
|
+
return None, T("disk_no_cfg", deps.lang)
|
|
559
|
+
res, err = deps.disk_c.ls(deps.lang)
|
|
560
|
+
return ("\n".join(res) if res else T("empty", deps.lang), None) if not err else (None, err)
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
@register(
|
|
564
|
+
name="disk_up",
|
|
565
|
+
triggers=_TRIGGERS_DISK,
|
|
566
|
+
description="上传文件到云盘",
|
|
567
|
+
params={"local": str, "remote": str},
|
|
568
|
+
security="sec_upload_disk",
|
|
569
|
+
aliases=["/disk_up"],
|
|
570
|
+
)
|
|
571
|
+
def _disk_up(deps, **kwargs):
|
|
572
|
+
from fr_cli.lang.i18n import T
|
|
573
|
+
if not _ensure_disk(deps):
|
|
574
|
+
return None, T("disk_no_cfg", deps.lang)
|
|
575
|
+
ok, m = deps.disk_c.up(kwargs["remote"], kwargs["local"], deps.lang)
|
|
576
|
+
return (m, None) if ok else (None, m)
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
@register(
|
|
580
|
+
name="disk_down",
|
|
581
|
+
triggers=_TRIGGERS_DISK,
|
|
582
|
+
description="从云盘下载文件",
|
|
583
|
+
params={"remote": str, "local": str},
|
|
584
|
+
security="sec_download_disk",
|
|
585
|
+
aliases=["/disk_down"],
|
|
586
|
+
)
|
|
587
|
+
def _disk_down(deps, **kwargs):
|
|
588
|
+
from fr_cli.lang.i18n import T
|
|
589
|
+
if not _ensure_disk(deps):
|
|
590
|
+
return None, T("disk_no_cfg", deps.lang)
|
|
591
|
+
loc = kwargs.get("local") or kwargs["remote"].split("/")[-1]
|
|
592
|
+
ok, m = deps.disk_c.down(kwargs["remote"], loc, deps.lang)
|
|
593
|
+
return (m, None) if ok else (None, m)
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
@register(
|
|
597
|
+
name="disk_cd",
|
|
598
|
+
triggers=_TRIGGERS_DISK,
|
|
599
|
+
description="切换云盘目录",
|
|
600
|
+
params={"path": str},
|
|
601
|
+
aliases=["/disk_cd"],
|
|
602
|
+
)
|
|
603
|
+
def _disk_cd(deps, **kwargs):
|
|
604
|
+
from fr_cli.lang.i18n import T
|
|
605
|
+
if not _ensure_disk(deps):
|
|
606
|
+
return None, T("disk_no_cfg", deps.lang)
|
|
607
|
+
ok, msg = deps.disk_c.cd(kwargs["path"], deps.lang)
|
|
608
|
+
return (msg, None) if ok else (None, msg)
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
@register(
|
|
612
|
+
name="disk_setup",
|
|
613
|
+
description="云盘配置向导",
|
|
614
|
+
params={},
|
|
615
|
+
aliases=["/disk_setup"],
|
|
616
|
+
)
|
|
617
|
+
def _disk_setup(deps, **kwargs):
|
|
618
|
+
from fr_cli.conf.wizard import disk_wizard
|
|
619
|
+
ok, deps.cfg = disk_wizard(deps.cfg, deps.lang)
|
|
620
|
+
if ok:
|
|
621
|
+
from fr_cli.weapon.disk import CloudDisk
|
|
622
|
+
deps.disk_c = CloudDisk(deps.cfg.get("disk", {}))
|
|
623
|
+
return ("OK", None) if ok else (None, "Cancelled")
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
# ------------------------------------------------------------------
|
|
627
|
+
# ========== 会话管理 ==========
|
|
628
|
+
# ------------------------------------------------------------------
|
|
629
|
+
@register(
|
|
630
|
+
name="save_session",
|
|
631
|
+
triggers=_TRIGGERS_SESSION,
|
|
632
|
+
description="保存会话",
|
|
633
|
+
params={"name": str},
|
|
634
|
+
aliases=["/save"],
|
|
635
|
+
needs_msgs=True,
|
|
636
|
+
)
|
|
637
|
+
def _save_session(deps, msgs=None, **kwargs):
|
|
638
|
+
from fr_cli.memory.history import save_sess
|
|
639
|
+
from fr_cli.conf.config import save_config
|
|
640
|
+
from fr_cli.lang.i18n import T
|
|
641
|
+
sn = kwargs["name"]
|
|
642
|
+
deps.cfg["session_name"] = sn
|
|
643
|
+
save_config(deps.cfg)
|
|
644
|
+
if save_sess(sn, msgs):
|
|
645
|
+
return T('ok_sess_save', deps.lang, sn), None
|
|
646
|
+
return None, "Save failed"
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
@register(
|
|
650
|
+
name="list_sessions",
|
|
651
|
+
triggers=_TRIGGERS_SESSION,
|
|
652
|
+
description="列出会话",
|
|
653
|
+
params={},
|
|
654
|
+
aliases=["/load"],
|
|
655
|
+
)
|
|
656
|
+
def _list_sessions(deps, **kwargs):
|
|
657
|
+
from fr_cli.memory.history import get_sessions
|
|
658
|
+
from fr_cli.lang.i18n import T
|
|
659
|
+
ss = get_sessions()
|
|
660
|
+
if not ss:
|
|
661
|
+
return None, T("no_sess", deps.lang)
|
|
662
|
+
return "\n".join([f"[{i}] {s['name']}" for i, s in enumerate(ss)]), None
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
@register(
|
|
666
|
+
name="export_session",
|
|
667
|
+
triggers=_TRIGGERS_SESSION,
|
|
668
|
+
description="导出会话",
|
|
669
|
+
params={},
|
|
670
|
+
aliases=["/export"],
|
|
671
|
+
needs_msgs=True,
|
|
672
|
+
)
|
|
673
|
+
def _export_session(deps, msgs=None, **kwargs):
|
|
674
|
+
from fr_cli.memory.history import export_md
|
|
675
|
+
from fr_cli.lang.i18n import T
|
|
676
|
+
out_dir = deps.vfs.cwd if deps.vfs else None
|
|
677
|
+
ok, path = export_md(msgs, deps.lang, out_dir)
|
|
678
|
+
return (T('ok_export', deps.lang, path), None) if ok else (None, "Export failed")
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
@register(
|
|
682
|
+
name="delete_session",
|
|
683
|
+
description="删除会话",
|
|
684
|
+
params={"id": str},
|
|
685
|
+
aliases=["/del"],
|
|
686
|
+
)
|
|
687
|
+
def _delete_session(deps, **kwargs):
|
|
688
|
+
from fr_cli.memory.history import get_sessions, del_sess
|
|
689
|
+
from fr_cli.lang.i18n import T
|
|
690
|
+
ss = get_sessions()
|
|
691
|
+
if not ss:
|
|
692
|
+
return None, T("no_sess", deps.lang)
|
|
693
|
+
sid = kwargs.get("id", "")
|
|
694
|
+
if sid and sid.isdigit():
|
|
695
|
+
idx = int(sid)
|
|
696
|
+
else:
|
|
697
|
+
idx = 0
|
|
698
|
+
ok = del_sess(idx)
|
|
699
|
+
return (T('ok_sess_del', deps.lang), None) if ok else (None, "Delete failed")
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
# ------------------------------------------------------------------
|
|
703
|
+
# ========== 配置管理 ==========
|
|
704
|
+
# ------------------------------------------------------------------
|
|
705
|
+
@register(
|
|
706
|
+
name="set_model",
|
|
707
|
+
triggers=_TRIGGERS_CONFIG,
|
|
708
|
+
description="切换模型",
|
|
709
|
+
params={"name": str},
|
|
710
|
+
aliases=["/model"],
|
|
711
|
+
)
|
|
712
|
+
def _set_model(deps, **kwargs):
|
|
713
|
+
from fr_cli.conf.config import save_config
|
|
714
|
+
from fr_cli.lang.i18n import T
|
|
715
|
+
deps.cfg["model"] = kwargs["name"]
|
|
716
|
+
deps.model_name = kwargs["name"]
|
|
717
|
+
save_config(deps.cfg)
|
|
718
|
+
return T('ok_model', deps.lang, kwargs["name"]), None
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
@register(
|
|
722
|
+
name="set_key",
|
|
723
|
+
triggers=_TRIGGERS_CONFIG,
|
|
724
|
+
description="设置API密钥",
|
|
725
|
+
params={"key": str},
|
|
726
|
+
aliases=["/key"],
|
|
727
|
+
)
|
|
728
|
+
def _set_key(deps, **kwargs):
|
|
729
|
+
from fr_cli.conf.config import save_config
|
|
730
|
+
from fr_cli.lang.i18n import T
|
|
731
|
+
deps.cfg["key"] = kwargs["key"]
|
|
732
|
+
save_config(deps.cfg)
|
|
733
|
+
return T('ok_key', deps.lang), None
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
@register(
|
|
737
|
+
name="set_limit",
|
|
738
|
+
triggers=_TRIGGERS_CONFIG,
|
|
739
|
+
description="设置Token上限",
|
|
740
|
+
params={"limit": int},
|
|
741
|
+
aliases=["/limit"],
|
|
742
|
+
)
|
|
743
|
+
def _set_limit(deps, **kwargs):
|
|
744
|
+
from fr_cli.conf.config import save_config
|
|
745
|
+
from fr_cli.lang.i18n import T
|
|
746
|
+
lim = int(kwargs["limit"])
|
|
747
|
+
if lim < 1000:
|
|
748
|
+
return None, T('err_limit', deps.lang)
|
|
749
|
+
deps.cfg["limit"] = lim
|
|
750
|
+
save_config(deps.cfg)
|
|
751
|
+
return T('ok_limit', deps.lang, lim), None
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
@register(
|
|
755
|
+
name="set_lang",
|
|
756
|
+
triggers=_TRIGGERS_CONFIG,
|
|
757
|
+
description="切换语言",
|
|
758
|
+
params={"code": str},
|
|
759
|
+
aliases=["/lang"],
|
|
760
|
+
)
|
|
761
|
+
def _set_lang(deps, **kwargs):
|
|
762
|
+
from fr_cli.conf.config import save_config
|
|
763
|
+
lc = kwargs["code"]
|
|
764
|
+
if lc in ['zh', 'en']:
|
|
765
|
+
deps.cfg["lang"] = lc
|
|
766
|
+
deps.lang = lc
|
|
767
|
+
save_config(deps.cfg)
|
|
768
|
+
return f"Language changed to {lc}", None
|
|
769
|
+
return None, "Invalid language. Use zh or en"
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
@register(
|
|
773
|
+
name="set_alias",
|
|
774
|
+
description="设置命令别名",
|
|
775
|
+
params={"key": str, "value": str},
|
|
776
|
+
aliases=["/alias"],
|
|
777
|
+
)
|
|
778
|
+
def _set_alias(deps, **kwargs):
|
|
779
|
+
from fr_cli.conf.config import save_config
|
|
780
|
+
from fr_cli.lang.i18n import T
|
|
781
|
+
k, v = kwargs["key"], kwargs.get("value", "")
|
|
782
|
+
if v:
|
|
783
|
+
aliases = deps.cfg.get("aliases", {})
|
|
784
|
+
aliases[k] = v
|
|
785
|
+
deps.cfg["aliases"] = aliases
|
|
786
|
+
save_config(deps.cfg)
|
|
787
|
+
return T('ok_alias_set', deps.lang, k, v), None
|
|
788
|
+
val = deps.cfg.get("aliases", {}).get(k, "")
|
|
789
|
+
return val if val else T("no_alias", deps.lang), None
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
# ------------------------------------------------------------------
|
|
793
|
+
# ========== 其他 ==========
|
|
794
|
+
# ------------------------------------------------------------------
|
|
795
|
+
@register(
|
|
796
|
+
name="undo",
|
|
797
|
+
description="撤销最近一轮对话",
|
|
798
|
+
params={},
|
|
799
|
+
aliases=["/undo"],
|
|
800
|
+
needs_msgs=True,
|
|
801
|
+
)
|
|
802
|
+
def _undo(deps, msgs=None, **kwargs):
|
|
803
|
+
from fr_cli.lang.i18n import T
|
|
804
|
+
if len(msgs) > 1 and msgs[-1]["role"] == "assistant":
|
|
805
|
+
msgs.pop()
|
|
806
|
+
return T('ok_undo', deps.lang), None
|
|
807
|
+
if len(msgs) > 1 and msgs[-1]["role"] == "user":
|
|
808
|
+
msgs.pop()
|
|
809
|
+
return T('ok_undo', deps.lang), None
|
|
810
|
+
return None, T('err_undo', deps.lang)
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
@register(
|
|
814
|
+
name="list_plugins",
|
|
815
|
+
description="列出已安装插件",
|
|
816
|
+
params={},
|
|
817
|
+
aliases=["/skills"],
|
|
818
|
+
)
|
|
819
|
+
def _list_plugins(deps, **kwargs):
|
|
820
|
+
from fr_cli.lang.i18n import T
|
|
821
|
+
if not deps.plugins:
|
|
822
|
+
return None, T("no_plugins", deps.lang)
|
|
823
|
+
return "\n".join([f"/{k}" for k in deps.plugins.keys()]), None
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
@register(
|
|
827
|
+
name="update_check",
|
|
828
|
+
description="检查更新",
|
|
829
|
+
params={},
|
|
830
|
+
aliases=["/update_check"],
|
|
831
|
+
)
|
|
832
|
+
def _update_check(deps, **kwargs):
|
|
833
|
+
from fr_cli.breakthrough.update import update_check
|
|
834
|
+
ok, info, err = update_check(verbose=False)
|
|
835
|
+
if err:
|
|
836
|
+
return None, f"检查失败: {err}"
|
|
837
|
+
if not ok:
|
|
838
|
+
return "当前已是最新版本。", None
|
|
839
|
+
ver = info.get("version", "?")
|
|
840
|
+
note = info.get("release_note", "")
|
|
841
|
+
return f"发现新版本: {ver}\n{note}", None
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
@register(
|
|
845
|
+
name="update_run",
|
|
846
|
+
description="执行更新",
|
|
847
|
+
params={},
|
|
848
|
+
aliases=["/update_run"],
|
|
849
|
+
)
|
|
850
|
+
def _update_run(deps, **kwargs):
|
|
851
|
+
from fr_cli.breakthrough.update import update_and_restart
|
|
852
|
+
ok, msg = update_and_restart(verbose=True, allow_restart=True)
|
|
853
|
+
return (msg, None) if ok else (None, msg)
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
# ------------------------------------------------------------------
|
|
857
|
+
# ========== 本地应用启动器 ==========
|
|
858
|
+
# ------------------------------------------------------------------
|
|
859
|
+
@register(
|
|
860
|
+
name="open_file",
|
|
861
|
+
triggers=["打开", "open", "启动", "launch", "浏览", "播放", "查看"],
|
|
862
|
+
description="用系统默认程序打开文件或 URL",
|
|
863
|
+
params={"path": str},
|
|
864
|
+
aliases=["/open"],
|
|
865
|
+
)
|
|
866
|
+
def _open_file(deps, **kwargs):
|
|
867
|
+
from fr_cli.weapon.launcher import open_file
|
|
868
|
+
ok, msg = open_file(kwargs["path"], deps.lang)
|
|
869
|
+
return (msg, None) if ok else (None, msg)
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
@register(
|
|
873
|
+
name="launch_app",
|
|
874
|
+
triggers=["打开应用", "启动程序", "运行软件", "launch app", "打开微信", "打开浏览器", "打开 Word", "打开 Excel"],
|
|
875
|
+
description="启动指定应用程序,可带文件或 URL 参数",
|
|
876
|
+
params={"name": str},
|
|
877
|
+
aliases=["/launch"],
|
|
878
|
+
)
|
|
879
|
+
def _launch_app(deps, **kwargs):
|
|
880
|
+
from fr_cli.weapon.launcher import launch_app
|
|
881
|
+
ok, msg = launch_app(kwargs["name"], kwargs.get("target"), deps.lang)
|
|
882
|
+
return (msg, None) if ok else (None, msg)
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
@register(
|
|
886
|
+
name="list_apps",
|
|
887
|
+
description="列出本机可用应用别名",
|
|
888
|
+
params={},
|
|
889
|
+
aliases=["/apps"],
|
|
890
|
+
)
|
|
891
|
+
def _list_apps(deps, **kwargs):
|
|
892
|
+
from fr_cli.weapon.launcher import list_apps
|
|
893
|
+
res, err = list_apps(deps.lang)
|
|
894
|
+
return (res, None) if not err else (None, err)
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
# ------------------------------------------------------------------
|
|
898
|
+
# ========== Agent 分身系统 ==========
|
|
899
|
+
# ------------------------------------------------------------------
|
|
900
|
+
@register(
|
|
901
|
+
name="agent_create",
|
|
902
|
+
triggers=["创建Agent", "新建Agent", "生成Agent", "create agent", "new agent"],
|
|
903
|
+
description="根据需求自动生成 Agent 分身",
|
|
904
|
+
params={"name": str, "description": str},
|
|
905
|
+
aliases=["/agent_create"],
|
|
906
|
+
)
|
|
907
|
+
def _agent_create(deps, **kwargs):
|
|
908
|
+
from fr_cli.agent.generator import generate_agent
|
|
909
|
+
from fr_cli.agent.manager import save_persona, save_skills, save_agent_code, create_agent_dir
|
|
910
|
+
name = kwargs["name"]
|
|
911
|
+
desc = kwargs["description"]
|
|
912
|
+
d = create_agent_dir(name)
|
|
913
|
+
result = generate_agent(deps.client, deps.model_name, name, desc, deps.lang)
|
|
914
|
+
if result["persona"]:
|
|
915
|
+
save_persona(name, result["persona"])
|
|
916
|
+
if result["skills"]:
|
|
917
|
+
save_skills(name, result["skills"])
|
|
918
|
+
if result["code"]:
|
|
919
|
+
save_agent_code(name, result["code"])
|
|
920
|
+
return f"Agent [{name}] 铸造完成!路径: {d}", None
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
def _make_compat_state(deps):
|
|
924
|
+
"""将 SimpleNamespace deps 包装为兼容 AppState 的对象,供 Agent executor 使用"""
|
|
925
|
+
class _CompatState:
|
|
926
|
+
def __init__(self, d):
|
|
927
|
+
for k, v in d.__dict__.items():
|
|
928
|
+
setattr(self, k, v)
|
|
929
|
+
compat = _CompatState(deps)
|
|
930
|
+
compat.executor = getattr(deps, 'executor', None)
|
|
931
|
+
return compat
|
|
932
|
+
|
|
933
|
+
|
|
934
|
+
@register(
|
|
935
|
+
name="agent_run",
|
|
936
|
+
triggers=["运行Agent", "调用Agent", "执行Agent", "run agent"],
|
|
937
|
+
description="运行指定本地 Agent",
|
|
938
|
+
params={"name": str},
|
|
939
|
+
aliases=["/agent_run"],
|
|
940
|
+
)
|
|
941
|
+
def _agent_run(deps, **kwargs):
|
|
942
|
+
from fr_cli.agent.executor import run_agent
|
|
943
|
+
result, err = run_agent(kwargs["name"], _make_compat_state(deps))
|
|
944
|
+
return (result, None) if not err else (None, err)
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
@register(
|
|
948
|
+
name="agent_call",
|
|
949
|
+
triggers=["调用Agent", "协作Agent", "agent_call", "召唤Agent"],
|
|
950
|
+
description="调用Agent(本地或远程)并传入任务描述,实现MasterAgent与其他Agent协作",
|
|
951
|
+
params={"name": str, "user_input": str},
|
|
952
|
+
aliases=["/agent_call"],
|
|
953
|
+
)
|
|
954
|
+
def _agent_call(deps, **kwargs):
|
|
955
|
+
"""MasterAgent 调用其他 Agent(支持本地和远程)"""
|
|
956
|
+
from fr_cli.agent.client import call_agent
|
|
957
|
+
result, err = call_agent(kwargs["name"], _make_compat_state(deps), user_input=kwargs.get("user_input", ""))
|
|
958
|
+
return (result, None) if not err else (None, err)
|
|
959
|
+
|
|
960
|
+
|
|
961
|
+
# ------------------------------------------------------------------
|
|
962
|
+
# ========== 数据卷轴(Excel / CSV)==========
|
|
963
|
+
# ------------------------------------------------------------------
|
|
964
|
+
@register(
|
|
965
|
+
name="read_excel",
|
|
966
|
+
triggers=["Excel", "表格", "xlsx", "读取Excel", "分析表格"],
|
|
967
|
+
description="读取 Excel 文件并返回数据摘要",
|
|
968
|
+
params={"path": str},
|
|
969
|
+
security="sec_read",
|
|
970
|
+
aliases=["/read_excel"],
|
|
971
|
+
)
|
|
972
|
+
def _read_excel(deps, **kwargs):
|
|
973
|
+
from fr_cli.weapon.dataframe import read_excel
|
|
974
|
+
res, err = read_excel(kwargs["path"], lang=deps.lang)
|
|
975
|
+
return (res, None) if not err else (None, err)
|
|
976
|
+
|
|
977
|
+
|
|
978
|
+
@register(
|
|
979
|
+
name="read_csv",
|
|
980
|
+
triggers=["CSV", "csv", "读取CSV", "分析CSV"],
|
|
981
|
+
description="读取 CSV 文件并返回数据摘要",
|
|
982
|
+
params={"path": str},
|
|
983
|
+
security="sec_read",
|
|
984
|
+
aliases=["/read_csv"],
|
|
985
|
+
)
|
|
986
|
+
def _read_csv(deps, **kwargs):
|
|
987
|
+
from fr_cli.weapon.dataframe import read_csv
|
|
988
|
+
res, err = read_csv(kwargs["path"], lang=deps.lang)
|
|
989
|
+
return (res, None) if not err else (None, err)
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
# ------------------------------------------------------------------
|
|
993
|
+
# ========== MCP 外部神通 ==========
|
|
994
|
+
# ------------------------------------------------------------------
|
|
995
|
+
@register(
|
|
996
|
+
name="mcp_list",
|
|
997
|
+
description="列出已配置的 MCP 服务器及其可用工具",
|
|
998
|
+
params={},
|
|
999
|
+
aliases=["/mcp_list"],
|
|
1000
|
+
)
|
|
1001
|
+
def _mcp_list(deps, **kwargs):
|
|
1002
|
+
mcp = getattr(deps, "mcp", None)
|
|
1003
|
+
if not mcp:
|
|
1004
|
+
return None, "MCP 管理器未初始化"
|
|
1005
|
+
servers = mcp.list_servers()
|
|
1006
|
+
if not servers:
|
|
1007
|
+
return "暂无 MCP 服务器配置。", None
|
|
1008
|
+
lines = ["📡 MCP 服务器列表:"]
|
|
1009
|
+
for s in servers:
|
|
1010
|
+
status = f"{GREEN}● 启用{RESET}" if s.get("enabled", True) else f"{RED}● 禁用{RESET}"
|
|
1011
|
+
lines.append(f" [{s['name']}] {status} | 传输: {s.get('transport', 'stdio')} | 命令: {s.get('command', 'N/A')}")
|
|
1012
|
+
return "\n".join(lines), None
|
|
1013
|
+
|
|
1014
|
+
|
|
1015
|
+
@register(
|
|
1016
|
+
name="mcp_call",
|
|
1017
|
+
description="调用指定 MCP 服务器的工具",
|
|
1018
|
+
params={"server": str, "tool": str, "arguments": dict},
|
|
1019
|
+
aliases=["/mcp_call"],
|
|
1020
|
+
)
|
|
1021
|
+
def _mcp_call(deps, **kwargs):
|
|
1022
|
+
mcp = getattr(deps, "mcp", None)
|
|
1023
|
+
if not mcp:
|
|
1024
|
+
return None, "MCP 管理器未初始化"
|
|
1025
|
+
server = kwargs.get("server", "")
|
|
1026
|
+
tool = kwargs.get("tool", "")
|
|
1027
|
+
arguments = kwargs.get("arguments", {})
|
|
1028
|
+
if isinstance(arguments, str):
|
|
1029
|
+
try:
|
|
1030
|
+
arguments = json.loads(arguments)
|
|
1031
|
+
except Exception:
|
|
1032
|
+
arguments = {}
|
|
1033
|
+
result, err = mcp.call_tool(server, tool, arguments)
|
|
1034
|
+
return (result, None) if not err else (None, err)
|