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.
@@ -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).replace(
1905
- "__MANUSCRIPT__", text or "(稿件为空!)")
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
- if bible:
2100
- sk_block = (sk_block + "\n\n" + bible) if sk_block else bible
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 and mode == "auto":
2802
- critics, route["critics"] = router.pick_critics(
2803
- agents, task.get("type") or "novel", stats, impl=impl)
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 and mode == "auto":
3370
- critics, route["critics"] = router.pick_critics(agents, task["type"], stats, impl=impl)
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/异常一律保存现场;收尾自身绝不抛错,