codebee 0.1.22 → 0.1.24
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 +28 -0
- package/README.md +20 -3
- package/app/core/aiflavor.py +63 -9
- package/app/core/attachments.py +117 -4
- package/app/core/dispatch.py +34 -5
- package/app/core/dispatch_log.py +113 -0
- package/app/core/errorlog.py +1 -43
- package/app/core/jobs.py +30 -4
- package/app/core/modelhub.py +2 -0
- package/app/core/paths.py +2 -2
- package/app/core/pipeline.py +252 -12
- package/app/core/portguard.py +111 -0
- package/app/core/portscan.py +188 -0
- package/app/core/redact.py +38 -0
- package/app/core/router.py +58 -14
- package/app/core/runner.py +15 -8
- package/app/core/selfupdate.py +86 -27
- package/app/core/skills.py +37 -19
- package/app/core/store.py +2 -4
- package/app/core/task_compile.py +5 -1
- package/app/core/usage.py +226 -24
- package/app/main.py +82 -9
- package/app/pet.py +34 -11
- package/app/ui/app.js +177 -1
- package/app/ui/i18n.js +36 -0
- package/app/ui/index.html +1144 -1137
- package/app/ui/style.css +34 -0
- package/package.json +1 -1
package/app/core/pipeline.py
CHANGED
|
@@ -19,7 +19,7 @@ import re
|
|
|
19
19
|
import threading
|
|
20
20
|
import time
|
|
21
21
|
|
|
22
|
-
from . import aiflavor, catalog, history, jobs, knowledge, manager, modelhub, mocks, paihang, planner, registry, router, runner, skills, store, task_compile, usage
|
|
22
|
+
from . import aiflavor, catalog, dispatch_log, history, jobs, knowledge, manager, modelhub, mocks, paihang, planner, registry, router, runner, skills, store, task_compile, usage
|
|
23
23
|
from . import builtin_agent
|
|
24
24
|
from . import diagnostics
|
|
25
25
|
from . import paths as paths_mod
|
|
@@ -60,6 +60,20 @@ def _task_images(task, workdir):
|
|
|
60
60
|
return []
|
|
61
61
|
|
|
62
62
|
|
|
63
|
+
def _refresh_attachment_context(task, workdir):
|
|
64
|
+
"""为新旧任务统一生成附件正文上下文;失败时保留原上下文。"""
|
|
65
|
+
if not task.get("attachments"):
|
|
66
|
+
return task
|
|
67
|
+
try:
|
|
68
|
+
from . import attachments as att_mod
|
|
69
|
+
out = dict(task)
|
|
70
|
+
out["context"] = att_mod.merge_context(
|
|
71
|
+
task.get("context") or "", task.get("attachments") or [], workdir=workdir)
|
|
72
|
+
return out
|
|
73
|
+
except Exception:
|
|
74
|
+
return task
|
|
75
|
+
|
|
76
|
+
|
|
63
77
|
def _ms_name(raw):
|
|
64
78
|
name = re.sub(r"[\\/\x00]+", "_", str(raw or "")).strip()
|
|
65
79
|
name = re.sub(r"\.{2,}", "_", name).lstrip(".")
|
|
@@ -253,6 +267,7 @@ def _drain_directives(run_id, workdir, role=None, step_n=None):
|
|
|
253
267
|
lines.append("本步为评审步骤:请把上述用户意见作为评分依据之一,"
|
|
254
268
|
"在相应维度的分数与 issues 中明确体现(引用用户原话)。")
|
|
255
269
|
imgs = []
|
|
270
|
+
attachment_paths = []
|
|
256
271
|
for m in msgs:
|
|
257
272
|
stamp = m.get("created_at") or ""
|
|
258
273
|
sender = m.get("sender") or "用户"
|
|
@@ -261,6 +276,7 @@ def _drain_directives(run_id, workdir, role=None, step_n=None):
|
|
|
261
276
|
else "- [%s %s](附件指令,见下方文件)" % (stamp, sender))
|
|
262
277
|
for rel in (m.get("attachments") or []):
|
|
263
278
|
rel = str(rel)
|
|
279
|
+
attachment_paths.append(rel)
|
|
264
280
|
low = rel.lower()
|
|
265
281
|
if low.endswith((".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp")):
|
|
266
282
|
ap = os.path.join(workdir or "", rel) if workdir else rel
|
|
@@ -271,6 +287,13 @@ def _drain_directives(run_id, workdir, role=None, step_n=None):
|
|
|
271
287
|
lines.append(" · 图片附件:%s" % rel)
|
|
272
288
|
else:
|
|
273
289
|
lines.append(" · 文件附件:%s(位于工作目录,可直接读取)" % rel)
|
|
290
|
+
if attachment_paths:
|
|
291
|
+
try:
|
|
292
|
+
from . import attachments as att_mod
|
|
293
|
+
items = att_mod.items_from_paths(attachment_paths, workdir)
|
|
294
|
+
lines.append(att_mod.context_block(items, workdir=workdir))
|
|
295
|
+
except Exception:
|
|
296
|
+
lines.append("附件预读失败:执行者必须逐个打开上述路径,无法读取时明确说明。")
|
|
274
297
|
return "\n".join(lines), imgs
|
|
275
298
|
|
|
276
299
|
|
|
@@ -609,6 +632,9 @@ def _record_usage(run_id, role, agent, res, source="pipeline", step=0):
|
|
|
609
632
|
try:
|
|
610
633
|
run = store.get_run(run_id) or {}
|
|
611
634
|
task = store.get_task(run.get("task_id")) if run.get("task_id") else None
|
|
635
|
+
provider = res.get("provider") or agent.get("provider") or {}
|
|
636
|
+
provider_id = provider.get("id") if isinstance(provider, dict) else ""
|
|
637
|
+
provider_name = provider.get("name") if isinstance(provider, dict) else ""
|
|
612
638
|
usage.record(
|
|
613
639
|
source=source, run_id=run_id, step=step,
|
|
614
640
|
task_id=run.get("task_id") or "",
|
|
@@ -617,13 +643,14 @@ def _record_usage(run_id, role, agent, res, source="pipeline", step=0):
|
|
|
617
643
|
agent_label=agent.get("label", ""),
|
|
618
644
|
tool=agent.get("kind", ""),
|
|
619
645
|
model=res.get("model") or "",
|
|
646
|
+
provider=res.get("provider_id") or provider_id or provider_name or "",
|
|
620
647
|
ok=bool(res.get("ok")),
|
|
621
648
|
duration_s=float(res.get("raw", {}).get("duration") or 0.0),
|
|
622
649
|
cost_usd=float(res.get("cost_usd") or 0.0),
|
|
623
650
|
usage=res.get("usage"))
|
|
624
651
|
# 告警模块:CLI 调用成功/失败上报(provider 名与 usage 台账一致)
|
|
625
652
|
from . import health
|
|
626
|
-
prov = agent.get("provider") or {}
|
|
653
|
+
prov = res.get("provider") or agent.get("provider") or {}
|
|
627
654
|
prov_name = (prov.get("name") if isinstance(prov, dict) else "") or ""
|
|
628
655
|
if prov_name:
|
|
629
656
|
if res.get("ok"):
|
|
@@ -663,6 +690,9 @@ __GOAL__
|
|
|
663
690
|
## 未通过的原因
|
|
664
691
|
__ISSUES__
|
|
665
692
|
|
|
693
|
+
## 原始背景与附件
|
|
694
|
+
__CONTEXT__
|
|
695
|
+
|
|
666
696
|
## 要求
|
|
667
697
|
- 只针对上述问题修复;不要无关重构。
|
|
668
698
|
- 完成后用 2-3 句话说明改了什么。
|
|
@@ -688,6 +718,9 @@ __GOAL__
|
|
|
688
718
|
## 验收命令
|
|
689
719
|
__VERIFY__
|
|
690
720
|
|
|
721
|
+
## 原始背景与附件要求
|
|
722
|
+
__CONTEXT__
|
|
723
|
+
|
|
691
724
|
## 变更内容(git diff,若为空表示无法获取)
|
|
692
725
|
__DIFF__
|
|
693
726
|
"""
|
|
@@ -746,6 +779,7 @@ def _run_review(run_id, task, workdir, reviewer, ev):
|
|
|
746
779
|
prompt = (CODE_REVIEW_PROMPT
|
|
747
780
|
.replace("__GOAL__", task["goal"])
|
|
748
781
|
.replace("__VERIFY__", task.get("verify_command") or "(未配置)")
|
|
782
|
+
.replace("__CONTEXT__", task.get("context") or "(无)")
|
|
749
783
|
.replace("__DIFF__", diff or "(无法获取 git diff,请综合任务目标谨慎评审)"))
|
|
750
784
|
res = _run_step(run_id, "review", reviewer, prompt, workdir, readonly=True, ev=ev,
|
|
751
785
|
images=_task_images(task, workdir))
|
|
@@ -883,6 +917,54 @@ def _code_bestof(run, task, impl, difficulty, ev):
|
|
|
883
917
|
pass
|
|
884
918
|
|
|
885
919
|
|
|
920
|
+
def _record_actual_route(run_id, task, agents, stats, implementer,
|
|
921
|
+
implementers=(), reviewer=None, critics=(), implement_reason="",
|
|
922
|
+
review_reason="", direct=False):
|
|
923
|
+
"""把引擎实际选中的执行者/评审者回写到可解释路由计划。"""
|
|
924
|
+
spec = task.get("_compiled_spec") or task_compile.compile_task(task)
|
|
925
|
+
impl_plan = router.route_plan(
|
|
926
|
+
agents, "implement", spec, stats, selected=implementer,
|
|
927
|
+
participants=implementers,
|
|
928
|
+
selection_reason=implement_reason)
|
|
929
|
+
if direct:
|
|
930
|
+
review_plan = {"role": "review", "selected": "", "participants": [],
|
|
931
|
+
"selection_reason": "", "candidates": [], "fallback": []}
|
|
932
|
+
else:
|
|
933
|
+
review_group = list(critics or ())
|
|
934
|
+
actual_reviewer = reviewer or (review_group[0] if review_group else None)
|
|
935
|
+
review_plan = router.route_plan(
|
|
936
|
+
agents, "review", spec, stats, selected=actual_reviewer,
|
|
937
|
+
participants=review_group, selection_reason=review_reason)
|
|
938
|
+
store.update_run(run_id, route_plan={
|
|
939
|
+
"task": spec, "implement": impl_plan, "review": review_plan,
|
|
940
|
+
})
|
|
941
|
+
task_id = task.get("id") or (store.get_run(run_id) or {}).get("task_id") or ""
|
|
942
|
+
difficulty = (task.get("difficulty") or "auto")
|
|
943
|
+
for plan in (impl_plan, review_plan):
|
|
944
|
+
dispatch_log.record_event(
|
|
945
|
+
run_id=run_id, task_id=task_id, task_type=spec.get("type") or "",
|
|
946
|
+
difficulty=difficulty, role=plan.get("role") or "",
|
|
947
|
+
phase="selected", selected=plan.get("selected") or "",
|
|
948
|
+
participants=plan.get("participants") or (),
|
|
949
|
+
candidates=plan.get("candidates") or (),
|
|
950
|
+
fallback=plan.get("fallback") or (),
|
|
951
|
+
selection_reason=plan.get("selection_reason") or "")
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
def _record_dispatch_completed(run_id, task, result, verify_pass=None, review_pass=None):
|
|
955
|
+
"""记录运行终态,供调度回放与线上指标复盘使用。"""
|
|
956
|
+
try:
|
|
957
|
+
spec = task.get("_compiled_spec") or task_compile.compile_task(task)
|
|
958
|
+
dispatch_log.record_event(
|
|
959
|
+
run_id=run_id,
|
|
960
|
+
task_id=task.get("id") or (store.get_run(run_id) or {}).get("task_id") or "",
|
|
961
|
+
task_type=spec.get("type") or "", difficulty=task.get("difficulty") or "auto",
|
|
962
|
+
role="", phase="completed", result=result,
|
|
963
|
+
verify_pass=verify_pass, review_pass=review_pass)
|
|
964
|
+
except Exception:
|
|
965
|
+
pass
|
|
966
|
+
|
|
967
|
+
|
|
886
968
|
def _run_code(run, task, agents, ev, stats, mode):
|
|
887
969
|
run_id = run["id"]
|
|
888
970
|
workdir = task["workdir"]
|
|
@@ -946,6 +1028,9 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
946
1028
|
reviewer, route["reviewer"] = router.pick_reviewer(agents, impl, "code", stats)
|
|
947
1029
|
else:
|
|
948
1030
|
reviewer, route["reviewer"] = _pick_reviewer_legacy(agents, impl)
|
|
1031
|
+
_record_actual_route(run_id, task, agents, stats, impl, reviewer=reviewer,
|
|
1032
|
+
implement_reason=route.get("implementer", ""),
|
|
1033
|
+
review_reason=route.get("reviewer", ""))
|
|
949
1034
|
store.update_run(run_id, route=route)
|
|
950
1035
|
|
|
951
1036
|
def implement_all(impl_agent, prefix_note):
|
|
@@ -1038,6 +1123,11 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
1038
1123
|
ok2, res = _run_one(other)
|
|
1039
1124
|
if ok2:
|
|
1040
1125
|
store.update_run(run_id, error="", route_note=note)
|
|
1126
|
+
_record_actual_route(
|
|
1127
|
+
run_id, task, agents, stats, other,
|
|
1128
|
+
implementers=[other], reviewer=reviewer,
|
|
1129
|
+
implement_reason=note,
|
|
1130
|
+
review_reason=route.get("reviewer", ""))
|
|
1041
1131
|
return True
|
|
1042
1132
|
res_err = "%s;换将后仍失败:%s" % (note, (res.get("error") or "")[:200])
|
|
1043
1133
|
else:
|
|
@@ -1049,8 +1139,9 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
1049
1139
|
return False
|
|
1050
1140
|
|
|
1051
1141
|
def review_and_score():
|
|
1052
|
-
review_json = _run_review(run_id, task, workdir, modelhub.bind_agent(reviewer, difficulty), ev)
|
|
1053
1142
|
verify_pass, verify_ran = _run_verify(run_id, task, workdir, ev)
|
|
1143
|
+
# 先让确定性验证落盘,再执行模型评审;终态判断会同时使用两份证据。
|
|
1144
|
+
review_json = _run_review(run_id, task, workdir, modelhub.bind_agent(reviewer, difficulty), ev)
|
|
1054
1145
|
return review_json, verify_pass, verify_ran
|
|
1055
1146
|
|
|
1056
1147
|
attempt_note = route.get("implementer", "") if mode == "auto" else ""
|
|
@@ -1068,12 +1159,20 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
1068
1159
|
prompt = (CODE_FIX_PROMPT
|
|
1069
1160
|
.replace("__GOAL__", task["goal"])
|
|
1070
1161
|
.replace("__ISSUES__", issues_txt)
|
|
1162
|
+
.replace("__CONTEXT__", task.get("context") or "(无)")
|
|
1071
1163
|
.replace("__VERIFY_HINT__", _verify_hint(task)))
|
|
1072
1164
|
res = _run_step(run_id, "fix-r%d" % round_no, modelhub.bind_agent(impl, difficulty),
|
|
1073
1165
|
prompt, workdir, readonly=False, ev=ev,
|
|
1074
1166
|
note="自动修复第 %d 轮" % round_no,
|
|
1075
1167
|
resume=resume_ctx["session"] if resume_ctx else impl_sid[0],
|
|
1076
1168
|
require_tools=True)
|
|
1169
|
+
if res["ok"]:
|
|
1170
|
+
_record_actual_route(
|
|
1171
|
+
run_id, task, agents, stats, impl,
|
|
1172
|
+
implementers=[impl], reviewer=reviewer,
|
|
1173
|
+
implement_reason="修复轮由 %s 完成" %
|
|
1174
|
+
(impl.get("label") or impl.get("id")),
|
|
1175
|
+
review_reason=route.get("reviewer", ""))
|
|
1077
1176
|
if impl.get("mode") == "mock" and res["ok"]:
|
|
1078
1177
|
pass # mock 不产生真实变更
|
|
1079
1178
|
review_json, verify_pass, verify_ran = review_and_score()
|
|
@@ -1097,6 +1196,11 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
1097
1196
|
impl["id"], other["id"], round_no + 1, other_reason)
|
|
1098
1197
|
store.update_run(run_id, error="")
|
|
1099
1198
|
impl = other
|
|
1199
|
+
_record_actual_route(
|
|
1200
|
+
run_id, task, agents, stats, impl,
|
|
1201
|
+
implementers=[impl], reviewer=reviewer,
|
|
1202
|
+
implement_reason=note,
|
|
1203
|
+
review_reason=route.get("reviewer", ""))
|
|
1100
1204
|
switched = True
|
|
1101
1205
|
round_no += 1
|
|
1102
1206
|
continue
|
|
@@ -1175,6 +1279,7 @@ __GOAL__
|
|
|
1175
1279
|
__CONTEXT__
|
|
1176
1280
|
|
|
1177
1281
|
## 要求
|
|
1282
|
+
- 背景与上下文里若有「附件材料」,先逐个读取再处理——附件是必要输入,不读附件就作答视为未完成。
|
|
1178
1283
|
- 能改直接改、能写直接写(限本工作目录内),产出文件一律 UTF-8 编码(PowerShell 写文件显式 -Encoding UTF8)。
|
|
1179
1284
|
- 正文直接交代结果与答案:不要写「本轮做了什么」这类开场总结,也不要「回复:」这类引导词。
|
|
1180
1285
|
- 回复的最后一行单独输出一行交代结果:
|
|
@@ -1204,6 +1309,7 @@ __GOAL__
|
|
|
1204
1309
|
__CONTEXT__
|
|
1205
1310
|
|
|
1206
1311
|
## 要求
|
|
1312
|
+
- 背景与上下文里若有「附件材料」,先逐个读取再处理——附件是必要输入,不读附件就作答视为未完成。
|
|
1207
1313
|
- 能改直接改、能写直接写(用工具,限本工作目录内),产出文件一律 UTF-8 编码。
|
|
1208
1314
|
- 完成后直接给用户结论与答案,需要时顺带交代产出/修改了哪些文件;不要写「本轮做了什么」这类开场白。
|
|
1209
1315
|
__FOLLOWUPS__"""
|
|
@@ -1326,6 +1432,13 @@ def _run_direct(run, task, agents, ev, stats, mode):
|
|
|
1326
1432
|
store.update_run(run_id, expected_status="running", status="failed",
|
|
1327
1433
|
error="没有可用智能体", ended_at=_now())
|
|
1328
1434
|
return
|
|
1435
|
+
actual_impl = impl or {
|
|
1436
|
+
"id": "builtin:%s:%s" % (bi.get("provider_id") or "provider", bi.get("model") or "model"),
|
|
1437
|
+
"label": "CodeBee · %s" % (bi.get("model") or bi.get("provider_name") or "内置模型"),
|
|
1438
|
+
"kind": "builtin",
|
|
1439
|
+
}
|
|
1440
|
+
_record_actual_route(run_id, task, agents, stats, actual_impl,
|
|
1441
|
+
implement_reason=route.get("implementer", ""), direct=True)
|
|
1329
1442
|
difficulty = task.get("difficulty") or "default"
|
|
1330
1443
|
step_wd = _resume_workdir(resume_ctx, workdir) if resume_ctx else workdir
|
|
1331
1444
|
store.update_run(run_id, route=route, difficulty=difficulty)
|
|
@@ -1358,6 +1471,8 @@ def _run_direct(run, task, agents, ev, stats, mode):
|
|
|
1358
1471
|
# 扫榜选材(借鉴 oh-story 扫榜):抓七猫排行榜公开数据注入,
|
|
1359
1472
|
# AI 做选题洞察;抓取失败回落普通直连提示词
|
|
1360
1473
|
prompt = paihang.rank_scan_prompt(task.get("goal") or "") or ""
|
|
1474
|
+
if prompt and task.get("context"):
|
|
1475
|
+
prompt += "\n\n## 用户背景与附件\n" + task["context"]
|
|
1361
1476
|
if not prompt:
|
|
1362
1477
|
if bi is not None:
|
|
1363
1478
|
prompt = (BUILTIN_DIRECT_PROMPT
|
|
@@ -1548,6 +1663,9 @@ __GOAL__
|
|
|
1548
1663
|
## 评审汇总(各维度均分与主要问题)
|
|
1549
1664
|
__CRITIQUE__
|
|
1550
1665
|
|
|
1666
|
+
## 原始背景与附件
|
|
1667
|
+
__CONTEXT__
|
|
1668
|
+
|
|
1551
1669
|
## 要求
|
|
1552
1670
|
- 针对性改进所有 major 问题;保持既定风格与设定。
|
|
1553
1671
|
- 完成后用 3 句话说明本轮改了什么。"""
|
|
@@ -1688,6 +1806,8 @@ def _ensure_critique_placeholders(tpl):
|
|
|
1688
1806
|
tpl += "\n\n## 待评审稿件\n---\n__MANUSCRIPT__\n---"
|
|
1689
1807
|
if "__DIMKEYS__" not in tpl:
|
|
1690
1808
|
tpl = ("请按维度打分(1-10 分)。\n\n" + tpl)
|
|
1809
|
+
if "__CONTEXT__" not in tpl:
|
|
1810
|
+
tpl += "\n\n## 原始任务背景与附件参考\n__CONTEXT__"
|
|
1691
1811
|
return tpl
|
|
1692
1812
|
|
|
1693
1813
|
|
|
@@ -1724,6 +1844,9 @@ __GOAL__
|
|
|
1724
1844
|
## 本章评审意见
|
|
1725
1845
|
__CRITIQUE__
|
|
1726
1846
|
|
|
1847
|
+
## 原始背景与附件
|
|
1848
|
+
__CONTEXT__
|
|
1849
|
+
|
|
1727
1850
|
## 要求
|
|
1728
1851
|
- 针对性解决所有 major 问题,保持与前后的剧情衔接;字数仍约 __WORDS__ 字。"""
|
|
1729
1852
|
|
|
@@ -1739,6 +1862,9 @@ SERIAL_GLOBAL_PROMPT = """你是网文主编(不要修改任何文件)。全
|
|
|
1739
1862
|
## 全书目标
|
|
1740
1863
|
__GOAL__
|
|
1741
1864
|
|
|
1865
|
+
## 原始背景与附件
|
|
1866
|
+
__CONTEXT__
|
|
1867
|
+
|
|
1742
1868
|
## 全文
|
|
1743
1869
|
---
|
|
1744
1870
|
__MANUSCRIPT__
|
|
@@ -1860,6 +1986,31 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
1860
1986
|
"""连载流水线:大纲 → 逐章起草/评审/修订 → 全局一致性评审 → 合并成书。"""
|
|
1861
1987
|
import json as _json
|
|
1862
1988
|
run_id = run["id"]
|
|
1989
|
+
actual_implementers = [impl]
|
|
1990
|
+
actual_critics = list(critics)
|
|
1991
|
+
current_impl = [impl]
|
|
1992
|
+
current_impl_reason = [route.get("author", "")]
|
|
1993
|
+
current_review_reason = [route.get("critics", "")]
|
|
1994
|
+
|
|
1995
|
+
def refresh_actual_route(primary_impl=None, implement_reason=None,
|
|
1996
|
+
review_reason=None):
|
|
1997
|
+
if primary_impl is not None:
|
|
1998
|
+
current_impl[0] = primary_impl
|
|
1999
|
+
if implement_reason is not None:
|
|
2000
|
+
current_impl_reason[0] = implement_reason
|
|
2001
|
+
if review_reason is not None:
|
|
2002
|
+
current_review_reason[0] = review_reason
|
|
2003
|
+
_record_actual_route(
|
|
2004
|
+
run_id, task, agents, stats, current_impl[0],
|
|
2005
|
+
implementers=actual_implementers, critics=actual_critics,
|
|
2006
|
+
implement_reason=current_impl_reason[0],
|
|
2007
|
+
review_reason=current_review_reason[0])
|
|
2008
|
+
|
|
2009
|
+
def remember_agent(bucket, agent):
|
|
2010
|
+
if agent and not any(x.get("id") == agent.get("id") for x in bucket):
|
|
2011
|
+
bucket.append(agent)
|
|
2012
|
+
|
|
2013
|
+
refresh_actual_route(impl)
|
|
1863
2014
|
workdir = task["workdir"]
|
|
1864
2015
|
# 续会话步骤的 CLI 启动目录(稿件读写仍用 workdir)
|
|
1865
2016
|
step_wd = _resume_workdir(resume_ctx, workdir) if resume_ctx else workdir
|
|
@@ -1901,8 +2052,9 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
1901
2052
|
kb = knowledge.block_for(task)
|
|
1902
2053
|
if kb:
|
|
1903
2054
|
tpl = tpl.replace("## 待评审稿件", "%s\n\n## 待评审稿件" % kb, 1)
|
|
1904
|
-
return tpl.replace("__DIMKEYS__", dimkey)
|
|
1905
|
-
|
|
2055
|
+
return (tpl.replace("__DIMKEYS__", dimkey)
|
|
2056
|
+
.replace("__CONTEXT__", task.get("context") or "(无)")
|
|
2057
|
+
.replace("__MANUSCRIPT__", text or "(稿件为空!)"))
|
|
1906
2058
|
|
|
1907
2059
|
# ---- 1) 大纲(断点续跑时直接继承上一遍,保证全书结构一致)
|
|
1908
2060
|
inherit = run.get("inherit") or {}
|
|
@@ -2051,6 +2203,10 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2051
2203
|
and cj.get("scores"):
|
|
2052
2204
|
cj_map[spare["id"]] = cj
|
|
2053
2205
|
scored += 1
|
|
2206
|
+
remember_agent(actual_critics, spare)
|
|
2207
|
+
refresh_actual_route(
|
|
2208
|
+
review_reason="章节评审补位:%s" %
|
|
2209
|
+
(spare.get("label") or spare.get("id")))
|
|
2054
2210
|
issues_all.extend({"chapter": i, **it}
|
|
2055
2211
|
for it in (cj.get("issues") or [])[:6])
|
|
2056
2212
|
break
|
|
@@ -2096,8 +2252,11 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2096
2252
|
else:
|
|
2097
2253
|
# stable_order:同一任务 8 个章节的技能块必须字节级一致(§07 T1.2' 前缀缓存)
|
|
2098
2254
|
sk_block, _ = skills.block_for(task, stable_order=True)
|
|
2099
|
-
|
|
2100
|
-
|
|
2255
|
+
if bible:
|
|
2256
|
+
sk_block = (sk_block + "\n\n" + bible) if sk_block else bible
|
|
2257
|
+
if task.get("context"):
|
|
2258
|
+
task_ctx = "## 任务背景与附件\n" + task["context"]
|
|
2259
|
+
sk_block = (sk_block + "\n\n" + task_ctx) if sk_block else task_ctx
|
|
2101
2260
|
kb_block = knowledge.block_for(task)
|
|
2102
2261
|
if kb_block:
|
|
2103
2262
|
sk_block = (sk_block + "\n\n" + kb_block) if sk_block else kb_block
|
|
@@ -2135,6 +2294,10 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2135
2294
|
good = False
|
|
2136
2295
|
txt = ""
|
|
2137
2296
|
use_prompt = prompt
|
|
2297
|
+
chapter_impl = impl
|
|
2298
|
+
chapter_reason = route.get("author", "")
|
|
2299
|
+
last_attempt_impl = impl
|
|
2300
|
+
last_attempt_reason = chapter_reason
|
|
2138
2301
|
for draft_attempt in range(3):
|
|
2139
2302
|
if draft_attempt:
|
|
2140
2303
|
# 30s / 60s 退避;ev.wait 睡等可被取消即刻唤醒
|
|
@@ -2185,6 +2348,9 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2185
2348
|
if not (other and other.get("mode") == "real"):
|
|
2186
2349
|
break
|
|
2187
2350
|
tried.add(other["id"])
|
|
2351
|
+
last_attempt_impl = other
|
|
2352
|
+
last_attempt_reason = "章节起草换将:%s" % (
|
|
2353
|
+
other_reason or other.get("label") or other.get("id"))
|
|
2188
2354
|
res = _run_step(run_id, "draft-c%d" % i,
|
|
2189
2355
|
modelhub.bind_agent(other, difficulty), use_prompt,
|
|
2190
2356
|
step_wd, readonly=False, ev=ev, timeout=2400,
|
|
@@ -2201,13 +2367,20 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2201
2367
|
pass
|
|
2202
2368
|
if good:
|
|
2203
2369
|
draft_sid = "" # 换将作者无本任会话,revise 另起
|
|
2370
|
+
chapter_impl = last_attempt_impl
|
|
2371
|
+
chapter_reason = last_attempt_reason
|
|
2204
2372
|
if not good:
|
|
2205
2373
|
time.sleep(3) # 落盘竞态宽限:CLI 崩溃退出前写的文件可能晚于
|
|
2206
2374
|
good, txt = _chapter_state() # 退出检查零点几秒才可见(c34 实测)
|
|
2375
|
+
if good:
|
|
2376
|
+
chapter_impl = last_attempt_impl
|
|
2377
|
+
chapter_reason = last_attempt_reason
|
|
2207
2378
|
if not good:
|
|
2208
2379
|
store.update_run(run_id, expected_status="running", status="failed",
|
|
2209
2380
|
error="第 %d 章起草失败: %s" % (i, (res or {}).get("error")), ended_at=_now())
|
|
2210
2381
|
return
|
|
2382
|
+
remember_agent(actual_implementers, chapter_impl)
|
|
2383
|
+
refresh_actual_route(chapter_impl, implement_reason=chapter_reason)
|
|
2211
2384
|
if not res["ok"]:
|
|
2212
2385
|
# 成品是文件不是退出码:CLI 超时但章稿已完整落盘(终章长文实测
|
|
2213
2386
|
# 反复出现——文件写完、收尾声明没等到)就送评审门把关,别整章作废
|
|
@@ -2294,6 +2467,13 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2294
2467
|
return
|
|
2295
2468
|
scored_variants.sort(key=lambda v: (-v["avg"], v["variant"]))
|
|
2296
2469
|
win = scored_variants[0]
|
|
2470
|
+
win_agent = next((a for a in pool if a.get("id") == win["agent"]), None)
|
|
2471
|
+
if win_agent is not None:
|
|
2472
|
+
remember_agent(actual_implementers, win_agent)
|
|
2473
|
+
refresh_actual_route(
|
|
2474
|
+
win_agent,
|
|
2475
|
+
implement_reason="同章多稿赛马胜出:%s" %
|
|
2476
|
+
(win_agent.get("label") or win_agent.get("id")))
|
|
2297
2477
|
# 收敛:赢家转正,败稿删除;胜者评审结果直接作为第 1 轮(不重评)
|
|
2298
2478
|
if win["file"] != ch_file:
|
|
2299
2479
|
try:
|
|
@@ -2397,10 +2577,15 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2397
2577
|
.replace("__I__", str(i)).replace("__FILE__", ch_file)
|
|
2398
2578
|
.replace("__GOAL__", task["goal"])
|
|
2399
2579
|
.replace("__CRITIQUE__", "\n".join(crit_lines))
|
|
2580
|
+
.replace("__CONTEXT__", task.get("context") or "(无)")
|
|
2400
2581
|
.replace("__WORDS__", str(wpc)))
|
|
2401
2582
|
_run_step(run_id, "revise-c%d" % i, modelhub.bind_agent(impl, difficulty), prompt,
|
|
2402
2583
|
step_wd, readonly=False, ev=ev, timeout=2400,
|
|
2403
2584
|
resume=resume_ctx["session"] if resume_ctx else draft_sid)
|
|
2585
|
+
remember_agent(actual_implementers, impl)
|
|
2586
|
+
refresh_actual_route(
|
|
2587
|
+
impl, implement_reason="章节修订:%s" %
|
|
2588
|
+
(impl.get("label") or impl.get("id")))
|
|
2404
2589
|
_check_cancel(ev)
|
|
2405
2590
|
chapter_scores.append({"chapter": i, "title": ch["title"], "means": means,
|
|
2406
2591
|
"passed": bool(means) and all(v >= threshold_ch for v in means.values()),
|
|
@@ -2449,6 +2634,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2449
2634
|
res = _run_step(run_id, "global-critique", modelhub.bind_agent(agent, difficulty),
|
|
2450
2635
|
(gtpl.replace("__DIMKEYS__", dimkey)
|
|
2451
2636
|
.replace("__GOAL__", task["goal"])
|
|
2637
|
+
.replace("__CONTEXT__", task.get("context") or "(无)")
|
|
2452
2638
|
.replace("__MANUSCRIPT__", full_text[:60000])),
|
|
2453
2639
|
workdir, readonly=True, ev=ev, timeout=2400)
|
|
2454
2640
|
gj = _critique_json(res, dims)
|
|
@@ -2477,6 +2663,10 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2477
2663
|
gmeans_acc.setdefault(d, []).extend(xs)
|
|
2478
2664
|
gscored += sc
|
|
2479
2665
|
if gscored:
|
|
2666
|
+
remember_agent(actual_critics, spare)
|
|
2667
|
+
refresh_actual_route(
|
|
2668
|
+
review_reason="全局评审补位:%s" %
|
|
2669
|
+
(spare.get("label") or spare.get("id")))
|
|
2480
2670
|
break
|
|
2481
2671
|
if not gscored:
|
|
2482
2672
|
store.update_run(run_id, expected_status="running", status="failed",
|
|
@@ -2526,6 +2716,10 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2526
2716
|
resume=resume_ctx["session"] if resume_ctx else None)
|
|
2527
2717
|
if not res["ok"]:
|
|
2528
2718
|
continue
|
|
2719
|
+
remember_agent(actual_implementers, impl)
|
|
2720
|
+
refresh_actual_route(
|
|
2721
|
+
impl, implement_reason="全局打磨:%s" %
|
|
2722
|
+
(impl.get("label") or impl.get("id")))
|
|
2529
2723
|
# 重评该章
|
|
2530
2724
|
cj_by_agent = {}
|
|
2531
2725
|
for agent in critics:
|
|
@@ -2798,9 +2992,16 @@ def _run_content_review(run, task, agents, ev, stats, mode):
|
|
|
2798
2992
|
store.update_run(run_id, expected_status="running", status="failed",
|
|
2799
2993
|
error="没有可用智能体", ended_at=_now())
|
|
2800
2994
|
return
|
|
2801
|
-
if resume_ctx is not None
|
|
2802
|
-
|
|
2803
|
-
|
|
2995
|
+
if resume_ctx is not None:
|
|
2996
|
+
if mode == "auto":
|
|
2997
|
+
critics, route["critics"] = router.pick_critics(
|
|
2998
|
+
agents, task.get("type") or "novel", stats, impl=impl)
|
|
2999
|
+
else:
|
|
3000
|
+
critics = _pick_critics_manual(agents, task)
|
|
3001
|
+
|
|
3002
|
+
_record_actual_route(run_id, task, agents, stats, impl, critics=critics,
|
|
3003
|
+
implement_reason=route.get("author", ""),
|
|
3004
|
+
review_reason=route.get("critics", ""))
|
|
2804
3005
|
|
|
2805
3006
|
# ---- 规划(小说为模板计划)
|
|
2806
3007
|
_wait_gate(run_id, ev)
|
|
@@ -2890,6 +3091,7 @@ def _run_content_review(run, task, agents, ev, stats, mode):
|
|
|
2890
3091
|
crit_prompt = (_ensure_critique_placeholders(
|
|
2891
3092
|
_tpl(task, "critique_prompt", NOVEL_CRITIQUE_PROMPT))
|
|
2892
3093
|
.replace("__DIMKEYS__", dimkey)
|
|
3094
|
+
.replace("__CONTEXT__", task.get("context") or "(无)")
|
|
2893
3095
|
.replace("__MANUSCRIPT__", manuscript or "(稿件为空!)"))
|
|
2894
3096
|
# AI 味确定性检测(借鉴 oh-story 去AI味):客观参考线随评审下发,
|
|
2895
3097
|
# 命中才追加——评审官结合上下文判断是否真问题,脚本不直接扣分
|
|
@@ -2957,6 +3159,7 @@ def _run_content_review(run, task, agents, ev, stats, mode):
|
|
|
2957
3159
|
prompt = (NOVEL_REVISE_PROMPT.replace("__FILE__", ms_name)
|
|
2958
3160
|
.replace("__ROLE__", _content_role(task))
|
|
2959
3161
|
.replace("__GOAL__", task["goal"])
|
|
3162
|
+
.replace("__CONTEXT__", task.get("context") or "(无)")
|
|
2960
3163
|
.replace("__CRITIQUE__", "\n".join(crit_lines)))
|
|
2961
3164
|
prompt += _content_contract(task)
|
|
2962
3165
|
_run_step(run_id, "revise-r%d" % r, modelhub.bind_agent(impl, difficulty), prompt,
|
|
@@ -3286,7 +3489,13 @@ def execute_run(run_id):
|
|
|
3286
3489
|
task_spec_summary=task_compile.summary(task_spec),
|
|
3287
3490
|
difficulty=task_spec["difficulty"])
|
|
3288
3491
|
task = dict(task)
|
|
3492
|
+
task = _refresh_attachment_context(task, task.get("workdir") or "")
|
|
3289
3493
|
task["_compiled_spec"] = task_spec
|
|
3494
|
+
# 运行内统一使用编译后的难度;store 中历史任务常带 difficulty=auto,
|
|
3495
|
+
# 不能让这个兼容值覆盖 easy/default/hard 的模型调度决策。
|
|
3496
|
+
task["difficulty"] = task_spec["difficulty"]
|
|
3497
|
+
# 同理,历史任务可能保存非法/过期 engine;执行以编译后的流程引擎为准。
|
|
3498
|
+
task["engine"] = task_spec["engine"]
|
|
3290
3499
|
# 代码版本检出:任务指定了基线版本时,先检出任务分支 tutti/<task-id> 再跑流水线。
|
|
3291
3500
|
# 显式意图不容静默降级——仓库缺失/脏工作区/引用不存在一律中止运行并报错,
|
|
3292
3501
|
# 绝不带着用户未提交改动切分支、也不悄悄退回当前 HEAD。
|
|
@@ -3366,8 +3575,12 @@ def execute_run(run_id):
|
|
|
3366
3575
|
store.update_run(run_id, expected_status="running", status="failed",
|
|
3367
3576
|
error="没有可用智能体", ended_at=_now())
|
|
3368
3577
|
return
|
|
3369
|
-
if resume_ctx is not None
|
|
3370
|
-
|
|
3578
|
+
if resume_ctx is not None:
|
|
3579
|
+
if mode == "auto":
|
|
3580
|
+
critics, route["critics"] = router.pick_critics(
|
|
3581
|
+
agents, task["type"], stats, impl=impl)
|
|
3582
|
+
else:
|
|
3583
|
+
critics = _pick_critics_manual(agents, task)
|
|
3371
3584
|
if task.get("serial"):
|
|
3372
3585
|
_run_serial_review(run, task, agents, ev, stats, mode,
|
|
3373
3586
|
critics, impl, route, resume_ctx, difficulty)
|
|
@@ -3387,6 +3600,33 @@ def execute_run(run_id):
|
|
|
3387
3600
|
except Exception:
|
|
3388
3601
|
pass
|
|
3389
3602
|
finally:
|
|
3603
|
+
# 全类型统一写调度终态与质量反馈。调用成功只代表传输可靠;真正用于
|
|
3604
|
+
# 在线推荐的成功率以 verify/review/publishable 等验收结果为准。
|
|
3605
|
+
try:
|
|
3606
|
+
final_run = store.get_run(run_id) or {}
|
|
3607
|
+
final_status = final_run.get("status") or ""
|
|
3608
|
+
if final_status in ("done", "failed"):
|
|
3609
|
+
final_verdict = final_run.get("verdict") or {}
|
|
3610
|
+
if "pass" in final_verdict:
|
|
3611
|
+
quality_ok = bool(final_verdict.get("pass"))
|
|
3612
|
+
elif "publishable" in final_verdict:
|
|
3613
|
+
quality_ok = bool(final_verdict.get("publishable"))
|
|
3614
|
+
else:
|
|
3615
|
+
quality_ok = final_status == "done"
|
|
3616
|
+
_record_dispatch_completed(
|
|
3617
|
+
run_id, task, "passed" if quality_ok else "failed",
|
|
3618
|
+
verify_pass=final_verdict.get("verify_pass"),
|
|
3619
|
+
review_pass=final_verdict.get(
|
|
3620
|
+
"review_pass", final_verdict.get("publishable")))
|
|
3621
|
+
final_agent = (((final_run.get("route_plan") or {})
|
|
3622
|
+
.get("implement") or {}).get("selected") or "")
|
|
3623
|
+
if final_agent.startswith("builtin:"):
|
|
3624
|
+
final_agent = "builtin"
|
|
3625
|
+
usage.record_quality_for_run(run_id, quality_ok, agent=final_agent)
|
|
3626
|
+
elif final_status == "cancelled":
|
|
3627
|
+
_record_dispatch_completed(run_id, task, "cancelled")
|
|
3628
|
+
except Exception:
|
|
3629
|
+
pass
|
|
3390
3630
|
# 任务分支收尾(git_rev 隔离链的第二半):先只读快照本 run 的全部变更
|
|
3391
3631
|
# 落 run 记录供人审,再把产物提交到 tutti/<task-id> 并切回原分支。
|
|
3392
3632
|
# 放 finally:done/failed/cancelled/异常一律保存现场;收尾自身绝不抛错,
|