codebee 0.1.28 → 0.1.30

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 CHANGED
@@ -6,6 +6,29 @@ README 元数据带回,供老版本在「发现新版本」时展示新版更
6
6
 
7
7
  ## 未发布
8
8
 
9
+ ## v0.1.30(2026-09-21)
10
+
11
+ ### ✨ 新功能
12
+
13
+ - 微信直连配置更省事:新增「扫描」按钮自动找出本机全部 Python(标注位数与是否已装依赖,点选即回填);选中未装依赖的 64 位解释器可「一键安装 wechatauto-replica」,不用再自己开 conda 敲命令。
14
+ - 桌面蜜蜂右键新增「群摘要」:直达群摘要面板(已打开的页面只聚焦不重复开)。
15
+ - 直连任务可选指定厂商与模型。
16
+
17
+ ### 🛠 问题修复
18
+
19
+ - 修复没装 Pillow 的电脑上蜜蜂永远显示手绘丑蜜蜂:现在用系统自带能力直接显示新形象贴图(普通用户升级即变好看,无需装任何额外依赖)。
20
+ - 禅道自动化四个开关与扫描间隔紧凑排成一行。
21
+
22
+ ## v0.1.29(2026-09-21)
23
+
24
+ ### ✨ 新功能
25
+
26
+ - 升级后蜜蜂自动换新装:修复「升级后还是旧的丑蜜蜂」——服务重启后旧蜜蜂会自动让位,由新服务拉起带新形象的新蜜蜂(此前旧蜜蜂占着位置永不退场)。
27
+
28
+ ### 🛠 问题修复
29
+
30
+ - 去掉蜜蜂底部的任务进度条(真机截图反馈):窗口收紧贴着蜂体,画面更干净。
31
+
9
32
  ## v0.1.28(2026-09-21)
10
33
 
11
34
  ### 🛠 问题修复
package/README.md CHANGED
@@ -21,8 +21,30 @@ Kimi Code、MiMo Code、Grok Build、Pi、DeepSeek Harness……),提供
21
21
  不会把你的任务内容交给任何第三方。
22
22
 
23
23
  <!-- relnotes:start -->
24
+ ### ✨ 新功能 · New
25
+
26
+ - 微信直连配置更省事:「扫描」自动找本机 Python(标注位数与依赖状态,点选即用),选中后可一键安装 wechatauto-replica。
27
+ - WeChat setup is now effortless: a Scan button finds local Pythons (with bitness and dependency badges), and one click installs wechatauto-replica.
28
+
29
+ - 桌面蜜蜂右键新增「群摘要」:直达群摘要面板,已打开的页面只聚焦不重复开。
30
+ - The desktop bee's right-click menu now has "Group Digests" — it focuses the existing page instead of opening a duplicate.
31
+
32
+ - 没装 Pillow 的电脑上蜜蜂也会显示新形象贴图了(修复手绘丑蜜蜂问题)。
33
+ - Bees now show the new artwork even without Pillow installed (fixes the ugly hand-drawn fallback).
34
+
24
35
  ### 🛠 问题修复 · Fixes
25
36
 
37
+ - 禅道自动化开关与扫描间隔紧凑排成一行。
38
+ - ZenTao automation toggles and the scan interval now fit on one tidy row.
39
+
40
+ ### 上一版亮点 · Previous release
41
+
42
+ - 升级后蜜蜂自动换新装:服务重启后旧蜜蜂自动让位,新蜜蜂带新形象上线。
43
+ - After upgrading, the desktop bee swaps itself for the new look: the old bee steps aside when the service restarts.
44
+
45
+ - 去掉蜜蜂底部的任务进度条,窗口更干净。
46
+ - Removed the task progress bar under the desktop bee for a cleaner look.
47
+
26
48
  - 「关于与更新」页的版本信息与更新说明改为左对齐阅读,不再整块居中。
27
49
  - The version info and release notes on the About page are now left-aligned for easier reading.
28
50
 
@@ -39,11 +39,40 @@ _SYSTEM_PROMPT = """你是 CodeBee 的内置执行智能体,直接完成用户
39
39
  - 回答用户的语言与用户一致(默认中文)。直接给结论和内容,不要输出任何机器标记或协议行。"""
40
40
 
41
41
 
42
- def resolve():
42
+ def resolve(provider_id="", model="", difficulty="default"):
43
43
  """选内置智能体可用的 (provider, model):编排者配置优先,否则首个可用供应商。
44
44
 
45
45
  返回 {prov, model, provider_id, provider_name} 或 None(无可用供应商 → 调用方
46
46
  回退 CLI 路径)。"""
47
+ provider_id = str(provider_id or "").strip()
48
+ model = str(model or "").strip()
49
+ if provider_id:
50
+ for p in modelhub.providers():
51
+ if p.get("id") != provider_id or not p.get("enabled", True) or not p.get("api_key"):
52
+ continue
53
+ selected = model
54
+ if selected:
55
+ try:
56
+ enabled_names = [m.get("name") for m in modelhub._enabled_models(p)]
57
+ except Exception:
58
+ enabled_names = []
59
+ if selected not in enabled_names:
60
+ return None
61
+ if not selected:
62
+ selected = (p.get("model_hard") if difficulty == "hard" else
63
+ p.get("model_easy") if difficulty == "easy" else
64
+ p.get("model") or "").strip()
65
+ if not selected:
66
+ try:
67
+ names = modelhub._enabled_models(p)
68
+ except Exception:
69
+ names = []
70
+ selected = names[0]["name"] if names else ""
71
+ if selected:
72
+ return {"prov": p, "model": selected,
73
+ "provider_id": p.get("id") or "",
74
+ "provider_name": p.get("name") or p.get("id") or ""}
75
+ return None
47
76
  orch = None
48
77
  try:
49
78
  orch = modelhub.resolve_orchestrator()
@@ -57,7 +86,9 @@ def resolve():
57
86
  for p in modelhub.providers():
58
87
  if not p.get("enabled", True) or not p.get("api_key"):
59
88
  continue
60
- m = (p.get("model") or "").strip()
89
+ m = ((p.get("model_hard") if difficulty == "hard" else
90
+ p.get("model_easy") if difficulty == "easy" else
91
+ p.get("model")) or "").strip()
61
92
  if not m:
62
93
  try:
63
94
  names = modelhub._enabled_models(p)
@@ -534,6 +565,9 @@ def run(bi, prompt, workdir, timeout=180, cancel_event=None, log=None, images=No
534
565
  for kk in keys:
535
566
  url, headers, body = _build_request(proto, pbase, kk["key"], model,
536
567
  system, msgs, tools_ok)
568
+ effort = str(bi.get("reasoning_effort") or "").strip().lower()
569
+ if effort in ("low", "medium", "high") and proto != "google":
570
+ body["reasoning_effort"] = effort
537
571
  status, data, err = _post_interruptible(url, headers, body,
538
572
  allow_private, timeout,
539
573
  cancel_event)
@@ -103,7 +103,9 @@ def _online_model_bonus(entry, task_type, role):
103
103
  rate = float(metrics.get("success_rate") or 0.0)
104
104
  success_score = max(-6.0, min(6.0, (rate - 0.75) * 18.0))
105
105
  p95 = max(0.0, float(metrics.get("p95_duration_s") or 0.0))
106
- latency_score = -min(4.0, max(0.0, (p95 - 45.0) / 12.0))
106
+ # 模型链也必须让真实延迟影响排序,不能让 P95 数分钟的链靠静态质量分
107
+ # 长期压在十几秒的健康链前面。
108
+ latency_score = -min(12.0, max(0.0, (p95 - 30.0) / 8.0))
107
109
  cost = max(0.0, float(metrics.get("avg_cost_usd") or 0.0))
108
110
  cost_score = -min(3.0, max(0.0, (cost - 0.01) / 0.01))
109
111
  total = round(success_score + latency_score + cost_score, 2)
package/app/core/flows.py CHANGED
@@ -36,7 +36,7 @@ BUILTIN_FLOWS = [
36
36
  "note": "CodeBee 直连模型 API 干活(无 CLI 进程),无可用供应商时回退本机 CLI;无拆解/评审(快)"},
37
37
  {"id": "code", "name": "代码", "icon": "i-code", "engine": "code", "builtin": True,
38
38
  "goal_hint": "要实现/修复什么(一句话)",
39
- "note": "实现 → 验证命令 → 跨厂商评审 → 自动修复/换将"},
39
+ "note": "按难度动态编排:简单任务走实现+验证短链,复杂任务增加规划、评审、修复与换将"},
40
40
  {"id": "novel", "name": "小说", "icon": "i-book-open", "engine": "review", "builtin": True,
41
41
  "manuscript": "manuscript.md",
42
42
  "rubric": ["情节", "人物", "文笔", "节奏", "吸引力"],
package/app/core/jobs.py CHANGED
@@ -86,6 +86,17 @@ def configure(max_workers):
86
86
  return _target
87
87
 
88
88
 
89
+ def capacity_status():
90
+ """返回当前并发槽状态,供会产生管理记录的入口做无副作用预检。"""
91
+ with _pool_lock:
92
+ return {
93
+ "accepting": not _restart_drain and _alive < _target,
94
+ "active": _alive,
95
+ "limit": _target,
96
+ "restarting": _restart_drain,
97
+ }
98
+
99
+
89
100
  def start_worker():
90
101
  """加载并发保护配置;线程只在真实任务到达时创建。"""
91
102
  global _started
@@ -309,6 +320,18 @@ def _run_job(job):
309
320
  pass
310
321
  finally:
311
322
  if run_id:
323
+ # 执行线程退出就是生命周期的最后边界。流水线分支若意外提前 return、
324
+ # 或内部吞掉异常却漏写终态,旧逻辑会让 run/task 永久停在 running。
325
+ try:
326
+ from . import store
327
+ latest = store.get_run(run_id)
328
+ if latest and latest.get("status") == "running":
329
+ store.update_run(
330
+ run_id, expected_status="running", status="failed",
331
+ ended_at=_now(),
332
+ error="执行流程已退出但未写入完成状态,已自动标记失败;可重试")
333
+ except Exception:
334
+ pass
312
335
  CANCELS.pop(run_id, None)
313
336
  try:
314
337
  _maybe_auto_resume(run_id)
@@ -2016,6 +2016,12 @@ def bind_agent(agent, difficulty="default", task_type="", role=""):
2016
2016
  a = dict(agent)
2017
2017
  a["binding_configured"] = configured
2018
2018
  a["binding_mode"] = binding_mode
2019
+ # 统一思考档位:支持该能力的 CLI(当前 Codex)会把它下发为原生参数;
2020
+ # 其他 CLI 仍通过 difficulty 的模型路由获得相同的快/标准/深思语义。
2021
+ explicit_thinking = str(agent.get("_thinking") or "").strip().lower()
2022
+ a["reasoning_effort"] = ({"low": "low", "standard": "medium", "high": "high"}
2023
+ .get(explicit_thinking)
2024
+ or {"easy": "low", "hard": "high"}.get(difficulty, "medium"))
2019
2025
  if not r:
2020
2026
  return a
2021
2027
  merged = dict(agent.get("env") or {})
@@ -928,6 +928,10 @@ def _run_code(run, task, agents, ev, stats, mode):
928
928
  # ---- 难度(影响模型选择):用户指定 > 启发式 > 规划器判定
929
929
  difficulty = task.get("difficulty") or "auto"
930
930
  explicit = difficulty in ("easy", "hard")
931
+ if mode == "fast":
932
+ difficulty, explicit = "easy", True
933
+ elif mode == "expert":
934
+ difficulty, explicit = "hard", True
931
935
  if not explicit:
932
936
  difficulty = modelhub.classify_difficulty(task["goal"], task.get("verify_command"))
933
937
 
@@ -944,14 +948,21 @@ def _run_code(run, task, agents, ev, stats, mode):
944
948
  error="没有可用智能体", ended_at=_now())
945
949
  return
946
950
 
947
- # ---- 规划
948
- if mode == "auto":
951
+ # 项目记忆既给规划器,也给快速路径的实现者;不能因省掉独立规划而漏掉。
952
+ user_context_len = len(task.get("context") or "")
953
+ project_memory = _read_project_memory(workdir)
954
+ if project_memory:
955
+ task = dict(task, context=(task.get("context") or "") + "\n\n" + project_memory)
956
+
957
+ # ---- 规划。低风险短任务直接把目标作为单步计划,省掉一次独立模型调用;
958
+ # 附件或较长的用户背景仍先规划,避免快速路径漏读约束。
959
+ fast_path = (mode == "fast" or
960
+ (mode == "auto" and difficulty == "easy"
961
+ and not task.get("attachments")
962
+ and user_context_len < 600))
963
+ planned = False
964
+ if mode == "auto" and not fast_path:
949
965
  _wait_gate(run_id, ev)
950
- # 项目记忆注入(借鉴 agentmemory 持久记忆):同工作目录此前代码任务留下的
951
- # 架构事实,让规划器不再对代码库一无所知
952
- _pm = _read_project_memory(workdir)
953
- if _pm:
954
- task = dict(task, context=(task.get("context") or "") + "\n\n" + _pm)
955
966
  plan_step, plan_log = store.add_step(run_id, "plan", impl["id"], impl.get("label"),
956
967
  note=route.get("implementer", ""))
957
968
  plan = planner.make_code_plan(_steered_task(run_id, task),
@@ -967,21 +978,39 @@ def _run_code(run, task, agents, ev, stats, mode):
967
978
  plan["source"], difficulty,
968
979
  ";".join(s["title"] for s in plan["steps"])[:160]),
969
980
  duration_s=0.1 if impl.get("mode") == "mock" else None)
981
+ planned = True
982
+ elif fast_path:
983
+ plan = {
984
+ "source": "dynamic-fast-path",
985
+ "difficulty": "easy",
986
+ "steps": [{"title": "直接实现并自检", "detail": task["goal"]}],
987
+ "workflow": {
988
+ "review_required": not bool(task.get("verify_command")),
989
+ "max_repair_rounds": 1,
990
+ "allow_switch": False,
991
+ },
992
+ }
970
993
  else:
971
994
  plan = {"source": "manual", "steps": [{"title": "实现任务", "detail": task["goal"]}]}
972
- store.update_run(run_id, plan=plan, difficulty=difficulty)
995
+ workflow = task_compile.code_workflow(
996
+ task, difficulty, plan=plan, mode=mode, planned=planned)
997
+ store.update_run(run_id, plan=plan, difficulty=difficulty, workflow=workflow)
973
998
  subtasks = plan["steps"]
974
999
  # 计划落盘(planning-with-files):磁盘上的计划与 spec/evidence 同居任务档案
975
1000
  _write_task_plan(task, workdir, plan)
976
1001
 
977
1002
  # ---- 评审者(有会话延续时评审者仍用新鲜上下文,避免偏见)
978
- if mode == "auto":
1003
+ if not workflow["review_required"]:
1004
+ reviewer = None
1005
+ route["reviewer"] = workflow["reason"]
1006
+ elif mode == "auto":
979
1007
  reviewer, route["reviewer"] = router.pick_reviewer(agents, impl, "code", stats)
980
1008
  else:
981
1009
  reviewer, route["reviewer"] = _pick_reviewer_legacy(agents, impl)
982
1010
  _record_actual_route(run_id, task, agents, stats, impl, reviewer=reviewer,
983
1011
  implement_reason=route.get("implementer", ""),
984
- review_reason=route.get("reviewer", ""))
1012
+ review_reason=route.get("reviewer", ""),
1013
+ direct=not workflow["review_required"])
985
1014
  store.update_run(run_id, route=route)
986
1015
 
987
1016
  def implement_all(impl_agent, prefix_note):
@@ -1078,7 +1107,8 @@ def _run_code(run, task, agents, ev, stats, mode):
1078
1107
  run_id, task, agents, stats, other,
1079
1108
  implementers=[other], reviewer=reviewer,
1080
1109
  implement_reason=note,
1081
- review_reason=route.get("reviewer", ""))
1110
+ review_reason=route.get("reviewer", ""),
1111
+ direct=not workflow["review_required"])
1082
1112
  return True
1083
1113
  res_err = "%s;换将后仍失败:%s" % (note, (res.get("error") or "")[:200])
1084
1114
  else:
@@ -1091,6 +1121,17 @@ def _run_code(run, task, agents, ev, stats, mode):
1091
1121
 
1092
1122
  def review_and_score():
1093
1123
  verify_pass, verify_ran = _run_verify(run_id, task, workdir, ev)
1124
+ if not workflow["review_required"]:
1125
+ return {
1126
+ "pass": verify_pass,
1127
+ "scores": {},
1128
+ "issues": ([] if verify_pass else [{
1129
+ "severity": "high", "title": "验证命令未通过",
1130
+ "detail": "动态短链省略模型评审,先修复确定性验证失败。",
1131
+ }]),
1132
+ "summary": "低风险短链:确定性验证%s,模型评审已省略。" %
1133
+ ("通过" if verify_pass else "未通过"),
1134
+ }, verify_pass, verify_ran
1094
1135
  # gate 验证(借鉴 pi-subagents 的 gate:"npm test"):verify 失败时先跳过
1095
1136
  # 模型评审直接进修复轮——评审此时只能复述「验证没过」,白烧一次调用。
1096
1137
  # 连续失败 >=2 轮后恢复评审参与诊断(模型能看出编译错误之外的病灶)。
@@ -1138,7 +1179,8 @@ def _run_code(run, task, agents, ev, stats, mode):
1138
1179
  implementers=[impl], reviewer=reviewer,
1139
1180
  implement_reason="修复轮由 %s 完成" %
1140
1181
  (impl.get("label") or impl.get("id")),
1141
- review_reason=route.get("reviewer", ""))
1182
+ review_reason=route.get("reviewer", ""),
1183
+ direct=not workflow["review_required"])
1142
1184
  if impl.get("mode") == "mock" and res["ok"]:
1143
1185
  pass # mock 不产生真实变更
1144
1186
  review_json, verify_pass, verify_ran = review_and_score()
@@ -1149,10 +1191,10 @@ def _run_code(run, task, agents, ev, stats, mode):
1149
1191
  "passed": passed})
1150
1192
  if passed:
1151
1193
  break
1152
- if round_no < router.MAX_REPAIR_ROUNDS and not switched:
1194
+ if round_no < workflow["max_repair_rounds"] and not switched:
1153
1195
  round_no += 1
1154
1196
  continue
1155
- if mode == "auto" and not switched:
1197
+ if mode == "auto" and workflow["allow_switch"] and not switched:
1156
1198
  ex = (impl["id"],)
1157
1199
  if impl.get("mode") == "real":
1158
1200
  ex += ("mock-a", "mock-b") # 真实实现者失败时不降级到 mock
@@ -1166,7 +1208,8 @@ def _run_code(run, task, agents, ev, stats, mode):
1166
1208
  run_id, task, agents, stats, impl,
1167
1209
  implementers=[impl], reviewer=reviewer,
1168
1210
  implement_reason=note,
1169
- review_reason=route.get("reviewer", ""))
1211
+ review_reason=route.get("reviewer", ""),
1212
+ direct=not workflow["review_required"])
1170
1213
  switched = True
1171
1214
  round_no += 1
1172
1215
  continue
@@ -1178,18 +1221,27 @@ def _run_code(run, task, agents, ev, stats, mode):
1178
1221
  verdict = {
1179
1222
  "type": "code", "engine": "code", "pass": overall_pass, "mode": mode,
1180
1223
  "verify_ran": verify_ran, "verify_pass": verify_pass,
1181
- "review_pass": bool(review_json.get("pass")),
1224
+ "review_pass": (bool(review_json.get("pass"))
1225
+ if workflow["review_required"] else None),
1226
+ "review_skipped": not workflow["review_required"],
1182
1227
  "scores": scores, "overall_score": overall_score,
1183
1228
  "issues": review_json.get("issues") or [],
1184
- "reviewer": reviewer["id"],
1229
+ "reviewer": reviewer["id"] if reviewer else "",
1185
1230
  "route": route, "repairs": repairs, "switched": switched,
1231
+ "workflow": workflow,
1186
1232
  }
1187
1233
 
1188
1234
  lines = [
1189
1235
  "# 代码任务报告:%s" % task["title"], "",
1190
1236
  "- 结论:**%s**" % ("✅ 通过" if overall_pass else "❌ 未通过"),
1191
1237
  "- 编排模式:%s 实现者:%s 评审者:%s" % (
1192
- "智能" if mode == "auto" else "手动", impl.get("label"), reviewer.get("label")),
1238
+ "智能" if mode == "auto" else "手动", impl.get("label"),
1239
+ reviewer.get("label") if reviewer else "按验证结果省略"),
1240
+ "- 动态步骤:规划 %s;实现 %d 步;验证 %s;评审 %d 人;最多修复 %d 轮;换将 %s" % (
1241
+ "独立执行" if workflow["planning"] == "llm" else "并入实现",
1242
+ workflow["implementation_steps"], "有" if workflow["verification"] else "无",
1243
+ workflow["reviewers"], workflow["max_repair_rounds"],
1244
+ "允许" if workflow["allow_switch"] else "关闭"),
1193
1245
  "- 验证命令:%s → %s" % (task.get("verify_command") or "(未配置)",
1194
1246
  "通过" if verify_pass else "未通过"),
1195
1247
  "- 综合评分:%s 修复/换将:%s" % (
@@ -1229,7 +1281,8 @@ def _run_code(run, task, agents, ev, stats, mode):
1229
1281
  summary="代码任务%s(验证%s / 评审%s%s)" % (
1230
1282
  "通过" if overall_pass else "未通过",
1231
1283
  "通过" if verify_pass else "未通过",
1232
- "通过" if review_json.get("pass") else "未通过",
1284
+ (("通过" if review_json.get("pass") else "未通过")
1285
+ if workflow["review_required"] else "按策略省略"),
1233
1286
  ",%d 轮修复" % (len(repairs) - 1) if len(repairs) > 1 else ""),
1234
1287
  ended_at=_now())
1235
1288
 
@@ -1379,12 +1432,27 @@ def _run_direct(run, task, agents, ev, stats, mode):
1379
1432
  route = {}
1380
1433
  resume_ctx = _valid_resume(task, agents)
1381
1434
  bi = None
1382
- if resume_ctx is None and not (mode == "manual" and task.get("implementer")):
1435
+ direct_provider = (task.get("direct_provider_id") or "").strip()
1436
+ direct_model = (task.get("direct_model") or "").strip()
1437
+ direct_difficulty = ("hard" if task.get("thinking") == "high" else
1438
+ "easy" if task.get("thinking") == "low" else "default")
1439
+ if resume_ctx is None and direct_provider:
1440
+ bi = builtin_agent.resolve(direct_provider, direct_model, direct_difficulty)
1441
+ if bi is None:
1442
+ store.update_run(run_id, expected_status="running", status="failed",
1443
+ error="指定的对话厂商或模型不可用,请改用自动推荐或检查配置",
1444
+ ended_at=_now())
1445
+ return
1446
+ elif resume_ctx is None and not (mode == "manual" and task.get("implementer")):
1383
1447
  try:
1384
- bi = builtin_agent.resolve()
1448
+ bi = builtin_agent.resolve(difficulty=("hard" if task.get("thinking") == "high"
1449
+ else "easy" if task.get("thinking") == "low"
1450
+ else "default"))
1385
1451
  except Exception:
1386
1452
  bi = None
1387
1453
  if bi is not None:
1454
+ bi["reasoning_effort"] = {"low": "low", "standard": "medium", "high": "high"}.get(
1455
+ task.get("thinking"), "medium")
1388
1456
  impl = None
1389
1457
  route["implementer"] = "CodeBee(%s · %s)" % (bi["provider_name"], bi["model"])
1390
1458
  elif resume_ctx is not None:
@@ -3029,6 +3097,11 @@ def _run_content_review(run, task, agents, ev, stats, mode):
3029
3097
  step_wd = _resume_workdir(resume_ctx, workdir) if resume_ctx else workdir
3030
3098
  difficulty = task.get("difficulty") or (
3031
3099
  "hard" if threshold >= 8.5 else "easy" if threshold <= 6 else "default")
3100
+ if mode == "fast":
3101
+ difficulty = "easy"
3102
+ elif mode == "expert":
3103
+ difficulty = "hard"
3104
+ workflow = task_compile.content_workflow(task, difficulty, mode=mode)
3032
3105
 
3033
3106
  # ---- 路由
3034
3107
  if resume_ctx is not None:
@@ -3052,6 +3125,10 @@ def _run_content_review(run, task, agents, ev, stats, mode):
3052
3125
  else:
3053
3126
  critics = _pick_critics_manual(agents, task)
3054
3127
 
3128
+ critic_pool = list(critics)
3129
+ if mode != "manual":
3130
+ critics = critic_pool[:workflow["reviewers"]]
3131
+
3055
3132
  _record_actual_route(run_id, task, agents, stats, impl, critics=critics,
3056
3133
  implement_reason=route.get("author", ""),
3057
3134
  review_reason=route.get("critics", ""))
@@ -3059,7 +3136,8 @@ def _run_content_review(run, task, agents, ev, stats, mode):
3059
3136
  # ---- 规划(小说为模板计划)
3060
3137
  _wait_gate(run_id, ev)
3061
3138
  plan = planner.make_novel_plan(_steered_task(run_id, task), impl, critics)
3062
- store.update_run(run_id, plan=plan, route=route, difficulty=difficulty)
3139
+ store.update_run(run_id, plan=plan, route=route, difficulty=difficulty,
3140
+ workflow=workflow)
3063
3141
 
3064
3142
  ms_path = os.path.join(workdir, ms_name)
3065
3143
 
@@ -3077,9 +3155,17 @@ def _run_content_review(run, task, agents, ev, stats, mode):
3077
3155
 
3078
3156
  # 编排者大纲:只对真实执行有意义;失败静默退回无大纲(喂入带指令的任务副本)
3079
3157
  outline = (planner.make_review_outline(_steered_task(run_id, task))
3080
- if impl.get("mode") != "mock" else None)
3158
+ if workflow["outline"] and impl.get("mode") != "mock" else None)
3081
3159
  if outline:
3082
- store.update_run(run_id, outline=outline)
3160
+ workflow = task_compile.content_workflow(
3161
+ task, difficulty, plan=outline, mode=mode)
3162
+ if mode != "manual":
3163
+ critics = critic_pool[:workflow["reviewers"]]
3164
+ store.update_run(run_id, outline=outline, workflow=workflow)
3165
+ _record_actual_route(run_id, task, agents, stats, impl, critics=critics,
3166
+ implement_reason=route.get("author", ""),
3167
+ review_reason=route.get("critics", ""))
3168
+ rounds = workflow["review_rounds"]
3083
3169
 
3084
3170
  # 1) 起草
3085
3171
  bestof_improvements = "" # 赛马败者精华(真实路径由选择器填充;mock 路径恒空)
@@ -3228,6 +3314,7 @@ def _run_content_review(run, task, agents, ev, stats, mode):
3228
3314
  "overall": overall, "mode": mode,
3229
3315
  "threshold": threshold, "rounds_used": history_rounds[-1]["round"] if history_rounds else 0,
3230
3316
  "scores": final_means, "history": history_rounds, "route": route,
3317
+ "workflow": workflow,
3231
3318
  }
3232
3319
 
3233
3320
  # 3) 报告
@@ -3237,8 +3324,12 @@ def _run_content_review(run, task, agents, ev, stats, mode):
3237
3324
  % ("✅ 达到发布标准" if publishable else "❌ 未达标,建议再修",
3238
3325
  overall, threshold, verdict["rounds_used"]),
3239
3326
  "- 编排模式:%s 起草/修订:%s 评审组:%s" % (
3240
- "智能" if mode == "auto" else "手动",
3241
- impl.get("label"), "、".join(a.get("label") for a in critics))]
3327
+ {"auto": "自动", "fast": "快速", "expert": "专家",
3328
+ "manual": "手动"}.get(mode, mode),
3329
+ impl.get("label"), "、".join(a.get("label") for a in critics)),
3330
+ "- 动态步骤:大纲 %s;评审 %d 人;最多 %d 轮(%s)" % (
3331
+ "启用" if workflow["outline"] else "省略",
3332
+ len(critics), rounds, workflow["reason"])]
3242
3333
  if route:
3243
3334
  lines.append("")
3244
3335
  lines.append("## 路由依据")
@@ -3589,6 +3680,7 @@ def execute_run(run_id):
3589
3680
  for _agent in agents:
3590
3681
  if isinstance(_agent, dict):
3591
3682
  _agent["_dispatch_task_type"] = task.get("type") or "direct"
3683
+ _agent["_thinking"] = task.get("thinking") or "auto"
3592
3684
  store.update_run(run_id, route_plan={
3593
3685
  "task": task_spec,
3594
3686
  "implement": router.route_plan(agents, "implement", task_spec, stats),
@@ -114,11 +114,13 @@ def _log_usage(source, role, task, res, agent=None, tool="", model="", provider=
114
114
  except Exception:
115
115
  pass
116
116
 
117
- CODE_PLAN_PROMPT = """你是技术负责人。请把下面的开发目标拆解为 __N__ 个以内、按顺序执行的子任务,
118
- 并判定任务难度。只输出一个 ```json 代码块,不要输出其他内容。JSON 结构:
119
- {"difficulty": "easy 或 hard", "subtasks": [{"title": "简短标题", "detail": "具体要做什么,给执行工程师的直接指令", "files": ["涉及的文件路径"]}]}
120
- 难度判定:常规增删改查/小函数/格式调整 = easy;跨模块改动/架构调整/复杂算法/安全相关 = hard。
121
- 子任务粒度要可独立验证;最后一个子任务必须包含整体联调/收尾。
117
+ CODE_PLAN_PROMPT = """你是技术负责人。请把下面的开发目标拆解为 __N__ 个以内、按顺序执行的子任务,
118
+ 并判定任务难度和后续质量步骤。只输出一个 ```json 代码块,不要输出其他内容。JSON 结构:
119
+ {"difficulty": "easy 或 hard", "workflow": {"review_required": true, "max_repair_rounds": 0, "allow_switch": false}, "subtasks": [{"title": "简短标题", "detail": "具体要做什么,给执行工程师的直接指令", "files": ["涉及的文件路径"]}]}
120
+ 难度判定:常规增删改查/小函数/格式调整 = easy;跨模块改动/架构调整/复杂算法/安全相关 = hard。
121
+ 质量步骤判定:涉及安全、数据迁移、并发、权限、公共 API 或跨模块改动时必须评审;
122
+ 有明确自动验收的低风险小改可不做模型评审。max_repair_rounds 取 0-2,只有复杂任务才建议换将。
123
+ 子任务粒度要可独立验证;最后一个子任务必须包含整体联调/收尾。
122
124
 
123
125
  ## 先探索再计划(重要,借鉴 OpenSpec explore)
124
126
  拆解之前先用你的读文件/搜索工具**实际查看工作目录**,找到目标相关的真实文件与函数,再据此拆解。
@@ -135,9 +137,12 @@ __CONTEXT__
135
137
  ## 验收命令(最终必须通过)
136
138
  __VERIFY__"""
137
139
 
138
- REVIEW_OUTLINE_PROMPT = """你是内容主编。请为下面的创作任务拟一份写作大纲(要点列表,3-8 条),
139
- 只输出一个 ```json 代码块,不要输出其他内容。JSON 结构:
140
- {"outline": ["要点1", "要点2", ...]}
140
+ REVIEW_OUTLINE_PROMPT = """你是内容主编。请为下面的创作任务拟一份写作大纲(要点列表,3-8 条),
141
+ 并按内容风险建议后续质量步骤。
142
+ 只输出一个 ```json 代码块,不要输出其他内容。JSON 结构:
143
+ {"outline": ["要点1", "要点2", ...], "workflow": {"reviewers": 1, "review_rounds": 1}}
144
+ reviewers 取 1-2,review_rounds 取 1-3;研究报告、技术方案、长文和高发布门槛应增加,
145
+ 普通邮件、短翻译、工作汇报等低风险短内容保持 1。
141
146
 
142
147
  ## 创作任务
143
148
  __GOAL__
@@ -403,9 +408,27 @@ def _norm_subtasks(data):
403
408
  return steps or None
404
409
 
405
410
 
406
- def _plan_difficulty(data):
411
+ def _plan_difficulty(data):
407
412
  d = str((data or {}).get("difficulty") or "").lower()
408
- return d if d in ("easy", "hard") else None
413
+ return d if d in ("easy", "hard") else None
414
+
415
+
416
+ def _plan_workflow(data):
417
+ """规范化规划器给出的后续步骤建议;最终安全下限由任务编译器裁决。"""
418
+ raw = (data or {}).get("workflow")
419
+ if not isinstance(raw, dict):
420
+ return None
421
+ out = {}
422
+ if isinstance(raw.get("review_required"), bool):
423
+ out["review_required"] = raw["review_required"]
424
+ try:
425
+ if "max_repair_rounds" in raw:
426
+ out["max_repair_rounds"] = max(0, min(2, int(raw["max_repair_rounds"])))
427
+ except (TypeError, ValueError):
428
+ pass
429
+ if isinstance(raw.get("allow_switch"), bool):
430
+ out["allow_switch"] = raw["allow_switch"]
431
+ return out or None
409
432
 
410
433
 
411
434
  def _orchestrator():
@@ -437,9 +460,10 @@ def make_code_plan(task, planner_agent, workdir, ev=None, resume=None, log_path=
437
460
  _log_usage("plan", "plan", task, res, agent=planner_agent)
438
461
  data = runner.extract_json(res.get("text") or "")
439
462
  steps = _norm_subtasks(data)
440
- if steps:
441
- return {"source": "llm(%s)" % planner_agent["id"], "steps": steps,
442
- "difficulty": _plan_difficulty(data)}
463
+ if steps:
464
+ return {"source": "llm(%s)" % planner_agent["id"], "steps": steps,
465
+ "difficulty": _plan_difficulty(data),
466
+ "workflow": _plan_workflow(data)}
443
467
  return _fallback_code_plan(task, "LLM 计划解析失败,退化为单步模板")
444
468
 
445
469
 
@@ -468,8 +492,9 @@ def _orch_code_plan(task, prov, model, log_path=None):
468
492
  if not steps:
469
493
  _append_log(log_path, "编排者计划返回内容无法解析为子任务,回退 CLI")
470
494
  return None
471
- return {"source": "编排者(%s · %s)" % (prov.get("name", prov["id"]), model),
472
- "steps": steps, "difficulty": _plan_difficulty(data)}
495
+ return {"source": "编排者(%s · %s)" % (prov.get("name", prov["id"]), model),
496
+ "steps": steps, "difficulty": _plan_difficulty(data),
497
+ "workflow": _plan_workflow(data)}
473
498
 
474
499
 
475
500
  def make_review_outline(task):
@@ -492,8 +517,20 @@ def make_review_outline(task):
492
517
  if not isinstance(outline, list):
493
518
  return None
494
519
  items = [str(x).strip()[:120] for x in outline if str(x).strip()][:8]
495
- return {"source": "编排者(%s · %s)" % (prov.get("name", prov["id"]), model),
496
- "items": items} if items else None
520
+ if not items:
521
+ return None
522
+ workflow = data.get("workflow") if isinstance(data.get("workflow"), dict) else {}
523
+ try:
524
+ reviewers = max(1, min(2, int(workflow.get("reviewers") or 1)))
525
+ except (TypeError, ValueError):
526
+ reviewers = 1
527
+ try:
528
+ review_rounds = max(1, min(3, int(workflow.get("review_rounds") or 1)))
529
+ except (TypeError, ValueError):
530
+ review_rounds = 1
531
+ return {"source": "编排者(%s · %s)" % (prov.get("name", prov["id"]), model),
532
+ "items": items,
533
+ "workflow": {"reviewers": reviewers, "review_rounds": review_rounds}}
497
534
 
498
535
 
499
536
  def make_novel_plan(task, author, critics):