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,154 @@
|
|
|
1
|
+
"""
|
|
2
|
+
自动更新模块 update.py
|
|
3
|
+
接口地址: https://up.seeknew.cn/v1/cliupdate
|
|
4
|
+
"""
|
|
5
|
+
import os, sys, json, platform, subprocess, shutil, tempfile, time, zipfile, hashlib
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Callable, Optional, Tuple
|
|
8
|
+
|
|
9
|
+
UPDATE_API_URL = "https://up.seeknew.cn/v1/cliupdate"
|
|
10
|
+
REQUEST_TIMEOUT = 15
|
|
11
|
+
PROJECT_ROOT = Path(__file__).resolve().parent
|
|
12
|
+
VERSION_FILE = PROJECT_ROOT / "__version__.txt"
|
|
13
|
+
|
|
14
|
+
def _read_local_version() -> str:
|
|
15
|
+
try:
|
|
16
|
+
if VERSION_FILE.is_file():
|
|
17
|
+
txt = VERSION_FILE.read_text(encoding="utf-8").strip()
|
|
18
|
+
if txt:
|
|
19
|
+
first_line = txt.splitlines()[0].strip().lower().lstrip('v')
|
|
20
|
+
return first_line
|
|
21
|
+
except Exception: pass
|
|
22
|
+
# Fallback to package __version__
|
|
23
|
+
try:
|
|
24
|
+
from fr_cli import __version__
|
|
25
|
+
return str(__version__).strip().lower().lstrip('v')
|
|
26
|
+
except Exception:
|
|
27
|
+
return "0.0.0"
|
|
28
|
+
|
|
29
|
+
def _save_local_version(version: str) -> None:
|
|
30
|
+
try: VERSION_FILE.write_text(str(version).strip() + "\n", encoding="utf-8")
|
|
31
|
+
except Exception: pass
|
|
32
|
+
|
|
33
|
+
def _parse_version_tuple(v: str) -> Tuple[int, ...]:
|
|
34
|
+
parts = v.replace("-", ".").split(".")
|
|
35
|
+
nums = []
|
|
36
|
+
for p in parts:
|
|
37
|
+
p = p.strip(); num = 0
|
|
38
|
+
if p and p[0].isdigit():
|
|
39
|
+
i = 0
|
|
40
|
+
while i < len(p) and p[i].isdigit(): i += 1
|
|
41
|
+
num = int(p[:i])
|
|
42
|
+
nums.append(num)
|
|
43
|
+
while len(nums) < 3: nums.append(0)
|
|
44
|
+
return tuple(nums[:3])
|
|
45
|
+
|
|
46
|
+
def _is_newer(remote: str, local: str) -> bool:
|
|
47
|
+
return _parse_version_tuple(remote) > _parse_version_tuple(local)
|
|
48
|
+
|
|
49
|
+
def _fetch_info() -> Tuple[Optional[dict], Optional[str]]:
|
|
50
|
+
try:
|
|
51
|
+
import urllib.request, ssl
|
|
52
|
+
ctx = ssl.create_default_context()
|
|
53
|
+
req = urllib.request.Request(UPDATE_API_URL, method="GET", headers={
|
|
54
|
+
"Accept": "application/json", "User-Agent": f"cli-update/{platform.system()}"
|
|
55
|
+
})
|
|
56
|
+
with urllib.request.urlopen(req, timeout=REQUEST_TIMEOUT, context=ctx) as resp:
|
|
57
|
+
data = json.loads(resp.read().decode("utf-8", errors="ignore"))
|
|
58
|
+
if isinstance(data, dict) and "version" in data and "download_url" in data: return data, None
|
|
59
|
+
if isinstance(data, dict) and "data" in data and isinstance(data["data"], dict):
|
|
60
|
+
inner = data["data"]
|
|
61
|
+
if "version" in inner and "download_url" in inner: return inner, None
|
|
62
|
+
return None, "Unrecognized schema."
|
|
63
|
+
except Exception as e: return None, str(e)
|
|
64
|
+
|
|
65
|
+
def _download(url: str) -> Tuple[Optional[bytes], Optional[str]]:
|
|
66
|
+
try:
|
|
67
|
+
import urllib.request, ssl
|
|
68
|
+
req = urllib.request.Request(url, headers={"User-Agent": "cli-update/1.0"})
|
|
69
|
+
with urllib.request.urlopen(req, timeout=120, context=ssl.create_default_context()) as resp:
|
|
70
|
+
data = resp.read()
|
|
71
|
+
return (data, None) if data else (None, "Empty payload.")
|
|
72
|
+
except Exception as e: return None, str(e)
|
|
73
|
+
|
|
74
|
+
def _apply_source_zip(zip_bytes: bytes, root: Path) -> Tuple[bool, str]:
|
|
75
|
+
try:
|
|
76
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
77
|
+
zpath = Path(tmp) / "u.zip"; zpath.write_bytes(zip_bytes)
|
|
78
|
+
ext = Path(tmp) / "ext"
|
|
79
|
+
with zipfile.ZipFile(zpath, 'r') as zf: zf.extractall(ext)
|
|
80
|
+
|
|
81
|
+
safe_exts = {".py",".sh",".bat",".md",".txt",".toml",".yaml",".yml",".html",".css",".js",".png",".jpg",".gif"}
|
|
82
|
+
skip = {"__version__.txt", "__config__.json"}
|
|
83
|
+
skip_dirs = {"data", "logs", "__pycache__", ".git", ".idea"}
|
|
84
|
+
|
|
85
|
+
for r, dirs, files in os.walk(ext):
|
|
86
|
+
rp = Path(r).relative_to(ext)
|
|
87
|
+
if any(p in skip_dirs for p in rp.parts): continue
|
|
88
|
+
for n in files:
|
|
89
|
+
s = Path(r) / n; d = root / rp / n
|
|
90
|
+
if n in skip or n.startswith(".") or d.suffix.lower() not in safe_exts: continue
|
|
91
|
+
try:
|
|
92
|
+
d.parent.mkdir(parents=True, exist_ok=True)
|
|
93
|
+
if d.exists():
|
|
94
|
+
shutil.copy2(str(d), str(d.with_suffix(d.suffix+".bak")))
|
|
95
|
+
shutil.copy2(str(s), str(d))
|
|
96
|
+
except Exception: pass
|
|
97
|
+
return True, "OK"
|
|
98
|
+
except Exception as e: return False, str(e)
|
|
99
|
+
|
|
100
|
+
def update_check(local_version: Optional[str] = None, verbose: bool = False) -> Tuple[bool, Optional[dict], Optional[str]]:
|
|
101
|
+
lv = local_version or _read_local_version()
|
|
102
|
+
if verbose: print(f"[Update] Local: {lv}")
|
|
103
|
+
info, err = _fetch_info()
|
|
104
|
+
if err: return False, None, err
|
|
105
|
+
rv = info.get("version", "0.0.0")
|
|
106
|
+
if verbose: print(f"[Update] Remote: {rv}")
|
|
107
|
+
return _is_newer(rv, lv), info, None
|
|
108
|
+
|
|
109
|
+
def update_and_restart(local_version: Optional[str] = None, verbose: bool = False, allow_restart: bool = True, on_before_restart: Optional[Callable] = None) -> Tuple[bool, str]:
|
|
110
|
+
has, info, err = update_check(local_version, verbose)
|
|
111
|
+
if err: return False, err
|
|
112
|
+
if not has or not info: return False, "Already up to date."
|
|
113
|
+
|
|
114
|
+
data, err = _download(info.get("download_url", ""))
|
|
115
|
+
if err or not data: return False, err or "Download failed."
|
|
116
|
+
|
|
117
|
+
sha_exp = info.get("sha256")
|
|
118
|
+
if sha_exp and hashlib.sha256(data).hexdigest().lower() != sha_exp.lower():
|
|
119
|
+
return False, "SHA256 mismatch."
|
|
120
|
+
|
|
121
|
+
ftype = info.get("file_type", "source_zip")
|
|
122
|
+
nver = info.get("version", "")
|
|
123
|
+
|
|
124
|
+
if ftype == "source_zip":
|
|
125
|
+
ok, msg = _apply_source_zip(data, PROJECT_ROOT)
|
|
126
|
+
if not ok: return False, msg
|
|
127
|
+
_save_local_version(nver)
|
|
128
|
+
if allow_restart:
|
|
129
|
+
if on_before_restart: on_before_restart()
|
|
130
|
+
subprocess.Popen([sys.executable, str(PROJECT_ROOT / "main.py")], cwd=str(PROJECT_ROOT))
|
|
131
|
+
time.sleep(0.5)
|
|
132
|
+
sys.stdout.flush()
|
|
133
|
+
sys.stderr.flush()
|
|
134
|
+
sys.exit(0)
|
|
135
|
+
return True, f"Updated to {nver}. Please restart."
|
|
136
|
+
else:
|
|
137
|
+
fname = os.path.basename(info.get("download_url", "update.bin"))
|
|
138
|
+
(PROJECT_ROOT / fname).write_bytes(data)
|
|
139
|
+
return True, f"Saved {fname}. Please update manually."
|
|
140
|
+
|
|
141
|
+
def cli_entry(args=None):
|
|
142
|
+
args = args or sys.argv[1:]
|
|
143
|
+
if not args or args[0].lower() == "check":
|
|
144
|
+
ok, info, err = update_check(verbose=True)
|
|
145
|
+
if err: print(f"Check failed: {err}"); return 1
|
|
146
|
+
if not ok: print("Already up to date."); return 0
|
|
147
|
+
print(f"New version: {info.get('version')}\nNote: {info.get('release_note', '')}"); return 0
|
|
148
|
+
elif args[0].lower() == "run":
|
|
149
|
+
ok, msg = update_and_restart(verbose=True)
|
|
150
|
+
print(f"{'Success' if ok else 'Failed'}: {msg}"); return 0 if ok else 1
|
|
151
|
+
else:
|
|
152
|
+
print("Usage: python update.py check | run"); return 2
|
|
153
|
+
|
|
154
|
+
if __name__ == "__main__": sys.exit(cli_entry())
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""
|
|
2
|
+
命令执行引擎
|
|
3
|
+
负责解析 AI 响应中的调用标记,并调度到统一注册表执行。
|
|
4
|
+
"""
|
|
5
|
+
import re
|
|
6
|
+
import json
|
|
7
|
+
import ast
|
|
8
|
+
from types import SimpleNamespace
|
|
9
|
+
from fr_cli.command.registry import get_registry
|
|
10
|
+
from fr_cli.addon.plugin import exec_plugin
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _build_deps(state):
|
|
14
|
+
"""根据 AppState 动态构建依赖命名空间(每次调用实时反射,避免快照过时)"""
|
|
15
|
+
return SimpleNamespace(
|
|
16
|
+
vfs=state.vfs,
|
|
17
|
+
mail_c=state.mail_c,
|
|
18
|
+
web_c=state.web_c,
|
|
19
|
+
disk_c=state.disk_c,
|
|
20
|
+
plugins=state.plugins,
|
|
21
|
+
lang=state.lang,
|
|
22
|
+
security=state.security,
|
|
23
|
+
cfg=state.cfg,
|
|
24
|
+
client=state.client,
|
|
25
|
+
model_name=state.model_name,
|
|
26
|
+
mcp=getattr(state, "mcp", None),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class CommandExecutor:
|
|
31
|
+
"""
|
|
32
|
+
命令执行器:解析 AI 回复中的调用标记,并通过注册表统一调度执行。
|
|
33
|
+
直接持有 AppState,每次调用时动态构建依赖快照,彻底消除状态过时问题。
|
|
34
|
+
|
|
35
|
+
公共接口(保持向后兼容):
|
|
36
|
+
- invoke_tool(tool_name, kwargs, msgs=None): 结构化工具调用
|
|
37
|
+
- execute(cmd_str, msgs=None): 命令字符串调用
|
|
38
|
+
- process_ai_commands(ai_response, msgs=None): 解析并执行 AI 回复中的命令标记
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, state):
|
|
42
|
+
self.state = state
|
|
43
|
+
self._reg = get_registry()
|
|
44
|
+
|
|
45
|
+
# ------------------------------------------------------------------
|
|
46
|
+
# 第一层:结构化工具调用
|
|
47
|
+
# ------------------------------------------------------------------
|
|
48
|
+
def invoke_tool(self, tool_name, kwargs, msgs=None):
|
|
49
|
+
"""根据工具名和结构化参数,通过注册表调度执行。返回 (result, error)"""
|
|
50
|
+
return self._reg.dispatch(_build_deps(self.state), tool_name, msgs=msgs, **kwargs)
|
|
51
|
+
|
|
52
|
+
# ------------------------------------------------------------------
|
|
53
|
+
# 第二层:传统命令解析(用户输入 / 插件调用)
|
|
54
|
+
# ------------------------------------------------------------------
|
|
55
|
+
def execute(self, cmd_str, msgs=None):
|
|
56
|
+
"""执行单个命令并返回结果 (result, error)
|
|
57
|
+
已分词检查插件后,直接通过注册表内部接口调度,避免重复 split。"""
|
|
58
|
+
parts = cmd_str.strip().split()
|
|
59
|
+
if not parts:
|
|
60
|
+
return None, "Empty command"
|
|
61
|
+
cmd = parts[0].lstrip("/")
|
|
62
|
+
# 插件命令优先直接处理(保持 mock 路径兼容)
|
|
63
|
+
if cmd in self.state.plugins:
|
|
64
|
+
p_args = ' '.join(parts[1:]) if len(parts) > 1 else ""
|
|
65
|
+
exec_plugin(cmd, self.state.plugins[cmd], p_args, self.state.lang)
|
|
66
|
+
return f"Plugin {cmd} executed", None
|
|
67
|
+
# 其余命令通过注册表内部接口直接调度,避免 dispatch_cmd 再次 split
|
|
68
|
+
return self._reg._dispatch_cmd_parts(_build_deps(self.state), parts, msgs=msgs)
|
|
69
|
+
|
|
70
|
+
# ------------------------------------------------------------------
|
|
71
|
+
# 第三层:AI 回复解析
|
|
72
|
+
# ------------------------------------------------------------------
|
|
73
|
+
def _loose_parse_kwargs(self, arg_str):
|
|
74
|
+
"""宽松解析 JSON 参数字符串(回退方案)"""
|
|
75
|
+
key_pattern = r'"(\w+)"\s*:\s*'
|
|
76
|
+
keys = list(re.finditer(key_pattern, arg_str))
|
|
77
|
+
if not keys:
|
|
78
|
+
return None
|
|
79
|
+
result = {}
|
|
80
|
+
for i, m in enumerate(keys):
|
|
81
|
+
key = m.group(1)
|
|
82
|
+
start = m.end()
|
|
83
|
+
if i + 1 < len(keys):
|
|
84
|
+
end = keys[i + 1].start()
|
|
85
|
+
else:
|
|
86
|
+
end = len(arg_str)
|
|
87
|
+
while end > 0 and arg_str[end - 1] in ' \t\n\r}':
|
|
88
|
+
end -= 1
|
|
89
|
+
val_str = arg_str[start:end].strip().rstrip(',').strip()
|
|
90
|
+
|
|
91
|
+
# 布尔值
|
|
92
|
+
if val_str == 'true':
|
|
93
|
+
result[key] = True
|
|
94
|
+
continue
|
|
95
|
+
if val_str == 'false':
|
|
96
|
+
result[key] = False
|
|
97
|
+
continue
|
|
98
|
+
if val_str == 'null':
|
|
99
|
+
result[key] = None
|
|
100
|
+
continue
|
|
101
|
+
# 数字
|
|
102
|
+
try:
|
|
103
|
+
if '.' in val_str:
|
|
104
|
+
result[key] = float(val_str)
|
|
105
|
+
else:
|
|
106
|
+
result[key] = int(val_str)
|
|
107
|
+
continue
|
|
108
|
+
except ValueError:
|
|
109
|
+
pass
|
|
110
|
+
# 字符串(去掉两端引号)
|
|
111
|
+
if val_str.startswith('"') and val_str.endswith('"'):
|
|
112
|
+
val_str = val_str[1:-1]
|
|
113
|
+
# 还原转义序列
|
|
114
|
+
QUOTE_PH = '\x00Q\x00'
|
|
115
|
+
val_str = val_str.replace('\\"', QUOTE_PH)
|
|
116
|
+
val_str = val_str.replace('\\\\', '\\')
|
|
117
|
+
val_str = val_str.replace('\\n', '\n')
|
|
118
|
+
val_str = val_str.replace('\\t', '\t')
|
|
119
|
+
val_str = val_str.replace('\\r', '\r')
|
|
120
|
+
val_str = val_str.replace(QUOTE_PH, '"')
|
|
121
|
+
result[key] = val_str
|
|
122
|
+
return result if result is not None else None
|
|
123
|
+
|
|
124
|
+
def _parse_tool_kwargs(self, arg_str):
|
|
125
|
+
"""安全解析工具参数字符串(JSON 或 Python dict)"""
|
|
126
|
+
arg_str = arg_str.strip()
|
|
127
|
+
if not arg_str:
|
|
128
|
+
return {}
|
|
129
|
+
|
|
130
|
+
# 预处理:将 JSON 字符串值内的原始换行替换为 \n 转义序列
|
|
131
|
+
fixed = ""
|
|
132
|
+
in_string = False
|
|
133
|
+
escape = False
|
|
134
|
+
for ch in arg_str:
|
|
135
|
+
if escape:
|
|
136
|
+
fixed += ch
|
|
137
|
+
escape = False
|
|
138
|
+
continue
|
|
139
|
+
if ch == '\\':
|
|
140
|
+
fixed += ch
|
|
141
|
+
escape = True
|
|
142
|
+
continue
|
|
143
|
+
if ch == '"':
|
|
144
|
+
in_string = not in_string
|
|
145
|
+
fixed += ch
|
|
146
|
+
continue
|
|
147
|
+
if ch in '\n\r' and in_string:
|
|
148
|
+
fixed += '\\n'
|
|
149
|
+
continue
|
|
150
|
+
fixed += ch
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
return json.loads(fixed)
|
|
154
|
+
except json.JSONDecodeError:
|
|
155
|
+
try:
|
|
156
|
+
return ast.literal_eval(arg_str)
|
|
157
|
+
except (ValueError, SyntaxError):
|
|
158
|
+
return self._loose_parse_kwargs(arg_str)
|
|
159
|
+
|
|
160
|
+
def _extract_tool_calls(self, text):
|
|
161
|
+
"""从文本中提取所有【调用:tool_name({...})】标记(支持嵌套括号,忽略字符串内的括号)"""
|
|
162
|
+
calls = []
|
|
163
|
+
i = 0
|
|
164
|
+
while True:
|
|
165
|
+
start = text.find('【调用:', i)
|
|
166
|
+
if start == -1:
|
|
167
|
+
break
|
|
168
|
+
paren = text.find('(', start)
|
|
169
|
+
if paren == -1:
|
|
170
|
+
break
|
|
171
|
+
tool_name = text[start + 4:paren].strip()
|
|
172
|
+
depth = 1
|
|
173
|
+
j = paren + 1
|
|
174
|
+
in_string = False
|
|
175
|
+
escape = False
|
|
176
|
+
while j < len(text) and depth > 0:
|
|
177
|
+
ch = text[j]
|
|
178
|
+
if escape:
|
|
179
|
+
escape = False
|
|
180
|
+
elif ch == '\\':
|
|
181
|
+
escape = True
|
|
182
|
+
elif ch == '"':
|
|
183
|
+
in_string = not in_string
|
|
184
|
+
elif not in_string:
|
|
185
|
+
if ch == '(':
|
|
186
|
+
depth += 1
|
|
187
|
+
elif ch == ')':
|
|
188
|
+
depth -= 1
|
|
189
|
+
j += 1
|
|
190
|
+
if depth != 0:
|
|
191
|
+
i = paren + 1
|
|
192
|
+
continue
|
|
193
|
+
arg_str = text[paren + 1:j - 1]
|
|
194
|
+
end = text.find('】', j - 1)
|
|
195
|
+
if end == -1:
|
|
196
|
+
break
|
|
197
|
+
marker = text[start:end + 1]
|
|
198
|
+
calls.append((tool_name, arg_str, marker))
|
|
199
|
+
i = end + 1
|
|
200
|
+
return calls
|
|
201
|
+
|
|
202
|
+
def process_ai_commands(self, ai_response, msgs=None):
|
|
203
|
+
"""
|
|
204
|
+
解析AI响应中的调用标记并自动执行
|
|
205
|
+
支持三种格式:
|
|
206
|
+
1. 【调用:tool_name({"参数": "值"})】(结构化调用)
|
|
207
|
+
2. 【命令:/command args】(插件 / 兼容命令)
|
|
208
|
+
3. file_operations/xxx(兼容旧模型输出)
|
|
209
|
+
返回 (clean_response, cmd_results)
|
|
210
|
+
"""
|
|
211
|
+
results = []
|
|
212
|
+
markers_to_remove = []
|
|
213
|
+
|
|
214
|
+
# ===== 格式1:【调用:...】 =====
|
|
215
|
+
for tool_name, arg_str, marker in self._extract_tool_calls(ai_response):
|
|
216
|
+
kwargs = self._parse_tool_kwargs(arg_str)
|
|
217
|
+
if kwargs is None:
|
|
218
|
+
results.append(f"❌ 参数解析失败: {tool_name}\n 原始参数: {arg_str}")
|
|
219
|
+
markers_to_remove.append(marker)
|
|
220
|
+
continue
|
|
221
|
+
result, error = self.invoke_tool(tool_name, kwargs, msgs)
|
|
222
|
+
if error:
|
|
223
|
+
results.append(f"❌ 工具调用失败: {tool_name}\n {error}")
|
|
224
|
+
else:
|
|
225
|
+
results.append(f"✅ 工具调用成功: {tool_name}\n 结果: {result}")
|
|
226
|
+
markers_to_remove.append(marker)
|
|
227
|
+
|
|
228
|
+
# ===== 格式2:【命令:...】 =====
|
|
229
|
+
pattern_cmd = r'【命令:(.*?)】'
|
|
230
|
+
for m in re.finditer(pattern_cmd, ai_response):
|
|
231
|
+
cmd_str = m.group(1).strip()
|
|
232
|
+
marker = m.group(0)
|
|
233
|
+
result, error = self.execute(cmd_str, msgs)
|
|
234
|
+
if error:
|
|
235
|
+
results.append(f"❌ 命令执行失败: {cmd_str}\n {error}")
|
|
236
|
+
else:
|
|
237
|
+
results.append(f"✅ 命令执行成功: {cmd_str}\n 结果: {result}")
|
|
238
|
+
markers_to_remove.append(marker)
|
|
239
|
+
|
|
240
|
+
# ===== 格式3:file_operations/xxx(兼容) =====
|
|
241
|
+
pattern2_quoted = r'file_operations\s*/(\w+)\s+(\S+)\s+"([\s\S]*?)"'
|
|
242
|
+
pattern2_plain = r'file_operations\s*/(\w+)\s+(.+)$'
|
|
243
|
+
for m in re.finditer(pattern2_quoted, ai_response):
|
|
244
|
+
action = m.group(1)
|
|
245
|
+
path = m.group(2)
|
|
246
|
+
content = m.group(3)
|
|
247
|
+
cmd_str = f"/{action} {path} {content}"
|
|
248
|
+
result, error = self.execute(cmd_str, msgs)
|
|
249
|
+
if error:
|
|
250
|
+
results.append(f"❌ 命令执行失败: {cmd_str}\n {error}")
|
|
251
|
+
else:
|
|
252
|
+
results.append(f"✅ 命令执行成功: {cmd_str}\n 结果: {result}")
|
|
253
|
+
markers_to_remove.append(m.group(0))
|
|
254
|
+
for m in re.finditer(pattern2_plain, ai_response, re.MULTILINE):
|
|
255
|
+
action = m.group(1)
|
|
256
|
+
args = m.group(2).strip()
|
|
257
|
+
if args.startswith('"') and args.endswith('"'):
|
|
258
|
+
args = args[1:-1]
|
|
259
|
+
already = any(m.group(0) in mk for mk in markers_to_remove)
|
|
260
|
+
if already:
|
|
261
|
+
continue
|
|
262
|
+
cmd_str = f"/{action} {args}"
|
|
263
|
+
result, error = self.execute(cmd_str, msgs)
|
|
264
|
+
if error:
|
|
265
|
+
results.append(f"❌ 命令执行失败: {cmd_str}\n {error}")
|
|
266
|
+
else:
|
|
267
|
+
results.append(f"✅ 命令执行成功: {cmd_str}\n 结果: {result}")
|
|
268
|
+
markers_to_remove.append(m.group(0))
|
|
269
|
+
|
|
270
|
+
# 清理回复文本:移除命令标记后,仅压缩因移除标记产生的连续多余空行,并去除首尾空白
|
|
271
|
+
clean_response = ai_response
|
|
272
|
+
for marker in markers_to_remove:
|
|
273
|
+
clean_response = clean_response.replace(marker, "")
|
|
274
|
+
clean_response = re.sub(r'\n\s*\n\s*\n+', '\n\n', clean_response).strip()
|
|
275
|
+
|
|
276
|
+
return clean_response, results
|