codebee 0.1.50 → 0.1.51
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/CHANGELOG.md +7 -0
- package/README.md +4 -4
- package/app/core/builtin_agent.py +172 -97
- package/app/core/jobs.py +170 -42
- package/app/core/manager.py +24 -7
- package/app/core/market.py +27 -1
- package/app/core/pipeline.py +35 -11
- package/app/core/planner.py +14 -5
- package/app/core/router.py +189 -104
- package/app/core/runner.py +34 -0
- package/app/core/settings.py +4 -3
- package/app/core/skills.py +36 -12
- package/app/core/zentao.py +126 -17
- package/app/main.py +29 -6
- package/app/ui/app.js +13256 -13171
- package/app/ui/i18n.js +56 -4
- package/app/ui/index.html +10 -10
- package/app/ui/style.css +12 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -23,6 +23,13 @@ README 元数据带回,供老版本在「发现新版本」时展示新版更
|
|
|
23
23
|
|
|
24
24
|
- **首选 KEY 超限自动切备用了**:同厂商多密钥此前只认欠费类文案——首选 KEY 撞上 429 限流(智谱「并发数超过限制」「每分钟Token数已超过上限」、claude 的 rate_limit_error、codex 的 "429 Too Many Requests" 等)只记一笔错误、不进冷却,每个新步骤仍从超限的首选 KEY 重新烧起。现在限流文案与欠费同口径:进冷却 30 分钟、密钥镜像自动切下一把、链展开跳过冷却中的 KEY(到期或「恢复」按钮一键回首选)。顺带补一处漏账:无显式链时「换模型回退」的链条目不带 KEY 标记,这类重试失败此前完全不记 KEY 账。
|
|
25
25
|
|
|
26
|
+
## v0.1.51(2026-09-22)
|
|
27
|
+
|
|
28
|
+
- 智能路由:绑定链可用性细化为「有备胎」维度(解析出的调用链有第二条及以上的任务加分),静态能力基线不再把配额烧干的供应商顶在前面
|
|
29
|
+
- 内置智能体:用户显式指定实现者时全程尊重(不再被自动路由旁观抢占);单次生成输出预算把推理模型的思考 token 也计入
|
|
30
|
+
- runner 错误识别降噪:审计日志错误行探针排除回显提示词里的普通词误判,尾段无错误信号时不强判失败
|
|
31
|
+
- 新增夜班竞品扫描脚本(scripts/borrow_scan_nightly.py)
|
|
32
|
+
|
|
26
33
|
## v0.1.50(2026-09-22)
|
|
27
34
|
|
|
28
35
|
- 今日体检修复:i18n 英文字典 22 个重复键清理——其中「 条/字/章」等 7 个值冲突属后值静默覆盖(如「第 N 章」曾被渲染成 "Chapter N chapters"),按使用现场裁定正确值,英文界面文案修复
|
package/README.md
CHANGED
|
@@ -21,11 +21,11 @@ Kimi Code、MiMo Code、Grok Build、Pi、DeepSeek Harness……),提供
|
|
|
21
21
|
不会把你的任务内容交给任何第三方。
|
|
22
22
|
|
|
23
23
|
<!-- relnotes:start -->
|
|
24
|
-
### 最新版更新内容(v0.1.
|
|
24
|
+
### 最新版更新内容(v0.1.51)
|
|
25
25
|
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
26
|
+
- 智能路由识别「有备胎」的绑定链并加分;配额烧干的供应商不再霸占前排
|
|
27
|
+
- 内置智能体尊重用户指定的实现者;推理思考 token 计入输出预算
|
|
28
|
+
- 错误识别降噪:提示词回显不再被误判为执行错误
|
|
29
29
|
<!-- relnotes:end -->
|
|
30
30
|
|
|
31
31
|
---
|
|
@@ -36,6 +36,10 @@ IMAGE_JPEG_BYTES = 3500 * 1024 # 缩放后仍超此大小 → 转 JPEG q85
|
|
|
36
36
|
IMAGE_MAX_BYTES = 5 * 1024 * 1024 # 单张最终字节硬上限(超出剔除并日志)
|
|
37
37
|
THINK_MAX_CHARS = 20000 # 单次生成保留的思考正文上限(防超长思维链撑爆 run.json)
|
|
38
38
|
STREAM_MAX_CHARS = 20000 # 运行中实时回答的保留上限(与 THINK_MAX_CHARS 同口径)
|
|
39
|
+
BASE_MAX_TOKENS = 8000 # 单次生成输出预算(推理模型的思考 token 也计入此额度)
|
|
40
|
+
MAX_TOKENS_CAP = 16000 # 思考撑爆预算后的提额上限(每轮 run 只提一次)
|
|
41
|
+
THINK_EXHAUST_MIN_CHARS = 1500 # 零正文时思考达到此长度 ≈ 思考占满输出预算
|
|
42
|
+
EMPTY_TEXT_MAX_STREAK = 3 # 连续零正文轮数上限(防同参数盲重试烧穿预算)
|
|
39
43
|
|
|
40
44
|
CMD_DEFAULT_TIMEOUT = 600 # run_command 默认超时(与 CLI 停滞看门狗同口径)
|
|
41
45
|
CMD_MAX_TIMEOUT = 1800 # 上限:模型传 timeout_sec 超过即钳到这
|
|
@@ -777,7 +781,8 @@ def _m_anthropic(m):
|
|
|
777
781
|
return {"role": "user", "content": [{"type": "text", "text": m.get("content") or ""}]}
|
|
778
782
|
|
|
779
783
|
|
|
780
|
-
def _build_request(proto, base, use_key, model, system, msgs, with_tools, stream=False
|
|
784
|
+
def _build_request(proto, base, use_key, model, system, msgs, with_tools, stream=False,
|
|
785
|
+
max_tokens=BASE_MAX_TOKENS):
|
|
781
786
|
"""按协议构造 (url, headers, body)。msgs 为内部统一形状。
|
|
782
787
|
|
|
783
788
|
stream=True 时按协议打开流式:google 换 :streamGenerateContent?alt=sse,
|
|
@@ -803,13 +808,13 @@ def _build_request(proto, base, use_key, model, system, msgs, with_tools, stream
|
|
|
803
808
|
"parts": parts})
|
|
804
809
|
body = {"contents": contents,
|
|
805
810
|
"systemInstruction": {"parts": [{"text": system}]},
|
|
806
|
-
"generationConfig": {"maxOutputTokens":
|
|
811
|
+
"generationConfig": {"maxOutputTokens": max_tokens}}
|
|
807
812
|
return url, headers, body
|
|
808
813
|
path = "/messages" if proto == "anthropic" else "/chat/completions"
|
|
809
814
|
url = (base + path) if base.endswith("/v1") else (base + "/v1" + path)
|
|
810
815
|
if proto == "anthropic":
|
|
811
816
|
headers = {"x-api-key": use_key, "anthropic-version": "2023-06-01"}
|
|
812
|
-
body = {"model": model, "max_tokens":
|
|
817
|
+
body = {"model": model, "max_tokens": max_tokens, "system": system,
|
|
813
818
|
"messages": [_m_anthropic(m) for m in msgs]}
|
|
814
819
|
if with_tools:
|
|
815
820
|
body["tools"] = _anthropic_tools()
|
|
@@ -825,7 +830,7 @@ def _build_request(proto, base, use_key, model, system, msgs, with_tools, stream
|
|
|
825
830
|
msgs_wire.extend(w)
|
|
826
831
|
else:
|
|
827
832
|
msgs_wire.append(w)
|
|
828
|
-
body = {"model": model, "max_tokens":
|
|
833
|
+
body = {"model": model, "max_tokens": max_tokens,
|
|
829
834
|
"messages": [{"role": "system", "content": system}] + msgs_wire}
|
|
830
835
|
if with_tools:
|
|
831
836
|
body["tools"] = _openai_tools()
|
|
@@ -934,6 +939,9 @@ def run(bi, prompt, workdir, timeout=180, cancel_event=None, log=None, images=No
|
|
|
934
939
|
last_err = ""
|
|
935
940
|
ok = False
|
|
936
941
|
reasons = [] # 各迭代的思考正文(按迭代顺序拼接,供步骤记录展示)
|
|
942
|
+
max_tokens = BASE_MAX_TOKENS # 思考占满预算时提额重试(跨迭代保持,见空正文分支)
|
|
943
|
+
escalated = False # 本轮 run 是否已提额(只提一次,防无限翻倍)
|
|
944
|
+
empty_streak = 0 # 连续零正文轮数(拿到正文/工具即清零)
|
|
937
945
|
|
|
938
946
|
def _fire(cb, *cb_args):
|
|
939
947
|
if cb is None:
|
|
@@ -964,108 +972,175 @@ def run(bi, prompt, workdir, timeout=180, cancel_event=None, log=None, images=No
|
|
|
964
972
|
return _fail("已取消")
|
|
965
973
|
reasons.append("") # 每轮迭代各占一段思考(跨轮拼接时分段,join 时滤空段)
|
|
966
974
|
done = False
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
#
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
975
|
+
reBump = False # 本轮要提额重试(同一迭代重跑,不烧迭代数)
|
|
976
|
+
stopit = False # 本轮判死(连续零正文),直接终局不再空转
|
|
977
|
+
for _bump in range(2): # 第 2 圈 = 提额后的同轮重试
|
|
978
|
+
for proto, pbase in candidates:
|
|
979
|
+
keys = modelhub._chain_keys(prov) or [{"key": prov.get("api_key") or "", "id": ""}]
|
|
980
|
+
for kk in keys:
|
|
981
|
+
got = False # 本 KEY 拿到了响应(区别于 HTTP 失败换下家)
|
|
982
|
+
stripped = False # thinking 对象被网关 400 拒后已摘除重发
|
|
983
|
+
while True:
|
|
984
|
+
url, headers, body = _build_request(
|
|
985
|
+
proto, pbase, kk["key"], model, system, msgs,
|
|
986
|
+
tools_ok, max_tokens=max_tokens)
|
|
987
|
+
sbody = None
|
|
988
|
+
if stream:
|
|
989
|
+
# 流式体单独构造(+stream / include_usage);非流式体留给回落重发
|
|
990
|
+
_, _, sbody = _build_request(
|
|
991
|
+
proto, pbase, kk["key"], model, system, msgs,
|
|
992
|
+
tools_ok, stream=True, max_tokens=max_tokens)
|
|
993
|
+
effort = str(bi.get("reasoning_effort") or "").strip().lower()
|
|
994
|
+
# reasoning_effort 是 OpenAI wire 字段;Anthropic thinking 使用
|
|
995
|
+
# 另一套对象结构,向兼容网关硬塞该字段会直接得到 400。
|
|
996
|
+
if effort in ("low", "medium", "high") and proto == "openai":
|
|
997
|
+
body["reasoning_effort"] = effort
|
|
998
|
+
if sbody is not None:
|
|
999
|
+
sbody["reasoning_effort"] = effort
|
|
1000
|
+
if effort in ("low", "medium") and proto == "anthropic" \
|
|
1001
|
+
and not stripped:
|
|
1002
|
+
# 「思考程度」此前只对 openai 面生效;GLM 这类推理模型
|
|
1003
|
+
# 在 Anthropic 兼容面上默认全功率思考,能把 max_tokens
|
|
1004
|
+
# 整个烧光(2026-09-22 glm-5.3-flash 47 分钟零正文案)。
|
|
1005
|
+
# low/medium 显式给思考预算,high 留默认(模型自决=放开想)。
|
|
1006
|
+
body["thinking"] = {
|
|
1007
|
+
"type": "enabled",
|
|
1008
|
+
"budget_tokens": 1024 if effort == "low" else 4096}
|
|
1009
|
+
if sbody is not None:
|
|
1010
|
+
sbody["thinking"] = dict(body["thinking"])
|
|
1011
|
+
text, calls, usage = "", [], {}
|
|
1012
|
+
streamed = False
|
|
1013
|
+
if sbody is not None:
|
|
1014
|
+
part = _post_sse_stream(
|
|
1015
|
+
url, headers, sbody, allow_private, timeout, proto,
|
|
1016
|
+
on_reason=_take_reason, on_text=on_stream,
|
|
1017
|
+
cancel_event=cancel_event)
|
|
1018
|
+
if part["error"] == "已取消":
|
|
1019
|
+
return _fail("已取消")
|
|
1020
|
+
if not part["error"] and part["events"]:
|
|
1021
|
+
# 网关确实按流回了:正文/工具取增量累加结果
|
|
1022
|
+
text, calls = part["text"], part["calls"]
|
|
1023
|
+
usage = part["usage"]
|
|
1024
|
+
streamed = True
|
|
1025
|
+
if not reasons or not reasons[-1]:
|
|
1026
|
+
# 调用方没挂实时回调(无可视化诉求)时思考只在
|
|
1027
|
+
# 累加器里:这里兜底收进结果,落库口径一致
|
|
1028
|
+
_take_reason(part["reasoning"])
|
|
1029
|
+
else:
|
|
1030
|
+
# 零事件(网关忽略 stream)或流式这一步失败(不认 stream 参数、
|
|
1031
|
+
# 断流等):都退回同一条 wire 的非流式重发——比直接判死这家
|
|
1032
|
+
# 供应商稳妥;非流式也失败才走下面的记账/换链。
|
|
1033
|
+
last_err = part["error"]
|
|
1034
|
+
if not streamed:
|
|
1035
|
+
status, data, err = _post_interruptible(url, headers, body,
|
|
1036
|
+
allow_private, timeout,
|
|
1037
|
+
cancel_event)
|
|
1038
|
+
if cancel_event is not None and cancel_event.is_set():
|
|
1039
|
+
# 取消先于一切记账:健康 KEY 不能因被放弃的请求背上冷却
|
|
1040
|
+
return _fail("已取消")
|
|
1041
|
+
if status == 0 or not (200 <= status < 300):
|
|
1042
|
+
msg = ""
|
|
1043
|
+
if isinstance(data, dict):
|
|
1044
|
+
e = data.get("error")
|
|
1045
|
+
msg = e.get("message", "") if isinstance(e, dict) else str(e)
|
|
1046
|
+
if status == 400 and isinstance(body.get("thinking"), dict) \
|
|
1047
|
+
and "thinking" in str(msg).lower() and not stripped:
|
|
1048
|
+
# 网关不认 thinking 对象:摘掉同 KEY 立即重发,
|
|
1049
|
+
# 别让「思考程度」偏好把健康 KEY 送进冷却
|
|
1050
|
+
body.pop("thinking", None)
|
|
1051
|
+
if sbody is not None:
|
|
1052
|
+
sbody.pop("thinking", None)
|
|
1053
|
+
stripped = True
|
|
1054
|
+
continue
|
|
1055
|
+
last_err = err or ("HTTP %s %s" % (status, str(msg)[:200]))
|
|
1056
|
+
try:
|
|
1057
|
+
modelhub.note_key_error(bi["provider_id"], kk.get("id") or "", last_err)
|
|
1058
|
+
except Exception:
|
|
1059
|
+
pass
|
|
1060
|
+
break # 换下一个 KEY
|
|
1061
|
+
text, calls, usage = _parse_reply(proto, data)
|
|
1062
|
+
# 非流式响应里的思考字段:补发一次,可视化与流式路径同形
|
|
1063
|
+
_take_reason(_reason_from_message(proto, data))
|
|
1020
1064
|
try:
|
|
1021
|
-
modelhub.
|
|
1065
|
+
modelhub.note_key_ok(bi["provider_id"], kk.get("id") or "")
|
|
1022
1066
|
except Exception:
|
|
1023
1067
|
pass
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
for k in total_usage:
|
|
1034
|
-
total_usage[k] += int(usage.get(k) or 0)
|
|
1035
|
-
iters = it
|
|
1036
|
-
if calls:
|
|
1037
|
-
if log:
|
|
1038
|
-
log("[迭代 %d] %s 请求工具: %s" % (
|
|
1039
|
-
it, model, ", ".join(c["name"] for c in calls)))
|
|
1040
|
-
_fire(on_activity, "请求工具: " + "、".join(c["name"] for c in calls))
|
|
1041
|
-
results = []
|
|
1042
|
-
for c in calls:
|
|
1043
|
-
out = _exec_tool(workdir, c["name"], c["args"], cancel_event)
|
|
1068
|
+
usage = _norm_usage(usage)
|
|
1069
|
+
for k in total_usage:
|
|
1070
|
+
total_usage[k] += int(usage.get(k) or 0)
|
|
1071
|
+
iters = it
|
|
1072
|
+
got = True
|
|
1073
|
+
break
|
|
1074
|
+
if not got:
|
|
1075
|
+
continue # 本 KEY 失败,换下一个
|
|
1076
|
+
if calls:
|
|
1044
1077
|
if log:
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
_fire(on_activity, "
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1078
|
+
log("[迭代 %d] %s 请求工具: %s" % (
|
|
1079
|
+
it, model, ", ".join(c["name"] for c in calls)))
|
|
1080
|
+
_fire(on_activity, "请求工具: " + "、".join(c["name"] for c in calls))
|
|
1081
|
+
results = []
|
|
1082
|
+
for c in calls:
|
|
1083
|
+
out = _exec_tool(workdir, c["name"], c["args"], cancel_event)
|
|
1084
|
+
if log:
|
|
1085
|
+
brief = out if len(out) <= 120 else out[:120] + "…"
|
|
1086
|
+
log("[工具] %s → %s" % (c["name"], brief.replace("\n", " ⏎ ")))
|
|
1087
|
+
_fire(on_activity, "%s %s" % (
|
|
1088
|
+
c["name"], str(c["args"].get("path")
|
|
1089
|
+
or c["args"].get("command") or "")[:80]))
|
|
1090
|
+
results.append((c["id"], out))
|
|
1091
|
+
msgs.append({"role": "assistant", "content": text, "tool_calls": calls})
|
|
1092
|
+
msgs.append({"role": "tool_results", "tool_results": results})
|
|
1093
|
+
empty_streak = 0
|
|
1094
|
+
done = True
|
|
1095
|
+
break
|
|
1096
|
+
if (text or "").strip():
|
|
1097
|
+
if log:
|
|
1098
|
+
log("[迭代 %d] 最终回答(%d 字)" % (it, len(text)))
|
|
1099
|
+
ok = True
|
|
1100
|
+
empty_streak = 0
|
|
1101
|
+
done = True
|
|
1102
|
+
break
|
|
1103
|
+
# —— 零正文零工具:分辨「思考占满输出预算」还是「模型真没说话」。
|
|
1104
|
+
# 推理模型的思考计入 max_tokens:想满了流会正常收,正文却是空的。
|
|
1105
|
+
rlen = len(reasons[-1] or "")
|
|
1106
|
+
out_tok = int(usage.get("output") or 0)
|
|
1107
|
+
exhausted = (rlen >= THINK_EXHAUST_MIN_CHARS
|
|
1108
|
+
or out_tok >= int(max_tokens * 0.9))
|
|
1109
|
+
empty_streak += 1
|
|
1110
|
+
if exhausted and not escalated:
|
|
1111
|
+
# 同轮提额重试一次:8K 想不完的 16K 大多就想完了
|
|
1112
|
+
escalated = True
|
|
1113
|
+
old = max_tokens
|
|
1114
|
+
max_tokens = min(max_tokens * 2, MAX_TOKENS_CAP)
|
|
1115
|
+
last_err = ("模型思考占满输出预算(max_tokens=%d,思考 %d 字零正文)"
|
|
1116
|
+
% (old, rlen))
|
|
1117
|
+
if log:
|
|
1118
|
+
log("[第 %d 轮] 思考 %d 字仍无正文/工具 → max_tokens %d→%d 同轮重试"
|
|
1119
|
+
% (it, rlen, old, max_tokens))
|
|
1120
|
+
_fire(on_activity, "思考占满输出预算,提高输出预算重试")
|
|
1121
|
+
reBump = True
|
|
1122
|
+
break
|
|
1123
|
+
if empty_streak >= EMPTY_TEXT_MAX_STREAK:
|
|
1124
|
+
last_err = ("模型思考连续占满输出预算(max_tokens 已提到 %d 仍无正文)"
|
|
1125
|
+
% max_tokens) if exhausted else \
|
|
1126
|
+
("模型连续 %d 轮零正文零工具" % empty_streak)
|
|
1127
|
+
done = True
|
|
1128
|
+
stopit = True
|
|
1129
|
+
break
|
|
1130
|
+
last_err = "模型未返回文本"
|
|
1056
1131
|
if log:
|
|
1057
|
-
log("[
|
|
1058
|
-
|
|
1132
|
+
log("[第 %d 轮] 模型零正文零工具(思考 %d 字),重试" % (it, rlen))
|
|
1133
|
+
_fire(on_activity, "模型零正文,重试")
|
|
1059
1134
|
done = True
|
|
1060
1135
|
break
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1136
|
+
if done or reBump:
|
|
1137
|
+
break
|
|
1138
|
+
if reBump:
|
|
1139
|
+
continue # 同一迭代用新预算重来
|
|
1140
|
+
break # 非提额路径一锤定音
|
|
1066
1141
|
if not done:
|
|
1067
1142
|
break # 所有 wire/KEY 都失败
|
|
1068
|
-
if ok:
|
|
1143
|
+
if ok or stopit:
|
|
1069
1144
|
break
|
|
1070
1145
|
if not ok and not text:
|
|
1071
1146
|
return _fail(last_err or "工具循环达上限仍无最终回答")
|