codebee 0.1.69 → 0.1.70

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.
Files changed (57) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +4 -3
  3. package/app/browser-final-1440.png +0 -0
  4. package/app/browser-final-390.png +0 -0
  5. package/app/browser-google-fallback.png +0 -0
  6. package/app/browser-google-test.png +0 -0
  7. package/app/browser-workbench-1440.png +0 -0
  8. package/app/browser-workbench-390.png +0 -0
  9. package/app/browser-workspace-google.png +0 -0
  10. package/app/browser-workspace-smoke.png +0 -0
  11. package/app/browser-workspace.png +0 -0
  12. package/app/core/audit.py +30 -0
  13. package/app/core/automation.py +107 -12
  14. package/app/core/benchstore.py +95 -0
  15. package/app/core/bookmeta.py +9 -4
  16. package/app/core/builtin_agent.py +472 -33
  17. package/app/core/catalog.py +7 -1
  18. package/app/core/dispatch.py +20 -3
  19. package/app/core/error_codes.py +11 -2
  20. package/app/core/evalbench.py +720 -0
  21. package/app/core/flows.py +228 -3
  22. package/app/core/jobs.py +22 -1
  23. package/app/core/manager.py +21 -0
  24. package/app/core/market_remote.py +10 -0
  25. package/app/core/mcp_client.py +287 -0
  26. package/app/core/modelhub.py +185 -2
  27. package/app/core/notify.py +118 -23
  28. package/app/core/novel_quality.py +189 -0
  29. package/app/core/paths.py +4 -0
  30. package/app/core/pipeline.py +426 -28
  31. package/app/core/planner.py +243 -22
  32. package/app/core/plugins.py +525 -0
  33. package/app/core/publish/browser.py +129 -13
  34. package/app/core/publish/fanqie.py +6 -0
  35. package/app/core/publish/flow.py +154 -5
  36. package/app/core/publish/flows-fanqie-calibrated.json +11 -7
  37. package/app/core/publish/ledger.py +29 -3
  38. package/app/core/publish/manager.py +134 -2
  39. package/app/core/publish/qimao.py +5 -0
  40. package/app/core/remotesync.py +121 -0
  41. package/app/core/runner.py +80 -5
  42. package/app/core/settings.py +39 -2
  43. package/app/core/settings_schema.py +8 -0
  44. package/app/core/store.py +30 -8
  45. package/app/core/usage.py +187 -14
  46. package/app/core/zentao.py +153 -4
  47. package/app/current-browser.png +0 -0
  48. package/app/main.py +279 -15
  49. package/app/mcp_server.py +175 -0
  50. package/app/pet.py +82 -21
  51. package/app/plugins/code-review-helper/.codex-plugin/plugin.json +18 -0
  52. package/app/plugins/code-review-helper/skills/review.md +16 -0
  53. package/app/ui/app.js +1380 -73
  54. package/app/ui/i18n.js +248 -6
  55. package/app/ui/index.html +277 -13
  56. package/app/ui/style.css +786 -0
  57. package/package.json +2 -1
@@ -144,6 +144,11 @@ detail 的清晰度按「初级工程师测试」校验:假设执行者没有
144
144
  只读 detail 就能照做不跑偏(借鉴 superpowers)。
145
145
  若工作目录为空(全新项目),files 写计划新建的文件路径。
146
146
 
147
+ ## 工作目录侦察(CodeBee 自动扫描的真实结构)
148
+ __RECON__
149
+ (这是执行前自动扫描的真实目录结构:API 编排者没有读文件工具,拆解以上方侦察
150
+ 为准,不要凭想象编路径;CLI 执行者有工具,可在实现时再用工具复核。)
151
+
147
152
  ## 开发目标
148
153
  __GOAL__
149
154
 
@@ -153,6 +158,84 @@ __CONTEXT__
153
158
  ## 验收命令(最终必须通过)
154
159
  __VERIFY__"""
155
160
 
161
+ # ---- 工作目录侦察(file-explorer 职能内置化,借鉴 freebuff 专职子 agent)----
162
+ # 给没有文件工具的直连编排者补「贴着真实代码库规划」的能力;CLI 规划者注入后
163
+ # 可省探索轮次。剪枝口径与 artifacts.py 一致(SKIP_DIRS + BUILD_DIRS),另加
164
+ # 本工具任务元数据目录。
165
+ _RECON_SKIP = {".git", "node_modules", "__pycache__", ".venv", "venv", ".idea",
166
+ ".vscode", "dist", "build", "target", "out", "bin", "obj",
167
+ ".next", ".nuxt", ".output", "__MACOSX", ".codebee", "_attachments"}
168
+ _RECON_ENTRY_FILES = ("README.md", "readme.md", "README.txt", "package.json",
169
+ "pyproject.toml", "requirements.txt", "pom.xml",
170
+ "build.gradle", "go.mod", "Cargo.toml", "composer.json")
171
+ _RECON_MAX_FILES = 120 # 目录树条目上限(超出省略并注明)
172
+ _RECON_MAX_CHARS = 2400 # 注入 prompt 的字符预算(约 1.2k token)
173
+
174
+
175
+ def workdir_recon(workdir, max_chars=_RECON_MAX_CHARS):
176
+ """规划前工作目录侦察:文件清单 + 项目清单文件头部摘要。输出纯文本块。
177
+
178
+ 任何异常都返回一句话说明——侦察永远不能挡规划。
179
+ """
180
+ try:
181
+ root = os.path.abspath(workdir or "")
182
+ if not workdir or not os.path.isdir(root):
183
+ return "(工作目录不存在或尚未创建——按全新项目规划)"
184
+ files = []
185
+ for dirpath, dirnames, filenames in os.walk(root):
186
+ rel = os.path.relpath(dirpath, root)
187
+ if rel != "." and rel.split(os.sep)[0] in _RECON_SKIP:
188
+ dirnames[:] = []
189
+ continue
190
+ dirnames[:] = [d for d in dirnames if d not in _RECON_SKIP]
191
+ depth = 0 if rel == "." else rel.count(os.sep) + 1
192
+ if depth > 4:
193
+ dirnames[:] = []
194
+ continue
195
+ prefix = "" if rel == "." else rel.replace(os.sep, "/") + "/"
196
+ for fn in filenames:
197
+ files.append(prefix + fn)
198
+ if not files:
199
+ return "(空目录——全新项目,可自由规划文件结构)"
200
+ total = len(files)
201
+ files.sort()
202
+ lines = ["共 %d 个文件%s:" % (total, "(截断显示)" if total > _RECON_MAX_FILES else "")]
203
+ lines += ["- " + f for f in files[:_RECON_MAX_FILES]]
204
+ if total > _RECON_MAX_FILES:
205
+ lines.append("…(其余 %d 个文件已省略)" % (total - _RECON_MAX_FILES))
206
+ shown = 0
207
+ for f in files:
208
+ if shown >= 3:
209
+ break
210
+ name = f.rsplit("/", 1)[-1]
211
+ if name in _RECON_ENTRY_FILES:
212
+ try:
213
+ with open(os.path.join(root, f.replace("/", os.sep)),
214
+ "r", encoding="utf-8", errors="replace") as fh:
215
+ head = "".join(fh.readlines()[:15])[:600]
216
+ except OSError:
217
+ continue
218
+ if head.strip():
219
+ lines += ["", "%s 头部:" % name, head]
220
+ shown += 1
221
+ out = "\n".join(lines)
222
+ if len(out) > max_chars:
223
+ out = out[:max_chars] + "\n…(侦察报告已截断)"
224
+ return out
225
+ except Exception as e:
226
+ return "(侦察失败:%s)" % e
227
+
228
+
229
+ def _code_plan_prompt(task):
230
+ """代码规划 prompt 组装(编排者与 CLI 两处共用同一份,保证同任务同 prompt)。"""
231
+ return (CODE_PLAN_PROMPT
232
+ .replace("__N__", str(MAX_SUBTASKS))
233
+ .replace("__GOAL__", task["goal"])
234
+ .replace("__CONTEXT__", task.get("context") or "(无)")
235
+ .replace("__VERIFY__", task.get("verify_command") or "(未配置)")
236
+ .replace("__RECON__", workdir_recon(task.get("workdir") or "")))
237
+
238
+
156
239
  REVIEW_OUTLINE_PROMPT = """你是内容主编。请为下面的创作任务拟一份写作大纲(要点列表,3-8 条),
157
240
  并按内容风险建议后续质量步骤。
158
241
  只输出一个 ```json 代码块,不要输出其他内容。JSON 结构:
@@ -171,10 +254,14 @@ SERIAL_OUTLINE_PROMPT = """你是网文主编,熟悉签约平台(番茄/七
171
254
  __SKILLS__
172
255
  请为下面的小说目标设计一份连载大纲:共 __N__ 章,每章约 __W__ 字。
173
256
  只输出一个 ```json 代码块,不要输出其他内容。JSON 结构:
174
- {"book_title": "书名", "chapters": [{"title": "章节标题", "beats": "本章剧情要点(50-120字:事件/冲突/推进)", "hook": "章末钩子(一句话)"}]__VOLUMES_KEY__}
257
+ {"book_title": "书名", "style_anchor": "全书文风锚(80字内,具体可执行:叙述视角与人称、句长基调、语域、对白风格、禁用表达)", "chapters": [{"title": "章节标题", "beats": "本章剧情要点(50-120字:事件/冲突/推进)", "hook": "章末钩子(一句话)", "highlight": "本章爽点/情绪爆点(一句话:谁在哪里完成什么反转或释放什么情绪)"}]__VOLUMES_KEY__}
175
258
  硬性要求:
176
- - 第 1-3 章是黄金三章:第 1 章开篇即冲突+人设立住,第 3 章末留大钩子;
177
- - 每章有明确冲突与剧情推进,禁止水字数的日常流水账;
259
+ - 第 1-3 章是黄金三章:第 1 章前 100 字出现主角、前 200 字出现异常或欲望、前 600 字进入具体困境,
260
+ 第 3 章末留大钩子;不要用世界观或环境描写拖延切入;
261
+ - 每章都写清“主角主动选择 → 阻碍 → 局面变化 → 章末钩子”,禁止水字数的日常流水账和硬旁白推剧情;
262
+ - 大纲中的情感转折必须给出触发事件和人物选择,不能只写“误会加深/感情升温”;
263
+ - style_anchor 是全书唯一的文风锚,每章起草/修订/评审都会注入:必须写明视角人称与句长基调等可执行约束,不要空话;
264
+ - 每章必须给出 highlight(爽点/情绪爆点):没有爆点的章节要重新设计,过渡章也要有小冲突小反转;
178
265
  - 结局必须闭环(完本感),主角有成长弧光;
179
266
  - 题材健康,无违规内容,符合平台签约调性。
180
267
  __VOLUMES__
@@ -190,14 +277,19 @@ SERIAL_CONTINUE_OUTLINE_PROMPT = """你是网文主编,熟悉签约平台(
190
277
  __SKILLS__
191
278
  这是一部长篇连载的续写:全书已完成前 __DONE__ 章,现在请规划第 __START__–__END__ 章
192
279
  (本批共 __N__ 章,每章约 __W__ 字)。只输出一个 ```json 代码块,不要输出其他内容。JSON 结构:
193
- {"book_title": "书名(与前文保持一致)", "chapters": [{"title": "章节标题", "beats": "本章剧情要点(50-120字:事件/冲突/推进)", "hook": "章末钩子(一句话)"}]__VOLUMES_KEY__}
280
+ {"book_title": "书名(与前文保持一致)", "style_anchor": "全书文风锚(80字内,必须延续下方给出的既定文风锚,不得另起炉灶)", "chapters": [{"title": "章节标题", "beats": "本章剧情要点(50-120字:事件/冲突/推进)", "hook": "章末钩子(一句话)", "highlight": "本章爽点/情绪爆点(一句话:谁在哪里完成什么反转或释放什么情绪)"}]__VOLUMES_KEY__}
194
281
  硬性要求:
195
282
  - 第 1 章直接衔接前文(见下方前情),不得跳线、不得重启设定、不得复述前文;
196
283
  - 主线沿既有脉络推进,新冲突尽量从已埋伏笔中生长,人物性格与前文一致;
197
- - 每章有明确冲突与剧情推进,禁止水字数的日常流水账;
284
+ - 每章都写清“主角主动选择 → 阻碍 → 局面变化 → 章末钩子”,禁止水字数的日常流水账;
285
+ - 复杂情感必须落到触发事件、动作/生理反应和潜台词,不能用一句概括代替;
286
+ - 延续既定叙述视角和语言锚点(见下方既定文风锚),删掉重复复述,避免节奏变慢;
198
287
  - 题材健康,无违规内容,符合平台签约调性。
199
288
  __VOLUMES__
200
289
 
290
+ ## 既定文风锚(前文已定,必须原样延续)
291
+ __STYLE_ANCHOR__
292
+
201
293
  ## 前情大纲(已完成章节,章号为全书章号)
202
294
  __PREV_OUTLINE__
203
295
 
@@ -242,13 +334,19 @@ def _norm_chapters(data, n, vol_per=0, vol_start=1, vol_plan=None):
242
334
  if not title:
243
335
  continue
244
336
  out.append({"title": title[:60], "beats": beats[:500],
245
- "hook": str(c.get("hook") or "").strip()[:200]})
337
+ "hook": str(c.get("hook") or "").strip()[:200],
338
+ "highlight": str(c.get("highlight") or "").strip()[:200]})
246
339
  if len(out) < min(n, max(2, n // 2)): # 至少给出半数章的大纲,否则视为失败(n=1 时至少 1 章)
247
340
  return None
248
341
  while len(out) < n: # 缺的章补模板位
249
342
  out.append({"title": "第 %d 章" % (len(out) + 1), "beats": "按全书目标推进剧情",
250
- "hook": ""})
343
+ "hook": "", "highlight": ""})
251
344
  res = {"book_title": str(data.get("book_title") or "").strip()[:40], "chapters": out[:n]}
345
+ # 全书文风锚:大纲一次性裁定,后续每章起草/修订/评审注入(pipeline 消费);
346
+ # 续写批次由提示词强制延续前批锚点,模型不另起炉灶
347
+ anchor = str(data.get("style_anchor") or "").strip()
348
+ if anchor:
349
+ res["style_anchor"] = anchor[:300]
252
350
  # 分卷:只收本批真正用到的卷的卷名/卷弧光(模型可能多写或乱写卷号,一律按
253
351
  # 本批章号范围过滤)。卷边界不在这里——它由 volumes.build_plan 从章号推导。
254
352
  if vol_plan:
@@ -315,6 +413,26 @@ def _prev_serial_story(task):
315
413
  return "\n".join(lines), max(best_i, 0), book_title, tail, volnames
316
414
 
317
415
 
416
+ def prev_style_anchor(task):
417
+ """沿 serial.continues 链回查最近一批大纲的文风锚(旧 → 新取最新的非空值)。
418
+ 续写批次的大纲提示词据此强制延续笔调——跨批次不换文风。找不到返回 ""。"""
419
+ from . import store # 惰性导入:同 _prev_serial_story
420
+ anchor, cur, seen = "", task, set()
421
+ while len(seen) < 10:
422
+ cont = str((cur.get("serial") or {}).get("continues") or "")
423
+ prev = store.get_task(cont) if cont else None
424
+ if not prev or prev["id"] in seen:
425
+ break
426
+ seen.add(prev["id"])
427
+ for r in store.task_runs(prev["id"]):
428
+ o = r.get("outline")
429
+ if o and not o.get("degraded") and str(o.get("style_anchor") or "").strip():
430
+ anchor = str(o["style_anchor"]).strip()
431
+ break
432
+ cur = prev
433
+ return anchor
434
+
435
+
318
436
  def _chain_volume_names(task):
319
437
  """沿 serial.continues 链收集各批次大纲里的卷名/卷弧光(旧 → 新,后者覆盖)。
320
438
  续写批次合并成书时要给全书(含此前各批)的卷都写上标题,那些卷的命名记在
@@ -408,6 +526,9 @@ def make_serial_outline(task, author_agent=None, workdir=None, ev=None, log_path
408
526
  .replace("__N__", str(n)).replace("__W__", str(wpc))
409
527
  .replace("__VOLUMES_KEY__", vol_key)
410
528
  .replace("__VOLUMES__", vol_block)
529
+ .replace("__STYLE_ANCHOR__", prev_style_anchor(task)
530
+ or "(前文未记录文风锚,请依据前情大纲与最新一章结尾自行归纳,"
531
+ "归纳后全批保持一致)")
411
532
  .replace("__PREV_OUTLINE__", prev_lines or "(无大纲记录,请依据下方最新一章结尾与小说目标衔接)")
412
533
  .replace("__PREV_TAIL__", prev_tail or "(无)")
413
534
  .replace("__GOAL__", task["goal"])
@@ -486,12 +607,22 @@ def make_serial_outline(task, author_agent=None, workdir=None, ev=None, log_path
486
607
  # 8 章大纲 + 经验包注入是重生成任务,300s 实测不够(claude CLI 必超时);
487
608
  # 超时可经 env TUTTI_OUTLINE_TIMEOUT 或设置 orchestrator.outline_timeout_s 调整
488
609
  _append_log(log_path, "===== 作者 CLI(%s)=====" % author_agent.get("id", "?"))
489
- res = runner.run_agent(modelhub.bind_agent(author_agent), prompt,
490
- workdir=workdir or task.get("workdir"), readonly=True,
491
- timeout=_deadline_timeout(deadline, _outline_timeout()),
492
- deadline=deadline, cancel_event=ev,
493
- log_path=log_path)
494
- _log_usage("outline", "outline", task, res, agent=author_agent)
610
+ bound = modelhub.bind_agent(author_agent)
611
+ # 死链闸门(与 pipeline._run_step 同语义):配过链但解析为空(全死/评测
612
+ # 过期探针失败)时判失败不裸奔回 CLI 本机默认——2026-09-28 七天上限案:
613
+ # 裸调走残留本机配置撞同一个上游,死因被掩盖成「作者 CLI 失败」。
614
+ # 大纲已有编排者兜底,不搬 pipeline 的同池补位机制。
615
+ if (bound.get("binding_configured")
616
+ and not (bound.get("call_chain") or bound.get("env"))):
617
+ res = {"ok": False, "error": modelhub.binding_dead_msg(
618
+ author_agent.get("id", "")), "text": ""}
619
+ else:
620
+ res = runner.run_agent(bound, prompt,
621
+ workdir=workdir or task.get("workdir"), readonly=True,
622
+ timeout=_deadline_timeout(deadline, _outline_timeout()),
623
+ deadline=deadline, cancel_event=ev,
624
+ log_path=log_path)
625
+ _log_usage("outline", "outline", task, res, agent=author_agent)
495
626
  if not res["ok"]:
496
627
  _append_log(log_path, "作者 CLI 失败:%s" % (res.get("error") or "")[:300])
497
628
  reason_tail = (res.get("error") or reason_tail)[:200]
@@ -521,6 +652,103 @@ def make_serial_outline(task, author_agent=None, workdir=None, ev=None, log_path
521
652
  return _mark(out)
522
653
 
523
654
 
655
+ # 开篇重设计提示词(开篇闸门专用):第 1 章签约评估的开篇吸引力未达线时,
656
+ # 回大纲层重设计前三章节拍而不是只修稿——切入点不行是结构病,修文案救不了。
657
+ SERIAL_OPENING_REBUILD_PROMPT = """你是网文主编,熟悉签约平台(番茄/七猫/起点)的过稿标准。
658
+ 这本书的第 1 章在签约预评估中「开篇吸引力」未达线,评审意见如下:
659
+ __CRITIQUE__
660
+
661
+ 请重设计前 __COUNT__ 章的大纲节拍(只动这 __COUNT__ 章,不碰后续章节),让开篇符合黄金一章标准:
662
+ 第 1 章前 100 字出现主角、前 200 字出现异常或冲突、前 600 字进入具体困境、章末留可兑现的钩子;
663
+ 切入点必须是具体事件(退婚/陷害/背叛/生死危机等),禁止世界观铺垫开局。
664
+ 只输出一个 ```json 代码块,不要输出其他内容。JSON 结构:
665
+ {"chapters": [{"title": "章节标题", "beats": "本章剧情要点(50-120字:事件/冲突/推进)", "hook": "章末钩子(一句话)", "highlight": "本章爽点/情绪爆点(一句话)"}]}
666
+ 章序与原大纲一致(从第 1 章起),必须保持书名、主角与核心设定不变,只强化切入与钩子设计。
667
+
668
+ ## 全书目标
669
+ __GOAL__
670
+
671
+ ## 原大纲(前 __COUNT__ 章)
672
+ __OUTLINE__"""
673
+
674
+
675
+ def merge_opening(outline, data, count=3):
676
+ """把开篇重设计输出合并回大纲:只替换前 count 章的节拍字段,其余一律不动。
677
+ 没有可采纳的节拍设计返回 None(上层回退普通修订,不中断 run)。"""
678
+ try:
679
+ count = max(1, int(count))
680
+ except (TypeError, ValueError):
681
+ count = 3
682
+ if not isinstance(outline, dict) or not isinstance(data, dict):
683
+ return None
684
+ cur = outline.get("chapters") or []
685
+ chs = data.get("chapters")
686
+ if not isinstance(chs, list):
687
+ return None
688
+ merged = 0
689
+ for idx, ch in enumerate(chs[:count]):
690
+ if idx >= len(cur) or not isinstance(ch, dict):
691
+ continue
692
+ beats = str(ch.get("beats") or "").strip()
693
+ if not beats:
694
+ continue # 没有节拍设计的条目不采纳(避免把原节拍洗成空)
695
+ title = str(ch.get("title") or "").strip()
696
+ if title:
697
+ cur[idx]["title"] = title[:60]
698
+ cur[idx]["beats"] = beats[:500]
699
+ cur[idx]["hook"] = str(ch.get("hook") or "").strip()[:200]
700
+ cur[idx]["highlight"] = str(ch.get("highlight") or "").strip()[:200]
701
+ merged += 1
702
+ return outline if merged else None
703
+
704
+
705
+ def redesign_opening(task, outline=None, critique="", author_agent=None, workdir=None,
706
+ ev=None, log_path=None, deadline=None, run_id=None):
707
+ """开篇闸门:重设计前三章大纲节拍。编排者优先;编排者未配置或两次尝试均
708
+ 失败时返回 None——重设计是增量优化,失败不能比普通修稿更脆(上层回退)。"""
709
+ if not isinstance(outline, dict) or not (outline.get("chapters") or []):
710
+ return None
711
+ count = min(3, len(outline["chapters"]))
712
+ head_txt = "\n".join(
713
+ "第 %d 章《%s》:%s%s%s" % (idx + 1, c.get("title", ""), c.get("beats", ""),
714
+ ("(章末钩子:%s)" % c["hook"]) if c.get("hook") else "",
715
+ ("(爆点:%s)" % c["highlight"]) if c.get("highlight") else "")
716
+ for idx, c in enumerate(outline["chapters"][:count]))
717
+ prompt = (SERIAL_OPENING_REBUILD_PROMPT
718
+ .replace("__CRITIQUE__", (critique or "(无具体意见,开篇评分未达线)")[:600])
719
+ .replace("__COUNT__", str(count))
720
+ .replace("__GOAL__", task.get("goal", ""))
721
+ .replace("__OUTLINE__", head_txt))
722
+ orch = _orchestrator()
723
+ if not orch:
724
+ _append_log(log_path, "开篇重设计:编排者未配置,回退普通修订")
725
+ return None
726
+ prov, model = orch
727
+ label = "%s · %s" % (prov.get("name", prov["id"]), model)
728
+ _append_log(log_path, "===== 开篇重设计(%s)=====" % label)
729
+ for _attempt in (1, 2):
730
+ cb = _log_streamer(log_path)
731
+ res = modelhub.chat(prov["id"], model, prompt,
732
+ max_tokens=8000, timeout=_deadline_timeout(deadline, 180),
733
+ on_delta=cb, reasoning_effort=_reasoning_effort(task),
734
+ cache_ttl=3600 if _attempt == 1 else 0)
735
+ cb.flush()
736
+ _log_usage("outline", "opening-rebuild", task, res, model=model,
737
+ provider=prov.get("name", prov.get("id", "")),
738
+ provider_id=prov.get("id", ""))
739
+ if res["ok"]:
740
+ merged = merge_opening(outline, runner.extract_json(res.get("text") or ""),
741
+ count=count)
742
+ if merged:
743
+ _append_log(log_path, "开篇重设计成功:前 %d 章节拍已更新" % count)
744
+ return merged
745
+ _append_log(log_path, "尝试 %d:输出不可合并" % _attempt)
746
+ else:
747
+ _append_log(log_path, "尝试 %d 失败:%s"
748
+ % (_attempt, (res.get("error") or "未知错误")[:300]))
749
+ return None
750
+
751
+
524
752
  def _norm_subtasks(data):
525
753
  """规范化 LLM 计划输出;不合规返回 None。
526
754
 
@@ -620,10 +848,7 @@ def make_code_plan(task, planner_agent, workdir, ev=None, resume=None, log_path=
620
848
  return _fallback_code_plan(task, "(无可用智能体)")
621
849
  if planner_agent.get("mode") == "mock":
622
850
  return _fallback_code_plan(task, "mock 模式:单步模板")
623
- prompt = (CODE_PLAN_PROMPT.replace("__N__", str(MAX_SUBTASKS))
624
- .replace("__GOAL__", task["goal"])
625
- .replace("__CONTEXT__", task.get("context") or "(无)")
626
- .replace("__VERIFY__", task.get("verify_command") or "(未配置)"))
851
+ prompt = _code_plan_prompt(task)
627
852
  res = runner.run_agent(planner_agent, prompt, workdir=workdir, readonly=True,
628
853
  timeout=_deadline_timeout(deadline, 300), deadline=deadline,
629
854
  cancel_event=ev, resume=resume,
@@ -646,11 +871,7 @@ def _fallback_code_plan(task, note):
646
871
  def _orch_code_plan(task, prov, model, log_path=None, deadline=None):
647
872
  cb = _log_streamer(log_path)
648
873
  # §07 T2.2:代码计划同任务同 prompt(断点续跑/重试),TTL 1h 精确缓存
649
- res = modelhub.chat(prov["id"], model,
650
- (CODE_PLAN_PROMPT.replace("__N__", str(MAX_SUBTASKS))
651
- .replace("__GOAL__", task["goal"])
652
- .replace("__CONTEXT__", task.get("context") or "(无)")
653
- .replace("__VERIFY__", task.get("verify_command") or "(未配置)")),
874
+ res = modelhub.chat(prov["id"], model, _code_plan_prompt(task),
654
875
  max_tokens=8000, timeout=_deadline_timeout(deadline, 300), on_delta=cb,
655
876
  reasoning_effort=_reasoning_effort(task),
656
877
  cache_ttl=3600)