codebee 0.1.66 → 0.1.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/CHANGELOG.md +543 -520
  2. package/LICENSE +21 -21
  3. package/README.md +11 -7
  4. package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +88 -0
  5. package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +126 -0
  6. package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
  7. package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +210 -0
  8. package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
  9. package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
  10. package/app/.playwright-cli/run-detail-after.png +0 -0
  11. package/app/core/acceptance.py +100 -0
  12. package/app/core/aiflavor.py +197 -197
  13. package/app/core/artifacts.py +148 -148
  14. package/app/core/assets.py +110 -0
  15. package/app/core/attachments.py +623 -532
  16. package/app/core/automation.py +758 -758
  17. package/app/core/backup.py +470 -470
  18. package/app/core/beekeeper.py +183 -183
  19. package/app/core/board.py +209 -209
  20. package/app/core/bookmeta.py +930 -796
  21. package/app/core/bookmeta_catalog.py +180 -180
  22. package/app/core/branching.py +137 -137
  23. package/app/core/builtin_agent.py +1234 -1234
  24. package/app/core/capability.py +144 -144
  25. package/app/core/catalog.py +42 -0
  26. package/app/core/chaptersafe.py +32 -32
  27. package/app/core/cleanup.py +303 -303
  28. package/app/core/compaction.py +201 -201
  29. package/app/core/covergen.py +356 -356
  30. package/app/core/defectretro.py +43 -0
  31. package/app/core/diagnostics.py +114 -114
  32. package/app/core/dispatch.py +173 -173
  33. package/app/core/dispatch_log.py +128 -113
  34. package/app/core/env_scrub.py +85 -85
  35. package/app/core/error_codes.py +23 -23
  36. package/app/core/errorlog.py +137 -137
  37. package/app/core/evaluation.py +174 -174
  38. package/app/core/failure_notify.py +290 -290
  39. package/app/core/flows.py +48 -2
  40. package/app/core/goal_service.py +167 -167
  41. package/app/core/health.py +374 -374
  42. package/app/core/history.py +38 -38
  43. package/app/core/hooks.py +162 -162
  44. package/app/core/jobs.py +46 -26
  45. package/app/core/knowledge.py +57 -3
  46. package/app/core/manager.py +2122 -2115
  47. package/app/core/market.py +362 -362
  48. package/app/core/market_remote.py +124 -26
  49. package/app/core/mocks.py +64 -64
  50. package/app/core/modelhub.py +51 -3
  51. package/app/core/notify.py +106 -106
  52. package/app/core/operations.py +228 -0
  53. package/app/core/paihang.py +138 -109
  54. package/app/core/paths.py +2 -2
  55. package/app/core/pipeline.py +221 -12
  56. package/app/core/planner.py +6 -1
  57. package/app/core/portguard.py +111 -111
  58. package/app/core/preview.py +293 -293
  59. package/app/core/publish/__init__.py +7 -7
  60. package/app/core/publish/auto.py +367 -367
  61. package/app/core/publish/browser.py +539 -533
  62. package/app/core/publish/fanqie.py +159 -128
  63. package/app/core/publish/flow.py +440 -432
  64. package/app/core/publish/flows-fanqie-calibrated.json +332 -332
  65. package/app/core/publish/flows-qimao-calibrated.json +7 -3
  66. package/app/core/publish/ledger.py +203 -198
  67. package/app/core/publish/manager.py +692 -623
  68. package/app/core/publish/qimao.py +141 -141
  69. package/app/core/publish/ws.py +139 -139
  70. package/app/core/redact.py +38 -38
  71. package/app/core/repeat_guard.py +129 -129
  72. package/app/core/revisions.py +51 -0
  73. package/app/core/router.py +398 -398
  74. package/app/core/runner.py +2067 -2050
  75. package/app/core/selfupdate.py +384 -384
  76. package/app/core/session_log.py +161 -161
  77. package/app/core/sessions.py +312 -312
  78. package/app/core/settings.py +149 -149
  79. package/app/core/settings_schema.py +262 -262
  80. package/app/core/share_page.py +114 -114
  81. package/app/core/skill_scan.py +122 -114
  82. package/app/core/skillpacks/fanqie-novel.md +80 -80
  83. package/app/core/skillpacks/market/character-bible.md +66 -66
  84. package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
  85. package/app/core/skillpacks/market/git-workflow.md +57 -57
  86. package/app/core/skillpacks/market/release-notes.md +72 -72
  87. package/app/core/skillpacks/market/weekly-report.md +71 -71
  88. package/app/core/skillpacks/market/worldview-consistency.md +70 -70
  89. package/app/core/skills.py +109 -2
  90. package/app/core/step_runner.py +132 -132
  91. package/app/core/store.py +278 -41
  92. package/app/core/task_compile.py +199 -199
  93. package/app/core/telemetry.py +292 -292
  94. package/app/core/tlsctx.py +64 -64
  95. package/app/core/token_meter.py +147 -147
  96. package/app/core/tracing.py +144 -0
  97. package/app/core/upstream.py +41 -41
  98. package/app/core/usage.py +862 -858
  99. package/app/core/volumes.py +389 -355
  100. package/app/core/wxdigest.py +858 -858
  101. package/app/core/zentao.py +2465 -2447
  102. package/app/main.py +2981 -2838
  103. package/app/pick_dialog.py +110 -110
  104. package/app/ui/app.js +14399 -13994
  105. package/app/ui/artdiff.js +74 -0
  106. package/app/ui/board.html +162 -162
  107. package/app/ui/board.js +305 -305
  108. package/app/ui/i18n.js +2770 -2716
  109. package/app/ui/icons/bee.svg +79 -79
  110. package/app/ui/index.html +6 -0
  111. package/app/ui/manifest.json +16 -16
  112. package/app/ui/qrcode.js +2297 -2297
  113. package/app/ui/style.css +96 -24
  114. package/bin/tutti.js +147 -147
  115. package/package.json +46 -46
  116. package/tools/wx_reader.py +261 -261
@@ -1,109 +1,138 @@
1
- # -*- coding: utf-8 -*-
2
- """扫榜选材:抓取公开排行榜页,产出选题分析素材。
3
-
4
- 数据源(双源聚合,任一失败不影响另一源):
5
- - 七猫:www.qimao.com/paihang/(公开可抓,2026-09-19 实测 200/106KB)
6
- - 番茄 Web 版:fanqienovel.com/rank(公开可抓,题材分类+书名在
7
- 中文 a 标签里;App 端接口有 SecuritySign/X-Argus 签名——那才是难路,
8
- Web 版榜单页不需要签名,别走弯路)
9
-
10
- 下载走 curl 子进程(与 covergen 同款),解析用宽松正则——页面结构变了
11
- 宁可返回空(调用方回落普通直连提示词),不做脆弱的强解析。
12
- """
13
- from __future__ import annotations
14
-
15
- import logging
16
- import re
17
-
18
- log = logging.getLogger(__name__)
19
-
20
- QIMAO_RANK_URL = "https://www.qimao.com/paihang/"
21
- FANQIE_RANK_URL = "https://fanqienovel.com/rank"
22
- _UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
23
- "(KHTML, like Gecko) Chrome/126 Safari/537.36")
24
-
25
- # 页面 UI 噪音词(非书目/分类):页面 chrome、导航、tab 名、登录/搜索等
26
- _NOISE = {"加入书架", "立即阅读", "开始阅读", "免费阅读", "全部", "分类", "排行",
27
- "排行榜", "完本", "连载中", "阅读", "书城", "男生", "女生",
28
- "帮助中心", "作家助手", "客服", "登录", "注册", "立即登录",
29
- "搜索", "热门作品排行", "大热榜", "新书榜", "完结榜", "收藏榜",
30
- "更新榜", "日榜", "月榜", "蝉联", "榜首", "热度", "免费", "畅销",
31
- "下载", "客户端", "首页", "我的", "设置", "更多", "返回", "关于"}
32
-
33
- _CJK_RE = re.compile(r">([\u4e00-\u9fa5]{2,12})<")
34
-
35
-
36
- def _curl_text(url):
37
- """curl 子进程抓页面文本;失败返回空串。"""
38
- from . import runner
39
- try:
40
- r = runner.run_process(
41
- argv=["curl", "-sS", "--max-time", "20", "-A", _UA, url],
42
- timeout=30)
43
- except Exception:
44
- return ""
45
- if not r.get("ok"):
46
- return ""
47
- return r.get("stdout") or ""
48
-
49
-
50
- def _extract_cjk(html, limit):
51
- """从 HTML 提取中文元素材(去 UI 噪音、保序去重)。"""
52
- items, seen = [], set()
53
- for t in _CJK_RE.findall(html or ""):
54
- if t in _NOISE or t in seen:
55
- continue
56
- seen.add(t)
57
- items.append(t)
58
- if len(items) >= limit:
59
- break
60
- return items
61
-
62
-
63
- def fetch_qimao_rank(limit=30):
64
- """七猫排行榜:返回元素列表,抓取失败返回 []。"""
65
- return _extract_cjk(_curl_text(QIMAO_RANK_URL), limit)
66
-
67
-
68
- def fetch_fanqie_rank(limit=30):
69
- """番茄 Web 版榜单:返回元素列表,抓取失败返回 []。"""
70
- return _extract_cjk(_curl_text(FANQIE_RANK_URL), limit)
71
-
72
-
73
- def fetch_rank_items(limit=40):
74
- """双源聚合:七猫+番茄各抓一份(去重合并,七猫在前)。
75
-
76
- 两源都失败才返回 []——单源挂了另一源仍可用,扫榜永不因单点挡任务。"""
77
- qm = fetch_qimao_rank(limit)
78
- fq = fetch_fanqie_rank(limit)
79
- merged, seen = [], set(qm)
80
- for t in fq:
81
- if t not in seen:
82
- seen.add(t)
83
- merged.append(t)
84
- return qm + merged
85
-
86
-
87
- def rank_scan_prompt(goal):
88
- """组装扫榜选材分析提示词。双源都空返回 None(调用方回落直连提示词)。"""
89
- qm = fetch_qimao_rank(30)
90
- fq = fetch_fanqie_rank(30)
91
- qm = [t for t in qm if t not in fq] # 双源重合的只留七猫份
92
- if not qm and not fq:
93
- return None
94
- blocks = []
95
- if qm:
96
- blocks.append("### 七猫排行榜素材\n" + "、".join(qm))
97
- if fq:
98
- blocks.append("### 番茄排行榜素材\n" + "、".join(fq))
99
- material = "\n\n".join(blocks)
100
- return (
101
- "你是网文选题分析师。以下是刚刚抓取的两个平台排行榜页面的书目与分类素材:\n\n"
102
- "## 榜单元素材\n%s\n\n"
103
- "## 用户想写的方向\n%s\n\n"
104
- "## 你的产出(Markdown 报告)\n"
105
- "1. **热门题材 Top3**:各自的共同特征与上榜代表书目(跨平台重合的题材单独点出——双平台都热说明是真风口)\n"
106
- "2. **高频人设/套路总结**:3-5 条,点名反复出现的元素\n"
107
- "3. **差异化切入建议**:2-3 个,结合用户方向给出「题材+人设」组合与一句话理由\n"
108
- "直接输出分析报告,不要复述素材清单,不要输出与报告无关的内容。"
109
- % (material, goal or "(用户未指定方向,按大盘热门分析)"))
1
+ # -*- coding: utf-8 -*-
2
+ """扫榜选材:抓取公开排行榜页,产出选题分析素材。
3
+
4
+ 数据源(四源聚合,任一失败不影响其余源;2026-09-24 实测全 200):
5
+ - 七猫:www.qimao.com/paihang/(106KB)
6
+ - 番茄 Web 版:fanqienovel.com/rank(题材分类+书名在中文 a 标签里;
7
+ App 端接口有 SecuritySign/X-Argus 签名——那才是难路,Web 版榜单页
8
+ 不需要签名,别走弯路)
9
+ - 起点移动版:m.qidian.com/rank(106KB,直接出书名;主站 www.qidian.com
10
+ 被 WAF 拦 202/209B,移动页不需要签名)
11
+ - 纵横:www.zongheng.com/rank(65KB,书名+统计标签混排,靠噪音词过滤)
12
+
13
+ 下载走 curl 子进程(与 covergen 同款),解析用宽松正则——页面结构变了
14
+ 宁可返回空(调用方回落普通直连提示词),不做脆弱的强解析。
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import logging
19
+ import re
20
+
21
+ log = logging.getLogger(__name__)
22
+
23
+ QIMAO_RANK_URL = "https://www.qimao.com/paihang/"
24
+ FANQIE_RANK_URL = "https://fanqienovel.com/rank"
25
+ QIDIAN_M_RANK_URL = "https://m.qidian.com/rank/"
26
+ ZONGHENG_RANK_URL = "https://www.zongheng.com/rank/"
27
+ _UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
28
+ "(KHTML, like Gecko) Chrome/126 Safari/537.36")
29
+
30
+ # 页面 UI 噪音词(非书目/分类):页面 chrome、导航、tab 名、登录/搜索、
31
+ # 榜单 tab 与统计标签(起点移动版 tab 名、纵横月票/人气/点击等)
32
+ _NOISE = {"加入书架", "立即阅读", "开始阅读", "免费阅读", "全部", "分类", "排行",
33
+ "排行榜", "完本", "连载中", "阅读", "书城", "男生", "女生",
34
+ "帮助中心", "作家助手", "客服", "登录", "注册", "立即登录",
35
+ "搜索", "热门作品排行", "大热榜", "新书榜", "完结榜", "收藏榜",
36
+ "更新榜", "日榜", "月榜", "蝉联", "榜首", "热度", "免费", "畅销",
37
+ "下载", "客户端", "首页", "我的", "设置", "更多", "返回", "关于",
38
+ "月票榜", "阅读榜", "书友榜", "推荐榜", "签约榜", "新人榜",
39
+ "畅销榜", "大家都在搜", "男生小说排行榜", "女生小说排行榜",
40
+ "男频阅读榜", "女频阅读榜", "男频新书榜", "番茄小说",
41
+ "登录后获得更多特色功能", "安装起点读书客户端", "看更多正版好书",
42
+ "月票", "人气", "点击", "万字", "推荐", "收藏", "鲜花", "打赏", "推荐票"}
43
+
44
+ _CJK_RE = re.compile(r">([\u4e00-\u9fa5]{2,12})<")
45
+
46
+
47
+ def _curl_text(url):
48
+ """curl 子进程抓页面文本;失败返回空串。"""
49
+ from . import runner
50
+ try:
51
+ r = runner.run_process(
52
+ argv=["curl", "-sS", "--max-time", "20", "-A", _UA, url],
53
+ timeout=30)
54
+ except Exception:
55
+ return ""
56
+ if not r.get("ok"):
57
+ return ""
58
+ return r.get("stdout") or ""
59
+
60
+
61
+ def _extract_cjk(html, limit):
62
+ """从 HTML 提取中文元素材(去 UI 噪音、保序去重)。"""
63
+ items, seen = [], set()
64
+ for t in _CJK_RE.findall(html or ""):
65
+ if t in _NOISE or t in seen:
66
+ continue
67
+ seen.add(t)
68
+ items.append(t)
69
+ if len(items) >= limit:
70
+ break
71
+ return items
72
+
73
+
74
+ def fetch_qimao_rank(limit=30):
75
+ """七猫排行榜:返回元素列表,抓取失败返回 []。"""
76
+ return _extract_cjk(_curl_text(QIMAO_RANK_URL), limit)
77
+
78
+
79
+ def fetch_fanqie_rank(limit=30):
80
+ """番茄 Web 版榜单:返回元素列表,抓取失败返回 []。"""
81
+ return _extract_cjk(_curl_text(FANQIE_RANK_URL), limit)
82
+
83
+
84
+ def fetch_qidian_rank(limit=30):
85
+ """起点移动版榜单:返回元素列表,抓取失败返回 []。"""
86
+ return _extract_cjk(_curl_text(QIDIAN_M_RANK_URL), limit)
87
+
88
+
89
+ def fetch_zongheng_rank(limit=30):
90
+ """纵横排行榜:返回元素列表,抓取失败返回 []。"""
91
+ return _extract_cjk(_curl_text(ZONGHENG_RANK_URL), limit)
92
+
93
+
94
+ # 源清单:顺序即素材拼接顺序(七猫/番茄为元老源保持在前)。
95
+ # 存函数名、调用期 globals() 解析——mock.patch 换模块属性才能生效
96
+ # (存函数引用会把定义期原函数钉死,测试会穿透打真网络)。
97
+ _SOURCES = (
98
+ ("七猫排行榜素材", "fetch_qimao_rank"),
99
+ ("番茄排行榜素材", "fetch_fanqie_rank"),
100
+ ("起点排行榜素材", "fetch_qidian_rank"),
101
+ ("纵横排行榜素材", "fetch_zongheng_rank"),
102
+ )
103
+
104
+
105
+ def fetch_rank_items(limit=40):
106
+ """四源聚合:各抓一份(跨源去重合并,按源清单顺序)。
107
+
108
+ 全部失败才返回 []——单源挂了其余源仍可用,扫榜永不因单点挡任务。"""
109
+ merged, seen = [], set()
110
+ for _label, fname in _SOURCES:
111
+ for t in globals()[fname](limit):
112
+ if t not in seen:
113
+ seen.add(t)
114
+ merged.append(t)
115
+ return merged
116
+
117
+
118
+ def rank_scan_prompt(goal):
119
+ """组装扫榜选材分析提示词。四源全空返回 None(调用方回落直连提示词)。"""
120
+ blocks, seen = [], set()
121
+ for label, fname in _SOURCES:
122
+ items = [t for t in globals()[fname](30) if t not in seen]
123
+ seen.update(items)
124
+ if items:
125
+ blocks.append("### %s\n%s" % (label, "、".join(items)))
126
+ if not blocks:
127
+ return None
128
+ material = "\n\n".join(blocks)
129
+ return (
130
+ "你是网文选题分析师。以下是刚刚抓取的各平台排行榜页面的书目与分类素材:\n\n"
131
+ "## 榜单元素材\n%s\n\n"
132
+ "## 用户想写的方向\n%s\n\n"
133
+ "## 你的产出(Markdown 报告)\n"
134
+ "1. **热门题材 Top3**:各自的共同特征与上榜代表书目(跨平台重合的题材单独点出——多平台都热说明是真风口)\n"
135
+ "2. **高频人设/套路总结**:3-5 条,点名反复出现的元素\n"
136
+ "3. **差异化切入建议**:2-3 个,结合用户方向给出「题材+人设」组合与一句话理由\n"
137
+ "直接输出分析报告,不要复述素材清单,不要输出与报告无关的内容。"
138
+ % (material, goal or "(用户未指定方向,按大盘热门分析)"))
package/app/core/paths.py CHANGED
@@ -54,8 +54,8 @@ def default_data_dir(repo=None, environ=None) -> Path:
54
54
  DATA_DIR = default_data_dir()
55
55
  TASKS_DIR = DATA_DIR / "tasks"
56
56
  RUNS_DIR = DATA_DIR / "runs"
57
- USAGE_DIR = DATA_DIR / "usage"
58
- ERRORS_DIR = DATA_DIR / "errors"
57
+ USAGE_DIR = DATA_DIR / "usage"
58
+ ERRORS_DIR = DATA_DIR / "errors"
59
59
  PUBLISH_DIR = DATA_DIR / "publish" # 一键发布:台账/作品登记/流程选择器覆盖/浏览器 profile
60
60
  CATALOG_FILE = DATA_DIR / "catalog.json"
61
61
  ENABLED_FILE = DATA_DIR / "orchestration.json"
@@ -19,7 +19,7 @@ import re
19
19
  import threading
20
20
  import time
21
21
 
22
- from . import aiflavor, attachments, branching, catalog, chaptersafe, dispatch_log, history, hooks, jobs, knowledge, manager, modelhub, mocks, paihang, planner, registry, router, runner, skills, store, task_compile, usage, volumes
22
+ from . import aiflavor, attachments, branching, catalog, chaptersafe, defectretro, dispatch_log, flows, history, hooks, jobs, knowledge, manager, modelhub, mocks, paihang, planner, registry, router, runner, skills, store, task_compile, usage, volumes
23
23
  from . import builtin_agent
24
24
  from . import diagnostics
25
25
  from . import paths as paths_mod
@@ -365,6 +365,44 @@ def _binding_dead_msg(agent):
365
365
  "请在「模型调度(可选)」页为该 CLI 指定已启用的供应商")
366
366
 
367
367
 
368
+ def _binding_brief(agent):
369
+ """步骤级绑定速记(借鉴 WorkDSH ResolvedExecutionBinding:模型目录只是发现
370
+ 信息,解析结果才是权威)。把本步实际执行用的 provider 链与上游 host 压成
371
+ 一行——配额烧错家/模型漂移直接对账步骤行,不必翻日志文本里的审计行。
372
+ 只记身份不记密钥:env 形态的注入也只取 URL 的 host 部分。"""
373
+ from . import upstream as _up
374
+ entries = []
375
+ for item in (agent.get("call_chain") or [])[:4]:
376
+ if not isinstance(item, dict):
377
+ continue
378
+ pid = str(item.get("provider_id") or "").strip()
379
+ prov = item.get("provider") if isinstance(item.get("provider"), dict) else {}
380
+ host = ""
381
+ try:
382
+ host = _up.normalize_upstream(
383
+ str(prov.get("base_url") or prov.get("baseUrl") or ""))
384
+ except Exception:
385
+ host = ""
386
+ if pid and host and host not in pid:
387
+ entries.append("%s@%s" % (pid, host))
388
+ else:
389
+ entries.append(pid or host or "?")
390
+ if not entries:
391
+ # env 注入形态(如 ANTHROPIC_BASE_URL):URL 值的 host 不敏感,可入账
392
+ for k, v in sorted((agent.get("env") or {}).items()):
393
+ s = str(v or "")
394
+ if s.startswith(("http://", "https://")):
395
+ try:
396
+ host = _up.normalize_upstream(s)
397
+ except Exception:
398
+ continue
399
+ if host:
400
+ entries.append("%s=%s" % (str(k)[:24], host))
401
+ if len(entries) >= 3:
402
+ break
403
+ return ",".join(entries[:3])[:120]
404
+
405
+
368
406
  def _run_step(run_id, role, agent, prompt, workdir, readonly, ev, timeout=runner.DEFAULT_TIMEOUT, note="", resume=None, images=None, require_tools=False, swap_guard=None):
369
407
  """执行一个智能体步骤并记录。返回 runner 统一结果。
370
408
 
@@ -402,7 +440,8 @@ def _run_step(run_id, role, agent, prompt, workdir, readonly, ev, timeout=runner
402
440
  note = ((note + ";") if note else "") + "⚠ " + dead_msg
403
441
  step, log_abs = store.add_step(run_id, role, agent["id"],
404
442
  agent.get("label", agent["id"]), note=note,
405
- model=agent.get("model") or "")
443
+ model=agent.get("model") or "",
444
+ provider=_binding_brief(agent))
406
445
  start = time.time()
407
446
  if dead_binding:
408
447
  from .error_codes import ErrorCode
@@ -473,7 +512,8 @@ def _run_builtin_step(run_id, role, bi, prompt, workdir, ev, note="", images=Non
473
512
  _wait_gate(run_id, ev)
474
513
  deadline = _ensure_budget(run_id)
475
514
  step, log_abs = store.add_step(run_id, role, "builtin", "CodeBee", note=note,
476
- model=bi.get("model") or "")
515
+ model=bi.get("model") or "",
516
+ provider=str(bi.get("provider_id") or "")[:120])
477
517
  start = time.time()
478
518
  agent_pseudo = {"id": "builtin", "label": "CodeBee", "kind": "builtin", "mode": "real",
479
519
  "provider": {"id": bi.get("provider_id") or "",
@@ -650,9 +690,15 @@ def _finish_step_result(run_id, step, res, role, agent, start):
650
690
  status = "timeout"
651
691
  else:
652
692
  status = "done" if res["ok"] else "failed"
693
+ step_summary = (res.get("text") or res.get("error") or "")[:600]
694
+ if raw.get("quiescence_unknown"):
695
+ # 杀树未确认清空(借鉴 WorkDSH 取消结算分层:kill 完成 ≠ 停稳):
696
+ # 孙进程可能仍在跑,孤儿清扫稍后兜底。只提示不改变终态语义。
697
+ step_summary = ((step_summary + "\n") if step_summary else "") + \
698
+ "⚠ 进程树未确认清空:孙进程可能仍在运行(孤儿清扫稍后兜底)"
653
699
  store.finish_step(run_id, step["n"],
654
700
  status,
655
- summary=((res.get("text") or res.get("error") or "")[:600]),
701
+ summary=step_summary,
656
702
  exit_code=res.get("raw", {}).get("exit_code"),
657
703
  cost_usd=res.get("cost_usd", 0.0),
658
704
  tokens=res.get("tokens", 0),
@@ -783,6 +829,7 @@ __CONTEXT__
783
829
  不要自造评分标准或质量闸门;超出任务目标的「顺手加固」一律不做。
784
830
  真实风险照样要找:发现就写进总结,由人决定要不要修。
785
831
  - 完成后只用 3-5 句话总结你改了什么、为什么。
832
+ - 交付物从简:回复不写开场白、不复述任务、不堆与改动无关的解释——代码、说明、总结直接给干货(借鉴 claude-token-efficient:输出 token 也是真金白银)。
786
833
  __VERIFY_HINT__"""
787
834
 
788
835
  CODE_FIX_PROMPT = """你是一名高级工程师。你之前的实现没有通过验收,请在当前工作目录中直接修复(直接修改/创建文件)。创建/写入的文件一律以 UTF-8 编码保存(PowerShell 显式加 -Encoding UTF8,禁止依赖系统默认编码)。
@@ -1915,6 +1962,10 @@ def _run_direct(run, task, agents, ev, stats, mode):
1915
1962
  # 扫榜选材(借鉴 oh-story 扫榜):抓七猫排行榜公开数据注入,
1916
1963
  # AI 做选题洞察;抓取失败回落普通直连提示词
1917
1964
  prompt = paihang.rank_scan_prompt(task.get("goal") or "") or ""
1965
+ if task.get("type") == "defect_retro" and bi is not None:
1966
+ # 缺陷复盘(借鉴 test-defect-retrospective):三视角分析框架
1967
+ # 注入,缺陷导出 CSV 走任务附件通道(框架恒有、不回落)
1968
+ prompt = defectretro.retro_prompt(task.get("goal") or "")
1918
1969
  if not prompt:
1919
1970
  if bi is not None:
1920
1971
  prompt = (BUILTIN_DIRECT_PROMPT
@@ -2038,6 +2089,10 @@ CONTENT_DELIVERY_CONTRACTS = {
2038
2089
  "novel": ("小说作者", [
2039
2090
  "遵守用户给定的题材、篇幅、视角和风格;未给出的核心设定不要擅自扩张。",
2040
2091
  "用场景、行动和对话推进冲突,人物动机与前后因果保持一致。",
2092
+ # Madman-Architect-Carpenter 写作框架(writing-with-agents 借鉴;Judge=评审链已有):
2093
+ # 先发散倾倒念头再组织结构成文,不要边写边自我审查把点子掐死在半路
2094
+ "成文分三步:先把不设限的情节/画面/台词念头快速倾倒成素材清单,再挑出主线组织成"
2095
+ "结构,最后按结构成文——初稿期不做质量审判。",
2041
2096
  ]),
2042
2097
  "article": ("平台内容主编", [
2043
2098
  "标题、开头钩子、正文层级和结尾行动建议要适配目标平台与读者。",
@@ -2079,6 +2134,9 @@ CONTENT_DELIVERY_CONTRACTS = {
2079
2134
  # sepia 分场合规则(PR 回复体裁):先答再铺陈;篇幅与利害成正比
2080
2135
  "第一句/第一段先给结论或答复(对方要做什么、答应还是不答应),再给必要背景。",
2081
2136
  "请求具体到动作与截止时间;篇幅与事情轻重成正比,删掉礼节性空话与自我表扬。",
2137
+ # agentic-inbox 借鉴(线程级交付契约):回复是对话不是独白
2138
+ "回复往来邮件时逐条回应对方提出的每个问题与请求(可用清单),不漏问、不回避;"
2139
+ "行动项落到具体动作+负责人+截止时间。",
2082
2140
  ]),
2083
2141
  "tech_proposal": ("解决方案架构师", [
2084
2142
  "覆盖现状与目标、约束、候选方案对比、推荐架构、实施阶段、风险与回滚、验收指标。",
@@ -2117,7 +2175,9 @@ def _content_contract(task):
2117
2175
  return "\n\n## 本类型交付约束\n" + "\n".join("- " + item for item in spec[1])
2118
2176
 
2119
2177
  # 调研报告类稿件的追加要求(借鉴 gpt-researcher 迭代深研):有网络/读文件工具时
2120
- # 多源交叉验证,单源结论降权——调研的可信度来自证据链而非文采
2178
+ # 多源交叉验证,单源结论降权——调研的可信度来自证据链而非文采。
2179
+ # 来源冲突裁决借鉴 deepresearch-agent 记忆层矛盾检测(Majority/Source-Weight 消解);
2180
+ # 子问题缺口补查借鉴 dzhng/deep-research 迭代循环
2121
2181
  RESEARCH_APPENDIX = """
2122
2182
 
2123
2183
  ## 调研要求(证据链)
@@ -2125,6 +2185,11 @@ RESEARCH_APPENDIX = """
2125
2185
  单源信息要标注「仅单一来源」。
2126
2186
  - 引用来源在文中用行内链接或脚注标明(域名即可,不编造 URL)。
2127
2187
  - 区分「事实」与「观点」:数据/时间/版本号给来源,预测/评价标明是分析。
2188
+ - 来源冲突显式裁决:同一事实多个来源说法不一致时,写明分歧点与各方依据,给出
2189
+ 倾向判断和理由(多数来源一致取多数,权威差异按来源可信度加权)——不各说一半,
2190
+ 也不悄悄只取其一。
2191
+ - 缺口驱动补查:起草前先列出报告要回答的子问题清单,逐个核对证据是否到位;
2192
+ 没找到答案的子问题明确标注「证据不足」,不用泛泛而谈填空。
2128
2193
  - 结构硬性要求:报告第一段必须是「**核心结论**」三行以内的要点摘要(结论先行),
2129
2194
  之后才展开分层论证 → 风险与局限(说明哪些结论证据不足)。"""
2130
2195
 
@@ -2144,10 +2209,10 @@ NOVEL_CRITIQUE_PROMPT = """你是严格的评审(不要使用任何工具、
2144
2209
  请输出一个 ```json 代码块,不要输出其他内容。JSON 结构:
2145
2210
  {
2146
2211
  "scores": {"__DIMKEYS__"},
2147
- "issues": [{"dim": "维度名", "severity": "major|minor", "note": "具体问题"}],
2212
+ "issues": [{"dim": "维度名", "severity": "major|minor", "note": "具体问题", "quote": "支撑该问题的稿件原文连续片段(≥8字,逐字摘录不许改写)"}],
2148
2213
  "summary": "一句话总评"
2149
2214
  }
2150
- 每个维度打 1-10 分(可为小数),宁严勿宽。
2215
+ 每个维度打 1-10 分(可为小数),宁严勿宽。major 问题必须给 quote(系统会逐条校验引文是否真在稿件中——编造的引文会被降档标记)。
2151
2216
 
2152
2217
  ## 待评审稿件
2153
2218
  ---
@@ -2260,6 +2325,18 @@ def _read_ledger(workdir):
2260
2325
  return txt or "(暂无记录)"
2261
2326
 
2262
2327
 
2328
+ def _read_ledger_raw(workdir):
2329
+ """账本原文(缺文件/读失败返回空串)——供 _ledger_watchlist 解析,
2330
+ 不带注入面的兜底文案。"""
2331
+ p = os.path.abspath(os.path.join(str(workdir or ""), LEDGER_FILE))
2332
+ if not _inside(workdir, p) or not os.path.isfile(p):
2333
+ return ""
2334
+ try:
2335
+ return _read_text_any_enc(p)
2336
+ except OSError:
2337
+ return ""
2338
+
2339
+
2263
2340
  def _shrink_context_block(sk_block, bible, budget=12000):
2264
2341
  """分层上下文降级(长提示词在容量受限通道上会挂起/秒拒,2026-09-17 讯飞实测)。
2265
2342
 
@@ -2369,10 +2446,11 @@ SERIAL_GLOBAL_PROMPT = """你是网文主编(不要修改任何文件)。全
2369
2446
  请输出一个 ```json 代码块,不要输出其他内容。JSON 结构:
2370
2447
  {
2371
2448
  "scores": {"__DIMKEYS__"},
2372
- "issues": [{"dim": "维度名", "severity": "major|minor", "note": "具体问题(指明哪一章)"}],
2449
+ "issues": [{"dim": "维度名", "severity": "major|minor", "note": "具体问题(指明哪一章)", "quote": "支撑该问题的稿件原文连续片段(≥8字,逐字摘录不许改写)"}],
2373
2450
  "summary": "一句话总评:是否达到可签约水平"
2374
2451
  }
2375
2452
  每个维度打 1-10 分,宁严勿宽。重点关注:主线一致性、人物弧光、节奏、爽点密度、完本感。
2453
+ major 问题必须给 quote(系统会逐条校验引文是否真在稿件中——编造的引文会被降档标记)。
2376
2454
 
2377
2455
  ## 全书目标
2378
2456
  __GOAL__
@@ -2446,6 +2524,87 @@ def _wc(text):
2446
2524
  return len(re.sub(r"\s", "", text or ""))
2447
2525
 
2448
2526
 
2527
+ _ANCHOR_EDGE_PUNCT = "。!?,、;:…—·,.!?;:\"'“”‘’()()[]【】《》<>*#-"
2528
+
2529
+
2530
+ def _norm_anchor(s):
2531
+ """锚校验归一:去全部空白 + 剥首尾标点(引文摘录常多带一个句号/引号,
2532
+ 中英混排空格差异也不应判假)。"""
2533
+ s = re.sub(r"\s+", "", str(s or ""))
2534
+ return s.strip(_ANCHOR_EDGE_PUNCT)
2535
+
2536
+
2537
+ def _anchor_issues(issues, manuscript):
2538
+ """评审证据锚(ARIS Anti-Autoresearch 借鉴):issue 带 quote(稿件原文
2539
+ 摘录)时做确定性 containment 校验——模型只提议发现,锚定事实由规则裁决。
2540
+
2541
+ 三态:quote 命中稿件 → anchored=True;quote 给了但对不上 → anchored=False
2542
+ (降档殿后+标记,不删除:模型改写式引用时保留可见性);没给 quote →
2543
+ anchored=None(老格式评审灰度兼容,不标记不排序影响)。返回 (issues, 未锚定数)。"""
2544
+ ms = _norm_anchor(manuscript)
2545
+ unanchored = 0
2546
+ for it in issues or []:
2547
+ q = _norm_anchor(it.get("quote"))
2548
+ if not q:
2549
+ it["anchored"] = None
2550
+ continue
2551
+ it["anchored"] = len(q) >= 6 and q in ms
2552
+ if it["anchored"] is False:
2553
+ unanchored += 1
2554
+ return issues, unanchored
2555
+
2556
+
2557
+ def _major_lines(majors, limit=8):
2558
+ """majors → 修订面意见行:锚定的在前,未锚定殿后并带标记(模型据此
2559
+ 分配采信权重);无 quote 的老格式行居中不动。"""
2560
+ ranked = sorted(majors or [],
2561
+ key=lambda x: {True: 0, False: 2}.get(x.get("anchored"), 1))
2562
+ lines = []
2563
+ for x in ranked[:limit]:
2564
+ mark = ("(未锚定:引文与稿件不符,酌情采信)"
2565
+ if x.get("anchored") is False else "")
2566
+ lines.append("- [%s] %s%s" % (x.get("dim", "?"),
2567
+ str(x.get("note", ""))[:140], mark))
2568
+ return lines
2569
+
2570
+
2571
+ _LEDGER_DONE_WORDS = ("已兑现", "已回收", "已解除", "已完成", "已了结", "已交付")
2572
+
2573
+
2574
+ def _ledger_watchlist(ledger_text, upto_chapter, min_age=3, limit=8):
2575
+ """陈年承诺督促(continuum 承诺/伏笔账借鉴):按「## 第 N 章」段解析
2576
+ 资源账本,承诺/伏笔类条目现状未了结、且距本章 ≥min_age 章未更新 →
2577
+ 观查清单(推送起草面督促推进/回收)。账本按章追加、同名条目以最后
2578
+ 出现的章为准;老账本无该类条目返回空。"""
2579
+ last = {} # (类型, 名称) -> (最后章号, 现状)
2580
+ cur = 0
2581
+ for ln in str(ledger_text or "").splitlines():
2582
+ m = re.match(r"##\s*第\s*(\d+)\s*章", ln.strip())
2583
+ if m:
2584
+ cur = int(m.group(1))
2585
+ continue
2586
+ s = ln.strip().lstrip("-*").strip()
2587
+ if not s or "|" not in s:
2588
+ continue
2589
+ parts = [p.strip() for p in s.split("|")]
2590
+ if len(parts) < 3:
2591
+ continue
2592
+ typ, name, status = parts[0], parts[1], "|".join(parts[2:])
2593
+ if "承诺" not in typ and "伏笔" not in typ:
2594
+ continue
2595
+ last[(typ, name)] = (cur, status)
2596
+ out = []
2597
+ for (typ, name), (ch_no, status) in last.items():
2598
+ if any(w in status for w in _LEDGER_DONE_WORDS):
2599
+ continue
2600
+ age = upto_chapter - ch_no
2601
+ if age >= min_age:
2602
+ out.append({"type": typ, "name": name, "status": status,
2603
+ "age": age, "last_chapter": ch_no})
2604
+ out.sort(key=lambda x: -x["age"])
2605
+ return out[:limit]
2606
+
2607
+
2449
2608
  def _write_chapter(workdir, i, text):
2450
2609
  # 安全落盘委托(inkos 安全章节工作区借鉴):tmp+原子改名,写一半
2451
2610
  # 崩溃不留半章冒充成稿;路径守卫在 chaptersafe(resolve+parents)
@@ -2702,6 +2861,19 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
2702
2861
  ev_check_lines = [[]] # 每章重置:第一份非空评审的逐项判定
2703
2862
  ledger_lines = [] # 本章全部评审的账本增量并集
2704
2863
  ledger_txt = _read_ledger(workdir) # 截至上一章的资源账本(起草注入)
2864
+ # 陈年承诺督促(continuum 借鉴):账本里 ≥3 章未推进且未了结的承诺/
2865
+ # 伏笔点名给起草面——「本章推进/回收其一,或明确有意留白」;老账本
2866
+ # 无承诺类条目时静默不占字节
2867
+ try:
2868
+ _wl = _ledger_watchlist(_read_ledger_raw(workdir), i)
2869
+ if _wl:
2870
+ ledger_txt += ("\n\n### ⚠ 陈年承诺/伏笔(多章未推进)\n"
2871
+ + "\n".join("- %s「%s」:%s(已 %d 章未更新——本章应推进、"
2872
+ "回收其一,或在大纲允许处明确留白)"
2873
+ % (w["type"], w["name"], w["status"], w["age"])
2874
+ for w in _wl))
2875
+ except Exception:
2876
+ pass
2705
2877
  prev = ""
2706
2878
  draft_sid = "" # §07 T1.1:本轮 draft/复用章的会话 id(revise 复用;reuse 时为空)
2707
2879
  if i > 1:
@@ -3234,9 +3406,11 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
3234
3406
  rounds_used = 2
3235
3407
  majors = [x for x in issues_all if x.get("chapter") == i
3236
3408
  and x.get("severity") == "major"][:8]
3409
+ majors, _unanchored = _anchor_issues(majors, text)
3237
3410
  crit_lines = ["- %s:%.1f(章阈值 %.1f)" % (d, means[d], threshold_ch) for d in dims]
3238
- crit_lines += ["- [%s] %s" % (x.get("dim", "?"), str(x.get("note", ""))[:140])
3239
- for x in majors]
3411
+ crit_lines += _major_lines(majors)
3412
+ if _unanchored:
3413
+ crit_lines.append("- ⚠ %d 条 major 意见的引文未能在稿件中锚定,已降序殿后" % _unanchored)
3240
3414
  if rnd == 1 and race_losers and impl.get("mode") == "real":
3241
3415
  # 赛马败者精华回收(连载版):一次选择器调用提炼落选稿优点,
3242
3416
  # 与评审意见一并喂给首轮修订;失败静默不影响修订
@@ -3962,8 +4136,10 @@ def _run_content_review(run, task, agents, ev, stats, mode):
3962
4136
  # 修订
3963
4137
  crit_lines = ["- %s:%.1f(阈值 %.1f)" % (d, means[d], threshold) for d in dims]
3964
4138
  majors = [i for i in issues_all if i.get("severity") == "major"][:8]
3965
- for i in majors:
3966
- crit_lines.append("- [%s] %s" % (i.get("dim", "?"), str(i.get("note", ""))[:120]))
4139
+ majors, _unanchored = _anchor_issues(majors, manuscript)
4140
+ crit_lines += _major_lines(majors)
4141
+ if _unanchored:
4142
+ crit_lines.append("- ⚠ %d 条 major 意见的引文未能在稿件中锚定,已降序殿后" % _unanchored)
3967
4143
  if r == 1 and bestof_improvements:
3968
4144
  # 败者精华回收(freebuff suggestedImprovements 的落地):终审从落选
3969
4145
  # 候选稿提炼的优点,首轮修订时与评审意见一并喂给作者
@@ -4387,6 +4563,39 @@ def execute_run(run_id):
4387
4563
  store.update_run(run_id, expected_status="running", status="failed",
4388
4564
  error="找不到任务 %s" % run.get("task_id"), ended_at=_now())
4389
4565
  return
4566
+ # 流程修订漂移诊断(借鉴 WorkDSH ADR-0010 修订钉住):任务参数创建时已
4567
+ # 固化,改流程不影响固化字段;但历史任务缺失字段的兜底仍取当前流程定义,
4568
+ # 续跑/重试可能静默吃到新流程。漂移不阻断——「改完流程再重跑」是合法
4569
+ # 路径——但必须留痕可对账:run 记结构化 flow_drift + 用户可见 warning。
4570
+ _drift = flows.flow_drift(task)
4571
+ if _drift:
4572
+ store.update_run(run_id, flow_drift=_drift)
4573
+ try:
4574
+ _warns = (store.get_run(run_id) or {}).get("warnings") or []
4575
+ store.update_run(run_id, warnings=_warns + [
4576
+ "流程「%s」在任务创建后已修改(%s → %s);任务固化参数不受影响,"
4577
+ "历史任务缺失字段的兜底取值已随新定义"
4578
+ % (_drift["flow"], _drift["pinned"][:8], _drift["current"][:8])])
4579
+ except Exception:
4580
+ pass
4581
+ print("[CodeBee] 流程漂移:%s(任务 %s 创建后流程已修改 %s → %s)"
4582
+ % (run_id, task.get("id"), _drift["pinned"][:8], _drift["current"][:8]),
4583
+ flush=True)
4584
+ # 附件完整性对账(借鉴 WorkDSH 输入引用修订):任务钉的附件指纹 vs 当前
4585
+ # 文件内容。换过/删过给明确诊断——续跑吃到「不是当初那份附件」不能无声。
4586
+ try:
4587
+ _adrift = attachments.verify_task(task, task.get("workdir"))
4588
+ if _adrift:
4589
+ store.update_run(run_id, attachment_drift=_adrift)
4590
+ _names = "、".join(d["name"] for d in _adrift[:3]) + (
4591
+ " 等 %d 项" % len(_adrift) if len(_adrift) > 3 else "")
4592
+ _warns2 = (store.get_run(run_id) or {}).get("warnings") or []
4593
+ store.update_run(run_id, warnings=_warns2 + [
4594
+ "附件在任务创建后已变化(%s):%s;本次执行使用当前文件内容"
4595
+ % (", ".join(sorted({d["status"] for d in _adrift})), _names)])
4596
+ print("[CodeBee] 附件漂移:%s(%s)" % (run_id, _names), flush=True)
4597
+ except Exception:
4598
+ pass
4390
4599
  try:
4391
4600
  _ensure_budget(run_id)
4392
4601
  except TaskTimeout: