codebee 0.1.24 → 0.1.26
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 +21 -1
- package/README.md +13 -0
- package/app/core/attachments.py +60 -3
- package/app/core/pipeline.py +147 -93
- package/app/core/prefs.py +70 -0
- package/app/core/store.py +33 -11
- package/app/main.py +15 -2
- package/app/pet.py +85 -1
- package/app/ui/app.js +147 -14
- package/app/ui/i18n.js +23 -0
- package/app/ui/index.html +9 -0
- package/app/ui/style.css +52 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,27 @@ CodeBee 的用户可感知变更记录。发布新版时:最上面加一节,
|
|
|
4
4
|
`<!-- relnotes:start -->…<!-- relnotes:end -->` 段(那段会被 `npm view` 的
|
|
5
5
|
README 元数据带回,供老版本在「发现新版本」时展示新版更新内容)。
|
|
6
6
|
|
|
7
|
-
## 未发布
|
|
7
|
+
## 未发布
|
|
8
|
+
|
|
9
|
+
## v0.1.26(2026-09-21)
|
|
10
|
+
|
|
11
|
+
### ✨ 新功能
|
|
12
|
+
|
|
13
|
+
- 顶栏新增统一操作状态中心:创建、删除、绑定、插件等用户写操作会显示进行中、完成或失败状态,并保留时间与错误原因,慢请求不再像页面卡死。
|
|
14
|
+
|
|
15
|
+
### 🛠 问题修复
|
|
16
|
+
|
|
17
|
+
- 后台维护写请求、任务澄清探测和轮询请求默认静默,避免把后台动作误显示成用户操作;移动端状态弹层自适应屏幕并支持减少动画。
|
|
18
|
+
|
|
19
|
+
## v0.1.25(2026-09-21)
|
|
20
|
+
|
|
21
|
+
### ✨ 新功能
|
|
22
|
+
|
|
23
|
+
- 文档类任务更专业:工作汇报/商务邮件/技术方案/技术文档四类任务新增「分场合写作硬规则」——汇报先给结论不流水铺陈、邮件先答复再给背景且篇幅与轻重成正比、技术方案从问题开场并保留被否决的真实备选、文档标题写结果且验收标准可逐条勾选。起草与修订全链路生效。
|
|
24
|
+
|
|
25
|
+
### 🛠 问题修复
|
|
26
|
+
|
|
27
|
+
- 任务创建的澄清问答与删除响应链路优化(并行改进,一并入库)。
|
|
8
28
|
|
|
9
29
|
## v0.1.24(2026-09-21)
|
|
10
30
|
|
package/README.md
CHANGED
|
@@ -23,6 +23,19 @@ Kimi Code、MiMo Code、Grok Build、Pi、DeepSeek Harness……),提供
|
|
|
23
23
|
<!-- relnotes:start -->
|
|
24
24
|
### ✨ 新功能 · New
|
|
25
25
|
|
|
26
|
+
- 顶栏新增统一操作状态中心:创建、删除、绑定、插件等写操作会显示进行中、完成或失败状态,并保留时间与错误原因。
|
|
27
|
+
- A unified operation center now shows create, delete, binding, and plugin actions as running, completed, or failed, with timestamps and error details.
|
|
28
|
+
|
|
29
|
+
### 🛠 问题修复 · Fixes
|
|
30
|
+
|
|
31
|
+
- 后台维护、澄清探测和轮询请求默认静默,移动端状态弹层自适应屏幕并支持减少动画。
|
|
32
|
+
- Background maintenance, clarification probes, and polling stay quiet by default; the mobile operation panel adapts to the viewport and respects reduced-motion settings.
|
|
33
|
+
|
|
34
|
+
### 上一版亮点 · Previous release
|
|
35
|
+
|
|
36
|
+
- 文档类任务更专业:汇报先给结论、邮件先答复再铺陈、技术方案从问题开场并保留被否决的真实备选、文档验收标准可逐条勾选。
|
|
37
|
+
- 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.
|
|
38
|
+
|
|
26
39
|
- 升级完成后自动重启生效,不用再手动点「重启服务」;页面几秒后自动恢复。有任务在跑时不会自动重启,会提示稍后手动重启。
|
|
27
40
|
- 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
41
|
|
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),
|
|
@@ -1140,10 +1091,25 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
1140
1091
|
|
|
1141
1092
|
def review_and_score():
|
|
1142
1093
|
verify_pass, verify_ran = _run_verify(run_id, task, workdir, ev)
|
|
1094
|
+
# gate 验证(借鉴 pi-subagents 的 gate:"npm test"):verify 失败时先跳过
|
|
1095
|
+
# 模型评审直接进修复轮——评审此时只能复述「验证没过」,白烧一次调用。
|
|
1096
|
+
# 连续失败 >=2 轮后恢复评审参与诊断(模型能看出编译错误之外的病灶)。
|
|
1097
|
+
if not verify_pass and verify_ran:
|
|
1098
|
+
verify_fail_streak[0] += 1
|
|
1099
|
+
if verify_fail_streak[0] < 2:
|
|
1100
|
+
return {"pass": False, "scores": {},
|
|
1101
|
+
"issues": [{"severity": "high", "title": "验证命令未通过",
|
|
1102
|
+
"detail": "验证命令 `%s` 未通过:先修复使验证转绿,无需等评审。"
|
|
1103
|
+
% task.get("verify_command")}],
|
|
1104
|
+
"summary": "gate:验证未通过,跳过模型评审直接修复"}, verify_pass, verify_ran
|
|
1105
|
+
else:
|
|
1106
|
+
verify_fail_streak[0] = 0
|
|
1143
1107
|
# 先让确定性验证落盘,再执行模型评审;终态判断会同时使用两份证据。
|
|
1144
1108
|
review_json = _run_review(run_id, task, workdir, modelhub.bind_agent(reviewer, difficulty), ev)
|
|
1145
1109
|
return review_json, verify_pass, verify_ran
|
|
1146
1110
|
|
|
1111
|
+
verify_fail_streak = [0] # gate:verify 连败计数(>=2 轮恢复评审参与诊断)
|
|
1112
|
+
|
|
1147
1113
|
attempt_note = route.get("implementer", "") if mode == "auto" else ""
|
|
1148
1114
|
round_no = 0
|
|
1149
1115
|
review_json, verify_pass, verify_ran = None, True, False
|
|
@@ -1159,8 +1125,8 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
1159
1125
|
prompt = (CODE_FIX_PROMPT
|
|
1160
1126
|
.replace("__GOAL__", task["goal"])
|
|
1161
1127
|
.replace("__ISSUES__", issues_txt)
|
|
1162
|
-
.replace("__CONTEXT__", task.get("context") or "(无)")
|
|
1163
1128
|
.replace("__VERIFY_HINT__", _verify_hint(task)))
|
|
1129
|
+
prompt = attachments.append_task_context(prompt, task)
|
|
1164
1130
|
res = _run_step(run_id, "fix-r%d" % round_no, modelhub.bind_agent(impl, difficulty),
|
|
1165
1131
|
prompt, workdir, readonly=False, ev=ev,
|
|
1166
1132
|
note="自动修复第 %d 轮" % round_no,
|
|
@@ -1471,8 +1437,6 @@ def _run_direct(run, task, agents, ev, stats, mode):
|
|
|
1471
1437
|
# 扫榜选材(借鉴 oh-story 扫榜):抓七猫排行榜公开数据注入,
|
|
1472
1438
|
# AI 做选题洞察;抓取失败回落普通直连提示词
|
|
1473
1439
|
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
1440
|
if not prompt:
|
|
1477
1441
|
if bi is not None:
|
|
1478
1442
|
prompt = (BUILTIN_DIRECT_PROMPT
|
|
@@ -1483,6 +1447,8 @@ def _run_direct(run, task, agents, ev, stats, mode):
|
|
|
1483
1447
|
prompt = (DIRECT_PROMPT
|
|
1484
1448
|
.replace("__GOAL__", task["goal"])
|
|
1485
1449
|
.replace("__CONTEXT__", task.get("context") or "(无)"))
|
|
1450
|
+
if task.get("attachments") and "codebee-attachments:start" not in prompt:
|
|
1451
|
+
prompt += "\n\n## 用户背景与附件\n" + (task.get("context") or "")
|
|
1486
1452
|
note = route.get("implementer", "")
|
|
1487
1453
|
images = _task_images(task, workdir)
|
|
1488
1454
|
else:
|
|
@@ -1598,6 +1564,9 @@ CONTENT_DELIVERY_CONTRACTS = {
|
|
|
1598
1564
|
"doc": ("技术文档编辑", [
|
|
1599
1565
|
"先明确读者、目的和前置条件,再按可执行步骤组织正文。",
|
|
1600
1566
|
"命令、参数、示例与限制必须一致;无法确认的内容明确标注。",
|
|
1567
|
+
# sepia 分场合规则(工单/文档体裁):标题=结果、验收可测试、链接不重复
|
|
1568
|
+
"标题写结果或结论(「如何迁移 X」优于「X 说明」),正文链接原文不整段复述。",
|
|
1569
|
+
"涉及需求或变更时给出可测试的验收标准(能被逐条勾选判定通过/不通过)。",
|
|
1601
1570
|
]),
|
|
1602
1571
|
"translation": ("专业译者与审校", [
|
|
1603
1572
|
"忠实保留原文含义、语气、数字、专名、占位符、链接和 Markdown 结构,不增译或漏译。",
|
|
@@ -1614,14 +1583,24 @@ CONTENT_DELIVERY_CONTRACTS = {
|
|
|
1614
1583
|
"weekly_report": ("业务汇报顾问", [
|
|
1615
1584
|
"按成果与影响、关键数据、问题阻塞、下步行动(负责人/时间)组织内容。",
|
|
1616
1585
|
"只使用用户提供或可核验的数据;缺失数字保留待补项,不虚构业绩。",
|
|
1586
|
+
# sepia 分场合规则(postmortem 体裁):先给结论;对机制严格不指名甩锅
|
|
1587
|
+
"第一段先给本期最重要的结论或结果,再展开支撑细节,不按时间流水铺陈。",
|
|
1588
|
+
"问题与阻塞直说机制原因,不带情绪也不指名甩锅;行动项必须落到负责人与时间。",
|
|
1617
1589
|
]),
|
|
1618
1590
|
"email": ("商务沟通顾问", [
|
|
1619
1591
|
"包含明确主题、称呼、来意、必要背景、请求/下一步和得体落款。",
|
|
1620
1592
|
"语气匹配双方关系;日期、承诺、附件与联系人不得凭空补造。",
|
|
1593
|
+
# sepia 分场合规则(PR 回复体裁):先答再铺陈;篇幅与利害成正比
|
|
1594
|
+
"第一句/第一段先给结论或答复(对方要做什么、答应还是不答应),再给必要背景。",
|
|
1595
|
+
"请求具体到动作与截止时间;篇幅与事情轻重成正比,删掉礼节性空话与自我表扬。",
|
|
1621
1596
|
]),
|
|
1622
1597
|
"tech_proposal": ("解决方案架构师", [
|
|
1623
1598
|
"覆盖现状与目标、约束、候选方案对比、推荐架构、实施阶段、风险与回滚、验收指标。",
|
|
1624
1599
|
"区分已知事实、假设和待验证项;成本收益给出计算口径而非虚构数字。",
|
|
1600
|
+
# sepia 分场合规则(技术文章体裁):从问题开场/真实死胡同/明确观点/带条件数字
|
|
1601
|
+
"从要解决的问题开场(不是从背景科普铺陈),让读者第一段就知道为什么非做不可。",
|
|
1602
|
+
"候选对比里至少保留一个真实分析过又被否决的方向,写清否决理由,不搞陪衬方案。",
|
|
1603
|
+
"必须有明确表态的推荐意见和取舍逻辑;关键数字一律带适用条件与计算口径。",
|
|
1625
1604
|
]),
|
|
1626
1605
|
"resume": ("招聘与简历顾问", [
|
|
1627
1606
|
"围绕目标岗位提炼真实经历,用行动、结果和技能关键词表达岗位匹配度。",
|
|
@@ -1663,9 +1642,6 @@ __GOAL__
|
|
|
1663
1642
|
## 评审汇总(各维度均分与主要问题)
|
|
1664
1643
|
__CRITIQUE__
|
|
1665
1644
|
|
|
1666
|
-
## 原始背景与附件
|
|
1667
|
-
__CONTEXT__
|
|
1668
|
-
|
|
1669
1645
|
## 要求
|
|
1670
1646
|
- 针对性改进所有 major 问题;保持既定风格与设定。
|
|
1671
1647
|
- 完成后用 3 句话说明本轮改了什么。"""
|
|
@@ -1746,6 +1722,43 @@ def _plot_modules(workdir):
|
|
|
1746
1722
|
"鼓励化用,不要照抄原句)\n\n" + txt)
|
|
1747
1723
|
|
|
1748
1724
|
|
|
1725
|
+
LEDGER_FILE = os.path.join(".codebee", "resource-ledger.md")
|
|
1726
|
+
_LEDGER_MAX_CHARS = 6000 # 账本注入上限:太老的状态让评审官收敛 recent 优先
|
|
1727
|
+
|
|
1728
|
+
|
|
1729
|
+
def _parse_tagged_lines(text, tag):
|
|
1730
|
+
"""从模型回复提取 <tag>...</tag> 块的非空行列表(缺失/空块返回 [])。"""
|
|
1731
|
+
m = re.search(r"<%s>([\s\S]*?)</%s>" % (tag, tag), text or "")
|
|
1732
|
+
if not m:
|
|
1733
|
+
return []
|
|
1734
|
+
return [ln.strip(" -*") for ln in m.group(1).splitlines() if ln.strip(" -*")]
|
|
1735
|
+
|
|
1736
|
+
|
|
1737
|
+
def _append_ledger(workdir, chapter, lines):
|
|
1738
|
+
"""资源账本追加:本章评审提炼的道具/伤情/承诺/伏笔增量,供下章起草注入。"""
|
|
1739
|
+
p = os.path.abspath(os.path.join(str(workdir or ""), LEDGER_FILE))
|
|
1740
|
+
if not _inside(workdir, p):
|
|
1741
|
+
raise ValueError("ledger 路径越界")
|
|
1742
|
+
os.makedirs(os.path.dirname(p), exist_ok=True)
|
|
1743
|
+
with open(p, "a", encoding="utf-8") as f:
|
|
1744
|
+
if f.tell() == 0:
|
|
1745
|
+
f.write("# 资源账本(每章评审自动追加:道具/伤情/承诺/伏笔的现状,"
|
|
1746
|
+
"写新章前必须核对,防止跨章穿帮)\n\n")
|
|
1747
|
+
f.write("## 第 %d 章\n%s\n" % (chapter, "\n".join("- " + x for x in lines)))
|
|
1748
|
+
|
|
1749
|
+
|
|
1750
|
+
def _read_ledger(workdir):
|
|
1751
|
+
"""读取资源账本注入文本(截至上一章)。不存在返回「(暂无记录)」。"""
|
|
1752
|
+
p = os.path.abspath(os.path.join(str(workdir or ""), LEDGER_FILE))
|
|
1753
|
+
if not _inside(workdir, p) or not os.path.isfile(p):
|
|
1754
|
+
return "(暂无记录,本章建立的新道具/伤情/承诺/伏笔会被账本自动登记)"
|
|
1755
|
+
try:
|
|
1756
|
+
txt = _read_text_any_enc(p)[-_LEDGER_MAX_CHARS:].strip()
|
|
1757
|
+
except OSError:
|
|
1758
|
+
return "(暂无记录)"
|
|
1759
|
+
return txt or "(暂无记录)"
|
|
1760
|
+
|
|
1761
|
+
|
|
1749
1762
|
def _shrink_context_block(sk_block, bible, budget=12000):
|
|
1750
1763
|
"""分层上下文降级(长提示词在容量受限通道上会挂起/秒拒,2026-09-17 讯飞实测)。
|
|
1751
1764
|
|
|
@@ -1806,8 +1819,6 @@ def _ensure_critique_placeholders(tpl):
|
|
|
1806
1819
|
tpl += "\n\n## 待评审稿件\n---\n__MANUSCRIPT__\n---"
|
|
1807
1820
|
if "__DIMKEYS__" not in tpl:
|
|
1808
1821
|
tpl = ("请按维度打分(1-10 分)。\n\n" + tpl)
|
|
1809
|
-
if "__CONTEXT__" not in tpl:
|
|
1810
|
-
tpl += "\n\n## 原始任务背景与附件参考\n__CONTEXT__"
|
|
1811
1822
|
return tpl
|
|
1812
1823
|
|
|
1813
1824
|
|
|
@@ -1834,6 +1845,9 @@ __OUTLINE__
|
|
|
1834
1845
|
## 前情提要(此前各章结尾摘录,衔接用)
|
|
1835
1846
|
__PREV__
|
|
1836
1847
|
|
|
1848
|
+
## 资源账本(道具/伤情/承诺/伏笔的现状登记,写本章前必须核对)
|
|
1849
|
+
__LEDGER__
|
|
1850
|
+
|
|
1837
1851
|
- 写完文件后,最终回复只输出一行:`第 __I__ 章完成(约 __WORDS__ 字)`——不要在回复里复述或解释正文。"""
|
|
1838
1852
|
|
|
1839
1853
|
SERIAL_REVISE_PROMPT = """你是一名网文作者。第 __I__ 章没有通过评审,请修订文件 `__FILE__`(直接改写该文件)。文件必须以 UTF-8 编码保存(PowerShell 显式加 -Encoding UTF8,禁止依赖默认编码)。
|
|
@@ -1844,9 +1858,6 @@ __GOAL__
|
|
|
1844
1858
|
## 本章评审意见
|
|
1845
1859
|
__CRITIQUE__
|
|
1846
1860
|
|
|
1847
|
-
## 原始背景与附件
|
|
1848
|
-
__CONTEXT__
|
|
1849
|
-
|
|
1850
1861
|
## 要求
|
|
1851
1862
|
- 针对性解决所有 major 问题,保持与前后的剧情衔接;字数仍约 __WORDS__ 字。"""
|
|
1852
1863
|
|
|
@@ -1862,9 +1873,6 @@ SERIAL_GLOBAL_PROMPT = """你是网文主编(不要修改任何文件)。全
|
|
|
1862
1873
|
## 全书目标
|
|
1863
1874
|
__GOAL__
|
|
1864
1875
|
|
|
1865
|
-
## 原始背景与附件
|
|
1866
|
-
__CONTEXT__
|
|
1867
|
-
|
|
1868
1876
|
## 全文
|
|
1869
1877
|
---
|
|
1870
1878
|
__MANUSCRIPT__
|
|
@@ -2035,14 +2043,19 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2035
2043
|
# 优先级最高的约束放最前面,写作者先读原则再读设定
|
|
2036
2044
|
if _RUN_CONSTITUTION:
|
|
2037
2045
|
bible = _RUN_CONSTITUTION + (bible or "")
|
|
2046
|
+
if task.get("context"):
|
|
2047
|
+
bible = task["context"] + ("\n\n" + bible if bible else "")
|
|
2038
2048
|
|
|
2039
2049
|
|
|
2040
|
-
def crit_prompt_for(text, note=""):
|
|
2050
|
+
def crit_prompt_for(text, note="", event_check=""):
|
|
2041
2051
|
tpl = _ensure_critique_placeholders(
|
|
2042
2052
|
_tpl(task, "critique_prompt", NOVEL_CRITIQUE_PROMPT))
|
|
2043
2053
|
if note:
|
|
2044
2054
|
tpl = tpl.replace("你是严格的评审",
|
|
2045
2055
|
"你是严格的评审(背景:%s,请结合全书目标评审本章节)" % note, 1)
|
|
2056
|
+
if event_check:
|
|
2057
|
+
# 逐项目标审稿(借鉴 AI-Novel-Writer v1.1):本章大纲要点逐项核对
|
|
2058
|
+
tpl = tpl.replace("## 待评审稿件", "%s\n\n## 待评审稿件" % event_check, 1)
|
|
2046
2059
|
# stable_order:评审分轮次调用,hits 中途变化会打碎前缀缓存(§07 T1.2')
|
|
2047
2060
|
sk, _ = skills.block_for(task, stable_order=True)
|
|
2048
2061
|
if sk:
|
|
@@ -2052,9 +2065,8 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2052
2065
|
kb = knowledge.block_for(task)
|
|
2053
2066
|
if kb:
|
|
2054
2067
|
tpl = tpl.replace("## 待评审稿件", "%s\n\n## 待评审稿件" % kb, 1)
|
|
2055
|
-
return
|
|
2056
|
-
|
|
2057
|
-
.replace("__MANUSCRIPT__", text or "(稿件为空!)"))
|
|
2068
|
+
return tpl.replace("__DIMKEYS__", dimkey).replace(
|
|
2069
|
+
"__MANUSCRIPT__", text or "(稿件为空!)")
|
|
2058
2070
|
|
|
2059
2071
|
# ---- 1) 大纲(断点续跑时直接继承上一遍,保证全书结构一致)
|
|
2060
2072
|
inherit = run.get("inherit") or {}
|
|
@@ -2120,6 +2132,22 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2120
2132
|
i = start + k - 1 # 全书章号:文件名/步骤角色/评分记录都按全书编号
|
|
2121
2133
|
ch = outline["chapters"][k - 1]
|
|
2122
2134
|
ch_file = "chapter-%02d.md" % i
|
|
2135
|
+
# 逐项目标审稿(借鉴 AI-Novel-Writer):本章大纲要点随评审下发,评审
|
|
2136
|
+
# 以 <event_check> 回逐项判定;资源账本(借鉴角色资源账本)以 <ledger>
|
|
2137
|
+
# 回本章道具/伤情/承诺/伏笔增量,评审达标后追加账本文件供下章注入。
|
|
2138
|
+
event_block = (
|
|
2139
|
+
"## 本章大纲核对(逐项目标审稿)\n"
|
|
2140
|
+
"本章按大纲应完成:\n- 剧情要点:%s\n- 章末钩子:%s\n"
|
|
2141
|
+
"评审时逐项判定「已完成 / 未完成 / 待核实」,判定必须引用正文证据"
|
|
2142
|
+
"(原文短句或位置),写在回复末尾的 <event_check> 块内(每项一行)。"
|
|
2143
|
+
"「铺垫了但没发生」不算已完成;未完成的项必须反映到对应维度评分。\n"
|
|
2144
|
+
"另在 <event_check> 块之后输出 <ledger> 块(没有新变化就整个省略):"
|
|
2145
|
+
"逐行列出本章新出现或状态变化的 道具/伤情/承诺/伏笔,格式:"
|
|
2146
|
+
"类型|名称|现状(一句话)。\n\n"
|
|
2147
|
+
% (ch.get("beats") or "按大纲推进", ch.get("hook") or "留下悬念"))
|
|
2148
|
+
ev_check_lines = [[]] # 每章重置:第一份非空评审的逐项判定
|
|
2149
|
+
ledger_lines = [] # 本章全部评审的账本增量并集
|
|
2150
|
+
ledger_txt = _read_ledger(workdir) # 截至上一章的资源账本(起草注入)
|
|
2123
2151
|
prev = ""
|
|
2124
2152
|
draft_sid = "" # §07 T1.1:本轮 draft/复用章的会话 id(revise 复用;reuse 时为空)
|
|
2125
2153
|
if i > 1:
|
|
@@ -2147,8 +2175,14 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2147
2175
|
rounds_used = 1
|
|
2148
2176
|
means = {}
|
|
2149
2177
|
|
|
2150
|
-
|
|
2151
|
-
|
|
2178
|
+
# 逐项目标审稿 + 资源账本的每章聚合桶(run_critique 内解析填充;
|
|
2179
|
+
# event_check 取第一份非空,ledger 全评审增量求并)
|
|
2180
|
+
ev_check_lines = [[]]
|
|
2181
|
+
ledger_lines = []
|
|
2182
|
+
|
|
2183
|
+
def run_critique(text, rnd, note_extra="", critic_sids=None, event_check=""):
|
|
2184
|
+
"""一轮多维评审:返回 (cj_by_agent, scored)。变体赛马与主循环共用。
|
|
2185
|
+
event_check:本章大纲核对块(逐项目标审稿),随评审下发并回收标记块。"""
|
|
2152
2186
|
cj_map, sids = {}, dict(critic_sids or {})
|
|
2153
2187
|
scored = 0 # 真正给出分数的评审数;失败/不可解析不得当成 0 分计入
|
|
2154
2188
|
for agent in critics:
|
|
@@ -2171,12 +2205,20 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2171
2205
|
note=("小说第 %d 章" % i) + (
|
|
2172
2206
|
"|你的专属评审视角:%s(其他评审会覆盖其余视角,"
|
|
2173
2207
|
"请深挖你的镜头,但所有维度仍需打分)" % lens)
|
|
2174
|
-
if lens else ""
|
|
2208
|
+
if lens else "",
|
|
2209
|
+
event_check=event_check) + note_extra,
|
|
2175
2210
|
workdir, readonly=True, ev=ev,
|
|
2176
2211
|
resume=sids.get(agent["id"]))
|
|
2177
2212
|
cj = _critique_json(res, dims)
|
|
2178
2213
|
if cj.get("scores"):
|
|
2179
2214
|
scored += 1
|
|
2215
|
+
# 逐项目标审稿 + 资源账本:解析评审回复里的标记块(借鉴
|
|
2216
|
+
# AI-Novel-Writer 逐项核对/角色资源账本)。多评审取第一份
|
|
2217
|
+
# 非空即可,聚合时已在前两处赋值处去重。
|
|
2218
|
+
evl = _parse_tagged_lines(res.get("text"), "event_check")
|
|
2219
|
+
if evl and not ev_check_lines[0]:
|
|
2220
|
+
ev_check_lines[0] = evl
|
|
2221
|
+
ledger_lines.extend(_parse_tagged_lines(res.get("text"), "ledger"))
|
|
2180
2222
|
# §07 T1.1:记录该评审的会话 id(第 2 轮复用)
|
|
2181
2223
|
csid = _resume_sid(agent, res.get("sid"))
|
|
2182
2224
|
if csid:
|
|
@@ -2196,7 +2238,8 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2196
2238
|
res = _run_step(run_id, role, modelhub.bind_agent(spare, difficulty),
|
|
2197
2239
|
crit_prompt_for(
|
|
2198
2240
|
text,
|
|
2199
|
-
note="小说第 %d 章" % i
|
|
2241
|
+
note="小说第 %d 章" % i,
|
|
2242
|
+
event_check=event_check) + note_extra,
|
|
2200
2243
|
workdir, readonly=True, ev=ev)
|
|
2201
2244
|
cj = runner.extract_json(res.get("text") or "")
|
|
2202
2245
|
if isinstance(cj, dict) and isinstance(cj.get("scores"), dict) \
|
|
@@ -2252,11 +2295,8 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2252
2295
|
else:
|
|
2253
2296
|
# stable_order:同一任务 8 个章节的技能块必须字节级一致(§07 T1.2' 前缀缓存)
|
|
2254
2297
|
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
|
|
2298
|
+
if bible:
|
|
2299
|
+
sk_block = (sk_block + "\n\n" + bible) if sk_block else bible
|
|
2260
2300
|
kb_block = knowledge.block_for(task)
|
|
2261
2301
|
if kb_block:
|
|
2262
2302
|
sk_block = (sk_block + "\n\n" + kb_block) if sk_block else kb_block
|
|
@@ -2271,6 +2311,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2271
2311
|
.replace("__GOAL__", task["goal"])
|
|
2272
2312
|
.replace("__OUTLINE__", outline_txt)
|
|
2273
2313
|
.replace("__PREV__", prev)
|
|
2314
|
+
.replace("__LEDGER__", ledger_txt)
|
|
2274
2315
|
.replace("__TITLE__", ch["title"])
|
|
2275
2316
|
.replace("__BEATS__", ch["beats"] or "按大纲推进")
|
|
2276
2317
|
.replace("__HOOK__", ch.get("hook") or "留下悬念")
|
|
@@ -2523,7 +2564,8 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2523
2564
|
if rnd == 1 and race_cj is not None:
|
|
2524
2565
|
cj_by_agent, scored = race_cj, race_scored
|
|
2525
2566
|
else:
|
|
2526
|
-
cj_by_agent, scored, sids_now = run_critique(
|
|
2567
|
+
cj_by_agent, scored, sids_now = run_critique(
|
|
2568
|
+
text, rnd, critic_sids=critic_sids, event_check=event_block)
|
|
2527
2569
|
critic_sids.update(sids_now)
|
|
2528
2570
|
if not scored:
|
|
2529
2571
|
# 「评不上」≠「评了 0 分」:全部评审失败时中止本轮,
|
|
@@ -2577,8 +2619,8 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2577
2619
|
.replace("__I__", str(i)).replace("__FILE__", ch_file)
|
|
2578
2620
|
.replace("__GOAL__", task["goal"])
|
|
2579
2621
|
.replace("__CRITIQUE__", "\n".join(crit_lines))
|
|
2580
|
-
.replace("__CONTEXT__", task.get("context") or "(无)")
|
|
2581
2622
|
.replace("__WORDS__", str(wpc)))
|
|
2623
|
+
prompt = attachments.append_task_context(prompt, task)
|
|
2582
2624
|
_run_step(run_id, "revise-c%d" % i, modelhub.bind_agent(impl, difficulty), prompt,
|
|
2583
2625
|
step_wd, readonly=False, ev=ev, timeout=2400,
|
|
2584
2626
|
resume=resume_ctx["session"] if resume_ctx else draft_sid)
|
|
@@ -2587,12 +2629,23 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2587
2629
|
impl, implement_reason="章节修订:%s" %
|
|
2588
2630
|
(impl.get("label") or impl.get("id")))
|
|
2589
2631
|
_check_cancel(ev)
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2632
|
+
cs_new = {"chapter": i, "title": ch["title"], "means": means,
|
|
2633
|
+
"passed": bool(means) and all(v >= threshold_ch for v in means.values()),
|
|
2634
|
+
"rounds": rounds_used,
|
|
2635
|
+
"words": _wc(_read_chapter(workdir, i))}
|
|
2636
|
+
if ev_check_lines[0]:
|
|
2637
|
+
cs_new["event_check"] = ev_check_lines[0][:8] # 逐项目标审稿结果
|
|
2638
|
+
chapter_scores.append(cs_new)
|
|
2594
2639
|
# 每章即时持久化:长篇中断/超时后可断点续跑,不丢已完成章的分数
|
|
2595
2640
|
store.update_run(run_id, chapter_scores=chapter_scores)
|
|
2641
|
+
# 资源账本(借鉴角色资源账本):评审提出的道具/伤情/承诺/伏笔增量
|
|
2642
|
+
# 追加到 .codebee/resource-ledger.md,下一章起草时注入,防跨章穿帮。
|
|
2643
|
+
# 失败静默——账本是增强不是硬依赖。
|
|
2644
|
+
if ledger_lines:
|
|
2645
|
+
try:
|
|
2646
|
+
_append_ledger(workdir, i, ledger_lines)
|
|
2647
|
+
except Exception:
|
|
2648
|
+
pass
|
|
2596
2649
|
# findings 沉淀(借鉴 agentmemory 持久记忆):章节标题+要点追加到
|
|
2597
2650
|
# .codebee/findings.md——后续章节起草时随圣经/模块库注入,弥补
|
|
2598
2651
|
# 前情提要只看近 2 章结尾的中期记忆空洞。失败静默。
|
|
@@ -2634,7 +2687,6 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2634
2687
|
res = _run_step(run_id, "global-critique", modelhub.bind_agent(agent, difficulty),
|
|
2635
2688
|
(gtpl.replace("__DIMKEYS__", dimkey)
|
|
2636
2689
|
.replace("__GOAL__", task["goal"])
|
|
2637
|
-
.replace("__CONTEXT__", task.get("context") or "(无)")
|
|
2638
2690
|
.replace("__MANUSCRIPT__", full_text[:60000])),
|
|
2639
2691
|
workdir, readonly=True, ev=ev, timeout=2400)
|
|
2640
2692
|
gj = _critique_json(res, dims)
|
|
@@ -2711,6 +2763,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2711
2763
|
.replace("__GOAL__", task["goal"])
|
|
2712
2764
|
.replace("__CRITIQUE__", crit)
|
|
2713
2765
|
.replace("__WORDS__", str(wpc)))
|
|
2766
|
+
prompt = attachments.append_task_context(prompt, task)
|
|
2714
2767
|
res = _run_step(run_id, "polish-c%d" % i, modelhub.bind_agent(impl, difficulty),
|
|
2715
2768
|
prompt, workdir, readonly=False, ev=ev, timeout=2400,
|
|
2716
2769
|
resume=resume_ctx["session"] if resume_ctx else None)
|
|
@@ -3091,8 +3144,9 @@ def _run_content_review(run, task, agents, ev, stats, mode):
|
|
|
3091
3144
|
crit_prompt = (_ensure_critique_placeholders(
|
|
3092
3145
|
_tpl(task, "critique_prompt", NOVEL_CRITIQUE_PROMPT))
|
|
3093
3146
|
.replace("__DIMKEYS__", dimkey)
|
|
3094
|
-
.replace("__CONTEXT__", task.get("context") or "(无)")
|
|
3095
3147
|
.replace("__MANUSCRIPT__", manuscript or "(稿件为空!)"))
|
|
3148
|
+
if task.get("context"):
|
|
3149
|
+
crit_prompt += "\n\n## 原始任务背景与附件参考\n" + task["context"]
|
|
3096
3150
|
# AI 味确定性检测(借鉴 oh-story 去AI味):客观参考线随评审下发,
|
|
3097
3151
|
# 命中才追加——评审官结合上下文判断是否真问题,脚本不直接扣分
|
|
3098
3152
|
_aiflavor_line = aiflavor.report_line(manuscript)
|
|
@@ -3159,9 +3213,9 @@ def _run_content_review(run, task, agents, ev, stats, mode):
|
|
|
3159
3213
|
prompt = (NOVEL_REVISE_PROMPT.replace("__FILE__", ms_name)
|
|
3160
3214
|
.replace("__ROLE__", _content_role(task))
|
|
3161
3215
|
.replace("__GOAL__", task["goal"])
|
|
3162
|
-
.replace("__CONTEXT__", task.get("context") or "(无)")
|
|
3163
3216
|
.replace("__CRITIQUE__", "\n".join(crit_lines)))
|
|
3164
3217
|
prompt += _content_contract(task)
|
|
3218
|
+
prompt = attachments.append_task_context(prompt, task)
|
|
3165
3219
|
_run_step(run_id, "revise-r%d" % r, modelhub.bind_agent(impl, difficulty), prompt,
|
|
3166
3220
|
workdir, readonly=False, ev=ev,
|
|
3167
3221
|
resume=resume_ctx["session"] if resume_ctx else None)
|
|
@@ -3489,7 +3543,7 @@ def execute_run(run_id):
|
|
|
3489
3543
|
task_spec_summary=task_compile.summary(task_spec),
|
|
3490
3544
|
difficulty=task_spec["difficulty"])
|
|
3491
3545
|
task = dict(task)
|
|
3492
|
-
task =
|
|
3546
|
+
task = attachments.refresh_task(task, task.get("workdir") or "")
|
|
3493
3547
|
task["_compiled_spec"] = task_spec
|
|
3494
3548
|
# 运行内统一使用编译后的难度;store 中历史任务常带 difficulty=auto,
|
|
3495
3549
|
# 不能让这个兼容值覆盖 easy/default/hard 的模型调度决策。
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""偏好记忆(借鉴 chinese-novelist-skill 的偏好记忆,2026-09-21):记住用户
|
|
3
|
+
上次创建任务时选择的类型与参数,新建任务时预填「最近一次的选择」而不是
|
|
4
|
+
写死的出厂默认——高频用户不用每次重调轮数/阈值/章节。
|
|
5
|
+
|
|
6
|
+
只记参数不记内容(目标/标题绝不落库);单文件带锁读写,损坏即弃。
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import threading
|
|
12
|
+
|
|
13
|
+
from . import paths
|
|
14
|
+
|
|
15
|
+
_LOCK = threading.Lock()
|
|
16
|
+
_FILE = paths.DATA_DIR / "last-prefs.json"
|
|
17
|
+
|
|
18
|
+
SAVE_KEYS = ("type", "rounds", "threshold", "best_of", "chapters",
|
|
19
|
+
"words_per_chapter", "variants")
|
|
20
|
+
_LIMITS = {"rounds": (1, 5), "threshold": (1.0, 10.0), "best_of": (1, 3),
|
|
21
|
+
"chapters": (1, 2000), "words_per_chapter": (200, 20000),
|
|
22
|
+
"variants": (1, 3)}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _clamp(key, value):
|
|
26
|
+
"""数值钳制:历史文件被手改出天际值时按参数合法域兜底。"""
|
|
27
|
+
lo, hi = _LIMITS.get(key), (0, 10 ** 9)
|
|
28
|
+
lo, hi = lo if lo else hi
|
|
29
|
+
try:
|
|
30
|
+
return max(lo, min(hi, type(lo)(value) if isinstance(lo, int)
|
|
31
|
+
else float(value)))
|
|
32
|
+
except (TypeError, ValueError):
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def record(payload):
|
|
37
|
+
"""从创建任务的 payload 提取可复用参数(存在才覆盖,单键独立保留)。"""
|
|
38
|
+
if not isinstance(payload, dict):
|
|
39
|
+
return
|
|
40
|
+
with _LOCK:
|
|
41
|
+
cur = load()
|
|
42
|
+
for key in SAVE_KEYS:
|
|
43
|
+
v = payload.get(key)
|
|
44
|
+
if v is None:
|
|
45
|
+
continue
|
|
46
|
+
if key == "type":
|
|
47
|
+
v = str(v)[:32]
|
|
48
|
+
if v:
|
|
49
|
+
cur["type"] = v
|
|
50
|
+
continue
|
|
51
|
+
c = _clamp(key, v)
|
|
52
|
+
if c is not None:
|
|
53
|
+
cur[key] = c
|
|
54
|
+
tmp = _FILE.with_suffix(".tmp")
|
|
55
|
+
try:
|
|
56
|
+
paths.DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
57
|
+
tmp.write_text(json.dumps(cur, ensure_ascii=False, indent=2),
|
|
58
|
+
encoding="utf-8")
|
|
59
|
+
tmp.replace(_FILE)
|
|
60
|
+
except OSError:
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def load():
|
|
65
|
+
"""读最近偏好;文件缺失/损坏返回 {}。"""
|
|
66
|
+
try:
|
|
67
|
+
data = json.loads(_FILE.read_text(encoding="utf-8"))
|
|
68
|
+
return data if isinstance(data, dict) else {}
|
|
69
|
+
except (OSError, ValueError):
|
|
70
|
+
return {}
|
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
|
@@ -308,6 +308,10 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
308
308
|
# 一键反馈 Issue 的预填摘要(标题+正文,全程脱敏,用户亲手提交)
|
|
309
309
|
from core import telemetry
|
|
310
310
|
return self._json(200, telemetry.issue_report(days=30))
|
|
311
|
+
if path == "/api/prefs":
|
|
312
|
+
# 偏好记忆:上次创建任务的类型/参数(新建表单预填用)
|
|
313
|
+
from core import prefs
|
|
314
|
+
return self._json(200, {"prefs": prefs.load()})
|
|
311
315
|
if path == "/api/ports":
|
|
312
316
|
# 端口占用诊断(借鉴 leftopen):谁在听、PID/进程/项目归属、
|
|
313
317
|
# 是否仅本机。?port=N 只看单端口。只读,不碰任何进程。
|
|
@@ -2051,7 +2055,15 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
2051
2055
|
return 200, {"task_id": task["id"], "run_id": run["id"]}
|
|
2052
2056
|
|
|
2053
2057
|
def _api_create_task(self):
|
|
2054
|
-
|
|
2058
|
+
body = self._body()
|
|
2059
|
+
status, resp = self._create_and_start(body)
|
|
2060
|
+
if status == 200:
|
|
2061
|
+
# 偏好记忆(借鉴 chinese-novelist-skill):记住本次选择,下次新建预填
|
|
2062
|
+
try:
|
|
2063
|
+
from core import prefs
|
|
2064
|
+
prefs.record(body)
|
|
2065
|
+
except Exception:
|
|
2066
|
+
pass
|
|
2055
2067
|
return self._json(status, resp)
|
|
2056
2068
|
|
|
2057
2069
|
def _api_task_clarify(self):
|
|
@@ -2077,8 +2089,9 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
2077
2089
|
"每个问题给出 2-4 个最常见的选项。只输出 JSON 数组,不要输出其他内容:\n"
|
|
2078
2090
|
'[{"q": "问题", "options": ["选项1", "选项2"]}]' % (ttype, goal[:200]))
|
|
2079
2091
|
try:
|
|
2092
|
+
# 澄清是可选增强,不能占住创建请求;主流程有自己的执行超时。
|
|
2080
2093
|
res = builtin_agent.run(bi, prompt, os.getcwd() if hasattr(os, "getcwd") else ".",
|
|
2081
|
-
timeout=
|
|
2094
|
+
timeout=8)
|
|
2082
2095
|
import json as _json
|
|
2083
2096
|
arr = None
|
|
2084
2097
|
text = (res.get("text") or "").strip()
|
package/app/pet.py
CHANGED
|
@@ -359,6 +359,85 @@ def global_lock_held():
|
|
|
359
359
|
return False
|
|
360
360
|
|
|
361
361
|
|
|
362
|
+
# 浏览器进程白名单:找已打开的 CodeBee 窗口时只认这些主进程的顶层窗口,
|
|
363
|
+
# 避免把桌宠自身(标题也是 CodeBee)或 start-public.bat 起的「CodeBee」
|
|
364
|
+
# 控制台窗口误当成浏览器置前。
|
|
365
|
+
_BROWSER_EXES = {
|
|
366
|
+
"msedge.exe", "chrome.exe", "chromium.exe", "firefox.exe",
|
|
367
|
+
"iexplore.exe", "opera.exe", "brave.exe", "vivaldi.exe",
|
|
368
|
+
"browser.exe", "waterfox.exe", "palemoon.exe", "seamonkey.exe",
|
|
369
|
+
"centbrowser.exe", "360chrome.exe", "qqbrowser.exe",
|
|
370
|
+
"sogouexplorer.exe", "maxthon.exe",
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _window_process_name(hwnd):
|
|
375
|
+
"""取顶层窗口所属进程的可执行文件名(小写);失败返回空串。"""
|
|
376
|
+
try:
|
|
377
|
+
import ctypes
|
|
378
|
+
from ctypes import wintypes
|
|
379
|
+
user32 = ctypes.windll.user32
|
|
380
|
+
kernel32 = ctypes.windll.kernel32
|
|
381
|
+
pid = wintypes.DWORD()
|
|
382
|
+
user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
|
|
383
|
+
if not pid.value:
|
|
384
|
+
return ""
|
|
385
|
+
kernel32.OpenProcess.restype = wintypes.HANDLE
|
|
386
|
+
# PROCESS_QUERY_LIMITED_INFORMATION=0x1000:只需取映像路径
|
|
387
|
+
h = kernel32.OpenProcess(0x1000, False, pid.value)
|
|
388
|
+
if not h:
|
|
389
|
+
return ""
|
|
390
|
+
try:
|
|
391
|
+
buf = ctypes.create_unicode_buffer(1024)
|
|
392
|
+
size = wintypes.DWORD(1024)
|
|
393
|
+
if kernel32.QueryFullProcessImageNameW(h, 0, buf,
|
|
394
|
+
ctypes.byref(size)):
|
|
395
|
+
return Path(buf.value).name.lower()
|
|
396
|
+
finally:
|
|
397
|
+
kernel32.CloseHandle(h)
|
|
398
|
+
except Exception:
|
|
399
|
+
pass
|
|
400
|
+
return ""
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _focus_codebee_window(ignore_hwnd=None):
|
|
404
|
+
"""Windows:已打开过 CodeBee 浏览器页时,把那个窗口找出来还原并置前,
|
|
405
|
+
避免每点一次桌宠就新开一个浏览器窗口。返回 True 表示已找到并尝试置前;
|
|
406
|
+
False 表示没有现成窗口,调用方应回退 webbrowser.open 新开。"""
|
|
407
|
+
try:
|
|
408
|
+
import ctypes
|
|
409
|
+
from ctypes import wintypes
|
|
410
|
+
user32 = ctypes.windll.user32
|
|
411
|
+
EnumWindowsProc = ctypes.WINFUNCTYPE(
|
|
412
|
+
wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)
|
|
413
|
+
found = []
|
|
414
|
+
|
|
415
|
+
def _cb(hwnd, _lp):
|
|
416
|
+
if hwnd == ignore_hwnd or not user32.IsWindowVisible(hwnd):
|
|
417
|
+
return True
|
|
418
|
+
if _window_process_name(hwnd) not in _BROWSER_EXES:
|
|
419
|
+
return True
|
|
420
|
+
length = user32.GetWindowTextLengthW(hwnd)
|
|
421
|
+
if length <= 0:
|
|
422
|
+
return True
|
|
423
|
+
buf = ctypes.create_unicode_buffer(length + 1)
|
|
424
|
+
user32.GetWindowTextW(hwnd, buf, length + 1)
|
|
425
|
+
if "CodeBee" in buf.value:
|
|
426
|
+
found.append(hwnd)
|
|
427
|
+
return True
|
|
428
|
+
|
|
429
|
+
user32.EnumWindows(EnumWindowsProc(_cb), 0)
|
|
430
|
+
if not found:
|
|
431
|
+
return False
|
|
432
|
+
target = found[0]
|
|
433
|
+
if user32.IsIconic(target):
|
|
434
|
+
user32.ShowWindow(target, 9) # SW_RESTORE:最小化先还原
|
|
435
|
+
user32.SetForegroundWindow(target)
|
|
436
|
+
return True
|
|
437
|
+
except Exception:
|
|
438
|
+
return False
|
|
439
|
+
|
|
440
|
+
|
|
362
441
|
class PetApp:
|
|
363
442
|
"""蜜蜂窗口:蜜蜂本体(精灵/手绘)+ 状态机动画 + 悬停任务清单 + 右键菜单。"""
|
|
364
443
|
|
|
@@ -1296,7 +1375,12 @@ class PetApp:
|
|
|
1296
1375
|
|
|
1297
1376
|
def _open_ui(self):
|
|
1298
1377
|
try:
|
|
1299
|
-
|
|
1378
|
+
url = "http://127.0.0.1:%d" % self.port
|
|
1379
|
+
# 已开过就不新开:Windows 上先找现有 CodeBee 浏览器窗口置前,
|
|
1380
|
+
# 找不到(没开/标签不在前台)才回退 webbrowser.open 新开。
|
|
1381
|
+
if os.name == "nt" and _focus_codebee_window(self.root.winfo_id()):
|
|
1382
|
+
return
|
|
1383
|
+
webbrowser.open(url)
|
|
1300
1384
|
except Exception:
|
|
1301
1385
|
pass
|
|
1302
1386
|
|
package/app/ui/app.js
CHANGED
|
@@ -43,11 +43,12 @@ function flowDesc(f) {
|
|
|
43
43
|
function renderTypeOptions() {
|
|
44
44
|
const sel = $("f-type");
|
|
45
45
|
if (!sel || !S.flows) return;
|
|
46
|
-
const prev = sel.value;
|
|
47
|
-
sel.innerHTML = (S.flows || []).map((f) => {
|
|
46
|
+
const prev = sel.value; sel.innerHTML = (S.flows || []).map((f) => {
|
|
48
47
|
return '<option value="' + esc(f.id) + '">' + esc(t(f.name)) + t("(") + flowDesc(f) + (f.builtin ? "" : t(" · 自定义")) + t(")") + "</option>";
|
|
49
48
|
}).join("");
|
|
50
49
|
if (prev && flowById(prev)) sel.value = prev;
|
|
50
|
+
// 偏好记忆:上次创建用的类型优先于出厂默认「直接执行」(老数据/已删流程回落)
|
|
51
|
+
else if (S.lastPrefs && S.lastPrefs.type && flowById(S.lastPrefs.type)) sel.value = S.lastPrefs.type;
|
|
51
52
|
// 首屏默认落在「直接执行」(快档位:单 CLI 直达,无拆解/评审);
|
|
52
53
|
// 老数据或该流程被删时保持首项,不硬造一个不存在的值
|
|
53
54
|
else if (flowById("direct")) sel.value = "direct";
|
|
@@ -160,9 +161,11 @@ function onTypeChange() {
|
|
|
160
161
|
if (goal && flow && flow.goal_hint) goal.placeholder = t(flow.goal_hint);
|
|
161
162
|
if (isReview && flow) {
|
|
162
163
|
if (flow.manuscript) $("f-manuscript").value = flow.manuscript;
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
164
|
+
// 偏好记忆(借鉴 chinese-novelist-skill):上次创建用过的参数优先于流程出厂默认
|
|
165
|
+
const lp = S.lastPrefs || {};
|
|
166
|
+
$("f-rounds").value = lp.rounds || flow.rounds || 2;
|
|
167
|
+
$("f-threshold").value = (lp.threshold != null ? lp.threshold : (flow.threshold || 7.0));
|
|
168
|
+
$("f-bestof").value = lp.best_of || flow.best_of || 1;
|
|
166
169
|
const rubric = $("f-rubric");
|
|
167
170
|
const previousFlow = rubric.dataset.flowId || "";
|
|
168
171
|
if (!S.flowRubricDrafts) S.flowRubricDrafts = {};
|
|
@@ -175,10 +178,10 @@ function onTypeChange() {
|
|
|
175
178
|
: (flow.rubric || []).join(", ");
|
|
176
179
|
rubric.dataset.flowId = flow.id;
|
|
177
180
|
}
|
|
178
|
-
// 连载参数预填(用户可改/可清空 =
|
|
181
|
+
// 连载参数预填(用户可改/可清空 = 单稿件模式);偏好记忆的最近值优先
|
|
179
182
|
if (flow.serial) {
|
|
180
|
-
if (!$("f-chapters").value) $("f-chapters").value = flow.serial.chapters || "";
|
|
181
|
-
if (!$("f-words-per-ch").value) $("f-words-per-ch").value = flow.serial.words_per_chapter || "";
|
|
183
|
+
if (!$("f-chapters").value) $("f-chapters").value = lp.chapters || flow.serial.chapters || "";
|
|
184
|
+
if (!$("f-words-per-ch").value) $("f-words-per-ch").value = lp.words_per_chapter || flow.serial.words_per_chapter || "";
|
|
182
185
|
} else clearSerialFields();
|
|
183
186
|
} else {
|
|
184
187
|
// 隐藏控件仍保留 DOM 值;换流程时清理,避免章节/圣经状态污染下一次提交。
|
|
@@ -301,6 +304,124 @@ let _requestBusyCount = 0;
|
|
|
301
304
|
let _lastActionButton = null;
|
|
302
305
|
let _lastActionAt = 0;
|
|
303
306
|
const _requestBusyButtons = new WeakMap();
|
|
307
|
+
const _operations = [];
|
|
308
|
+
let _operationSeq = 0;
|
|
309
|
+
|
|
310
|
+
function operationLabel(path, opts) {
|
|
311
|
+
if (opts && opts.operation === false) return "";
|
|
312
|
+
if (opts && typeof opts.operation === "string") return t(opts.operation);
|
|
313
|
+
if (opts && opts.busy === false && opts.operation !== true) return "";
|
|
314
|
+
const method = String((opts && opts.method) || "GET").toUpperCase();
|
|
315
|
+
if (method === "GET" || method === "HEAD") return "";
|
|
316
|
+
// 只自动记录刚由用户点击触发的写操作;后台维护请求需显式传 operation 才显示。
|
|
317
|
+
if (!(opts && opts.operation === true) && performance.now() - _lastActionAt >= 2000) return "";
|
|
318
|
+
const clean = String(path || "").split("?")[0];
|
|
319
|
+
const rules = [
|
|
320
|
+
[/^\/api\/tasks$/, "创建任务"],
|
|
321
|
+
[/\/tasks\/[^/]+\/delete$/, "删除任务"],
|
|
322
|
+
[/\/tasks\/[^/]+\/archive$/, "归档任务"],
|
|
323
|
+
[/\/tasks\/[^/]+\/retry$/, "重试任务"],
|
|
324
|
+
[/\/tasks\/[^/]+\/continue$/, "创建续写任务"],
|
|
325
|
+
[/\/tasks\/[^/]+\/rename$/, "重命名任务"],
|
|
326
|
+
[/\/runs\/[^/]+\/cancel$/, "取消运行"],
|
|
327
|
+
[/\/runs\/[^/]+\/delete$/, "删除运行"],
|
|
328
|
+
[/\/runs\/(delete-batch|clear)$/, "清理运行记录"],
|
|
329
|
+
[/\/attachments(?:\/[^/]+)?$/, "上传附件"],
|
|
330
|
+
[/\/selfupdate\/apply$/, "升级 CodeBee"],
|
|
331
|
+
[/\/market\//, "更新插件"],
|
|
332
|
+
[/\/models\/binding$/, "更新智能体绑定"],
|
|
333
|
+
[/\/models\//, "更新模型配置"],
|
|
334
|
+
];
|
|
335
|
+
const hit = rules.find((it) => it[0].test(clean));
|
|
336
|
+
return t(hit ? hit[1] : "提交操作");
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function operationStart(path, opts) {
|
|
340
|
+
const label = operationLabel(path, opts);
|
|
341
|
+
if (!label) return null;
|
|
342
|
+
const op = { id: ++_operationSeq, label, status: "running", at: Date.now(), error: "" };
|
|
343
|
+
_operations.unshift(op);
|
|
344
|
+
if (_operations.length > 12) _operations.length = 12;
|
|
345
|
+
renderOperationCenter();
|
|
346
|
+
return op.id;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function operationFinish(id, status, error) {
|
|
350
|
+
if (!id) return;
|
|
351
|
+
const op = _operations.find((it) => it.id === id);
|
|
352
|
+
if (!op) return;
|
|
353
|
+
op.status = status;
|
|
354
|
+
op.error = String(error || "").slice(0, 240);
|
|
355
|
+
op.endedAt = Date.now();
|
|
356
|
+
renderOperationCenter();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function operationTime(ts) {
|
|
360
|
+
try { return new Date(ts).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); }
|
|
361
|
+
catch (e) { return ""; }
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function renderOperationCenter() {
|
|
365
|
+
const button = $("btn-ops");
|
|
366
|
+
const list = $("op-list");
|
|
367
|
+
if (!button || !list) return;
|
|
368
|
+
button.classList.toggle("hidden", !_operations.length);
|
|
369
|
+
const running = _operations.filter((it) => it.status === "running").length;
|
|
370
|
+
const failed = _operations.filter((it) => it.status === "failed").length;
|
|
371
|
+
button.classList.toggle("running", running > 0);
|
|
372
|
+
button.classList.toggle("failed", !running && failed > 0);
|
|
373
|
+
button.classList.toggle("done", !running && !failed && _operations.length > 0);
|
|
374
|
+
const pill = $("op-pill-text");
|
|
375
|
+
if (pill) pill.textContent = running ? t("{0} 项处理中").replace("{0}", running)
|
|
376
|
+
: failed ? t("{0} 项失败").replace("{0}", failed) : t("操作完成");
|
|
377
|
+
list.innerHTML = _operations.map((op) => {
|
|
378
|
+
const status = op.status === "running" ? t("处理中")
|
|
379
|
+
: op.status === "failed" ? t("失败") : t("已完成");
|
|
380
|
+
const remove = op.status === "running" ? "" :
|
|
381
|
+
'<button type="button" class="op-dismiss" aria-label="' + esc(t("移除")) +
|
|
382
|
+
'" title="' + esc(t("移除")) + '" onclick="dismissOperation(event,' + op.id + ')">' +
|
|
383
|
+
'<svg class="ico" aria-hidden="true"><use href="#i-x"></use></svg></button>';
|
|
384
|
+
return '<div class="op-row ' + op.status + '"><span class="op-state" aria-hidden="true"></span>' +
|
|
385
|
+
'<div class="op-copy"><div><b>' + esc(op.label) + '</b><span>' + esc(status) + " · " +
|
|
386
|
+
esc(operationTime(op.endedAt || op.at)) + '</span></div>' +
|
|
387
|
+
(op.error ? '<p title="' + esc(op.error) + '">' + esc(op.error) + "</p>" : "") +
|
|
388
|
+
"</div>" + remove + "</div>";
|
|
389
|
+
}).join("");
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function toggleOperationCenter(force) {
|
|
393
|
+
const panel = $("op-center");
|
|
394
|
+
const button = $("btn-ops");
|
|
395
|
+
if (!panel || !button) return;
|
|
396
|
+
const open = force === undefined ? panel.classList.contains("hidden") : !!force;
|
|
397
|
+
panel.classList.toggle("hidden", !open);
|
|
398
|
+
button.setAttribute("aria-expanded", open ? "true" : "false");
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function clearFinishedOperations() {
|
|
402
|
+
for (let i = _operations.length - 1; i >= 0; i--) {
|
|
403
|
+
if (_operations[i].status === "done") _operations.splice(i, 1);
|
|
404
|
+
}
|
|
405
|
+
renderOperationCenter();
|
|
406
|
+
if (!_operations.length) toggleOperationCenter(false);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function dismissOperation(event, id) {
|
|
410
|
+
if (event) event.stopPropagation();
|
|
411
|
+
const i = _operations.findIndex((it) => it.id === id && it.status !== "running");
|
|
412
|
+
if (i >= 0) _operations.splice(i, 1);
|
|
413
|
+
renderOperationCenter();
|
|
414
|
+
if (!_operations.length) toggleOperationCenter(false);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
window.toggleOperationCenter = toggleOperationCenter;
|
|
418
|
+
window.clearFinishedOperations = clearFinishedOperations;
|
|
419
|
+
window.dismissOperation = dismissOperation;
|
|
420
|
+
|
|
421
|
+
document.addEventListener("click", (e) => {
|
|
422
|
+
const wrap = $("op-wrap");
|
|
423
|
+
if (wrap && !wrap.contains(e.target)) toggleOperationCenter(false);
|
|
424
|
+
});
|
|
304
425
|
|
|
305
426
|
/* 记录发起请求的按钮。写请求统一在 api() 里挂忙碌态,避免每个业务入口各写一套;
|
|
306
427
|
* 确认框的按钮不覆盖原始操作按钮,但会续期,使“确认后执行”仍反馈在原按钮上。 */
|
|
@@ -394,10 +515,12 @@ async function api(path, opts) {
|
|
|
394
515
|
var timer = setTimeout(() => ctrl.abort(), opts.timeout);
|
|
395
516
|
}
|
|
396
517
|
const busyToken = requestBusyStart(opts);
|
|
518
|
+
const operationToken = operationStart(path, opts);
|
|
397
519
|
const fetchOpts = Object.assign({}, opts);
|
|
398
520
|
delete fetchOpts.timeout;
|
|
399
521
|
delete fetchOpts.busy;
|
|
400
522
|
delete fetchOpts.busyElement;
|
|
523
|
+
delete fetchOpts.operation;
|
|
401
524
|
try {
|
|
402
525
|
let res;
|
|
403
526
|
try {
|
|
@@ -412,7 +535,11 @@ async function api(path, opts) {
|
|
|
412
535
|
try { data = await res.json(); } catch (e) { /* ignore */ }
|
|
413
536
|
if (res.status === 423 && data && data.control) setControl(data.control);
|
|
414
537
|
if (!res.ok) throw new Error((data && data.error) || ("HTTP " + res.status));
|
|
538
|
+
operationFinish(operationToken, "done");
|
|
415
539
|
return data;
|
|
540
|
+
} catch (e) {
|
|
541
|
+
operationFinish(operationToken, "failed", e && e.message ? e.message : e);
|
|
542
|
+
throw e;
|
|
416
543
|
} finally {
|
|
417
544
|
if (timer) clearTimeout(timer);
|
|
418
545
|
requestBusyEnd(busyToken);
|
|
@@ -2026,7 +2153,8 @@ async function createTask() {
|
|
|
2026
2153
|
msg.textContent = t("目标有点简短,先问几个问题…");
|
|
2027
2154
|
try {
|
|
2028
2155
|
const cq = await api("/api/tasks/clarify", {
|
|
2029
|
-
|
|
2156
|
+
// 澄清只是增强,不能让建任务被模型调用拖住;超时后直接按原目标开跑。
|
|
2157
|
+
method: "POST", timeout: 8000, operation: false,
|
|
2030
2158
|
body: JSON.stringify({ goal: payload.goal, type: payload.type }) });
|
|
2031
2159
|
const qs = (cq && cq.questions) || [];
|
|
2032
2160
|
if (qs.length) {
|
|
@@ -2034,7 +2162,10 @@ async function createTask() {
|
|
|
2034
2162
|
renderClarify(qs, payload.goal, resetSubmit);
|
|
2035
2163
|
return;
|
|
2036
2164
|
}
|
|
2037
|
-
} catch (e) {
|
|
2165
|
+
} catch (e) {
|
|
2166
|
+
// 澄清服务不可用或超时不阻塞主流程,明确告诉用户已经自动继续。
|
|
2167
|
+
msg.textContent = t("澄清响应较慢,已直接创建任务…");
|
|
2168
|
+
}
|
|
2038
2169
|
}
|
|
2039
2170
|
S.clarifyDone = false;
|
|
2040
2171
|
if (!payload.workdir) delete payload.workdir; // 留空 → 服务端用「默认保存路径」(编排设置可改)
|
|
@@ -2085,7 +2216,7 @@ async function createTask() {
|
|
|
2085
2216
|
if (critics.length) payload.critics = critics;
|
|
2086
2217
|
}
|
|
2087
2218
|
try {
|
|
2088
|
-
const r = await api("/api/tasks", { method: "POST", timeout: 30000,
|
|
2219
|
+
const r = await api("/api/tasks", { method: "POST", timeout: 30000, operation: "创建任务",
|
|
2089
2220
|
body: JSON.stringify(payload) });
|
|
2090
2221
|
msg.textContent = t("已创建,跳转运行页…");
|
|
2091
2222
|
// 先把新任务刷进 state 再跳:chatEngineIsDirect 靠 S.state.tasks 判引擎,
|
|
@@ -2378,7 +2509,9 @@ async function deleteTask(id) {
|
|
|
2378
2509
|
if (!await uiConfirm(t("删除该任务及其全部运行记录(含日志与报告)?不可恢复。"), { ok: t("删除"), danger: true })) return;
|
|
2379
2510
|
let gone = false;
|
|
2380
2511
|
try {
|
|
2381
|
-
|
|
2512
|
+
toast(t("正在删除任务…"));
|
|
2513
|
+
await api("/api/tasks/" + encodeURIComponent(id) + "/delete",
|
|
2514
|
+
{ method: "POST", operation: "删除任务" });
|
|
2382
2515
|
} catch (e) {
|
|
2383
2516
|
// 任务已不在(他端删过 / 重复点):不报错晾着,照常把视图清掉
|
|
2384
2517
|
if (!/任务不存在/.test(e.message)) { toast(t("删除失败:") + e.message, true); return; }
|
|
@@ -3796,11 +3929,11 @@ async function renderRunDetail() {
|
|
|
3796
3929
|
S.cancelTargetRunId = active ? run.id : null;
|
|
3797
3930
|
$("btn-delete").classList.toggle("hidden", active);
|
|
3798
3931
|
$("btn-share").classList.toggle("hidden", active); // 分享页:结束后可生成自包含 HTML
|
|
3932
|
+
$("btn-talk").classList.toggle("hidden", !(run.task_id && !chatEngineIsDirect(run)));
|
|
3933
|
+
const rcTask = ((S.state || {}).tasks || []).find((x) => x.id === run.task_id);
|
|
3799
3934
|
// 归档按钮:非运行中任务可归档/取消归档(此前只有侧栏右键菜单入口,
|
|
3800
3935
|
// 用户反馈「归档按钮不见了」——补显式入口,文案随状态切换)
|
|
3801
3936
|
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
3937
|
setRetryBtn(run, rcTask);
|
|
3805
3938
|
$("btn-continue").classList.toggle("hidden",
|
|
3806
3939
|
!(rcTask && rcTask.serial && run.status !== "running" && run.status !== "queued"));
|
package/app/ui/i18n.js
CHANGED
|
@@ -1276,6 +1276,29 @@
|
|
|
1276
1276
|
"正在下载安装,插件包较大时需要约一分钟…": "Downloading and installing — large plugin packages can take about a minute…",
|
|
1277
1277
|
"请求超时,请重试或检查网络": "Request timed out — try again or check your network",
|
|
1278
1278
|
"操作处理中": "Working on it",
|
|
1279
|
+
"操作状态": "Operations",
|
|
1280
|
+
"清除已完成": "Clear completed",
|
|
1281
|
+
"创建任务": "Create task",
|
|
1282
|
+
"删除任务": "Delete task",
|
|
1283
|
+
"归档任务": "Archive task",
|
|
1284
|
+
"重试任务": "Retry task",
|
|
1285
|
+
"创建续写任务": "Create continuation",
|
|
1286
|
+
"重命名任务": "Rename task",
|
|
1287
|
+
"取消运行": "Cancel run",
|
|
1288
|
+
"删除运行": "Delete run",
|
|
1289
|
+
"清理运行记录": "Clear run history",
|
|
1290
|
+
"上传附件": "Upload attachment",
|
|
1291
|
+
"升级 CodeBee": "Upgrade CodeBee",
|
|
1292
|
+
"更新插件": "Update plugin",
|
|
1293
|
+
"更新模型配置": "Update model settings",
|
|
1294
|
+
"更新智能体绑定": "Update agent binding",
|
|
1295
|
+
"提交操作": "Submit operation",
|
|
1296
|
+
"{0} 项处理中": "{0} in progress",
|
|
1297
|
+
"{0} 项失败": "{0} failed",
|
|
1298
|
+
"操作完成": "Operation complete",
|
|
1299
|
+
"处理中": "In progress",
|
|
1300
|
+
"失败": "Failed",
|
|
1301
|
+
"已完成": "Completed",
|
|
1279
1302
|
"该插件含脚本/钩子/MCP 组件,仅支持纯技能类插件": "Contains scripts/hooks/MCP components — only pure skill plugins are supported",
|
|
1280
1303
|
" 个供应商(google 等协议)仅登记,不支持注入 CLI,未出现在上面的下拉中。": " provider(s) (google etc.) are registry-only, can't inject into CLIs, and are hidden from the dropdown above.",
|
|
1281
1304
|
" 个降级备选)。": " fallback(s)).",
|
package/app/ui/index.html
CHANGED
|
@@ -214,6 +214,15 @@
|
|
|
214
214
|
<div id="health-banner" class="health-banner hidden" onclick="healthBannerClick()" title="供应商健康告警" aria-label="供应商健康告警" data-i18n-title="供应商健康告警" data-i18n-aria="供应商健康告警"></div>
|
|
215
215
|
<div class="top-actions">
|
|
216
216
|
<button id="ctrl-pill" class="pill ctrl icon-only hidden" onclick="ctrlClick()" title="多端控制权" aria-label="多端控制权" data-i18n-aria="多端控制权" data-i18n-title="多端控制权"><svg class="ico" aria-hidden="true"><use href="#i-unlock"></use></svg></button>
|
|
217
|
+
<div id="op-wrap" class="op-wrap">
|
|
218
|
+
<button id="btn-ops" class="pill op-pill hidden" type="button" onclick="toggleOperationCenter()" title="操作状态" aria-label="操作状态" aria-haspopup="dialog" aria-expanded="false" aria-controls="op-center" data-i18n-title="操作状态" data-i18n-aria="操作状态">
|
|
219
|
+
<svg class="ico" aria-hidden="true"><use href="#i-history"></use></svg><span id="op-pill-text"></span>
|
|
220
|
+
</button>
|
|
221
|
+
<div id="op-center" class="op-center hidden" role="dialog" aria-label="操作状态" data-i18n-aria="操作状态">
|
|
222
|
+
<div class="op-center-head"><b data-i18n="操作状态">操作状态</b><button type="button" class="ghost small" onclick="clearFinishedOperations()" data-i18n="清除已完成">清除已完成</button></div>
|
|
223
|
+
<div id="op-list" class="op-list" aria-live="polite"></div>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
217
226
|
<!-- 语言快捷切换:语言名用母语写死(中文/English),刻意不做 i18n——任何母语的人都要能读懂并选中 -->
|
|
218
227
|
<div class="lang-wrap">
|
|
219
228
|
<button id="btn-lang" class="pill icon-only" type="button" onclick="toggleLangMenu()" title="语言 / Language" aria-label="切换语言" aria-haspopup="listbox" aria-controls="lang-menu" data-i18n-title="语言 / Language" data-i18n-aria="切换语言"><svg class="ico" aria-hidden="true"><use href="#i-globe"></use></svg></button>
|
package/app/ui/style.css
CHANGED
|
@@ -5456,3 +5456,55 @@ button.request-busy > * { opacity: .18; }
|
|
|
5456
5456
|
.rd-route-row { grid-template-columns: 38px minmax(72px, 1fr); }
|
|
5457
5457
|
.rd-route-reason { grid-column: 2; }
|
|
5458
5458
|
}
|
|
5459
|
+
|
|
5460
|
+
/* ---------------- 统一操作状态中心(所有用户触发的写请求) ---------------- */
|
|
5461
|
+
.op-wrap { position: relative; }
|
|
5462
|
+
.op-pill { min-width: 118px; justify-content: center; }
|
|
5463
|
+
.op-pill.running { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 55%, var(--border)); }
|
|
5464
|
+
.op-pill.failed { color: var(--bad); border-color: color-mix(in srgb, var(--bad) 55%, var(--border)); }
|
|
5465
|
+
.op-pill.done { color: var(--ok); }
|
|
5466
|
+
.op-pill.running .ico { animation: op-center-spin 1.1s linear infinite; }
|
|
5467
|
+
@keyframes op-center-spin { to { transform: rotate(-360deg); } }
|
|
5468
|
+
.op-center {
|
|
5469
|
+
position: absolute; z-index: 140; top: calc(100% + 9px); right: 0;
|
|
5470
|
+
width: min(370px, calc(100vw - 24px)); max-height: min(480px, calc(100vh - 88px));
|
|
5471
|
+
overflow: hidden; border: 1px solid var(--border-strong); border-radius: 8px;
|
|
5472
|
+
background: var(--panel); box-shadow: var(--shadow-lg);
|
|
5473
|
+
}
|
|
5474
|
+
.op-center.hidden { display: none; }
|
|
5475
|
+
.op-center-head {
|
|
5476
|
+
display: flex; align-items: center; justify-content: space-between; gap: 12px;
|
|
5477
|
+
min-height: 48px; padding: 8px 10px 8px 14px; border-bottom: 1px solid var(--border);
|
|
5478
|
+
}
|
|
5479
|
+
.op-center-head b { font-size: 13px; }
|
|
5480
|
+
.op-list { max-height: min(430px, calc(100vh - 140px)); overflow: auto; }
|
|
5481
|
+
.op-row {
|
|
5482
|
+
display: grid; grid-template-columns: 10px minmax(0, 1fr) 28px; gap: 9px;
|
|
5483
|
+
align-items: start; min-height: 52px; padding: 10px 9px 10px 14px;
|
|
5484
|
+
border-bottom: 1px solid var(--border);
|
|
5485
|
+
}
|
|
5486
|
+
.op-row:last-child { border-bottom: 0; }
|
|
5487
|
+
.op-state { width: 8px; height: 8px; margin-top: 6px; border-radius: 50%; background: var(--ok); }
|
|
5488
|
+
.op-row.running .op-state { background: var(--accent); animation: pulse 1.1s ease-in-out infinite; }
|
|
5489
|
+
.op-row.failed .op-state { background: var(--bad); }
|
|
5490
|
+
.op-copy { min-width: 0; }
|
|
5491
|
+
.op-copy > div { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
|
|
5492
|
+
.op-copy b { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12.5px; }
|
|
5493
|
+
.op-copy span { flex: none; color: var(--muted); font-size: 10.5px; }
|
|
5494
|
+
.op-copy p {
|
|
5495
|
+
margin: 4px 0 0; color: var(--bad); font-size: 11px; line-height: 1.4;
|
|
5496
|
+
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
|
|
5497
|
+
}
|
|
5498
|
+
.op-dismiss {
|
|
5499
|
+
width: 26px; height: 26px; padding: 5px; border: 0; border-radius: 6px;
|
|
5500
|
+
color: var(--muted); background: transparent; cursor: pointer;
|
|
5501
|
+
}
|
|
5502
|
+
.op-dismiss:hover { color: var(--text); background: var(--panel2); }
|
|
5503
|
+
@media (max-width: 720px) {
|
|
5504
|
+
.op-pill { min-width: 34px; width: 34px; height: 32px; padding: 5px; }
|
|
5505
|
+
.op-pill #op-pill-text { display: none; }
|
|
5506
|
+
.op-center { position: fixed; top: 58px; right: 8px; width: calc(100vw - 16px); }
|
|
5507
|
+
}
|
|
5508
|
+
@media (prefers-reduced-motion: reduce) {
|
|
5509
|
+
.op-pill.running .ico, .op-row.running .op-state { animation: none; }
|
|
5510
|
+
}
|
package/package.json
CHANGED