codebee 0.1.24 → 0.1.25

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,16 @@ README 元数据带回,供老版本在「发现新版本」时展示新版更
6
6
 
7
7
  ## 未发布
8
8
 
9
+ ## v0.1.25(2026-09-21)
10
+
11
+ ### ✨ 新功能
12
+
13
+ - 文档类任务更专业:工作汇报/商务邮件/技术方案/技术文档四类任务新增「分场合写作硬规则」——汇报先给结论不流水铺陈、邮件先答复再给背景且篇幅与轻重成正比、技术方案从问题开场并保留被否决的真实备选、文档标题写结果且验收标准可逐条勾选。起草与修订全链路生效。
14
+
15
+ ### 🛠 问题修复
16
+
17
+ - 任务创建的澄清问答与删除响应链路优化(并行改进,一并入库)。
18
+
9
19
  ## v0.1.24(2026-09-21)
10
20
 
11
21
  ### ✨ 新功能
package/README.md CHANGED
@@ -23,6 +23,9 @@ Kimi Code、MiMo Code、Grok Build、Pi、DeepSeek Harness……),提供
23
23
  <!-- relnotes:start -->
24
24
  ### ✨ 新功能 · New
25
25
 
26
+ - 文档类任务更专业:汇报先给结论、邮件先答复再铺陈、技术方案从问题开场并保留被否决的真实备选、文档验收标准可逐条勾选。
27
+ - Smarter document tasks: reports lead with conclusions, emails answer first, proposals open at the problem and keep a real rejected alternative, docs get testable acceptance criteria.
28
+
26
29
  - 升级完成后自动重启生效,不用再手动点「重启服务」;页面几秒后自动恢复。有任务在跑时不会自动重启,会提示稍后手动重启。
27
30
  - Updates finish on their own: after upgrading, the service restarts itself — no manual restart needed. If tasks are running, it waits and tells you instead.
28
31
 
@@ -324,7 +324,9 @@ def _decode_text(data):
324
324
  return ""
325
325
  if b"\x00" in data[:8192] and not data.startswith((b"\xff\xfe", b"\xfe\xff")):
326
326
  return ""
327
- for enc in ("utf-8-sig", "utf-16", "gb18030"):
327
+ encodings = (("utf-16",) if data.startswith((b"\xff\xfe", b"\xfe\xff"))
328
+ else ("utf-8-sig", "gb18030"))
329
+ for enc in encodings:
328
330
  try:
329
331
  return data.decode(enc)
330
332
  except (UnicodeDecodeError, LookupError):
@@ -350,7 +352,7 @@ def _item_preview(item, workdir, limit):
350
352
  if not _inside(workdir, path) or not path.is_file():
351
353
  return rel, "", "文件不存在或路径无效,必须明确告知用户"
352
354
  try:
353
- data = path.read_bytes()[:max(limit * 4, 4096)]
355
+ data = path.read_bytes()
354
356
  except OSError:
355
357
  return rel, "", "读取失败,必须明确告知用户"
356
358
  text = _decode_text(data).replace("\x00", "").strip()
@@ -425,13 +427,68 @@ def merge_context(context, items, workdir=None, max_chars=INLINE_TOTAL_CHARS):
425
427
  text = str(context or "")
426
428
  text = re.sub(r"\n?<!-- codebee-attachments:start -->[\s\S]*?"
427
429
  r"<!-- codebee-attachments:end -->", "", text).rstrip()
428
- old = text.find("\n## 附件材料")
430
+ legacy_header = "## 附件材料(位于工作目录 _attachments/,可直接读取)"
431
+ # 旧版 context 经过 strip 后可能从标题开头,没有前导换行。
432
+ old = text.find("\n" + legacy_header)
429
433
  if old >= 0:
434
+ old += 1
435
+ elif text.startswith(legacy_header):
436
+ old = 0
437
+ if old >= 0 and text.rstrip().endswith("绝不允许不读附件就凭空作答。"):
430
438
  text = text[:old].rstrip()
431
439
  block = context_block(items, workdir=workdir, max_chars=max_chars)
432
440
  return (text + block).strip() if block else text
433
441
 
434
442
 
443
+ def refresh_task(task, workdir=None):
444
+ """返回带最新附件预读上下文的任务副本,兼容升级前创建的历史任务。"""
445
+ if not task.get("attachments"):
446
+ return task
447
+ out = dict(task)
448
+ out["context"] = merge_context(task.get("context"), task["attachments"],
449
+ workdir=workdir or task.get("workdir"))
450
+ return out
451
+
452
+
453
+ def context_for_paths(paths_, workdir):
454
+ """运行中追加附件的预读块。"""
455
+ return context_block(items_from_paths(paths_, workdir), workdir=workdir)
456
+
457
+
458
+ def append_task_context(prompt, task, heading="原始背景与附件"):
459
+ """修复/修订轮重新携带任务上下文,避免换将或无会话时丢附件。"""
460
+ context = str(task.get("context") or "").strip()
461
+ if not context or context in prompt:
462
+ return prompt
463
+ return prompt + "\n\n## %s\n%s" % (heading, context)
464
+
465
+
466
+ def directive_lines(messages, workdir):
467
+ """运行中消息渲染为 prompt 行,并收集可传给视觉模型的图片绝对路径。"""
468
+ lines, images, paths_ = [], [], []
469
+ for msg in messages or []:
470
+ stamp, sender = msg.get("created_at") or "", msg.get("sender") or "用户"
471
+ text = (msg.get("text") or "").strip()
472
+ lines.append("- [%s %s] %s" % (stamp, sender, text) if text else
473
+ "- [%s %s](附件指令,见下方文件)" % (stamp, sender))
474
+ for raw in msg.get("attachments") or []:
475
+ rel = norm_rel(raw)
476
+ if not rel:
477
+ continue
478
+ paths_.append(rel)
479
+ mime = mimetypes.guess_type(rel)[0] or ""
480
+ path = Path(workdir) / rel
481
+ if mime.startswith("image/"):
482
+ if _inside(workdir, path) and path.is_file():
483
+ images.append(str(path))
484
+ lines.append(" · 图片附件:%s(请查看图片内容)" % rel)
485
+ else:
486
+ lines.append(" · 文件附件:%s(位于工作目录,可直接读取)" % rel)
487
+ if paths_:
488
+ lines.append(context_for_paths(paths_, workdir))
489
+ return lines, images
490
+
491
+
435
492
  def image_paths(task, workdir, limit=6):
436
493
  """任务图片附件的绝对路径(传给 codex --image)。缺失的跳过。"""
437
494
  out = []
@@ -19,7 +19,7 @@ import re
19
19
  import threading
20
20
  import time
21
21
 
22
- from . import aiflavor, catalog, dispatch_log, history, jobs, knowledge, manager, modelhub, mocks, paihang, planner, registry, router, runner, skills, store, task_compile, usage
22
+ from . import aiflavor, attachments, 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
@@ -53,25 +53,7 @@ def _inside(dirpath, target):
53
53
 
54
54
  def _task_images(task, workdir):
55
55
  """任务的图片附件绝对路径(仅 codex 原生 -i 用)。无附件/异常返回空列表。"""
56
- try:
57
- from . import attachments as att_mod
58
- return att_mod.image_paths(task, workdir)
59
- except Exception:
60
- return []
61
-
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
56
+ return attachments.image_paths(task, workdir)
75
57
 
76
58
 
77
59
  def _ms_name(raw):
@@ -266,34 +248,8 @@ def _drain_directives(run_id, workdir, role=None, step_n=None):
266
248
  if _is_review_role(role):
267
249
  lines.append("本步为评审步骤:请把上述用户意见作为评分依据之一,"
268
250
  "在相应维度的分数与 issues 中明确体现(引用用户原话)。")
269
- imgs = []
270
- attachment_paths = []
271
- for m in msgs:
272
- stamp = m.get("created_at") or ""
273
- sender = m.get("sender") or "用户"
274
- text = (m.get("text") or "").strip()
275
- lines.append("- [%s %s] %s" % (stamp, sender, text) if text
276
- else "- [%s %s](附件指令,见下方文件)" % (stamp, sender))
277
- for rel in (m.get("attachments") or []):
278
- rel = str(rel)
279
- attachment_paths.append(rel)
280
- low = rel.lower()
281
- if low.endswith((".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp")):
282
- ap = os.path.join(workdir or "", rel) if workdir else rel
283
- if workdir and os.path.isfile(ap):
284
- imgs.append(ap)
285
- lines.append(" · 图片附件:%s(请查看图片内容)" % rel)
286
- else:
287
- lines.append(" · 图片附件:%s" % rel)
288
- else:
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("附件预读失败:执行者必须逐个打开上述路径,无法读取时明确说明。")
251
+ msg_lines, imgs = attachments.directive_lines(msgs, workdir)
252
+ lines.extend(msg_lines)
297
253
  return "\n".join(lines), imgs
298
254
 
299
255
 
@@ -690,9 +646,6 @@ __GOAL__
690
646
  ## 未通过的原因
691
647
  __ISSUES__
692
648
 
693
- ## 原始背景与附件
694
- __CONTEXT__
695
-
696
649
  ## 要求
697
650
  - 只针对上述问题修复;不要无关重构。
698
651
  - 完成后用 2-3 句话说明改了什么。
@@ -718,9 +671,6 @@ __GOAL__
718
671
  ## 验收命令
719
672
  __VERIFY__
720
673
 
721
- ## 原始背景与附件要求
722
- __CONTEXT__
723
-
724
674
  ## 变更内容(git diff,若为空表示无法获取)
725
675
  __DIFF__
726
676
  """
@@ -779,8 +729,9 @@ def _run_review(run_id, task, workdir, reviewer, ev):
779
729
  prompt = (CODE_REVIEW_PROMPT
780
730
  .replace("__GOAL__", task["goal"])
781
731
  .replace("__VERIFY__", task.get("verify_command") or "(未配置)")
782
- .replace("__CONTEXT__", task.get("context") or "(无)")
783
732
  .replace("__DIFF__", diff or "(无法获取 git diff,请综合任务目标谨慎评审)"))
733
+ if task.get("context"):
734
+ prompt += "\n\n## 原始背景与附件要求\n" + task["context"]
784
735
  res = _run_step(run_id, "review", reviewer, prompt, workdir, readonly=True, ev=ev,
785
736
  images=_task_images(task, workdir))
786
737
  if reviewer.get("mode") == "mock":
@@ -1000,7 +951,7 @@ def _run_code(run, task, agents, ev, stats, mode):
1000
951
  # 架构事实,让规划器不再对代码库一无所知
1001
952
  _pm = _read_project_memory(workdir)
1002
953
  if _pm:
1003
- task = dict(task, context=((task.get("context") or "") + "\n\n" + _pm)[:8000])
954
+ task = dict(task, context=(task.get("context") or "") + "\n\n" + _pm)
1004
955
  plan_step, plan_log = store.add_step(run_id, "plan", impl["id"], impl.get("label"),
1005
956
  note=route.get("implementer", ""))
1006
957
  plan = planner.make_code_plan(_steered_task(run_id, task),
@@ -1159,8 +1110,8 @@ def _run_code(run, task, agents, ev, stats, mode):
1159
1110
  prompt = (CODE_FIX_PROMPT
1160
1111
  .replace("__GOAL__", task["goal"])
1161
1112
  .replace("__ISSUES__", issues_txt)
1162
- .replace("__CONTEXT__", task.get("context") or "(无)")
1163
1113
  .replace("__VERIFY_HINT__", _verify_hint(task)))
1114
+ prompt = attachments.append_task_context(prompt, task)
1164
1115
  res = _run_step(run_id, "fix-r%d" % round_no, modelhub.bind_agent(impl, difficulty),
1165
1116
  prompt, workdir, readonly=False, ev=ev,
1166
1117
  note="自动修复第 %d 轮" % round_no,
@@ -1471,8 +1422,6 @@ def _run_direct(run, task, agents, ev, stats, mode):
1471
1422
  # 扫榜选材(借鉴 oh-story 扫榜):抓七猫排行榜公开数据注入,
1472
1423
  # AI 做选题洞察;抓取失败回落普通直连提示词
1473
1424
  prompt = paihang.rank_scan_prompt(task.get("goal") or "") or ""
1474
- if prompt and task.get("context"):
1475
- prompt += "\n\n## 用户背景与附件\n" + task["context"]
1476
1425
  if not prompt:
1477
1426
  if bi is not None:
1478
1427
  prompt = (BUILTIN_DIRECT_PROMPT
@@ -1483,6 +1432,8 @@ def _run_direct(run, task, agents, ev, stats, mode):
1483
1432
  prompt = (DIRECT_PROMPT
1484
1433
  .replace("__GOAL__", task["goal"])
1485
1434
  .replace("__CONTEXT__", task.get("context") or "(无)"))
1435
+ if task.get("attachments") and "codebee-attachments:start" not in prompt:
1436
+ prompt += "\n\n## 用户背景与附件\n" + (task.get("context") or "")
1486
1437
  note = route.get("implementer", "")
1487
1438
  images = _task_images(task, workdir)
1488
1439
  else:
@@ -1598,6 +1549,9 @@ CONTENT_DELIVERY_CONTRACTS = {
1598
1549
  "doc": ("技术文档编辑", [
1599
1550
  "先明确读者、目的和前置条件,再按可执行步骤组织正文。",
1600
1551
  "命令、参数、示例与限制必须一致;无法确认的内容明确标注。",
1552
+ # sepia 分场合规则(工单/文档体裁):标题=结果、验收可测试、链接不重复
1553
+ "标题写结果或结论(「如何迁移 X」优于「X 说明」),正文链接原文不整段复述。",
1554
+ "涉及需求或变更时给出可测试的验收标准(能被逐条勾选判定通过/不通过)。",
1601
1555
  ]),
1602
1556
  "translation": ("专业译者与审校", [
1603
1557
  "忠实保留原文含义、语气、数字、专名、占位符、链接和 Markdown 结构,不增译或漏译。",
@@ -1614,14 +1568,24 @@ CONTENT_DELIVERY_CONTRACTS = {
1614
1568
  "weekly_report": ("业务汇报顾问", [
1615
1569
  "按成果与影响、关键数据、问题阻塞、下步行动(负责人/时间)组织内容。",
1616
1570
  "只使用用户提供或可核验的数据;缺失数字保留待补项,不虚构业绩。",
1571
+ # sepia 分场合规则(postmortem 体裁):先给结论;对机制严格不指名甩锅
1572
+ "第一段先给本期最重要的结论或结果,再展开支撑细节,不按时间流水铺陈。",
1573
+ "问题与阻塞直说机制原因,不带情绪也不指名甩锅;行动项必须落到负责人与时间。",
1617
1574
  ]),
1618
1575
  "email": ("商务沟通顾问", [
1619
1576
  "包含明确主题、称呼、来意、必要背景、请求/下一步和得体落款。",
1620
1577
  "语气匹配双方关系;日期、承诺、附件与联系人不得凭空补造。",
1578
+ # sepia 分场合规则(PR 回复体裁):先答再铺陈;篇幅与利害成正比
1579
+ "第一句/第一段先给结论或答复(对方要做什么、答应还是不答应),再给必要背景。",
1580
+ "请求具体到动作与截止时间;篇幅与事情轻重成正比,删掉礼节性空话与自我表扬。",
1621
1581
  ]),
1622
1582
  "tech_proposal": ("解决方案架构师", [
1623
1583
  "覆盖现状与目标、约束、候选方案对比、推荐架构、实施阶段、风险与回滚、验收指标。",
1624
1584
  "区分已知事实、假设和待验证项;成本收益给出计算口径而非虚构数字。",
1585
+ # sepia 分场合规则(技术文章体裁):从问题开场/真实死胡同/明确观点/带条件数字
1586
+ "从要解决的问题开场(不是从背景科普铺陈),让读者第一段就知道为什么非做不可。",
1587
+ "候选对比里至少保留一个真实分析过又被否决的方向,写清否决理由,不搞陪衬方案。",
1588
+ "必须有明确表态的推荐意见和取舍逻辑;关键数字一律带适用条件与计算口径。",
1625
1589
  ]),
1626
1590
  "resume": ("招聘与简历顾问", [
1627
1591
  "围绕目标岗位提炼真实经历,用行动、结果和技能关键词表达岗位匹配度。",
@@ -1663,9 +1627,6 @@ __GOAL__
1663
1627
  ## 评审汇总(各维度均分与主要问题)
1664
1628
  __CRITIQUE__
1665
1629
 
1666
- ## 原始背景与附件
1667
- __CONTEXT__
1668
-
1669
1630
  ## 要求
1670
1631
  - 针对性改进所有 major 问题;保持既定风格与设定。
1671
1632
  - 完成后用 3 句话说明本轮改了什么。"""
@@ -1806,8 +1767,6 @@ def _ensure_critique_placeholders(tpl):
1806
1767
  tpl += "\n\n## 待评审稿件\n---\n__MANUSCRIPT__\n---"
1807
1768
  if "__DIMKEYS__" not in tpl:
1808
1769
  tpl = ("请按维度打分(1-10 分)。\n\n" + tpl)
1809
- if "__CONTEXT__" not in tpl:
1810
- tpl += "\n\n## 原始任务背景与附件参考\n__CONTEXT__"
1811
1770
  return tpl
1812
1771
 
1813
1772
 
@@ -1844,9 +1803,6 @@ __GOAL__
1844
1803
  ## 本章评审意见
1845
1804
  __CRITIQUE__
1846
1805
 
1847
- ## 原始背景与附件
1848
- __CONTEXT__
1849
-
1850
1806
  ## 要求
1851
1807
  - 针对性解决所有 major 问题,保持与前后的剧情衔接;字数仍约 __WORDS__ 字。"""
1852
1808
 
@@ -1862,9 +1818,6 @@ SERIAL_GLOBAL_PROMPT = """你是网文主编(不要修改任何文件)。全
1862
1818
  ## 全书目标
1863
1819
  __GOAL__
1864
1820
 
1865
- ## 原始背景与附件
1866
- __CONTEXT__
1867
-
1868
1821
  ## 全文
1869
1822
  ---
1870
1823
  __MANUSCRIPT__
@@ -2035,6 +1988,8 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
2035
1988
  # 优先级最高的约束放最前面,写作者先读原则再读设定
2036
1989
  if _RUN_CONSTITUTION:
2037
1990
  bible = _RUN_CONSTITUTION + (bible or "")
1991
+ if task.get("context"):
1992
+ bible = task["context"] + ("\n\n" + bible if bible else "")
2038
1993
 
2039
1994
 
2040
1995
  def crit_prompt_for(text, note=""):
@@ -2052,9 +2007,8 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
2052
2007
  kb = knowledge.block_for(task)
2053
2008
  if kb:
2054
2009
  tpl = tpl.replace("## 待评审稿件", "%s\n\n## 待评审稿件" % kb, 1)
2055
- return (tpl.replace("__DIMKEYS__", dimkey)
2056
- .replace("__CONTEXT__", task.get("context") or "(无)")
2057
- .replace("__MANUSCRIPT__", text or "(稿件为空!)"))
2010
+ return tpl.replace("__DIMKEYS__", dimkey).replace(
2011
+ "__MANUSCRIPT__", text or "(稿件为空!)")
2058
2012
 
2059
2013
  # ---- 1) 大纲(断点续跑时直接继承上一遍,保证全书结构一致)
2060
2014
  inherit = run.get("inherit") or {}
@@ -2252,11 +2206,8 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
2252
2206
  else:
2253
2207
  # stable_order:同一任务 8 个章节的技能块必须字节级一致(§07 T1.2' 前缀缓存)
2254
2208
  sk_block, _ = skills.block_for(task, stable_order=True)
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
2209
+ if bible:
2210
+ sk_block = (sk_block + "\n\n" + bible) if sk_block else bible
2260
2211
  kb_block = knowledge.block_for(task)
2261
2212
  if kb_block:
2262
2213
  sk_block = (sk_block + "\n\n" + kb_block) if sk_block else kb_block
@@ -2577,8 +2528,8 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
2577
2528
  .replace("__I__", str(i)).replace("__FILE__", ch_file)
2578
2529
  .replace("__GOAL__", task["goal"])
2579
2530
  .replace("__CRITIQUE__", "\n".join(crit_lines))
2580
- .replace("__CONTEXT__", task.get("context") or "(无)")
2581
2531
  .replace("__WORDS__", str(wpc)))
2532
+ prompt = attachments.append_task_context(prompt, task)
2582
2533
  _run_step(run_id, "revise-c%d" % i, modelhub.bind_agent(impl, difficulty), prompt,
2583
2534
  step_wd, readonly=False, ev=ev, timeout=2400,
2584
2535
  resume=resume_ctx["session"] if resume_ctx else draft_sid)
@@ -2634,7 +2585,6 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
2634
2585
  res = _run_step(run_id, "global-critique", modelhub.bind_agent(agent, difficulty),
2635
2586
  (gtpl.replace("__DIMKEYS__", dimkey)
2636
2587
  .replace("__GOAL__", task["goal"])
2637
- .replace("__CONTEXT__", task.get("context") or "(无)")
2638
2588
  .replace("__MANUSCRIPT__", full_text[:60000])),
2639
2589
  workdir, readonly=True, ev=ev, timeout=2400)
2640
2590
  gj = _critique_json(res, dims)
@@ -2711,6 +2661,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
2711
2661
  .replace("__GOAL__", task["goal"])
2712
2662
  .replace("__CRITIQUE__", crit)
2713
2663
  .replace("__WORDS__", str(wpc)))
2664
+ prompt = attachments.append_task_context(prompt, task)
2714
2665
  res = _run_step(run_id, "polish-c%d" % i, modelhub.bind_agent(impl, difficulty),
2715
2666
  prompt, workdir, readonly=False, ev=ev, timeout=2400,
2716
2667
  resume=resume_ctx["session"] if resume_ctx else None)
@@ -3091,8 +3042,9 @@ def _run_content_review(run, task, agents, ev, stats, mode):
3091
3042
  crit_prompt = (_ensure_critique_placeholders(
3092
3043
  _tpl(task, "critique_prompt", NOVEL_CRITIQUE_PROMPT))
3093
3044
  .replace("__DIMKEYS__", dimkey)
3094
- .replace("__CONTEXT__", task.get("context") or "(无)")
3095
3045
  .replace("__MANUSCRIPT__", manuscript or "(稿件为空!)"))
3046
+ if task.get("context"):
3047
+ crit_prompt += "\n\n## 原始任务背景与附件参考\n" + task["context"]
3096
3048
  # AI 味确定性检测(借鉴 oh-story 去AI味):客观参考线随评审下发,
3097
3049
  # 命中才追加——评审官结合上下文判断是否真问题,脚本不直接扣分
3098
3050
  _aiflavor_line = aiflavor.report_line(manuscript)
@@ -3159,9 +3111,9 @@ def _run_content_review(run, task, agents, ev, stats, mode):
3159
3111
  prompt = (NOVEL_REVISE_PROMPT.replace("__FILE__", ms_name)
3160
3112
  .replace("__ROLE__", _content_role(task))
3161
3113
  .replace("__GOAL__", task["goal"])
3162
- .replace("__CONTEXT__", task.get("context") or "(无)")
3163
3114
  .replace("__CRITIQUE__", "\n".join(crit_lines)))
3164
3115
  prompt += _content_contract(task)
3116
+ prompt = attachments.append_task_context(prompt, task)
3165
3117
  _run_step(run_id, "revise-r%d" % r, modelhub.bind_agent(impl, difficulty), prompt,
3166
3118
  workdir, readonly=False, ev=ev,
3167
3119
  resume=resume_ctx["session"] if resume_ctx else None)
@@ -3489,7 +3441,7 @@ def execute_run(run_id):
3489
3441
  task_spec_summary=task_compile.summary(task_spec),
3490
3442
  difficulty=task_spec["difficulty"])
3491
3443
  task = dict(task)
3492
- task = _refresh_attachment_context(task, task.get("workdir") or "")
3444
+ task = attachments.refresh_task(task, task.get("workdir") or "")
3493
3445
  task["_compiled_spec"] = task_spec
3494
3446
  # 运行内统一使用编译后的难度;store 中历史任务常带 difficulty=auto,
3495
3447
  # 不能让这个兼容值覆盖 easy/default/hard 的模型调度决策。
package/app/core/store.py CHANGED
@@ -682,11 +682,33 @@ def update_run(run_id, expected_status=None, **fields):
682
682
  return run
683
683
 
684
684
 
685
- def run_dir(run_id):
686
- return paths.RUNS_DIR / run_id
687
-
688
-
689
- def delete_run(run_id):
685
+ def run_dir(run_id):
686
+ return paths.RUNS_DIR / run_id
687
+
688
+
689
+ def _schedule_run_dir_cleanup(run_id):
690
+ """把运行目录快速移出可见路径,再后台删除大日志目录。
691
+
692
+ 删除任务/运行记录不能让 HTTP 请求同步递归扫描数千个日志文件。
693
+ 同盘 rename 是 O(1),原路径立即消失;后台失败也不会影响内存状态。
694
+ """
695
+ source = paths.RUNS_DIR / str(run_id)
696
+ if not source.exists():
697
+ return
698
+ target = paths.RUNS_DIR / (".deleting-%s-%s" % (run_id, secrets.token_hex(4)))
699
+ try:
700
+ source.rename(target)
701
+ except OSError:
702
+ target = source
703
+
704
+ def _remove():
705
+ shutil.rmtree(target, ignore_errors=True)
706
+
707
+ threading.Thread(target=_remove, name="codebee-delete-%s" % run_id[:12],
708
+ daemon=True).start()
709
+
710
+
711
+ def delete_run(run_id):
690
712
  """删除一条运行记录(内存 + 磁盘目录)。返回 (ok, 错误信息)。"""
691
713
  if not _valid_id(run_id):
692
714
  return False, "非法的记录 ID"
@@ -697,7 +719,7 @@ def delete_run(run_id):
697
719
  if run.get("status") in ("queued", "running"):
698
720
  return False, "运行中的记录不能删除,请先取消"
699
721
  del _RUNS[run_id]
700
- shutil.rmtree(paths.RUNS_DIR / run_id, ignore_errors=True)
722
+ _schedule_run_dir_cleanup(run_id)
701
723
  bump_state()
702
724
  return True, ""
703
725
 
@@ -1030,7 +1052,7 @@ def delete_runs(run_ids):
1030
1052
  skipped += 1
1031
1053
  continue
1032
1054
  del _RUNS[rid]
1033
- shutil.rmtree(paths.RUNS_DIR / rid, ignore_errors=True)
1055
+ _schedule_run_dir_cleanup(rid)
1034
1056
  deleted += 1
1035
1057
  if deleted:
1036
1058
  bump_state()
@@ -1049,7 +1071,7 @@ def clear_runs():
1049
1071
  for rid in targets:
1050
1072
  del _RUNS[rid]
1051
1073
  for rid in targets:
1052
- shutil.rmtree(paths.RUNS_DIR / rid, ignore_errors=True)
1074
+ _schedule_run_dir_cleanup(rid)
1053
1075
  bump_state()
1054
1076
  return len(targets), skipped
1055
1077
 
@@ -1088,9 +1110,9 @@ def delete_task(task_id):
1088
1110
  del _TASKS[task_id]
1089
1111
  for rid in run_ids:
1090
1112
  _RUNS.pop(rid, None)
1091
- for rid in run_ids:
1092
- shutil.rmtree(paths.RUNS_DIR / rid, ignore_errors=True)
1093
- (paths.TASKS_DIR / (task_id + ".json")).unlink(missing_ok=True)
1113
+ for rid in run_ids:
1114
+ _schedule_run_dir_cleanup(rid)
1115
+ (paths.TASKS_DIR / (task_id + ".json")).unlink(missing_ok=True)
1094
1116
  bump_state()
1095
1117
  return True, ""
1096
1118
 
package/app/main.py CHANGED
@@ -2077,8 +2077,9 @@ class Handler(BaseHTTPRequestHandler):
2077
2077
  "每个问题给出 2-4 个最常见的选项。只输出 JSON 数组,不要输出其他内容:\n"
2078
2078
  '[{"q": "问题", "options": ["选项1", "选项2"]}]' % (ttype, goal[:200]))
2079
2079
  try:
2080
+ # 澄清是可选增强,不能占住创建请求;主流程有自己的执行超时。
2080
2081
  res = builtin_agent.run(bi, prompt, os.getcwd() if hasattr(os, "getcwd") else ".",
2081
- timeout=60)
2082
+ timeout=8)
2082
2083
  import json as _json
2083
2084
  arr = None
2084
2085
  text = (res.get("text") or "").strip()
package/app/ui/app.js CHANGED
@@ -2026,7 +2026,8 @@ async function createTask() {
2026
2026
  msg.textContent = t("目标有点简短,先问几个问题…");
2027
2027
  try {
2028
2028
  const cq = await api("/api/tasks/clarify", {
2029
- method: "POST", timeout: 75000,
2029
+ // 澄清只是增强,不能让建任务被模型调用拖住;超时后直接按原目标开跑。
2030
+ method: "POST", timeout: 8000,
2030
2031
  body: JSON.stringify({ goal: payload.goal, type: payload.type }) });
2031
2032
  const qs = (cq && cq.questions) || [];
2032
2033
  if (qs.length) {
@@ -2034,7 +2035,10 @@ async function createTask() {
2034
2035
  renderClarify(qs, payload.goal, resetSubmit);
2035
2036
  return;
2036
2037
  }
2037
- } catch (e) { /* 澄清失败照常创建 */ }
2038
+ } catch (e) {
2039
+ // 澄清服务不可用或超时不阻塞主流程,明确告诉用户已经自动继续。
2040
+ msg.textContent = t("澄清响应较慢,已直接创建任务…");
2041
+ }
2038
2042
  }
2039
2043
  S.clarifyDone = false;
2040
2044
  if (!payload.workdir) delete payload.workdir; // 留空 → 服务端用「默认保存路径」(编排设置可改)
@@ -2378,6 +2382,7 @@ async function deleteTask(id) {
2378
2382
  if (!await uiConfirm(t("删除该任务及其全部运行记录(含日志与报告)?不可恢复。"), { ok: t("删除"), danger: true })) return;
2379
2383
  let gone = false;
2380
2384
  try {
2385
+ toast(t("正在删除任务…"));
2381
2386
  await api("/api/tasks/" + encodeURIComponent(id) + "/delete", { method: "POST" });
2382
2387
  } catch (e) {
2383
2388
  // 任务已不在(他端删过 / 重复点):不报错晾着,照常把视图清掉
@@ -3796,11 +3801,11 @@ async function renderRunDetail() {
3796
3801
  S.cancelTargetRunId = active ? run.id : null;
3797
3802
  $("btn-delete").classList.toggle("hidden", active);
3798
3803
  $("btn-share").classList.toggle("hidden", active); // 分享页:结束后可生成自包含 HTML
3804
+ $("btn-talk").classList.toggle("hidden", !(run.task_id && !chatEngineIsDirect(run)));
3805
+ const rcTask = ((S.state || {}).tasks || []).find((x) => x.id === run.task_id);
3799
3806
  // 归档按钮:非运行中任务可归档/取消归档(此前只有侧栏右键菜单入口,
3800
3807
  // 用户反馈「归档按钮不见了」——补显式入口,文案随状态切换)
3801
3808
  syncArchBtn(rcTask, active);
3802
- $("btn-talk").classList.toggle("hidden", !(run.task_id && !chatEngineIsDirect(run)));
3803
- const rcTask = ((S.state || {}).tasks || []).find((x) => x.id === run.task_id);
3804
3809
  setRetryBtn(run, rcTask);
3805
3810
  $("btn-continue").classList.toggle("hidden",
3806
3811
  !(rcTask && rcTask.serial && run.status !== "running" && run.status !== "queued"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codebee",
3
- "version": "0.1.24",
3
+ "version": "0.1.25",
4
4
  "description": "CodeBee · 多智能体编排台 — 统一编排本机 AI 编码 CLI(Codex / Claude Code / QwenCode / OpenCode / Aider…):目标→拆解→路由→执行→验证→跨厂商评审→修复→报告。纯 Python 标准库实现,本地 Web UI。",
5
5
  "license": "MIT",
6
6
  "bin": {