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 +10 -0
- package/README.md +3 -0
- package/app/core/attachments.py +60 -3
- package/app/core/pipeline.py +35 -83
- package/app/core/store.py +33 -11
- package/app/main.py +2 -1
- package/app/ui/app.js +9 -4
- package/package.json +1 -1
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
|
|
package/app/core/attachments.py
CHANGED
|
@@ -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
|
-
|
|
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()
|
|
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
|
-
|
|
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 = []
|
package/app/core/pipeline.py
CHANGED
|
@@ -19,7 +19,7 @@ import re
|
|
|
19
19
|
import threading
|
|
20
20
|
import time
|
|
21
21
|
|
|
22
|
-
from . import aiflavor, catalog, 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
|
-
|
|
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
|
-
|
|
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=(
|
|
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
|
|
2056
|
-
|
|
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
|
-
|
|
2256
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
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
|
-
|
|
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