codebee 0.1.3 → 0.1.4
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.
- package/app/core/builtin_agent.py +382 -0
- package/app/core/flows.py +1 -1
- package/app/core/jobs.py +441 -424
- package/app/core/pipeline.py +134 -31
- package/app/main.py +16 -5
- package/app/ui/app.js +3 -1
- package/app/ui/i18n.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""内置执行智能体:不经外部 CLI,直连供应商 API 完成直连(direct)任务。
|
|
3
|
+
|
|
4
|
+
为什么:direct 是快档位,典型诉求是问答/轻量改写。拉起 codex/claude CLI 要付
|
|
5
|
+
进程与框架开销,且 CLI 的事件流日志(启动命令、提示词回显、重连报错)会污染
|
|
6
|
+
对话视图。内置智能体直接打 chat API + 工具循环:
|
|
7
|
+
- 模型选择:编排者配置(resolve_orchestrator)优先;未启用时扫首个可用供应商;
|
|
8
|
+
- 工具:list_files / read_file / write_file,全部锁死在工作目录内;
|
|
9
|
+
- 协议:anthropic / openai 走原生工具循环;google 无工具协议(纯文本直答)。
|
|
10
|
+
密钥冷却/多 KEY 展开复用 modelhub 既有记账(note_key_ok / note_key_error)。
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from . import modelhub
|
|
19
|
+
|
|
20
|
+
MAX_TOOL_ITERS = 16 # 单步工具循环上限(防模型打转)
|
|
21
|
+
READ_MAX_BYTES = 64 * 1024 # read_file 单次读取上限
|
|
22
|
+
LIST_MAX_ENTRIES = 200
|
|
23
|
+
|
|
24
|
+
_SYSTEM_PROMPT = """你是 CodeBee 的内置执行智能体,直接完成用户交代的任务。用户的目标、背景与工作目录内的附件就是全部输入。
|
|
25
|
+
|
|
26
|
+
## 工作方式
|
|
27
|
+
- 需要读文件、看目录、写文件时调用工具;所有路径都是工作目录内的相对路径。
|
|
28
|
+
- 产出文件一律 UTF-8 编码。
|
|
29
|
+
- 回答用户的语言与用户一致(默认中文)。直接给结论和内容,不要输出任何机器标记或协议行。"""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def resolve():
|
|
33
|
+
"""选内置智能体可用的 (provider, model):编排者配置优先,否则首个可用供应商。
|
|
34
|
+
|
|
35
|
+
返回 {prov, model, provider_id, provider_name} 或 None(无可用供应商 → 调用方
|
|
36
|
+
回退 CLI 路径)。"""
|
|
37
|
+
orch = None
|
|
38
|
+
try:
|
|
39
|
+
orch = modelhub.resolve_orchestrator()
|
|
40
|
+
except Exception:
|
|
41
|
+
orch = None
|
|
42
|
+
if orch:
|
|
43
|
+
prov, model = orch
|
|
44
|
+
return {"prov": prov, "model": model,
|
|
45
|
+
"provider_id": prov.get("id") or "",
|
|
46
|
+
"provider_name": prov.get("name") or prov.get("id") or ""}
|
|
47
|
+
for p in modelhub.providers():
|
|
48
|
+
if not p.get("enabled", True) or not p.get("api_key"):
|
|
49
|
+
continue
|
|
50
|
+
m = (p.get("model") or "").strip()
|
|
51
|
+
if not m:
|
|
52
|
+
try:
|
|
53
|
+
names = modelhub._enabled_models(p)
|
|
54
|
+
except Exception:
|
|
55
|
+
names = []
|
|
56
|
+
m = names[0]["name"] if names else ""
|
|
57
|
+
if m:
|
|
58
|
+
return {"prov": p, "model": m, "provider_id": p.get("id") or "",
|
|
59
|
+
"provider_name": p.get("name") or p.get("id") or ""}
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ---------------------------------------------------------------- 工作目录工具
|
|
64
|
+
# 模型给的相对路径不可信:逐段拒绝 .. 与盘符,resolve() 归一后确认仍位于
|
|
65
|
+
# 工作目录之下(base not in p.parents 即越界,同 skills/market 的守卫惯用法)。
|
|
66
|
+
|
|
67
|
+
def _tool_list_files(workdir, args):
|
|
68
|
+
rel = str(args.get("path") or "").replace("\\", "/").strip("/")
|
|
69
|
+
if ".." in Path(rel).parts or any(":" in seg for seg in Path(rel).parts):
|
|
70
|
+
return "(非法路径: %s)" % rel
|
|
71
|
+
base = Path(workdir or ".").resolve()
|
|
72
|
+
p = (base / rel).resolve() if rel else base
|
|
73
|
+
if base not in p.parents and p != base:
|
|
74
|
+
return "(路径越界: %s)" % rel
|
|
75
|
+
if not p.is_dir():
|
|
76
|
+
return "(目录不存在: %s)" % (rel or ".")
|
|
77
|
+
out = []
|
|
78
|
+
for root, dirs, files in os.walk(str(p)):
|
|
79
|
+
dirs[:] = [d for d in dirs if d not in (".git", "__pycache__", "node_modules")]
|
|
80
|
+
for f in files:
|
|
81
|
+
fp = os.path.join(root, f)
|
|
82
|
+
relp = os.path.relpath(fp, str(base)).replace(os.sep, "/")
|
|
83
|
+
try:
|
|
84
|
+
size = os.path.getsize(fp)
|
|
85
|
+
except OSError:
|
|
86
|
+
size = -1
|
|
87
|
+
out.append("%s (%d B)" % (relp, size))
|
|
88
|
+
if len(out) >= LIST_MAX_ENTRIES:
|
|
89
|
+
return "\n".join(out) + "\n…(截断,共 200+ 项)"
|
|
90
|
+
return "\n".join(out) or "(空目录)"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _tool_read_file(workdir, args):
|
|
94
|
+
rel = str(args.get("path") or "").replace("\\", "/").strip("/")
|
|
95
|
+
if not rel or ".." in Path(rel).parts or any(":" in seg for seg in Path(rel).parts):
|
|
96
|
+
return "(非法路径: %s)" % rel
|
|
97
|
+
base = Path(workdir or ".").resolve()
|
|
98
|
+
p = (base / rel).resolve()
|
|
99
|
+
if base not in p.parents:
|
|
100
|
+
return "(路径越界: %s)" % rel
|
|
101
|
+
if not p.is_file():
|
|
102
|
+
return "(文件不存在: %s)" % rel
|
|
103
|
+
data = p.read_bytes()[:READ_MAX_BYTES + 1]
|
|
104
|
+
truncated = len(data) > READ_MAX_BYTES
|
|
105
|
+
text = data[:READ_MAX_BYTES].decode("utf-8", "replace")
|
|
106
|
+
return ("…(超过 64KB 已截断)\n" if truncated else "") + text
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _tool_write_file(workdir, args):
|
|
110
|
+
rel = str(args.get("path") or "").replace("\\", "/").strip("/")
|
|
111
|
+
if not rel or ".." in Path(rel).parts or any(":" in seg for seg in Path(rel).parts):
|
|
112
|
+
return "(非法路径: %s)" % rel
|
|
113
|
+
content = str(args.get("content") if args.get("content") is not None else "")
|
|
114
|
+
base = Path(workdir or ".").resolve()
|
|
115
|
+
dest = (base / rel).resolve()
|
|
116
|
+
if base not in dest.parents:
|
|
117
|
+
return "(路径越界: %s)" % rel
|
|
118
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
119
|
+
dest.write_text(content, encoding="utf-8")
|
|
120
|
+
return "已写入 %s(%d 字符,UTF-8)" % (rel, len(content))
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
TOOLS_SPEC = [
|
|
124
|
+
{"name": "list_files", "description": "列出工作目录(或其子目录)下的文件",
|
|
125
|
+
"args": {"path": "子目录相对路径,留空=根目录"}},
|
|
126
|
+
{"name": "read_file", "description": "读取工作目录内一个文本文件(UTF-8,超 64KB 截断)",
|
|
127
|
+
"args": {"path": "文件相对路径"}},
|
|
128
|
+
{"name": "write_file", "description": "把文本内容写入工作目录内一个文件(UTF-8,父目录自动创建)",
|
|
129
|
+
"args": {"path": "文件相对路径", "content": "完整文本内容"}},
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
_TOOL_IMPL = {"list_files": _tool_list_files, "read_file": _tool_read_file,
|
|
133
|
+
"write_file": _tool_write_file}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _exec_tool(workdir, name, args):
|
|
137
|
+
fn = _TOOL_IMPL.get(name or "")
|
|
138
|
+
if fn is None:
|
|
139
|
+
return "(未知工具: %s)" % name
|
|
140
|
+
try:
|
|
141
|
+
return fn(workdir, args or {})
|
|
142
|
+
except Exception as e:
|
|
143
|
+
return "工具执行失败: %s" % (e)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# ---------------------------------------------------------------- 协议适配
|
|
147
|
+
|
|
148
|
+
def _openai_tools():
|
|
149
|
+
return [{"type": "function", "function": {
|
|
150
|
+
"name": t["name"], "description": t["description"],
|
|
151
|
+
"parameters": {"type": "object",
|
|
152
|
+
"properties": {k: {"type": "string", "description": v}
|
|
153
|
+
for k, v in t["args"].items()},
|
|
154
|
+
"required": list(t["args"].keys())}}}
|
|
155
|
+
for t in TOOLS_SPEC]
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _anthropic_tools():
|
|
159
|
+
return [{"name": t["name"], "description": t["description"],
|
|
160
|
+
"input_schema": {"type": "object",
|
|
161
|
+
"properties": {k: {"type": "string", "description": v}
|
|
162
|
+
for k, v in t["args"].items()},
|
|
163
|
+
"required": list(t["args"].keys())}}
|
|
164
|
+
for t in TOOLS_SPEC]
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _post_json(url, headers, body, allow_private, timeout):
|
|
168
|
+
"""传输层单点(测试在这里打桩)。"""
|
|
169
|
+
return modelhub._post_json_http(url, headers, body, allow_private, timeout=timeout)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _m_openai(m):
|
|
173
|
+
"""内部消息 → openai 消息。tool_results: [(call_id, 结果文本)]。"""
|
|
174
|
+
if m["role"] == "assistant":
|
|
175
|
+
out = {"role": "assistant", "content": m.get("content") or ""}
|
|
176
|
+
if m.get("tool_calls"):
|
|
177
|
+
out["tool_calls"] = [{"id": tc["id"], "type": "function",
|
|
178
|
+
"function": {"name": tc["name"],
|
|
179
|
+
"arguments": json.dumps(tc["args"], ensure_ascii=False)}}
|
|
180
|
+
for tc in m["tool_calls"]]
|
|
181
|
+
return out
|
|
182
|
+
if m["role"] == "tool_results":
|
|
183
|
+
return [{"role": "tool", "tool_call_id": cid, "content": text}
|
|
184
|
+
for cid, text in m["tool_results"]]
|
|
185
|
+
return {"role": "user", "content": m.get("content") or ""}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _m_anthropic(m):
|
|
189
|
+
if m["role"] == "assistant":
|
|
190
|
+
blocks = []
|
|
191
|
+
if m.get("content"):
|
|
192
|
+
blocks.append({"type": "text", "text": m["content"]})
|
|
193
|
+
for tc in (m.get("tool_calls") or []):
|
|
194
|
+
blocks.append({"type": "tool_use", "id": tc["id"], "name": tc["name"],
|
|
195
|
+
"input": tc["args"]})
|
|
196
|
+
return {"role": "assistant", "content": blocks or [{"type": "text", "text": ""}]}
|
|
197
|
+
if m["role"] == "tool_results":
|
|
198
|
+
return {"role": "user", "content": [
|
|
199
|
+
{"type": "tool_result", "tool_use_id": cid, "content": text}
|
|
200
|
+
for cid, text in m["tool_results"]]}
|
|
201
|
+
return {"role": "user", "content": [{"type": "text", "text": m.get("content") or ""}]}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _build_request(proto, base, use_key, model, system, msgs, with_tools):
|
|
205
|
+
"""按协议构造 (url, headers, body)。msgs 为内部统一形状。"""
|
|
206
|
+
base = (base or "").rstrip("/")
|
|
207
|
+
if proto == "google":
|
|
208
|
+
if base.endswith("/v1beta"):
|
|
209
|
+
url = base + "/models/%s:generateContent" % model
|
|
210
|
+
else:
|
|
211
|
+
url = base + "/v1beta/models/%s:generateContent" % model
|
|
212
|
+
headers = {"x-goog-api-key": use_key}
|
|
213
|
+
contents = [{"role": ("user" if m["role"] == "user" else "model"),
|
|
214
|
+
"parts": [{"text": m.get("content") or ""}]}
|
|
215
|
+
for m in msgs if m["role"] in ("user", "assistant")]
|
|
216
|
+
body = {"contents": contents,
|
|
217
|
+
"systemInstruction": {"parts": [{"text": system}]},
|
|
218
|
+
"generationConfig": {"maxOutputTokens": 8000}}
|
|
219
|
+
return url, headers, body
|
|
220
|
+
path = "/messages" if proto == "anthropic" else "/chat/completions"
|
|
221
|
+
url = (base + path) if base.endswith("/v1") else (base + "/v1" + path)
|
|
222
|
+
if proto == "anthropic":
|
|
223
|
+
headers = {"x-api-key": use_key, "anthropic-version": "2023-06-01"}
|
|
224
|
+
body = {"model": model, "max_tokens": 8000, "system": system,
|
|
225
|
+
"messages": [_m_anthropic(m) for m in msgs]}
|
|
226
|
+
if with_tools:
|
|
227
|
+
body["tools"] = _anthropic_tools()
|
|
228
|
+
return url, headers, body
|
|
229
|
+
headers = {"Authorization": "Bearer " + use_key}
|
|
230
|
+
# tool_results 消息展开成多条 role=tool(_m_openai 对它返回列表,不能嵌套)
|
|
231
|
+
msgs_wire = []
|
|
232
|
+
for m in msgs:
|
|
233
|
+
w = _m_openai(m)
|
|
234
|
+
if isinstance(w, list):
|
|
235
|
+
msgs_wire.extend(w)
|
|
236
|
+
else:
|
|
237
|
+
msgs_wire.append(w)
|
|
238
|
+
body = {"model": model, "max_tokens": 8000,
|
|
239
|
+
"messages": [{"role": "system", "content": system}] + msgs_wire}
|
|
240
|
+
if with_tools:
|
|
241
|
+
body["tools"] = _openai_tools()
|
|
242
|
+
return url, headers, body
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _parse_reply(proto, data):
|
|
246
|
+
"""从响应解析 → (文本, [tool_call], usage)。
|
|
247
|
+
tool_call = {id, name, args};usage = {input, output, cached, total}。"""
|
|
248
|
+
usage = {"input": 0, "output": 0, "cached": 0, "total": 0}
|
|
249
|
+
if proto == "google":
|
|
250
|
+
cand = (data.get("candidates") or [{}])[0]
|
|
251
|
+
parts = (cand.get("content") or {}).get("parts") or []
|
|
252
|
+
text = "\n".join(p.get("text", "") for p in parts if isinstance(p, dict))
|
|
253
|
+
um = data.get("usageMetadata") or {}
|
|
254
|
+
usage["input"] = int(um.get("promptTokenCount") or 0)
|
|
255
|
+
usage["output"] = int(um.get("candidatesTokenCount") or 0)
|
|
256
|
+
usage["total"] = int(um.get("totalTokenCount") or 0)
|
|
257
|
+
return text.strip(), [], usage
|
|
258
|
+
if proto == "anthropic":
|
|
259
|
+
blocks = data.get("content") or []
|
|
260
|
+
text = "\n".join(b.get("text", "") for b in blocks
|
|
261
|
+
if isinstance(b, dict) and b.get("type") == "text")
|
|
262
|
+
calls = [{"id": b.get("id") or "", "name": b.get("name") or "",
|
|
263
|
+
"args": b.get("input") or {}}
|
|
264
|
+
for b in blocks if isinstance(b, dict) and b.get("type") == "tool_use"]
|
|
265
|
+
u = data.get("usage") or {}
|
|
266
|
+
usage["input"] = int(u.get("input_tokens") or 0)
|
|
267
|
+
usage["output"] = int(u.get("output_tokens") or 0)
|
|
268
|
+
usage["cached"] = (int(u.get("cache_read_input_tokens") or 0)
|
|
269
|
+
+ int(u.get("cache_creation_input_tokens") or 0))
|
|
270
|
+
usage["total"] = usage["input"] + usage["output"] + usage["cached"]
|
|
271
|
+
return text.strip(), calls, usage
|
|
272
|
+
choice = (data.get("choices") or [{}])[0]
|
|
273
|
+
msg = choice.get("message") or {}
|
|
274
|
+
calls = []
|
|
275
|
+
for tc in (msg.get("tool_calls") or []):
|
|
276
|
+
fn = tc.get("function") or {}
|
|
277
|
+
try:
|
|
278
|
+
args = json.loads(fn.get("arguments") or "{}")
|
|
279
|
+
except Exception:
|
|
280
|
+
args = {}
|
|
281
|
+
if not isinstance(args, dict):
|
|
282
|
+
args = {}
|
|
283
|
+
calls.append({"id": tc.get("id") or ("call_%d" % (len(calls) + 1)),
|
|
284
|
+
"name": fn.get("name") or "", "args": args})
|
|
285
|
+
u = data.get("usage") or {}
|
|
286
|
+
usage["input"] = int(u.get("prompt_tokens") or 0)
|
|
287
|
+
usage["output"] = int(u.get("completion_tokens") or 0)
|
|
288
|
+
usage["total"] = int(u.get("total_tokens") or 0) or (usage["input"] + usage["output"])
|
|
289
|
+
return (msg.get("content") or "").strip(), calls, usage
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
# ---------------------------------------------------------------- 主循环
|
|
293
|
+
|
|
294
|
+
def run(bi, prompt, workdir, timeout=180, cancel_event=None, log=None):
|
|
295
|
+
"""跑一次内置智能体(内部自带工具循环直到给出最终回答)。
|
|
296
|
+
|
|
297
|
+
bi: resolve() 的返回;prompt: 本轮完整输入(目标/续轮块由 pipeline 拼);
|
|
298
|
+
log: 追加一行日志的回调(步骤日志)。返回 runner 风格统一结果:
|
|
299
|
+
{ok, text, usage, error, model, provider_name, provider_id, iterations, cost_usd}。
|
|
300
|
+
"""
|
|
301
|
+
prov = bi["prov"]
|
|
302
|
+
model = bi["model"]
|
|
303
|
+
allow_private = bool(prov.get("allow_private"))
|
|
304
|
+
system = _SYSTEM_PROMPT + "\n\n## 工作目录\n%s" % os.path.abspath(workdir or ".")
|
|
305
|
+
msgs = [{"role": "user", "content": prompt}]
|
|
306
|
+
candidates = list(modelhub._protocol_candidates(prov))
|
|
307
|
+
tools_ok = all(proto != "google" for proto, _ in candidates) # google wire 无工具协议
|
|
308
|
+
total_usage = {"input": 0, "cached": 0, "output": 0, "total": 0}
|
|
309
|
+
text = ""
|
|
310
|
+
iters = 0
|
|
311
|
+
last_err = ""
|
|
312
|
+
ok = False
|
|
313
|
+
|
|
314
|
+
def _fail(err):
|
|
315
|
+
return {"ok": False, "text": "", "usage": dict(total_usage), "error": err,
|
|
316
|
+
"model": model, "provider_name": bi["provider_name"],
|
|
317
|
+
"provider_id": bi["provider_id"], "iterations": iters, "cost_usd": 0.0}
|
|
318
|
+
|
|
319
|
+
for it in range(1, MAX_TOOL_ITERS + 1):
|
|
320
|
+
if cancel_event is not None and cancel_event.is_set():
|
|
321
|
+
return _fail("已取消")
|
|
322
|
+
done = False
|
|
323
|
+
for proto, pbase in candidates:
|
|
324
|
+
keys = modelhub._chain_keys(prov) or [{"key": prov.get("api_key") or "", "id": ""}]
|
|
325
|
+
for kk in keys:
|
|
326
|
+
url, headers, body = _build_request(proto, pbase, kk["key"], model,
|
|
327
|
+
system, msgs, tools_ok)
|
|
328
|
+
status, data, err = _post_json(url, headers, body, allow_private, timeout)
|
|
329
|
+
if status == 0 or not (200 <= status < 300):
|
|
330
|
+
msg = ""
|
|
331
|
+
if isinstance(data, dict):
|
|
332
|
+
e = data.get("error")
|
|
333
|
+
msg = e.get("message", "") if isinstance(e, dict) else str(e)
|
|
334
|
+
last_err = err or ("HTTP %s %s" % (status, str(msg)[:200]))
|
|
335
|
+
try:
|
|
336
|
+
modelhub.note_key_error(bi["provider_id"], kk.get("id") or "", last_err)
|
|
337
|
+
except Exception:
|
|
338
|
+
pass
|
|
339
|
+
continue
|
|
340
|
+
try:
|
|
341
|
+
modelhub.note_key_ok(bi["provider_id"], kk.get("id") or "")
|
|
342
|
+
except Exception:
|
|
343
|
+
pass
|
|
344
|
+
text, calls, usage = _parse_reply(proto, data)
|
|
345
|
+
for k in total_usage:
|
|
346
|
+
total_usage[k] += int(usage.get(k) or 0)
|
|
347
|
+
iters = it
|
|
348
|
+
if calls:
|
|
349
|
+
if log:
|
|
350
|
+
log("[迭代 %d] %s 请求工具: %s" % (
|
|
351
|
+
it, model, ", ".join(c["name"] for c in calls)))
|
|
352
|
+
results = []
|
|
353
|
+
for c in calls:
|
|
354
|
+
out = _exec_tool(workdir, c["name"], c["args"])
|
|
355
|
+
if log:
|
|
356
|
+
brief = out if len(out) <= 120 else out[:120] + "…"
|
|
357
|
+
log("[工具] %s → %s" % (c["name"], brief.replace("\n", " ⏎ ")))
|
|
358
|
+
results.append((c["id"], out))
|
|
359
|
+
msgs.append({"role": "assistant", "content": text, "tool_calls": calls})
|
|
360
|
+
msgs.append({"role": "tool_results", "tool_results": results})
|
|
361
|
+
done = True
|
|
362
|
+
break
|
|
363
|
+
if (text or "").strip():
|
|
364
|
+
if log:
|
|
365
|
+
log("[迭代 %d] 最终回答(%d 字)" % (it, len(text)))
|
|
366
|
+
ok = True
|
|
367
|
+
done = True
|
|
368
|
+
break
|
|
369
|
+
last_err = "模型未返回文本"
|
|
370
|
+
done = True
|
|
371
|
+
break
|
|
372
|
+
if done:
|
|
373
|
+
break
|
|
374
|
+
if not done:
|
|
375
|
+
break # 所有 wire/KEY 都失败
|
|
376
|
+
if ok:
|
|
377
|
+
break
|
|
378
|
+
if not ok and not text:
|
|
379
|
+
return _fail(last_err or "工具循环达上限仍无最终回答")
|
|
380
|
+
return {"ok": True, "text": (text or "").strip(), "usage": dict(total_usage),
|
|
381
|
+
"error": "", "model": model, "provider_name": bi["provider_name"],
|
|
382
|
+
"provider_id": bi["provider_id"], "iterations": iters, "cost_usd": 0.0}
|
package/app/core/flows.py
CHANGED
|
@@ -33,7 +33,7 @@ ENGINE_DEFAULTS = {
|
|
|
33
33
|
BUILTIN_FLOWS = [
|
|
34
34
|
{"id": "direct", "name": "直接执行", "icon": "i-chat", "engine": "direct", "builtin": True,
|
|
35
35
|
"goal_hint": "让 AI 直接做什么(一句话,可带附件)",
|
|
36
|
-
"note": "
|
|
36
|
+
"note": "内置智能体直连模型 API 干活(无 CLI 进程),无可用供应商时回退本机 CLI;无拆解/评审(快)"},
|
|
37
37
|
{"id": "code", "name": "代码", "icon": "i-code", "engine": "code", "builtin": True,
|
|
38
38
|
"goal_hint": "要实现/修复什么(一句话)",
|
|
39
39
|
"note": "实现 → 验证命令 → 跨厂商评审 → 自动修复/换将"},
|