codebee 0.1.5 → 0.1.6
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 +8 -0
- package/README.md +4 -5
- package/app/core/health.py +55 -1
- package/app/core/jobs.py +10 -2
- package/app/core/manager.py +9 -1
- package/app/core/modelhub.py +66 -15
- package/app/core/pipeline.py +126 -60
- package/app/core/registry.py +4 -0
- package/app/core/router.py +5 -4
- package/app/core/runner.py +12 -3
- package/app/core/selfupdate.py +7 -1
- package/app/ui/app.js +168 -66
- package/app/ui/i18n.js +13 -5
- package/app/ui/icons/bee.svg +79 -0
- package/app/ui/index.html +2 -1
- package/app/ui/style.css +189 -342
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,14 @@ CodeBee 的用户可感知变更记录。发布新版时:最上面加一节,
|
|
|
4
4
|
`<!-- relnotes:start -->…<!-- relnotes:end -->` 段(那段会被 `npm view` 的
|
|
5
5
|
README 元数据带回,供老版本在「发现新版本」时展示新版更新内容)。
|
|
6
6
|
|
|
7
|
+
## v0.1.6(2026-09-18)
|
|
8
|
+
|
|
9
|
+
- 连载韧性:网关限流/欠费时章稿失败先退避重试同作者(保住连载风格一致),重试穷尽才判死整 run;自动续跑改为延迟 5 分钟入队、上限提升到 3 次
|
|
10
|
+
- 修复:任务分支从游离 HEAD 检出时「切回原分支」原地空转的假成功;游离基线的合并现在显式拒绝并说明
|
|
11
|
+
- kimi 接入:僵尸 kimi 进程自动检测清理;CodeBee 托管配置按 TOML 语义渲染
|
|
12
|
+
- 修复:generic 类 CLI 经 npm .cmd 垫片时提示词被 cmd 重解析截烂(改 node 直启)
|
|
13
|
+
- 界面:新增蜜蜂图标,蜂巢工作台与皮肤视觉精修
|
|
14
|
+
|
|
7
15
|
## v0.1.5(2026-09-16)
|
|
8
16
|
|
|
9
17
|
- 更新提示升级:发现新版本时直接展示「新版本更新内容」,升级后首次打开自动弹一次「本次更新内容」
|
package/README.md
CHANGED
|
@@ -21,12 +21,11 @@
|
|
|
21
21
|
零第三方依赖:纯 Python 标准库(3.8+),本地 Web 界面,数据全部落盘可回放。
|
|
22
22
|
|
|
23
23
|
<!-- relnotes:start -->
|
|
24
|
-
### 最新版更新内容(v0.1.
|
|
24
|
+
### 最新版更新内容(v0.1.6)
|
|
25
25
|
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
- 完整更新日志见 CHANGELOG.md
|
|
26
|
+
- 连载更抗限流:章稿失败先退避重试同作者,自动续跑延迟 5 分钟、上限 3 次
|
|
27
|
+
- 修复任务分支游离 HEAD 的假成功切换与合并;kimi 僵尸进程检测与配置渲染修复
|
|
28
|
+
- 修复 generic CLI 经 npm 垫片时提示词被截烂;界面新增蜜蜂图标与视觉精修
|
|
30
29
|
<!-- relnotes:end -->
|
|
31
30
|
|
|
32
31
|
## 作者侧命令行(可选)
|
package/app/core/health.py
CHANGED
|
@@ -157,6 +157,58 @@ def report_failure(provider: str, error: str = "", *, model: str = "",
|
|
|
157
157
|
_persist()
|
|
158
158
|
|
|
159
159
|
|
|
160
|
+
# ------------------------------------------------- 静态死链告警(绑定层)
|
|
161
|
+
|
|
162
|
+
def report_binding_dead(key: str, error: str = ""):
|
|
163
|
+
"""静态死链告警:某 CLI 的绑定链在起跑前就已全部失效(厂商停用/删除/无密钥、
|
|
164
|
+
模型停用、协议不匹配)。与运行期 report_failure 的区别:不计数、不进探针,
|
|
165
|
+
立即置 down+告警(尊重静默);绑定恢复后由 report_binding_ok 自动解除。
|
|
166
|
+
伪供应商名带「绑定链·」前缀,不会与真实厂商名相撞。"""
|
|
167
|
+
if not key:
|
|
168
|
+
return
|
|
169
|
+
with _LOCK:
|
|
170
|
+
name = "绑定链·%s" % key
|
|
171
|
+
st = _PROVIDERS.get(name)
|
|
172
|
+
if st is None:
|
|
173
|
+
st = _PROVIDERS.setdefault(name, {
|
|
174
|
+
"name": name, "provider_id": "", "model": "",
|
|
175
|
+
"status": "ok", "consecutive_failures": 0,
|
|
176
|
+
"first_fail_at": 0, "last_fail_at": 0, "last_ok_at": 0,
|
|
177
|
+
"last_error": "", "alerted": False, "silenced": False,
|
|
178
|
+
"silence_until": 0, "probe_next_at": 0, "probe_backoff_idx": 0,
|
|
179
|
+
"recovered_at": 0,
|
|
180
|
+
})
|
|
181
|
+
st["static"] = True
|
|
182
|
+
st["binding_key"] = str(key)
|
|
183
|
+
st["name"] = name
|
|
184
|
+
st["status"] = "down"
|
|
185
|
+
st["last_fail_at"] = _now()
|
|
186
|
+
st["last_error"] = str(error or "")[:300]
|
|
187
|
+
st["probe_next_at"] = 0
|
|
188
|
+
if not _effective_silenced(st) and not st.get("alerted"):
|
|
189
|
+
st["alerted"] = True
|
|
190
|
+
log.warning("[health] ⚠️ 绑定链告警:%s 全部失效(%s)",
|
|
191
|
+
name, st["last_error"][:120])
|
|
192
|
+
_persist()
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def report_binding_ok(key: str):
|
|
196
|
+
"""绑定链恢复可用:自动解除该 CLI 的静态死链告警(无记录时零开销)。"""
|
|
197
|
+
if not key:
|
|
198
|
+
return
|
|
199
|
+
with _LOCK:
|
|
200
|
+
st = _PROVIDERS.get("绑定链·%s" % key)
|
|
201
|
+
if not st or not st.get("static"):
|
|
202
|
+
return
|
|
203
|
+
st["status"] = "recovered"
|
|
204
|
+
st["alerted"] = False
|
|
205
|
+
st["silenced"] = False
|
|
206
|
+
st["silence_until"] = 0
|
|
207
|
+
st["recovered_at"] = _now()
|
|
208
|
+
log.info("[health] 绑定链 %s 已恢复(静态告警解除)", st["name"])
|
|
209
|
+
_persist()
|
|
210
|
+
|
|
211
|
+
|
|
160
212
|
# ---------------------------------------------------------------- 手动操作
|
|
161
213
|
|
|
162
214
|
def silence(provider: str, minutes: int = 0):
|
|
@@ -215,6 +267,7 @@ def snapshot():
|
|
|
215
267
|
"last_error": st.get("last_error") or "",
|
|
216
268
|
"alerting": alerting,
|
|
217
269
|
"silenced": silenced,
|
|
270
|
+
"static": bool(st.get("static")),
|
|
218
271
|
})
|
|
219
272
|
# 稳定排序:告警中 > 故障中 > 恢复 > 正常
|
|
220
273
|
rank = {"down": 0, "failing": 1, "recovered": 2, "ok": 3}
|
|
@@ -271,7 +324,8 @@ def _probe_loop():
|
|
|
271
324
|
try:
|
|
272
325
|
with _LOCK:
|
|
273
326
|
targets = [(name, st) for name, st in _PROVIDERS.items()
|
|
274
|
-
if st.get("
|
|
327
|
+
if not st.get("static") # 静态死链无端点可探,绑定恢复时自动解除
|
|
328
|
+
and st.get("status") in ("failing", "down")
|
|
275
329
|
and (st.get("probe_next_at") or 0) <= _now()]
|
|
276
330
|
for name, st in targets:
|
|
277
331
|
idx = int(st.get("probe_backoff_idx") or 0)
|
package/app/core/jobs.py
CHANGED
|
@@ -134,7 +134,9 @@ def cancel_event_for(run_id):
|
|
|
134
134
|
return ev
|
|
135
135
|
|
|
136
136
|
|
|
137
|
-
AUTO_RESUME_MAX =
|
|
137
|
+
AUTO_RESUME_MAX = 3 # 连载任务自动续跑上限(超时/中断后自动接着写,无需人工)
|
|
138
|
+
AUTO_RESUME_DELAY_S = 300 # 自动续跑延迟入队秒数:网关限流/欠费窗口通常分钟级,
|
|
139
|
+
# 立即重排会撞在同一堵墙上把续跑次数烧光(2026-09-17 七猫实测)
|
|
138
140
|
|
|
139
141
|
|
|
140
142
|
def _maybe_auto_resume(run_id):
|
|
@@ -160,7 +162,13 @@ def _maybe_auto_resume(run_id):
|
|
|
160
162
|
return False
|
|
161
163
|
store.update_run(new_run["id"], auto_resumes=int(run.get("auto_resumes") or 0) + 1,
|
|
162
164
|
auto_resumed_from=run_id)
|
|
163
|
-
|
|
165
|
+
|
|
166
|
+
def _enqueue():
|
|
167
|
+
_QUEUE.put({"kind": "orchestration",
|
|
168
|
+
"run_id": new_run["id"], "task_id": task["id"]})
|
|
169
|
+
t = threading.Timer(AUTO_RESUME_DELAY_S, _enqueue)
|
|
170
|
+
t.daemon = True
|
|
171
|
+
t.start()
|
|
164
172
|
return True
|
|
165
173
|
except Exception:
|
|
166
174
|
return False
|
package/app/core/manager.py
CHANGED
|
@@ -78,6 +78,8 @@ def sweep_orphan_cli_processes():
|
|
|
78
78
|
按「Tutti 调用签名 + 父进程已死」双条件匹配,不误杀用户自己在用的 CLI:
|
|
79
79
|
opencode:命令行含 opencode + --model(同步写入的 provider 固定 orch)
|
|
80
80
|
codex:命令行含 codex + --skip-git-repo-check(Tutti 专属 flag 组合)
|
|
81
|
+
kimi:命令行含 kimi-code/dist/main.mjs(node 直启路径,2026-09-17 实测
|
|
82
|
+
僵尸 kimi 会占住讯飞网关同钥请求队列,堵死后续所有 kimi 调用)
|
|
81
83
|
claude 不扫(签名与用户手动使用难区分)。返回清扫数量。"""
|
|
82
84
|
ps_exe = os.path.join(os.environ.get("SystemRoot", r"C:\Windows"),
|
|
83
85
|
"System32", "WindowsPowerShell", "v1.0", "powershell.exe")
|
|
@@ -100,7 +102,8 @@ def sweep_orphan_cli_processes():
|
|
|
100
102
|
cl = str(i.get("CommandLine") or "").lower()
|
|
101
103
|
ppid = i.get("ParentProcessId")
|
|
102
104
|
is_ours = (("opencode" in cl and "--model" in cl)
|
|
103
|
-
or ("codex" in cl and "--skip-git-repo-check" in cl)
|
|
105
|
+
or ("codex" in cl and "--skip-git-repo-check" in cl)
|
|
106
|
+
or ("kimi-code" in cl and "main.mjs" in cl))
|
|
104
107
|
if not is_ours or ppid in live or not i.get("ProcessId"):
|
|
105
108
|
continue
|
|
106
109
|
try:
|
|
@@ -832,6 +835,11 @@ def _sync_codex_settings(entry, model, cp):
|
|
|
832
835
|
name = cp.get("name", "orch")
|
|
833
836
|
def q(v):
|
|
834
837
|
return '"%s"' % str(v).replace("\\", "\\\\").replace('"', '\\"')
|
|
838
|
+
if (cp.get("wire_api") or "responses") == "chat":
|
|
839
|
+
# codex 0.154+ 起 chat wire 被官方移除,写进 config.toml 会让 CLI 连配置
|
|
840
|
+
# 都载入不了(Error loading config.toml)——宁可明确拒绝也不落坏配置。
|
|
841
|
+
return ("供应商只有 chat completions wire,codex 0.154+ 已移除支持,未写入"
|
|
842
|
+
" config.toml——请为 codex 绑定 responses 兼容的供应商")
|
|
835
843
|
pairs = [("name", q(cp.get("name", name))),
|
|
836
844
|
("base_url", q(cp.get("base_url", ""))),
|
|
837
845
|
("env_key", q(cp.get("env_key", "ORCH_API_KEY"))),
|
package/app/core/modelhub.py
CHANGED
|
@@ -454,6 +454,7 @@ def model_ops(provider_id, names, op):
|
|
|
454
454
|
if changed:
|
|
455
455
|
_promote(models, newly_enabled) # 启用的置顶,停用的退到启用块之后
|
|
456
456
|
_save(data)
|
|
457
|
+
sync_binding_alerts() # 模型启停/恢复 → 绑定链死活立即刷新告警
|
|
457
458
|
return changed, ""
|
|
458
459
|
|
|
459
460
|
|
|
@@ -473,6 +474,7 @@ def _restore_all(provider_id):
|
|
|
473
474
|
m["enabled"] = True
|
|
474
475
|
_promote(models, names) # 恢复 = 重新启用:同样置顶
|
|
475
476
|
_save(data)
|
|
477
|
+
sync_binding_alerts()
|
|
476
478
|
return None
|
|
477
479
|
|
|
478
480
|
|
|
@@ -925,6 +927,8 @@ def providers_op(ids, op):
|
|
|
925
927
|
not (p.get("id") in sel and p.get("enabled", True)),
|
|
926
928
|
not bool(p.get("enabled", True))))
|
|
927
929
|
_save(data)
|
|
930
|
+
if changed or op == "delete":
|
|
931
|
+
sync_binding_alerts() # 厂商启停/删除 → 绑定链死活立即刷新告警
|
|
928
932
|
return changed, ("" if changed else "所选供应商已是目标状态")
|
|
929
933
|
|
|
930
934
|
|
|
@@ -1967,6 +1971,56 @@ def _protocol_candidates(prov):
|
|
|
1967
1971
|
if (caps.get(p) or {}).get("base")]
|
|
1968
1972
|
|
|
1969
1973
|
|
|
1974
|
+
def bindable_protocols(agent_kind_or_id):
|
|
1975
|
+
"""该 CLI 可绑定的 wire 协议(与 resolve_binding 的 allowed 一致)。
|
|
1976
|
+
供死链告警/失败文案解释「为什么绑不上」:claude 只认 anthropic,
|
|
1977
|
+
codex/dsh 只认 openai,其余开放双协议(含 wire 适配)。"""
|
|
1978
|
+
if _deepseek_env_target(agent_kind_or_id) or agent_kind_or_id in ("codex-cli", "codex"):
|
|
1979
|
+
return ("openai",)
|
|
1980
|
+
if agent_kind_or_id in ("claude-code", "claude"):
|
|
1981
|
+
return ("anthropic",)
|
|
1982
|
+
return tuple(_BINDABLE_PROTOCOLS)
|
|
1983
|
+
|
|
1984
|
+
|
|
1985
|
+
def binding_dead_msg(cli_id):
|
|
1986
|
+
"""死链失败/告警文案(pipeline 死链闸门与本模块 sync 共用):说明为什么
|
|
1987
|
+
不回落本机默认 + 该 CLI 需要什么协议的供应商。"""
|
|
1988
|
+
try:
|
|
1989
|
+
protos = bindable_protocols(cli_id)
|
|
1990
|
+
except Exception:
|
|
1991
|
+
protos = ()
|
|
1992
|
+
hint = ("该 CLI 仅接受 %s 协议的已启用供应商;" % "、".join(protos)) if protos else ""
|
|
1993
|
+
return ("绑定链全部失效(链上供应商已停用/删除/无密钥,或模型已停用),"
|
|
1994
|
+
"本步判失败、不回落 CLI 本机默认——%s请在「CLI 绑定」页为该 CLI 绑定已启用的供应商" % hint)
|
|
1995
|
+
|
|
1996
|
+
|
|
1997
|
+
def sync_binding_alerts():
|
|
1998
|
+
"""厂商/模型启停、删除、恢复后立即重评估各 CLI 绑定链的静态死链告警:
|
|
1999
|
+
恢复的当场解除、新死的当场亮起——不必等下一次步骤执行才发现
|
|
2000
|
+
(2026-09-17 起与死链硬失败闸门配套)。幂等:health 侧静默/恢复语义不变。"""
|
|
2001
|
+
from . import health
|
|
2002
|
+
try:
|
|
2003
|
+
binds = bindings()
|
|
2004
|
+
except Exception:
|
|
2005
|
+
return
|
|
2006
|
+
for cli_id in sorted(binds.keys()):
|
|
2007
|
+
b = binds.get(cli_id) or {}
|
|
2008
|
+
if not _binding_chain(b):
|
|
2009
|
+
continue # 没配过链的 CLI 不归静态告警管(执行层闸门在跑时兜)
|
|
2010
|
+
try:
|
|
2011
|
+
r = resolve_binding(cli_id)
|
|
2012
|
+
ok = bool(r and r.get("call_chain"))
|
|
2013
|
+
except Exception:
|
|
2014
|
+
ok = False
|
|
2015
|
+
try:
|
|
2016
|
+
if ok:
|
|
2017
|
+
health.report_binding_ok(cli_id)
|
|
2018
|
+
else:
|
|
2019
|
+
health.report_binding_dead(cli_id, binding_dead_msg(cli_id))
|
|
2020
|
+
except Exception:
|
|
2021
|
+
pass # 告警是尽力而为的旁路:persist 失败(如目录不可用)不拖累操作本身
|
|
2022
|
+
|
|
2023
|
+
|
|
1970
2024
|
def resolve_binding(agent_kind_or_id, difficulty="default"):
|
|
1971
2025
|
"""返回 {env:{}, model:..., model_fallbacks:[...], codex_provider:..., call_chain:[...]} 或 None。
|
|
1972
2026
|
|
|
@@ -1981,17 +2035,11 @@ def resolve_binding(agent_kind_or_id, difficulty="default"):
|
|
|
1981
2035
|
provs = {p.get("id"): p for p in providers()}
|
|
1982
2036
|
routing = bool(b.get("difficulty_routing"))
|
|
1983
2037
|
tier = difficulty if difficulty in ("easy", "hard") else None
|
|
1984
|
-
# dsh
|
|
1985
|
-
# anthropic
|
|
1986
|
-
|
|
1987
|
-
# 2026-09-15
|
|
1988
|
-
|
|
1989
|
-
# 「codex 拿到 ANTHROPIC_* env 却缺 ORCH_API_KEY」这类必然失败的组合
|
|
1990
|
-
# (症状:Missing environment variable: ORCH_API_KEY)。
|
|
1991
|
-
if agent_kind_or_id in ("codex-cli", "codex"):
|
|
1992
|
-
allowed = ("openai",)
|
|
1993
|
-
elif agent_kind_or_id in ("claude-code", "claude"):
|
|
1994
|
-
allowed = ("anthropic",)
|
|
2038
|
+
# 协议必须匹配(bindable_protocols):dsh 只吃 OpenAI 兼容端点,codex 只吃
|
|
2039
|
+
# openai wire(codex_provider 机制),claude 只吃 anthropic wire——混着注入
|
|
2040
|
+
# 会产生「codex 拿到 ANTHROPIC_* env 却缺 ORCH_API_KEY」这类必然失败的组合
|
|
2041
|
+
# (2026-09-15 连载验收实测,症状:Missing environment variable: ORCH_API_KEY)。
|
|
2042
|
+
allowed = bindable_protocols(agent_kind_or_id)
|
|
1995
2043
|
|
|
1996
2044
|
if chain:
|
|
1997
2045
|
entries = []
|
|
@@ -2018,8 +2066,10 @@ def resolve_binding(agent_kind_or_id, difficulty="default"):
|
|
|
2018
2066
|
continue # 原生协议与适配过的 wire 都不匹配:跳过
|
|
2019
2067
|
if prov.get("name") in down_set:
|
|
2020
2068
|
continue # 健康监测判定 down:跳过,省掉无效等待
|
|
2021
|
-
if _is_codex_target(agent_kind_or_id) and codex_wire_blocked(prov):
|
|
2022
|
-
continue # codex
|
|
2069
|
+
if _is_codex_target(agent_kind_or_id) and (ep[2] == "chat" or codex_wire_blocked(prov)):
|
|
2070
|
+
continue # codex 0.154+ 只讲 responses wire:chat-only 供应商在起跑前
|
|
2071
|
+
# 就剔除(此前撞了才冷却 30 分钟,每轮白烧一次注定失败的
|
|
2072
|
+
# 尝试——2026-09-17 续4 连载 c35 实测)
|
|
2023
2073
|
if model and not _model_bindable(prov, model):
|
|
2024
2074
|
continue # 模型被停用/删除:该条跳过(2026-09-15 告警弹框「禁用该模型」)
|
|
2025
2075
|
# 多 KEY:同一厂商按 KEY 展开成多条,顺序即调用顺序。欠费的 KEY 被
|
|
@@ -2055,11 +2105,12 @@ def resolve_binding(agent_kind_or_id, difficulty="default"):
|
|
|
2055
2105
|
prov = provs.get(pid)
|
|
2056
2106
|
if not prov or not prov.get("enabled", True) or not prov.get("api_key"):
|
|
2057
2107
|
return None
|
|
2058
|
-
if _is_codex_target(agent_kind_or_id) and codex_wire_blocked(prov):
|
|
2059
|
-
return None # codex wire 不兼容冷却中:解析为空 → 路由绑定分自动转负
|
|
2060
2108
|
ep = _entry_endpoint(prov, allowed)
|
|
2061
2109
|
if not ep:
|
|
2062
2110
|
return None # google 只登记;dsh 只接受 OpenAI 兼容端点;未适配的不硬塞
|
|
2111
|
+
if _is_codex_target(agent_kind_or_id) and (ep[2] == "chat" or codex_wire_blocked(prov)):
|
|
2112
|
+
return None # codex 0.154+ 只讲 responses:chat-only 供应商直接判不可绑
|
|
2113
|
+
# (解析为空 → 死链闸门/路由降权接手,不浪费 CLI 尝试)
|
|
2063
2114
|
names = [m["name"] for m in _enabled_models(prov)]
|
|
2064
2115
|
model = prov.get("model_" + tier) or "" if (routing and tier) else ""
|
|
2065
2116
|
if not model:
|
package/app/core/pipeline.py
CHANGED
|
@@ -273,18 +273,51 @@ def _wait_gate(run_id, ev):
|
|
|
273
273
|
time.sleep(1.0)
|
|
274
274
|
|
|
275
275
|
|
|
276
|
+
def _binding_dead_msg(agent):
|
|
277
|
+
"""死链失败文案:委托 modelhub 单一真源(告警 sync 共用同一文案)。"""
|
|
278
|
+
try:
|
|
279
|
+
from . import modelhub
|
|
280
|
+
return modelhub.binding_dead_msg(agent.get("id") or "")
|
|
281
|
+
except Exception:
|
|
282
|
+
return ("绑定链全部失效,本步判失败、不回落 CLI 本机默认——"
|
|
283
|
+
"请在「CLI 绑定」页为该 CLI 绑定已启用的供应商")
|
|
284
|
+
|
|
285
|
+
|
|
276
286
|
def _run_step(run_id, role, agent, prompt, workdir, readonly, ev, timeout=runner.DEFAULT_TIMEOUT, note="", resume=None, images=None, require_tools=False):
|
|
277
287
|
"""执行一个智能体步骤并记录。返回 runner 统一结果。"""
|
|
278
288
|
_wait_gate(run_id, ev)
|
|
279
|
-
#
|
|
280
|
-
# 2026-09-
|
|
281
|
-
#
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
289
|
+
# 绑定解析为空:旧语义是回落 CLI 本机默认继续跑,2026-09-16 实测这种状态
|
|
290
|
+
# 会静默烧本机默认供应商的配额(用户以为在用自己配的模型)。2026-09-17 起
|
|
291
|
+
# 改为「告警 + 本步直接判失败」——宁可失败不静默降级;auto 流程实现步的
|
|
292
|
+
# 既有换将会接手健康 CLI,真没有可用 CLI 时运行以明确的绑定错误收场。
|
|
293
|
+
dead_binding = (agent.get("mode") == "real"
|
|
294
|
+
and not (agent.get("call_chain") or agent.get("env")))
|
|
295
|
+
dead_msg = _binding_dead_msg(agent) if dead_binding else ""
|
|
296
|
+
if dead_binding:
|
|
297
|
+
try:
|
|
298
|
+
from . import health
|
|
299
|
+
health.report_binding_dead(agent["id"], dead_msg)
|
|
300
|
+
except Exception:
|
|
301
|
+
pass
|
|
302
|
+
note = ((note + ";") if note else "") + "⚠ " + dead_msg
|
|
303
|
+
elif agent.get("mode") == "real":
|
|
304
|
+
try: # 绑定恢复:自动解除该 CLI 的静态死链告警
|
|
305
|
+
from . import health
|
|
306
|
+
health.report_binding_ok(agent["id"])
|
|
307
|
+
except Exception:
|
|
308
|
+
pass
|
|
285
309
|
step, log_abs = store.add_step(run_id, role, agent["id"],
|
|
286
310
|
agent.get("label", agent["id"]), note=note)
|
|
287
311
|
start = time.time()
|
|
312
|
+
if dead_binding:
|
|
313
|
+
from .error_codes import ErrorCode
|
|
314
|
+
res = {"ok": False, "text": "", "json": None, "cost_usd": 0.0,
|
|
315
|
+
"tokens": 0, "usage": None, "error": dead_msg,
|
|
316
|
+
"error_code": ErrorCode.ENV_BLOCK,
|
|
317
|
+
"raw": {"exit_code": None}, "kind": agent.get("kind", "generic"),
|
|
318
|
+
"model": agent.get("model")}
|
|
319
|
+
_finish_step_result(run_id, step, res, role, agent, start)
|
|
320
|
+
return res
|
|
288
321
|
if agent.get("mode") == "mock":
|
|
289
322
|
time.sleep(0.3)
|
|
290
323
|
res = {"ok": True, "text": "[mock] %s" % prompt[:80], "json": None,
|
|
@@ -1601,12 +1634,19 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
1601
1634
|
res = None
|
|
1602
1635
|
good = False
|
|
1603
1636
|
txt = ""
|
|
1637
|
+
use_prompt = prompt
|
|
1604
1638
|
for draft_attempt in range(3):
|
|
1605
1639
|
if draft_attempt:
|
|
1606
1640
|
if ev is not None and ev.is_set():
|
|
1607
1641
|
break
|
|
1608
1642
|
time.sleep(30 * draft_attempt) # 30s / 60s 退避
|
|
1609
|
-
|
|
1643
|
+
if draft_attempt and len(prompt) > 12000 and sk_block and sk_block in prompt:
|
|
1644
|
+
# 长提示词在容量受限通道(讯飞托管 35B 等)上会挂起/秒拒
|
|
1645
|
+
# ——降级重试:经验库块截到 4K 字,保留大纲/前情/本章要点
|
|
1646
|
+
# (2026-09-17 七猫实测:全量 30KB 对讯飞必挂)
|
|
1647
|
+
use_prompt = prompt.replace(
|
|
1648
|
+
sk_block, sk_block[:4000] + "\n\n(经验库已因通道容量限制精简)")
|
|
1649
|
+
res = _run_step(run_id, "draft-c%d" % i, modelhub.bind_agent(impl, difficulty), use_prompt,
|
|
1610
1650
|
step_wd, readonly=False, ev=ev, timeout=2400,
|
|
1611
1651
|
resume=resume_ctx["session"] if resume_ctx else None,
|
|
1612
1652
|
images=_task_images(task, workdir),
|
|
@@ -1626,15 +1666,23 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
1626
1666
|
break
|
|
1627
1667
|
if ev is not None and ev.is_set():
|
|
1628
1668
|
break
|
|
1629
|
-
# 同作者重试穷尽 →
|
|
1630
|
-
#
|
|
1669
|
+
# 同作者重试穷尽 → 起草换将:按路由分序逐个试备选(最多 2 个,
|
|
1670
|
+
# 只试一个会让第二名没机会——2026-09-17 c35 实测 opencode 顶在
|
|
1671
|
+
# 前面,能干活的 kimi 永远轮不上)。连载不断档优先,风格差异交
|
|
1672
|
+
# 评审门与后续 revise 拉回。
|
|
1631
1673
|
if not good:
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1674
|
+
tried = {impl["id"], "mock-a", "mock-b"}
|
|
1675
|
+
for _alt in range(2):
|
|
1676
|
+
if good or (ev is not None and ev.is_set()):
|
|
1677
|
+
break
|
|
1678
|
+
other, other_reason = router.pick(
|
|
1679
|
+
agents, "implement", task.get("type") or "serial", None,
|
|
1680
|
+
exclude=tried)
|
|
1681
|
+
if not (other and other.get("mode") == "real"):
|
|
1682
|
+
break
|
|
1683
|
+
tried.add(other["id"])
|
|
1636
1684
|
res = _run_step(run_id, "draft-c%d" % i,
|
|
1637
|
-
modelhub.bind_agent(other, difficulty),
|
|
1685
|
+
modelhub.bind_agent(other, difficulty), use_prompt,
|
|
1638
1686
|
step_wd, readonly=False, ev=ev, timeout=2400,
|
|
1639
1687
|
images=_task_images(task, workdir),
|
|
1640
1688
|
note="起草换将 %s → %s:%s" % (
|
|
@@ -1649,6 +1697,9 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
1649
1697
|
pass
|
|
1650
1698
|
if good:
|
|
1651
1699
|
draft_sid = "" # 换将作者无本任会话,revise 另起
|
|
1700
|
+
if not good:
|
|
1701
|
+
time.sleep(3) # 落盘竞态宽限:CLI 崩溃退出前写的文件可能晚于
|
|
1702
|
+
good, txt = _chapter_state() # 退出检查零点几秒才可见(c34 实测)
|
|
1652
1703
|
if not good:
|
|
1653
1704
|
store.update_run(run_id, status="failed",
|
|
1654
1705
|
error="第 %d 章起草失败: %s" % (i, (res or {}).get("error")), ended_at=_now())
|
|
@@ -1668,53 +1719,68 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
1668
1719
|
# 均分最高者为正稿。变体写隔离文件 chapter-XX-vK.md,赢家改名、
|
|
1669
1720
|
# 败稿删除;变体 0 = 本任作者(revise 会话沿用),其余取跨族优先的
|
|
1670
1721
|
# 其他真实智能体,不足时同作者开新会话凑数。
|
|
1671
|
-
pool = [impl]
|
|
1672
|
-
others = [a for a in agents if a.get("mode") == "real" and a["id"] != impl["id"]]
|
|
1673
|
-
others.sort(key=lambda a: 0 if a.get("kind") != impl.get("kind") else 1)
|
|
1674
|
-
pool += others[:n_variants - 1]
|
|
1675
|
-
while len(pool) < n_variants:
|
|
1676
|
-
pool.append(impl) # 不够就同作者再开一路(新会话天然出不同稿)
|
|
1677
|
-
results = {}
|
|
1678
|
-
|
|
1679
|
-
def _draft_one(kk, agent):
|
|
1680
|
-
vfile = "chapter-%02d-v%d.md" % (i, kk)
|
|
1681
|
-
r = _run_step(run_id, "draft-c%d-v%d" % (i, kk),
|
|
1682
|
-
modelhub.bind_agent(agent, difficulty),
|
|
1683
|
-
_draft_prompt(vfile), step_wd, readonly=False, ev=ev,
|
|
1684
|
-
timeout=2400,
|
|
1685
|
-
# 赛马只在全新起草时启用(无续会话),每路都是新会话
|
|
1686
|
-
images=_task_images(task, workdir),
|
|
1687
|
-
note="赛马变体 %d/%d(%s)" % (kk + 1, len(pool), agent.get("id")))
|
|
1688
|
-
results[kk] = (vfile, agent, r)
|
|
1689
|
-
|
|
1690
|
-
threads = []
|
|
1691
|
-
for kk, agent in enumerate(pool):
|
|
1692
|
-
th = threading.Thread(target=_draft_one, args=(kk, agent),
|
|
1693
|
-
name="race-%s-c%d-v%d" % (run_id, i, kk), daemon=True)
|
|
1694
|
-
threads.append(th)
|
|
1695
|
-
th.start()
|
|
1696
|
-
for th in threads:
|
|
1697
|
-
th.join(3000)
|
|
1698
|
-
_check_cancel(ev)
|
|
1699
|
-
|
|
1700
1722
|
scored_variants = []
|
|
1701
|
-
for
|
|
1702
|
-
|
|
1703
|
-
if
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1723
|
+
for race_round in range(2):
|
|
1724
|
+
# 全变体失败(网关突发限流)→ 60s 退避重赛一轮,别一章判死
|
|
1725
|
+
if race_round:
|
|
1726
|
+
if ev is not None and ev.is_set():
|
|
1727
|
+
break
|
|
1728
|
+
time.sleep(60)
|
|
1729
|
+
for kk in range(n_variants):
|
|
1730
|
+
# 清上一轮残稿:防陈旧半成品被本轮评分误认成新成品
|
|
1731
|
+
try:
|
|
1732
|
+
os.remove(os.path.join(workdir, "chapter-%02d-v%d.md" % (i, kk)))
|
|
1733
|
+
except OSError:
|
|
1734
|
+
pass
|
|
1735
|
+
pool = [impl]
|
|
1736
|
+
others = [a for a in agents if a.get("mode") == "real" and a["id"] != impl["id"]]
|
|
1737
|
+
others.sort(key=lambda a: 0 if a.get("kind") != impl.get("kind") else 1)
|
|
1738
|
+
pool += others[:n_variants - 1]
|
|
1739
|
+
while len(pool) < n_variants:
|
|
1740
|
+
pool.append(impl) # 不够就同作者再开一路(新会话天然出不同稿)
|
|
1741
|
+
results = {}
|
|
1742
|
+
|
|
1743
|
+
def _draft_one(kk, agent):
|
|
1744
|
+
vfile = "chapter-%02d-v%d.md" % (i, kk)
|
|
1745
|
+
r = _run_step(run_id, "draft-c%d-v%d" % (i, kk),
|
|
1746
|
+
modelhub.bind_agent(agent, difficulty),
|
|
1747
|
+
_draft_prompt(vfile), step_wd, readonly=False, ev=ev,
|
|
1748
|
+
timeout=2400,
|
|
1749
|
+
# 赛马只在全新起草时启用(无续会话),每路都是新会话
|
|
1750
|
+
images=_task_images(task, workdir),
|
|
1751
|
+
note="赛马变体 %d/%d(%s)" % (kk + 1, len(pool), agent.get("id")))
|
|
1752
|
+
results[kk] = (vfile, agent, r)
|
|
1753
|
+
|
|
1754
|
+
threads = []
|
|
1755
|
+
for kk, agent in enumerate(pool):
|
|
1756
|
+
th = threading.Thread(target=_draft_one, args=(kk, agent),
|
|
1757
|
+
name="race-%s-c%d-v%d" % (run_id, i, kk), daemon=True)
|
|
1758
|
+
threads.append(th)
|
|
1759
|
+
th.start()
|
|
1760
|
+
for th in threads:
|
|
1761
|
+
th.join(3000)
|
|
1762
|
+
_check_cancel(ev)
|
|
1763
|
+
|
|
1764
|
+
scored_variants = []
|
|
1765
|
+
for kk in range(len(pool)):
|
|
1766
|
+
vfile, agent, r = results.get(kk, (None, None, None))
|
|
1767
|
+
if vfile is None:
|
|
1768
|
+
continue
|
|
1769
|
+
txt = _read_variant(workdir, i, kk)
|
|
1770
|
+
ok_text = txt and _wc(txt) >= int(wpc * 0.6)
|
|
1771
|
+
if r is not None and not r["ok"] and not ok_text:
|
|
1772
|
+
continue # 这一路彻底失败(无成品也不够长)
|
|
1773
|
+
if not ok_text:
|
|
1774
|
+
continue
|
|
1775
|
+
cj_map, sc, sids2 = run_critique(
|
|
1776
|
+
txt, 1, note_extra="(本稿为同章赛马变体 %d/%d,只评这一份)" % (kk + 1, len(pool)))
|
|
1777
|
+
m = means_of(cj_map)
|
|
1778
|
+
avg = round(sum(m.values()) / max(1, len(m)), 2) if m else 0.0
|
|
1779
|
+
scored_variants.append({"variant": kk, "agent": agent.get("id"),
|
|
1780
|
+
"file": vfile, "means": m, "avg": avg,
|
|
1781
|
+
"cj": cj_map, "scored": sc, "sids": sids2})
|
|
1782
|
+
if scored_variants:
|
|
1783
|
+
break
|
|
1718
1784
|
if not scored_variants:
|
|
1719
1785
|
store.update_run(run_id, status="failed",
|
|
1720
1786
|
error="第 %d 章赛马全部变体起草失败" % i, ended_at=_now())
|
package/app/core/registry.py
CHANGED
|
@@ -66,6 +66,10 @@ def _build_agent(entry):
|
|
|
66
66
|
# 小时级 token 配额(可选,0/缺省=不限):路由时对本小时用量超标的
|
|
67
67
|
# 智能体降权(munder-difflin 式配额感知),订阅型 CLI 不至于被单任务打爆
|
|
68
68
|
"quota_tokens_per_hour": int(orch.get("quota_tokens_per_hour") or 0),
|
|
69
|
+
# 整包透传 orch:per-agent 运行时开关(timeout_ms 5E、stall_timeout_s
|
|
70
|
+
# 看门狗等)都在 runner 侧读取——此前只挑字段,catalog 上配的
|
|
71
|
+
# timeout_ms/stall 根本流不到 runner(2026-09-17 连载 c35 实测)
|
|
72
|
+
"orch": orch,
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
|
package/app/core/router.py
CHANGED
|
@@ -14,9 +14,10 @@ MAX_REPAIR_ROUNDS = 2 # 自动修复循环上限
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def _binding_bonus(agent_id):
|
|
17
|
-
"""绑定链可用性加分/减分:链上有可用条目 +8
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
"""绑定链可用性加分/减分:链上有可用条目 +8,解析为空 -25。2026-09-16 实测:
|
|
18
|
+
静态能力基线让配额烧干的 codex 永远压过健康备用 CLI,绑定空的 CLI 更是连
|
|
19
|
+
用户配置的模型都没用上——先按「能不能按配置跑起来」校准。2026-09-17 起
|
|
20
|
+
空链步骤在 pipeline 直接判失败(不再静默回落本机默认),此处只管排序。"""
|
|
20
21
|
try:
|
|
21
22
|
from . import modelhub
|
|
22
23
|
b = modelhub.resolve_binding(agent_id)
|
|
@@ -49,7 +50,7 @@ def score(agent, role, ttype, stats=None):
|
|
|
49
50
|
if bb > 0:
|
|
50
51
|
btxt = ",绑定链可用(+%s)" % bb
|
|
51
52
|
elif bb < 0:
|
|
52
|
-
btxt = "
|
|
53
|
+
btxt = ",绑定链为空:相关步骤将判失败(%s)" % bb
|
|
53
54
|
hb = _history_bonus(stats, agent.get("id"), ttype)
|
|
54
55
|
total = base + bb + hb
|
|
55
56
|
hs = (stats.get(agent.get("id")) or {}).get(ttype)
|
package/app/core/runner.py
CHANGED
|
@@ -824,9 +824,18 @@ def run_agent(agent, prompt, workdir=None, readonly=True,
|
|
|
824
824
|
if orch_timeout_ms:
|
|
825
825
|
timeout = float(orch_timeout_ms) / 1000.0
|
|
826
826
|
kind = agent.get("kind", "generic")
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
827
|
+
if kind == "codex":
|
|
828
|
+
stall_t = _stall_timeout("TUTTI_CODEX_STALL_TIMEOUT", 600)
|
|
829
|
+
elif kind == "claude":
|
|
830
|
+
stall_t = _stall_timeout("TUTTI_CLAUDE_STALL_TIMEOUT", 600)
|
|
831
|
+
else:
|
|
832
|
+
# 数据驱动:catalog orch.stall_timeout_s——给 kimi 这类边写边吐进度行的
|
|
833
|
+
# CLI 配置后,静默挂死 10 分钟即杀(不必耗满总超时);qwen/mimo 这类
|
|
834
|
+
# 结束才一次性输出的留 0(开了必误杀),靠总超时兜底
|
|
835
|
+
try:
|
|
836
|
+
stall_t = max(0, int((agent.get("orch") or {}).get("stall_timeout_s") or 0))
|
|
837
|
+
except Exception:
|
|
838
|
+
stall_t = 0
|
|
830
839
|
# 5G:approval NEVER 一线(无人值守不静默降级)
|
|
831
840
|
ok, reason = _check_approval(agent)
|
|
832
841
|
if not ok:
|
package/app/core/selfupdate.py
CHANGED
|
@@ -76,7 +76,8 @@ def _npm_meta():
|
|
|
76
76
|
"""npm view <pkg> --json;返回 (latest, relnotes, err)。
|
|
77
77
|
|
|
78
78
|
relnotes 来自 registry 元数据里的 README(与查新同一条 npm 通道,不依赖
|
|
79
|
-
GitHub
|
|
79
|
+
GitHub 连通性),展示「新版本更新内容」用。部分 npm 版本 --json 不带
|
|
80
|
+
readme 字段,此时回退到 `npm view <pkg> readme` 纯文本再提取。"""
|
|
80
81
|
r = runner.run_process(
|
|
81
82
|
argv=["cmd", "/c", "npm", "view", _PKG_NAME, "--json"], timeout=60)
|
|
82
83
|
if not r["ok"]:
|
|
@@ -92,6 +93,11 @@ def _npm_meta():
|
|
|
92
93
|
if not ver: # 旧 npm --json 失败时退回纯文本解析
|
|
93
94
|
m = re.search(r"\d+\.\d+\.\d+[\w.\-]*", r["stdout"] or "")
|
|
94
95
|
ver = m.group(0) if m else ""
|
|
96
|
+
if ver and not notes:
|
|
97
|
+
r2 = runner.run_process(
|
|
98
|
+
argv=["cmd", "/c", "npm", "view", _PKG_NAME, "readme"], timeout=60)
|
|
99
|
+
if r2["ok"]:
|
|
100
|
+
notes = _relnotes(r2["stdout"] or "")
|
|
95
101
|
return ver, notes, ("" if ver else "npm 输出无法解析")
|
|
96
102
|
|
|
97
103
|
|