codebee 0.1.20 → 0.1.21
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 +6 -0
- package/README.md +8 -6
- package/app/core/automation.py +6 -6
- package/app/core/capability.py +16 -2
- package/app/core/catalog.py +1 -1
- package/app/core/covergen.py +40 -4
- package/app/core/dispatch.py +141 -0
- package/app/core/flows.py +5 -0
- package/app/core/jobs.py +281 -166
- package/app/core/manager.py +30 -23
- package/app/core/market_remote.py +66 -22
- package/app/core/modelhub.py +145 -11
- package/app/core/pipeline.py +111 -30
- package/app/core/router.py +18 -7
- package/app/core/runner.py +2 -1
- package/app/core/selfupdate.py +24 -14
- package/app/core/settings.py +3 -3
- package/app/core/skills.py +43 -0
- package/app/core/store.py +20 -14
- package/app/core/zentao.py +26 -6
- package/app/main.py +20 -12
- package/app/pet.py +2 -2
- package/app/ui/app.js +62 -58
- package/app/ui/i18n.js +21 -3
- package/app/ui/index.html +11 -11
- package/app/ui/style.css +2 -0
- package/package.json +1 -1
package/app/core/pipeline.py
CHANGED
|
@@ -321,7 +321,7 @@ def _binding_dead_msg(agent):
|
|
|
321
321
|
return modelhub.binding_dead_msg(agent.get("id") or "")
|
|
322
322
|
except Exception:
|
|
323
323
|
return ("绑定链全部失效,本步判失败、不回落 CLI 本机默认——"
|
|
324
|
-
"
|
|
324
|
+
"请在「模型调度(可选)」页为该 CLI 指定已启用的供应商")
|
|
325
325
|
|
|
326
326
|
|
|
327
327
|
def _run_step(run_id, role, agent, prompt, workdir, readonly, ev, timeout=runner.DEFAULT_TIMEOUT, note="", resume=None, images=None, require_tools=False):
|
|
@@ -907,12 +907,18 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
907
907
|
else:
|
|
908
908
|
impl, route["implementer"] = router.pick(agents, "implement", "code", stats)
|
|
909
909
|
if impl is None:
|
|
910
|
-
store.update_run(run_id,
|
|
910
|
+
store.update_run(run_id, expected_status="running", status="failed",
|
|
911
|
+
error="没有可用智能体", ended_at=_now())
|
|
911
912
|
return
|
|
912
913
|
|
|
913
914
|
# ---- 规划
|
|
914
915
|
if mode == "auto":
|
|
915
916
|
_wait_gate(run_id, ev)
|
|
917
|
+
# 项目记忆注入(借鉴 agentmemory 持久记忆):同工作目录此前代码任务留下的
|
|
918
|
+
# 架构事实,让规划器不再对代码库一无所知
|
|
919
|
+
_pm = _read_project_memory(workdir)
|
|
920
|
+
if _pm:
|
|
921
|
+
task = dict(task, context=((task.get("context") or "") + "\n\n" + _pm)[:8000])
|
|
916
922
|
plan_step, plan_log = store.add_step(run_id, "plan", impl["id"], impl.get("label"),
|
|
917
923
|
note=route.get("implementer", ""))
|
|
918
924
|
plan = planner.make_code_plan(_steered_task(run_id, task),
|
|
@@ -961,8 +967,13 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
961
967
|
register_default_namespaces()
|
|
962
968
|
if ss_get("cascade", "enabled"):
|
|
963
969
|
from . import capability
|
|
970
|
+
mh_data = modelhub._load()
|
|
964
971
|
agt_b = capability.cascade_reorder(
|
|
965
|
-
agt_b, capability.make_tier_lookup(modelhub.providers())
|
|
972
|
+
agt_b, capability.make_tier_lookup(modelhub.providers()),
|
|
973
|
+
providers=modelhub.providers(),
|
|
974
|
+
pricing=mh_data.get("pricing") or {},
|
|
975
|
+
difficulty=difficulty, task_type=task.get("type") or "code",
|
|
976
|
+
role="implement")
|
|
966
977
|
except Exception:
|
|
967
978
|
pass
|
|
968
979
|
for i, sub in enumerate(subtasks):
|
|
@@ -1033,7 +1044,8 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
1033
1044
|
res_err = "实现步骤失败(无其他真实 CLI 可换将): %s" % res.get("error")
|
|
1034
1045
|
else:
|
|
1035
1046
|
res_err = "实现步骤失败: %s" % res.get("error")
|
|
1036
|
-
store.update_run(run_id,
|
|
1047
|
+
store.update_run(run_id, expected_status="running", status="failed",
|
|
1048
|
+
error=res_err, ended_at=_now())
|
|
1037
1049
|
return False
|
|
1038
1050
|
|
|
1039
1051
|
def review_and_score():
|
|
@@ -1129,7 +1141,21 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
1129
1141
|
lines += ["", "## 评审总评", "", review_json.get("summary", ""), ""]
|
|
1130
1142
|
store.write_report(run_id, "\n".join(lines))
|
|
1131
1143
|
_write_task_evidence(run_id, task, workdir, _evidence_lines_from_run(run_id, task))
|
|
1132
|
-
|
|
1144
|
+
# 项目记忆沉淀(借鉴 agentmemory 持久记忆):代码任务成功后提取架构事实
|
|
1145
|
+
# (改动文件/验收结果/修复轮数),追加到 .codebee/project-memory.md——
|
|
1146
|
+
# 同目录后续 code 任务规划前自动注入,让编排者「知道这个代码库的脾气」
|
|
1147
|
+
try:
|
|
1148
|
+
_diff = _git_diff(workdir)
|
|
1149
|
+
_files_touched = sorted(set(re.findall(
|
|
1150
|
+
r"(?:^|\n)diff --git a/(\S+) b/(\S+)", _diff or "")))
|
|
1151
|
+
_touched_str = "、".join(sorted(set(b for _, b in _files_touched)))[:500] if _files_touched else ""
|
|
1152
|
+
_mem_lines = ["改动文件:%s" % (_touched_str or "(无 diff)"),
|
|
1153
|
+
"验收:%s" % ("通过" if verify_pass else "未通过"),
|
|
1154
|
+
"修复轮数:%d" % (len(repairs) - 1)]
|
|
1155
|
+
_write_project_memory(task, workdir, _mem_lines)
|
|
1156
|
+
except Exception:
|
|
1157
|
+
pass
|
|
1158
|
+
store.update_run(run_id, expected_status="running", status="done", verdict=verdict,
|
|
1133
1159
|
summary="代码任务%s(验证%s / 评审%s%s)" % (
|
|
1134
1160
|
"通过" if overall_pass else "未通过",
|
|
1135
1161
|
"通过" if verify_pass else "未通过",
|
|
@@ -1297,7 +1323,8 @@ def _run_direct(run, task, agents, ev, stats, mode):
|
|
|
1297
1323
|
else:
|
|
1298
1324
|
impl, route["implementer"] = router.pick(agents, "implement", task["type"], stats)
|
|
1299
1325
|
if impl is None and bi is None:
|
|
1300
|
-
store.update_run(run_id,
|
|
1326
|
+
store.update_run(run_id, expected_status="running", status="failed",
|
|
1327
|
+
error="没有可用智能体", ended_at=_now())
|
|
1301
1328
|
return
|
|
1302
1329
|
difficulty = task.get("difficulty") or "default"
|
|
1303
1330
|
step_wd = _resume_workdir(resume_ctx, workdir) if resume_ctx else workdir
|
|
@@ -1371,7 +1398,7 @@ def _run_direct(run, task, agents, ev, stats, mode):
|
|
|
1371
1398
|
readonly=False, ev=ev, note=note,
|
|
1372
1399
|
resume=sid or None, images=images)
|
|
1373
1400
|
if not res["ok"]:
|
|
1374
|
-
store.update_run(run_id, status="failed",
|
|
1401
|
+
store.update_run(run_id, expected_status="running", status="failed",
|
|
1375
1402
|
error="执行失败: %s" % res.get("error"), ended_at=_now())
|
|
1376
1403
|
return
|
|
1377
1404
|
turns += 1
|
|
@@ -1403,7 +1430,7 @@ def _run_direct(run, task, agents, ev, stats, mode):
|
|
|
1403
1430
|
report += ["## 最近一轮输出", "", last_text[-5000:], ""]
|
|
1404
1431
|
store.write_report(run_id, "\n".join(report))
|
|
1405
1432
|
_write_task_evidence(run_id, task, workdir, _evidence_lines_from_run(run_id, task))
|
|
1406
|
-
store.update_run(run_id, status="done", verdict=verdict,
|
|
1433
|
+
store.update_run(run_id, expected_status="running", status="done", verdict=verdict,
|
|
1407
1434
|
summary="直连完成(%d 轮):%s" % (turns, last_text[:160]),
|
|
1408
1435
|
ended_at=_now())
|
|
1409
1436
|
|
|
@@ -1886,7 +1913,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
1886
1913
|
# 历史遗留:降级/模板大纲被继承时,真实任务宁可中止重生成,也不按空模板写全书
|
|
1887
1914
|
if (outline.get("degraded") or outline.get("source") == "template") \
|
|
1888
1915
|
and impl.get("mode") != "mock":
|
|
1889
|
-
store.update_run(run_id, status="failed",
|
|
1916
|
+
store.update_run(run_id, expected_status="running", status="failed",
|
|
1890
1917
|
error="继承的大纲为降级模板(无真实情节),已中止以重新生成大纲",
|
|
1891
1918
|
ended_at=_now())
|
|
1892
1919
|
return
|
|
@@ -1909,7 +1936,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
1909
1936
|
store.finish_step(run_id, outline_step["n"], "failed",
|
|
1910
1937
|
summary="大纲降级:%s" % (outline.get("degraded_reason") or "编排者不可用"),
|
|
1911
1938
|
duration_s=None)
|
|
1912
|
-
store.update_run(run_id, status="failed",
|
|
1939
|
+
store.update_run(run_id, expected_status="running", status="failed",
|
|
1913
1940
|
error="%s,已中止以免按空模板写全书"
|
|
1914
1941
|
% (outline.get("degraded_reason") or "编排者不可用"),
|
|
1915
1942
|
ended_at=_now())
|
|
@@ -2178,7 +2205,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2178
2205
|
time.sleep(3) # 落盘竞态宽限:CLI 崩溃退出前写的文件可能晚于
|
|
2179
2206
|
good, txt = _chapter_state() # 退出检查零点几秒才可见(c34 实测)
|
|
2180
2207
|
if not good:
|
|
2181
|
-
store.update_run(run_id, status="failed",
|
|
2208
|
+
store.update_run(run_id, expected_status="running", status="failed",
|
|
2182
2209
|
error="第 %d 章起草失败: %s" % (i, (res or {}).get("error")), ended_at=_now())
|
|
2183
2210
|
return
|
|
2184
2211
|
if not res["ok"]:
|
|
@@ -2262,7 +2289,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2262
2289
|
if scored_variants:
|
|
2263
2290
|
break
|
|
2264
2291
|
if not scored_variants:
|
|
2265
|
-
store.update_run(run_id, status="failed",
|
|
2292
|
+
store.update_run(run_id, expected_status="running", status="failed",
|
|
2266
2293
|
error="第 %d 章赛马全部变体起草失败" % i, ended_at=_now())
|
|
2267
2294
|
return
|
|
2268
2295
|
scored_variants.sort(key=lambda v: (-v["avg"], v["variant"]))
|
|
@@ -2273,7 +2300,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2273
2300
|
os.replace(os.path.join(workdir, win["file"]),
|
|
2274
2301
|
os.path.join(workdir, ch_file))
|
|
2275
2302
|
except OSError as e:
|
|
2276
|
-
store.update_run(run_id, status="failed",
|
|
2303
|
+
store.update_run(run_id, expected_status="running", status="failed",
|
|
2277
2304
|
error="第 %d 章赛马收卷失败: %r" % (i, e), ended_at=_now())
|
|
2278
2305
|
return
|
|
2279
2306
|
for v in scored_variants[1:]:
|
|
@@ -2321,7 +2348,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2321
2348
|
if not scored:
|
|
2322
2349
|
# 「评不上」≠「评了 0 分」:全部评审失败时中止本轮,
|
|
2323
2350
|
# 让自动续跑换个时机重试,而不是以 0 分误判章稿质量。
|
|
2324
|
-
store.update_run(run_id, status="failed",
|
|
2351
|
+
store.update_run(run_id, expected_status="running", status="failed",
|
|
2325
2352
|
error="第 %d 章评审全部失败(评审模型不可用或输出不可解析),"
|
|
2326
2353
|
"已中止以免以 0 分误判质量" % i, ended_at=_now())
|
|
2327
2354
|
return
|
|
@@ -2452,7 +2479,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2452
2479
|
if gscored:
|
|
2453
2480
|
break
|
|
2454
2481
|
if not gscored:
|
|
2455
|
-
store.update_run(run_id, status="failed",
|
|
2482
|
+
store.update_run(run_id, expected_status="running", status="failed",
|
|
2456
2483
|
error="全局一致性评审全部失败(评审模型不可用或输出不可解析),"
|
|
2457
2484
|
"已中止以免把「无法评审」误判为「未达标」。"
|
|
2458
2485
|
"各章稿件已全部落盘,修复评审链后续跑可直接收尾",
|
|
@@ -2621,7 +2648,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2621
2648
|
lines.append("(无 major 问题)")
|
|
2622
2649
|
store.write_report(run_id, "\n".join(lines))
|
|
2623
2650
|
_write_task_evidence(run_id, task, workdir, _evidence_lines_from_run(run_id, task))
|
|
2624
|
-
store.update_run(run_id, status="done", verdict=verdict,
|
|
2651
|
+
store.update_run(run_id, expected_status="running", status="done", verdict=verdict,
|
|
2625
2652
|
summary="连载任务%s(%s,约 %d 字,综合 %.1f)" % (
|
|
2626
2653
|
"达标" if publishable else "未达标", scope_txt,
|
|
2627
2654
|
total_words, overall),
|
|
@@ -2768,7 +2795,8 @@ def _run_content_review(run, task, agents, ev, stats, mode):
|
|
|
2768
2795
|
impl, route["author"] = router.pick(agents, "implement", task_type, stats)
|
|
2769
2796
|
critics, route["critics"] = router.pick_critics(agents, task_type, stats, impl=impl)
|
|
2770
2797
|
if impl is None:
|
|
2771
|
-
store.update_run(run_id,
|
|
2798
|
+
store.update_run(run_id, expected_status="running", status="failed",
|
|
2799
|
+
error="没有可用智能体", ended_at=_now())
|
|
2772
2800
|
return
|
|
2773
2801
|
if resume_ctx is not None and mode == "auto":
|
|
2774
2802
|
critics, route["critics"] = router.pick_critics(
|
|
@@ -2847,7 +2875,8 @@ def _run_content_review(run, task, agents, ev, stats, mode):
|
|
|
2847
2875
|
resume=resume_ctx["session"] if resume_ctx else None,
|
|
2848
2876
|
images=_task_images(task, workdir))
|
|
2849
2877
|
if not draft_res["ok"]:
|
|
2850
|
-
store.update_run(run_id,
|
|
2878
|
+
store.update_run(run_id, expected_status="running", status="failed",
|
|
2879
|
+
error="起草失败: %s" % draft_res.get("error"),
|
|
2851
2880
|
ended_at=_now())
|
|
2852
2881
|
return
|
|
2853
2882
|
|
|
@@ -2979,7 +3008,7 @@ def _run_content_review(run, task, agents, ev, stats, mode):
|
|
|
2979
3008
|
lines += ["", "## 稿件位置", "", "`%s`" % ms_path, ""]
|
|
2980
3009
|
store.write_report(run_id, "\n".join(lines))
|
|
2981
3010
|
_write_task_evidence(run_id, task, workdir, _evidence_lines_from_run(run_id, task))
|
|
2982
|
-
store.update_run(run_id, status="done", verdict=verdict,
|
|
3011
|
+
store.update_run(run_id, expected_status="running", status="done", verdict=verdict,
|
|
2983
3012
|
summary="评审任务%s(综合 %.1f)" % ("达标" if publishable else "未达标", overall),
|
|
2984
3013
|
ended_at=_now())
|
|
2985
3014
|
|
|
@@ -3024,13 +3053,13 @@ def _run_serial_qa(run, task, agents, ev):
|
|
|
3024
3053
|
res = _run_step(run_id, "qa", modelhub.bind_agent(agent, "default"),
|
|
3025
3054
|
prompt, workdir, readonly=True, ev=ev, timeout=1200)
|
|
3026
3055
|
if res.get("ok") and (res.get("text") or "").strip():
|
|
3027
|
-
store.update_run(run_id, status="done", ended_at=_now(),
|
|
3056
|
+
store.update_run(run_id, expected_status="running", status="done", ended_at=_now(),
|
|
3028
3057
|
verdict={"qa": True,
|
|
3029
3058
|
"answered_by": agent.get("id")})
|
|
3030
3059
|
return
|
|
3031
3060
|
errors.append("%s:%s" % (agent.get("id"),
|
|
3032
3061
|
(res.get("error") or "无输出")[:120]))
|
|
3033
|
-
store.update_run(run_id, status="failed", ended_at=_now(),
|
|
3062
|
+
store.update_run(run_id, expected_status="running", status="failed", ended_at=_now(),
|
|
3034
3063
|
error="答疑失败(执行/评审链不可用)——" + ";".join(errors[-3:]))
|
|
3035
3064
|
|
|
3036
3065
|
|
|
@@ -3051,6 +3080,43 @@ def _read_constitution(workdir):
|
|
|
3051
3080
|
"与其他要求冲突时以宪章为准)\n\n" + txt + "\n\n")
|
|
3052
3081
|
|
|
3053
3082
|
|
|
3083
|
+
def _write_project_memory(task, workdir, lines):
|
|
3084
|
+
"""项目记忆持久化(借鉴 agentmemory):代码任务成功后把架构事实追加到
|
|
3085
|
+
.codebee/project-memory.md——同目录后续 code 任务规划前自动注入,
|
|
3086
|
+
让编排者「知道这个代码库的脾气」而非每次从零摸索。失败静默。"""
|
|
3087
|
+
if not lines:
|
|
3088
|
+
return ""
|
|
3089
|
+
try:
|
|
3090
|
+
pm = os.path.join(workdir, ".codebee", "project-memory.md")
|
|
3091
|
+
os.makedirs(os.path.dirname(pm), exist_ok=True)
|
|
3092
|
+
header_needed = not os.path.isfile(pm)
|
|
3093
|
+
with open(pm, "a", encoding="utf-8") as f:
|
|
3094
|
+
if header_needed:
|
|
3095
|
+
f.write("# 项目记忆(每次代码任务完成后自动追加,供后续任务参考)\n\n")
|
|
3096
|
+
f.write("### %s · %s\n" % (task.get("title") or "", _now()))
|
|
3097
|
+
for ln in lines:
|
|
3098
|
+
f.write("- %s\n" % str(ln)[:300])
|
|
3099
|
+
f.write("\n")
|
|
3100
|
+
return pm
|
|
3101
|
+
except Exception:
|
|
3102
|
+
return ""
|
|
3103
|
+
|
|
3104
|
+
|
|
3105
|
+
def _read_project_memory(workdir, cap=4000):
|
|
3106
|
+
"""读取项目记忆供规划提示词注入。超出上限截断到最新条目。"""
|
|
3107
|
+
p = os.path.join(workdir or "", ".codebee", "project-memory.md")
|
|
3108
|
+
if not _inside(workdir, p) or not os.path.isfile(p):
|
|
3109
|
+
return ""
|
|
3110
|
+
try:
|
|
3111
|
+
txt = _read_text_any_enc(p)[:cap].strip()
|
|
3112
|
+
except OSError:
|
|
3113
|
+
return ""
|
|
3114
|
+
if not txt:
|
|
3115
|
+
return ""
|
|
3116
|
+
return ("## 项目记忆(此前代码任务在此工作目录留下的架构事实,"
|
|
3117
|
+
"规划时优先参考)\n\n" + txt + "\n\n")
|
|
3118
|
+
|
|
3119
|
+
|
|
3054
3120
|
def _write_task_spec(task, workdir):
|
|
3055
3121
|
"""任务规格落盘 .codebee/spec.md(借鉴 agent-orchestrator 的 .spec/PROMPT.md 与
|
|
3056
3122
|
planning-with-files 的文件化计划):任务定义随工作目录留存、随任务分支版本化,
|
|
@@ -3201,10 +3267,18 @@ def execute_run(run_id):
|
|
|
3201
3267
|
error="排队期间被取消")
|
|
3202
3268
|
return
|
|
3203
3269
|
task = store.get_task(run.get("task_id"))
|
|
3204
|
-
|
|
3270
|
+
# 生产 enqueue 已完成 queued→running 认领;测试/兼容调用也可能直接从
|
|
3271
|
+
# queued 进入。按初读状态做 CAS 起跑确认:取消若恰好落在读取之后,当前
|
|
3272
|
+
# 状态已是 cancelled,写入会失败并立即退出,不产生 Git/文件副作用。
|
|
3273
|
+
initial_status = run.get("status")
|
|
3274
|
+
if initial_status not in ("queued", "running"):
|
|
3275
|
+
return
|
|
3276
|
+
if store.update_run(run_id, expected_status=initial_status, status="running",
|
|
3277
|
+
started_at=_now()) is None:
|
|
3278
|
+
return
|
|
3205
3279
|
if task is None:
|
|
3206
|
-
store.update_run(run_id,
|
|
3207
|
-
ended_at=_now())
|
|
3280
|
+
store.update_run(run_id, expected_status="running", status="failed",
|
|
3281
|
+
error="找不到任务 %s" % run.get("task_id"), ended_at=_now())
|
|
3208
3282
|
return
|
|
3209
3283
|
# 代码版本检出:任务指定了基线版本时,先检出任务分支 tutti/<task-id> 再跑流水线。
|
|
3210
3284
|
# 显式意图不容静默降级——仓库缺失/脏工作区/引用不存在一律中止运行并报错,
|
|
@@ -3215,8 +3289,8 @@ def execute_run(run_id):
|
|
|
3215
3289
|
ok, err, gitinfo = gitmod.prepare_checkout(
|
|
3216
3290
|
task["workdir"], task["git_rev"], task["id"])
|
|
3217
3291
|
if not ok:
|
|
3218
|
-
store.update_run(run_id,
|
|
3219
|
-
ended_at=_now())
|
|
3292
|
+
store.update_run(run_id, expected_status="running", status="failed",
|
|
3293
|
+
error="代码版本检出失败:%s" % err, ended_at=_now())
|
|
3220
3294
|
return
|
|
3221
3295
|
git_ctx = gitinfo
|
|
3222
3296
|
store.update_run(run_id, git=gitinfo)
|
|
@@ -3240,6 +3314,11 @@ def execute_run(run_id):
|
|
|
3240
3314
|
if extra:
|
|
3241
3315
|
agents.append(extra)
|
|
3242
3316
|
stats = history.agent_stats()
|
|
3317
|
+
# 运行级任务画像:所有后续 bind_agent 调用共享同一预置类型,
|
|
3318
|
+
# 模型级联因此覆盖 direct/code/review/serial/translation 等全部引擎。
|
|
3319
|
+
for _agent in agents:
|
|
3320
|
+
if isinstance(_agent, dict):
|
|
3321
|
+
_agent["_dispatch_task_type"] = task.get("type") or "direct"
|
|
3243
3322
|
mode = task.get("mode") or ("manual" if task.get("implementer") else "auto")
|
|
3244
3323
|
store.update_run(run_id, mode=mode)
|
|
3245
3324
|
# engine 决定流水线:code=实现/验证/评审/修复;review=起草/多维评审/修订/门禁;
|
|
@@ -3272,7 +3351,8 @@ def execute_run(run_id):
|
|
|
3272
3351
|
impl, route["author"] = router.pick(agents, "implement", task["type"], stats)
|
|
3273
3352
|
critics, route["critics"] = router.pick_critics(agents, task["type"], stats, impl=impl)
|
|
3274
3353
|
if impl is None:
|
|
3275
|
-
store.update_run(run_id,
|
|
3354
|
+
store.update_run(run_id, expected_status="running", status="failed",
|
|
3355
|
+
error="没有可用智能体", ended_at=_now())
|
|
3276
3356
|
return
|
|
3277
3357
|
if resume_ctx is not None and mode == "auto":
|
|
3278
3358
|
critics, route["critics"] = router.pick_critics(agents, task["type"], stats, impl=impl)
|
|
@@ -3282,11 +3362,12 @@ def execute_run(run_id):
|
|
|
3282
3362
|
else:
|
|
3283
3363
|
_run_content_review(run, task, agents, ev, stats, mode)
|
|
3284
3364
|
except Cancelled:
|
|
3285
|
-
store.update_run(run_id,
|
|
3365
|
+
store.update_run(run_id, expected_status="running",
|
|
3366
|
+
status="cancelled", ended_at=_now())
|
|
3286
3367
|
except Exception as e:
|
|
3287
3368
|
import traceback
|
|
3288
|
-
store.update_run(run_id,
|
|
3289
|
-
ended_at=_now())
|
|
3369
|
+
store.update_run(run_id, expected_status="running", status="failed",
|
|
3370
|
+
error=repr(e)[:500], ended_at=_now())
|
|
3290
3371
|
try:
|
|
3291
3372
|
err_path = store.run_dir(run_id) / "error.log"
|
|
3292
3373
|
if _inside(str(store.run_dir(run_id).parent), str(err_path)):
|
package/app/core/router.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"""智能路由:能力基线 × 历史胜率 × 角色约束 → 选智能体,并给出可解释的理由。"""
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from . import history
|
|
5
|
+
from . import dispatch, history
|
|
6
6
|
|
|
7
7
|
# 各类智能体的能力基线(0-100)。真实 CLI 里官方双雄最高。
|
|
8
8
|
CAPABILITY = {
|
|
@@ -13,15 +13,19 @@ CAPABILITY = {
|
|
|
13
13
|
MAX_REPAIR_ROUNDS = 2 # 自动修复循环上限
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
def _binding_bonus(agent_id):
|
|
16
|
+
def _binding_bonus(agent_id, dispatch_mode=False):
|
|
17
17
|
"""绑定链可用性加分/减分:链上有可用条目 +8,解析为空 -25。2026-09-16 实测:
|
|
18
18
|
静态能力基线让配额烧干的 codex 永远压过健康备用 CLI,绑定空的 CLI 更是连
|
|
19
19
|
用户配置的模型都没用上——先按「能不能按配置跑起来」校准。2026-09-17 起
|
|
20
20
|
空链步骤在 pipeline 直接判失败(不再静默回落本机默认),此处只管排序。"""
|
|
21
21
|
try:
|
|
22
22
|
from . import modelhub
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
pref = modelhub._binding_for(agent_id)
|
|
24
|
+
configured = bool(modelhub._binding_chain(pref) or pref.get("provider_id"))
|
|
25
|
+
if not configured:
|
|
26
|
+
return 0.0 if dispatch_mode else -25.0
|
|
27
|
+
b = modelhub.resolve_binding(agent_id)
|
|
28
|
+
return 8.0 if (b and b.get("call_chain")) else -25.0
|
|
25
29
|
except Exception:
|
|
26
30
|
return 0.0
|
|
27
31
|
|
|
@@ -45,14 +49,20 @@ def score(agent, role, ttype, stats=None):
|
|
|
45
49
|
(封顶 -45,足以盖过历史加分),满额后仅在没有其他选择时才会被选中。"""
|
|
46
50
|
stats = stats or {}
|
|
47
51
|
base = CAPABILITY.get(agent.get("kind"), 60)
|
|
48
|
-
|
|
52
|
+
use_dispatch = bool(agent.get("_dispatch_task_type") or
|
|
53
|
+
agent.get("dispatch_enabled"))
|
|
54
|
+
bb = _binding_bonus(agent.get("id"), dispatch_mode=use_dispatch)
|
|
49
55
|
btxt = ""
|
|
50
56
|
if bb > 0:
|
|
51
57
|
btxt = ",绑定链可用(+%s)" % bb
|
|
52
58
|
elif bb < 0:
|
|
53
59
|
btxt = ",绑定链为空:相关步骤将判失败(%s)" % bb
|
|
54
60
|
hb = _history_bonus(stats, agent.get("id"), ttype)
|
|
55
|
-
|
|
61
|
+
# 保持公开 score() 的历史绝对分值;运行级候选由 pipeline 标记画像后
|
|
62
|
+
# 才启用能力亲和度,避免旧插件/测试调用被新权重悄然改变。
|
|
63
|
+
affinity, affinity_txt = (dispatch.agent_affinity(agent.get("kind"), ttype, role)
|
|
64
|
+
if use_dispatch else (0.0, "兼容模式"))
|
|
65
|
+
total = base + bb + hb + affinity
|
|
56
66
|
hs = (stats.get(agent.get("id")) or {}).get(ttype)
|
|
57
67
|
htxt = (",历史 %d/%d 胜(%s)" % (hs["wins"], hs["runs"], "%+.1f" % hb)) if hs else ",无历史记录"
|
|
58
68
|
quota_txt = ""
|
|
@@ -69,7 +79,8 @@ def score(agent, role, ttype, stats=None):
|
|
|
69
79
|
if penalty:
|
|
70
80
|
total += penalty
|
|
71
81
|
quota_txt = ",本小时 %d/%d tokens(%s)" % (used, quota, penalty)
|
|
72
|
-
return total, "能力基线 %d%s%s%s,总分 %s" % (
|
|
82
|
+
return total, "能力基线 %d,%s%s%s%s,总分 %s" % (
|
|
83
|
+
base, affinity_txt, btxt, htxt, quota_txt, round(total, 1))
|
|
73
84
|
|
|
74
85
|
|
|
75
86
|
def pick(agents, role, ttype, stats=None, exclude=()):
|
package/app/core/runner.py
CHANGED
|
@@ -76,7 +76,8 @@ def _npm_shim_bypass(argv):
|
|
|
76
76
|
and str(argv[2]).lower().endswith((".cmd", ".bat")):
|
|
77
77
|
shim = argv[2]
|
|
78
78
|
try:
|
|
79
|
-
|
|
79
|
+
with open(shim, encoding="utf-8", errors="replace") as fh:
|
|
80
|
+
text = fh.read()
|
|
80
81
|
except Exception:
|
|
81
82
|
return argv
|
|
82
83
|
m = re.search(r'%_prog%"\s+"?%dp0%(\\[^"\n]+?\.(?:mjs|js))"?', text)
|
package/app/core/selfupdate.py
CHANGED
|
@@ -176,16 +176,15 @@ def apply_upgrade():
|
|
|
176
176
|
try:
|
|
177
177
|
jobs.enqueue({"kind": "selfupgrade", "run_id": run["id"]})
|
|
178
178
|
except Exception:
|
|
179
|
-
#
|
|
180
|
-
|
|
181
|
-
log.exception("selfupdate: 升级任务入队失败 run=%s", run["id"])
|
|
179
|
+
# run 已持久化;启动失败时显式收口,版本页不能停在误导性的待启动状态。
|
|
180
|
+
log.exception("selfupdate: 升级任务启动失败 run=%s", run["id"])
|
|
182
181
|
try:
|
|
183
182
|
store.update_run(run["id"], status="failed",
|
|
184
|
-
error="
|
|
183
|
+
error="升级任务启动失败,本次未排队,请稍后重试",
|
|
185
184
|
ended_at=time.strftime("%Y-%m-%d %H:%M:%S"))
|
|
186
185
|
except Exception:
|
|
187
186
|
log.exception("selfupdate: 升级运行失败收口失败 run=%s", run["id"])
|
|
188
|
-
return {"error": "
|
|
187
|
+
return {"error": "升级任务启动失败,本次未排队,请稍后重试", "run_id": run["id"]}
|
|
189
188
|
return {"run_id": run["id"]}
|
|
190
189
|
|
|
191
190
|
|
|
@@ -210,23 +209,34 @@ def _log_note(log_path, text):
|
|
|
210
209
|
pass
|
|
211
210
|
|
|
212
211
|
|
|
213
|
-
def run_upgrade(run_id, log_path):
|
|
212
|
+
def run_upgrade(run_id, log_path, cancel_event=None):
|
|
214
213
|
"""worker 线程里执行升级命令(run/step 生命周期由 jobs 层管)。
|
|
215
214
|
|
|
216
215
|
包目录被其他进程占用(EBUSY/EPERM:打开包目录的资源管理器/终端窗口、
|
|
217
216
|
杀毒或索引扫描)是升级失败的最常见原因,且多为暂时性——自动重试
|
|
218
217
|
_RETRY_DELAYS 轮,仍败则给人话结论(原始 npm 输出在步骤日志里可查)。"""
|
|
219
|
-
res = {}
|
|
220
|
-
for attempt, delay in enumerate((0,) + _RETRY_DELAYS):
|
|
221
|
-
if
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
218
|
+
res = {}
|
|
219
|
+
for attempt, delay in enumerate((0,) + _RETRY_DELAYS):
|
|
220
|
+
if cancel_event is not None and cancel_event.is_set():
|
|
221
|
+
return {"ok": False, "exit_code": None, "error": "用户主动取消",
|
|
222
|
+
"cancelled": True}
|
|
223
|
+
if delay:
|
|
224
|
+
_log_note(log_path, "目录被占用(EBUSY/EPERM),%d 秒后自动重试(第 %d/%d 次)"
|
|
225
|
+
% (delay, attempt, len(_RETRY_DELAYS)))
|
|
226
|
+
if cancel_event is not None and cancel_event.wait(delay):
|
|
227
|
+
return {"ok": False, "exit_code": None, "error": "用户主动取消",
|
|
228
|
+
"cancelled": True}
|
|
229
|
+
if cancel_event is None:
|
|
230
|
+
time.sleep(delay)
|
|
231
|
+
res = runner.run_process(
|
|
226
232
|
argv=_npm_argv("install", "-g", _PKG_NAME + "@latest"),
|
|
227
233
|
# Windows 上 npm 换版本靠把包目录整体改名(codebee → .codebee-xxx);
|
|
228
234
|
# cwd 若落在本包内,目录被自身进程占用,rename 必报 EBUSY——钉在包外
|
|
229
|
-
cwd=str(Path.home()), timeout=900, log_path=log_path
|
|
235
|
+
cwd=str(Path.home()), timeout=900, log_path=log_path,
|
|
236
|
+
cancel_event=cancel_event)
|
|
237
|
+
if res.get("cancelled"):
|
|
238
|
+
return {"ok": False, "exit_code": res.get("exit_code"),
|
|
239
|
+
"error": "用户主动取消", "cancelled": True}
|
|
230
240
|
if res["ok"] or not _locked_error(res):
|
|
231
241
|
break
|
|
232
242
|
if res["ok"]:
|
package/app/core/settings.py
CHANGED
|
@@ -21,7 +21,7 @@ _FILE = paths.DATA_DIR / "settings.json"
|
|
|
21
21
|
# 运行时出现(空闲 90s 隐身)。
|
|
22
22
|
# cleanup_enabled / cleanup_retention_days:每日垃圾清理(core/cleanup.py)——
|
|
23
23
|
# 运行过程日志/发布截图/bak 残留等超期自动清理;retention 为保留天数。
|
|
24
|
-
DEFAULTS = {"max_concurrent_jobs":
|
|
24
|
+
DEFAULTS = {"max_concurrent_jobs": 12, "default_workdir": "", "hooks_token": "",
|
|
25
25
|
"telemetry_errors": True, "publish_daily_cap": 10,
|
|
26
26
|
"publish_fail_streak": 3, "notify_webhook": "", "notify_base_url": "",
|
|
27
27
|
"pet_enabled": True, "pet_mode": "always", "pet_skin": "plush",
|
|
@@ -29,8 +29,8 @@ DEFAULTS = {"max_concurrent_jobs": 6, "default_workdir": "", "hooks_token": "",
|
|
|
29
29
|
# 桌宠形象白名单(与 app/pet.py 的 SKINS 对齐;这里不 import pet 模块,避免
|
|
30
30
|
# core 反向依赖 app 根目录脚本)
|
|
31
31
|
PET_SKINS = ("plush", "robot")
|
|
32
|
-
#
|
|
33
|
-
# 同任务单飞守卫在 jobs
|
|
32
|
+
# 并发保护上限 12:任务有空位即直接启动,满载明确返回忙,不进入等待队列;
|
|
33
|
+
# 同任务单飞守卫在 jobs 层。默认取上限,对齐「默认不排队」的使用预期。
|
|
34
34
|
MIN_WORKERS, MAX_WORKERS = 1, 12
|
|
35
35
|
|
|
36
36
|
|
package/app/core/skills.py
CHANGED
|
@@ -394,9 +394,50 @@ def relevance_top(lessons, task, limit):
|
|
|
394
394
|
grams = _text_bigrams(x.get("title")) | _text_bigrams(x.get("content"))
|
|
395
395
|
return (-len(probe & grams), x.get("id") or "")
|
|
396
396
|
|
|
397
|
+
# 使用反馈闭环(pmb「量化记忆真实帮助」借鉴):被选中次数多的教训排前
|
|
398
|
+
hits = _load_hits()
|
|
399
|
+
def rank(x):
|
|
400
|
+
grams = _text_bigrams(x.get("title")) | _text_bigrams(x.get("content"))
|
|
401
|
+
overlap = -len(probe & grams)
|
|
402
|
+
lid = x.get("id") or ""
|
|
403
|
+
return (overlap, -hits.get(lid, 0), lid)
|
|
404
|
+
|
|
397
405
|
return sorted(lessons, key=rank)[:limit]
|
|
398
406
|
|
|
399
407
|
|
|
408
|
+
def _hits_path():
|
|
409
|
+
return paths.DATA_DIR / "skill_hits.json"
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _load_hits():
|
|
413
|
+
"""读取教训使用计数({lesson_id: 次数})。文件缺失/损坏返回空 dict。"""
|
|
414
|
+
try:
|
|
415
|
+
p = _hits_path()
|
|
416
|
+
if p.is_file():
|
|
417
|
+
import json
|
|
418
|
+
return json.loads(p.read_text(encoding="utf-8"))
|
|
419
|
+
except Exception:
|
|
420
|
+
pass
|
|
421
|
+
return {}
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _bump_hits(ids):
|
|
425
|
+
"""注入后递增使用计数并持久化(fire-and-forget,失败静默)。"""
|
|
426
|
+
try:
|
|
427
|
+
import json
|
|
428
|
+
p = _hits_path()
|
|
429
|
+
hits = _load_hits()
|
|
430
|
+
for lid in ids:
|
|
431
|
+
if lid:
|
|
432
|
+
hits[lid] = hits.get(lid, 0) + 1
|
|
433
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
434
|
+
tmp = p.with_suffix(".tmp")
|
|
435
|
+
tmp.write_text(json.dumps(hits, ensure_ascii=False), encoding="utf-8")
|
|
436
|
+
tmp.replace(p)
|
|
437
|
+
except Exception:
|
|
438
|
+
pass
|
|
439
|
+
|
|
440
|
+
|
|
400
441
|
def block_for(task, scope_override=None, *, stable_order=False):
|
|
401
442
|
"""生成注入提示词的经验块。命中即计数。返回 (文本, 命中的 id 列表)。
|
|
402
443
|
|
|
@@ -440,6 +481,8 @@ def block_for(task, scope_override=None, *, stable_order=False):
|
|
|
440
481
|
text = "## 经验库(写作/工程规范 + 历史教训,必须遵守)\n\n" + "\n\n".join(parts)
|
|
441
482
|
if len(text) > MAX_INJECT_CHARS:
|
|
442
483
|
text = text[:MAX_INJECT_CHARS] + "\n…(已截断)"
|
|
484
|
+
if used:
|
|
485
|
+
_bump_hits(used) # 使用反馈闭环:被选中的教训递增计数,下次排序升权
|
|
443
486
|
if used:
|
|
444
487
|
bump_hits(used)
|
|
445
488
|
return text, used
|
package/app/core/store.py
CHANGED
|
@@ -448,11 +448,16 @@ def load_all():
|
|
|
448
448
|
# 错误串(`[91m[1mError:`、U+FFFD 乱码墙),读盘时统一洗一遍——
|
|
449
449
|
# 老运行不必等重跑才干净(2026-09-20「咋还有乱码」实测)
|
|
450
450
|
_sanitize_run_text(r)
|
|
451
|
-
#
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
451
|
+
# 直接执行线程不跨进程:running 与无截止时间的 queued 是上次
|
|
452
|
+
# 进程中断残骸。带 resume_enqueue_at 的 queued 是有意的自动续跑
|
|
453
|
+
# 退避,保留给 jobs.restore_deferred_resumes 重建 Timer。
|
|
454
|
+
interrupted = (r.get("status") == "running" or
|
|
455
|
+
(r.get("status") == "queued" and
|
|
456
|
+
not r.get("resume_enqueue_at")))
|
|
457
|
+
if interrupted:
|
|
458
|
+
r["status"] = "failed"
|
|
459
|
+
r["error"] = r.get("error") or "服务重启中断,可重试"
|
|
460
|
+
_save_json(p, r)
|
|
456
461
|
_RUNS[r["id"]] = r
|
|
457
462
|
except Exception:
|
|
458
463
|
pass
|
|
@@ -507,10 +512,11 @@ def get_run(run_id):
|
|
|
507
512
|
return _RUNS.get(run_id)
|
|
508
513
|
|
|
509
514
|
|
|
510
|
-
def list_runs(limit=60):
|
|
511
|
-
with LOCK:
|
|
512
|
-
ids = sorted(_RUNS.keys(), reverse=True)
|
|
513
|
-
|
|
515
|
+
def list_runs(limit=60):
|
|
516
|
+
with LOCK:
|
|
517
|
+
ids = sorted(_RUNS.keys(), reverse=True)
|
|
518
|
+
selected = ids if limit is None else ids[:limit]
|
|
519
|
+
return [_RUNS[i] for i in selected]
|
|
514
520
|
|
|
515
521
|
|
|
516
522
|
def latest_run_by_task():
|
|
@@ -635,17 +641,17 @@ def task_side(task_id):
|
|
|
635
641
|
}
|
|
636
642
|
|
|
637
643
|
|
|
638
|
-
def update_run(run_id, expected_status=None, **fields):
|
|
644
|
+
def update_run(run_id, expected_status=None, **fields):
|
|
639
645
|
"""更新运行字段。expected_status 非 None 时做 CAS(§2C):
|
|
640
646
|
当前状态不等于 expected_status 则拒绝写入并返回 None,
|
|
641
647
|
防止陈旧执行方(被取消的 worker、崩溃恢复前的旧线程)覆盖新状态
|
|
642
648
|
——防御模式「异步状态不是同步状态」。不传则保持原行为。"""
|
|
643
649
|
with LOCK:
|
|
644
650
|
run = _RUNS.get(run_id)
|
|
645
|
-
if not run:
|
|
646
|
-
return None
|
|
647
|
-
if expected_status is not None and run.get("status") != expected_status:
|
|
648
|
-
return None
|
|
651
|
+
if not run:
|
|
652
|
+
return None
|
|
653
|
+
if expected_status is not None and run.get("status") != expected_status:
|
|
654
|
+
return None
|
|
649
655
|
for k in ("error", "summary"):
|
|
650
656
|
if isinstance(fields.get(k), str):
|
|
651
657
|
# 错误串多是我们自己拼的 CLI 尾巴(含 ANSI/覆写/乱码墙):
|