codebee 0.1.76 → 0.1.78

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 (115) hide show
  1. package/CHANGELOG.md +633 -624
  2. package/LICENSE +21 -21
  3. package/README.md +508 -509
  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 -100
  12. package/app/core/aiflavor.py +197 -197
  13. package/app/core/analytics.py +38 -38
  14. package/app/core/artifacts.py +148 -148
  15. package/app/core/assets.py +110 -110
  16. package/app/core/attachments.py +623 -623
  17. package/app/core/audit.py +30 -30
  18. package/app/core/backup.py +470 -470
  19. package/app/core/beekeeper.py +183 -183
  20. package/app/core/benchstore.py +95 -95
  21. package/app/core/board.py +209 -209
  22. package/app/core/bookmeta.py +1036 -1036
  23. package/app/core/bookmeta_catalog.py +180 -180
  24. package/app/core/branching.py +137 -137
  25. package/app/core/builtin_agent.py +1680 -1680
  26. package/app/core/capability.py +144 -144
  27. package/app/core/chaptersafe.py +32 -32
  28. package/app/core/cleanup.py +303 -303
  29. package/app/core/compaction.py +201 -201
  30. package/app/core/contracts.py +255 -255
  31. package/app/core/covergen.py +356 -356
  32. package/app/core/defectretro.py +43 -43
  33. package/app/core/diagnostics.py +114 -114
  34. package/app/core/dispatch.py +190 -190
  35. package/app/core/dispatch_log.py +128 -128
  36. package/app/core/env_scrub.py +85 -85
  37. package/app/core/errorlog.py +137 -137
  38. package/app/core/evaluation.py +174 -174
  39. package/app/core/failure_notify.py +290 -290
  40. package/app/core/goal_service.py +167 -167
  41. package/app/core/health.py +389 -389
  42. package/app/core/history.py +38 -38
  43. package/app/core/hooks.py +162 -162
  44. package/app/core/manager.py +2232 -2232
  45. package/app/core/mocks.py +64 -64
  46. package/app/core/notify.py +201 -201
  47. package/app/core/novel_quality.py +189 -189
  48. package/app/core/openapi.py +154 -154
  49. package/app/core/operations.py +228 -228
  50. package/app/core/paihang.py +145 -145
  51. package/app/core/pipeline.py +29 -0
  52. package/app/core/planner.py +88 -0
  53. package/app/core/portguard.py +111 -111
  54. package/app/core/presence.py +77 -77
  55. package/app/core/preview.py +293 -293
  56. package/app/core/prompt_guard.py +78 -78
  57. package/app/core/publish/__init__.py +7 -7
  58. package/app/core/publish/browser.py +655 -655
  59. package/app/core/publish/fanqie.py +165 -165
  60. package/app/core/publish/flow.py +640 -640
  61. package/app/core/publish/flows-fanqie-calibrated.json +336 -336
  62. package/app/core/publish/manager.py +1118 -1118
  63. package/app/core/publish/qimao.py +176 -176
  64. package/app/core/publish/ws.py +139 -139
  65. package/app/core/quality_gate.py +194 -194
  66. package/app/core/redact.py +38 -38
  67. package/app/core/repeat_guard.py +129 -129
  68. package/app/core/retrieval.py +137 -137
  69. package/app/core/router.py +398 -398
  70. package/app/core/runner.py +2238 -2238
  71. package/app/core/selfupdate.py +384 -384
  72. package/app/core/session_log.py +161 -161
  73. package/app/core/sessions.py +312 -312
  74. package/app/core/settings_schema.py +270 -270
  75. package/app/core/share_page.py +114 -114
  76. package/app/core/skill_scan.py +139 -139
  77. package/app/core/skillpacks/market/character-bible.md +66 -66
  78. package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
  79. package/app/core/skillpacks/market/git-workflow.md +57 -57
  80. package/app/core/skillpacks/market/release-notes.md +72 -72
  81. package/app/core/skillpacks/market/weekly-report.md +71 -71
  82. package/app/core/skillpacks/market/worldview-consistency.md +70 -70
  83. package/app/core/skills.py +1 -1
  84. package/app/core/step_runner.py +132 -132
  85. package/app/core/story_tracking.py +275 -275
  86. package/app/core/task_compile.py +199 -199
  87. package/app/core/telemetry.py +292 -292
  88. package/app/core/tlsctx.py +64 -64
  89. package/app/core/token_meter.py +147 -147
  90. package/app/core/tracing.py +144 -144
  91. package/app/core/upstream.py +41 -41
  92. package/app/core/usage.py +1035 -1035
  93. package/app/core/volumes.py +389 -389
  94. package/app/core/wxdigest.py +858 -858
  95. package/app/core/zentao.py +2692 -2692
  96. package/app/mcp_server.py +244 -244
  97. package/app/output/_createbook_body.json +1 -0
  98. package/app/output/_probe_fq_click.py +50 -0
  99. package/app/output/_probe_fq_form.py +42 -0
  100. package/app/output/_probe_fq_modal.py +55 -0
  101. package/app/output/_probe_fq_points.py +71 -0
  102. package/app/output/_probe_fq_submit.py +34 -0
  103. package/app/output/_probe_fq_watch.py +62 -0
  104. package/app/pick_dialog.py +110 -110
  105. package/app/ui/artdiff.js +74 -74
  106. package/app/ui/board.html +162 -162
  107. package/app/ui/board.js +305 -305
  108. package/app/ui/i18n.js +2 -2
  109. package/app/ui/icons/bee.svg +79 -79
  110. package/app/ui/manifest.json +16 -16
  111. package/app/ui/qrcode.js +2297 -2297
  112. package/app/ui/style.css +7086 -7086
  113. package/bin/tutti.js +147 -147
  114. package/package.json +48 -48
  115. package/tools/wx_reader.py +261 -261
@@ -1,145 +1,145 @@
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
-
121
- 报告契约含逐条 A-E 跨平台证据分级(借鉴 career-ops 的分级报告交付形态):
122
- 分级只认素材实际出现情况——抑制单源孤证被模型放大成「大热题材」。"""
123
- blocks, seen = [], set()
124
- for label, fname in _SOURCES:
125
- items = [t for t in globals()[fname](30) if t not in seen]
126
- seen.update(items)
127
- if items:
128
- blocks.append("### %s\n%s" % (label, "、".join(items)))
129
- if not blocks:
130
- return None
131
- material = "\n\n".join(blocks)
132
- return (
133
- "你是网文选题分析师。以下是刚刚抓取的各平台排行榜页面的书目与分类素材:\n\n"
134
- "## 榜单元素材\n%s\n\n"
135
- "## 用户想写的方向\n%s\n\n"
136
- "## 你的产出(Markdown 报告)\n"
137
- "报告第一行给图例(一行):「证据分级:A=三平台及以上重合,B=两平台重合,"
138
- "C=单平台多次出现,D=单平台仅一次,E=素材无直接证据、系推断」。\n"
139
- "1. **热门题材 Top3**:各自的共同特征与上榜代表书目(跨平台重合的题材单独点出——多平台都热说明是真风口)\n"
140
- "2. **高频人设/套路总结**:3-5 条,点名反复出现的元素\n"
141
- "3. **差异化切入建议**:2-3 个,结合用户方向给出「题材+人设」组合与一句话理由\n"
142
- "以上每一项结论末尾标注【证据X】(X 为 A-E 之一)。分级只看素材里真实出现的"
143
- "情况:组合条目按其组成元素的较低档取值,拿不准就降一档、不升级。\n"
144
- "直接输出分析报告,不要复述素材清单,不要输出与报告无关的内容。"
145
- % (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
+
121
+ 报告契约含逐条 A-E 跨平台证据分级(借鉴 career-ops 的分级报告交付形态):
122
+ 分级只认素材实际出现情况——抑制单源孤证被模型放大成「大热题材」。"""
123
+ blocks, seen = [], set()
124
+ for label, fname in _SOURCES:
125
+ items = [t for t in globals()[fname](30) if t not in seen]
126
+ seen.update(items)
127
+ if items:
128
+ blocks.append("### %s\n%s" % (label, "、".join(items)))
129
+ if not blocks:
130
+ return None
131
+ material = "\n\n".join(blocks)
132
+ return (
133
+ "你是网文选题分析师。以下是刚刚抓取的各平台排行榜页面的书目与分类素材:\n\n"
134
+ "## 榜单元素材\n%s\n\n"
135
+ "## 用户想写的方向\n%s\n\n"
136
+ "## 你的产出(Markdown 报告)\n"
137
+ "报告第一行给图例(一行):「证据分级:A=三平台及以上重合,B=两平台重合,"
138
+ "C=单平台多次出现,D=单平台仅一次,E=素材无直接证据、系推断」。\n"
139
+ "1. **热门题材 Top3**:各自的共同特征与上榜代表书目(跨平台重合的题材单独点出——多平台都热说明是真风口)\n"
140
+ "2. **高频人设/套路总结**:3-5 条,点名反复出现的元素\n"
141
+ "3. **差异化切入建议**:2-3 个,结合用户方向给出「题材+人设」组合与一句话理由\n"
142
+ "以上每一项结论末尾标注【证据X】(X 为 A-E 之一)。分级只看素材里真实出现的"
143
+ "情况:组合条目按其组成元素的较低档取值,拿不准就降一档、不升级。\n"
144
+ "直接输出分析报告,不要复述素材清单,不要输出与报告无关的内容。"
145
+ % (material, goal or "(用户未指定方向,按大盘热门分析)"))
@@ -2521,6 +2521,20 @@ def _ensure_critique_placeholders(tpl):
2521
2521
 
2522
2522
  # ---------------------------------------------------------------- 连载引擎(长篇小说:逐章打磨)
2523
2523
 
2524
+ def serial_locked_chapter_block(workdir, chapter):
2525
+ """本章锁定章纲注入块(起草/修订共用):工作目录章纲/*ch-<N>.md 存在时
2526
+ 以最高优先级随提示词下发——批次大纲可能被继承链带偏,锁定章纲才是作者
2527
+ 逐章确认过的事实标准(2026-10-03 不周山 rogue 大纲实案的治本闸)。
2528
+ 无章纲返回 ""(老书零噪音)。"""
2529
+ text = planner.locked_chapter_outline_text(workdir, chapter)
2530
+ if not text:
2531
+ return ""
2532
+ return ("## 锁定章纲(作者已确认,最高优先级)\n"
2533
+ "本章标题、视角(POV)、时间地点、事件节拍与章末落点必须逐项对齐下方章纲;"
2534
+ "若与其他要点冲突,以锁定章纲为准;其后章目的伏笔揭示位不得提前烧毁。\n\n"
2535
+ + text)
2536
+
2537
+
2524
2538
  SERIAL_CHAPTER_PROMPT = """你是一名网文作者(写作规范见下方经验库)。本书信息如下,请先完整读完再执行末尾的「本章任务」。
2525
2539
 
2526
2540
  __SKILLS__
@@ -2539,6 +2553,8 @@ __VOLUME__
2539
2553
  - 剧情要点:__BEATS__
2540
2554
  - 章末钩子:__HOOK__
2541
2555
  - 本章爽点/情绪爆点:__HIGHLIGHT__(必须按特写镜头写厚:铺垫在前、放大在中、余波在后,用动作/五感/生理反应/环境反馈呈现,禁止一句话带过)
2556
+ __LOCKED_CH__
2557
+ __STALE_NOTE__
2542
2558
  - 正文约 __WORDS__ 字,中文,直接开写正文(可含本章标题行)。
2543
2559
 
2544
2560
  ## 前情提要(此前各章结尾摘录,衔接用)
@@ -2565,6 +2581,7 @@ __VOLUME__
2565
2581
  ## 本章按大纲应完成
2566
2582
  __BEATS__
2567
2583
  (本章爽点/情绪爆点:__HIGHLIGHT__)
2584
+ __LOCKED_CH__
2568
2585
 
2569
2586
  ## 本章评审意见
2570
2587
  __CRITIQUE__
@@ -3464,6 +3481,13 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
3464
3481
  store.finish_step(run_id, step["n"], "done", summary="(mock)第 %d 章草稿落盘" % i,
3465
3482
  duration_s=0.2)
3466
3483
  else:
3484
+ # 重写未达标章时旧稿仍在场:不点破来历,作者会「校验沿用」直接交卷
3485
+ # (2026-10-03 实案:agent 对照章纲逐项核对旧稿后原样保留,补钩修订落空)。
3486
+ # 必须明说旧稿未过审、要求覆盖。
3487
+ stale_note = ""
3488
+ if os.path.exists(os.path.join(workdir, ch_file)):
3489
+ stale_note = ("- 注意:目录里已有一版**未通过评审**的旧稿 `%s`——不要校验沿用、"
3490
+ "不要增量修补,按本章任务直接覆盖重写。" % ch_file)
3467
3491
  # stable_order:同一任务 8 个章节的技能块必须字节级一致(§07 T1.2' 前缀缓存)
3468
3492
  sk_block, _ = skills.block_for(task, stable_order=True, run_id=run_id)
3469
3493
  if bible:
@@ -3501,6 +3525,8 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
3501
3525
  .replace("__TITLE__", ch["title"])
3502
3526
  .replace("__BEATS__", branch_beats or ch["beats"] or "按大纲推进")
3503
3527
  .replace("__HOOK__", branch_hook or ch.get("hook") or "留下悬念")
3528
+ .replace("__LOCKED_CH__", serial_locked_chapter_block(workdir, i))
3529
+ .replace("__STALE_NOTE__", stale_note)
3504
3530
  .replace("__HIGHLIGHT__", ch.get("highlight") or "按剧情要点自然铺设一处小冲突/小反转")
3505
3531
  .replace("__STYLE__", style_tpl)
3506
3532
  .replace("__QUALITY_GATE__", novel_quality.opening_requirements(i, wpc))
@@ -3895,6 +3921,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
3895
3921
  .replace("__GOAL__", task["goal"])
3896
3922
  .replace("__VOLUME__", vol_block_for(i))
3897
3923
  .replace("__BEATS__", ch.get("beats") or "按大纲推进")
3924
+ .replace("__LOCKED_CH__", serial_locked_chapter_block(workdir, i))
3898
3925
  .replace("__HIGHLIGHT__", ch.get("highlight")
3899
3926
  or "按剧情要点自然铺设一处小冲突/小反转")
3900
3927
  .replace("__STYLE__", style_tpl)
@@ -4049,6 +4076,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
4049
4076
  .replace("__GOAL__", task["goal"])
4050
4077
  .replace("__VOLUME__", vol_block_for(j))
4051
4078
  .replace("__BEATS__", _wch.get("beats") or "按大纲推进")
4079
+ .replace("__LOCKED_CH__", serial_locked_chapter_block(workdir, j))
4052
4080
  .replace("__HIGHLIGHT__", _wch.get("highlight")
4053
4081
  or "按剧情要点自然铺设一处小冲突/小反转")
4054
4082
  .replace("__STYLE__", style_tpl)
@@ -4241,6 +4269,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
4241
4269
  .replace("__GOAL__", task["goal"])
4242
4270
  .replace("__VOLUME__", vol_block_for(i))
4243
4271
  .replace("__BEATS__", ch.get("beats") or "按大纲推进")
4272
+ .replace("__LOCKED_CH__", serial_locked_chapter_block(workdir, i))
4244
4273
  .replace("__HIGHLIGHT__", ch.get("highlight")
4245
4274
  or "按剧情要点自然铺设一处小冲突/小反转")
4246
4275
  .replace("__CRITIQUE__", crit)
@@ -11,6 +11,7 @@ import json
11
11
  import os
12
12
  import re
13
13
  import time
14
+ from pathlib import Path
14
15
 
15
16
  from . import knowledge, modelhub, runner, skills, usage, volumes
16
17
 
@@ -283,6 +284,8 @@ __SKILLS__
283
284
  硬性要求:
284
285
  - 第 1 章直接衔接前文(见下方前情),不得跳线、不得重启设定、不得复述前文;
285
286
  - 主线沿既有脉络推进,新冲突尽量从已埋伏笔中生长,人物性格与前文一致;
287
+ - 若下方给出「锁定章纲」:本批各章 title/beats/hook 必须逐章从对应章纲提炼对齐
288
+ (标题沿用、视角不换、节拍不裁并、伏笔揭示位不提前),不得虚构章纲之外的主线事件;
286
289
  - 每章都写清“主角主动选择 → 阻碍 → 局面变化 → 章末钩子”,禁止水字数的日常流水账;
287
290
  - 复杂情感必须落到触发事件、动作/生理反应和潜台词,不能用一句概括代替;
288
291
  - 延续既定叙述视角和语言锚点(见下方既定文风锚),删掉重复复述,避免节奏变慢;
@@ -295,6 +298,9 @@ __STYLE_ANCHOR__
295
298
  ## 前情大纲(已完成章节,章号为全书章号)
296
299
  __PREV_OUTLINE__
297
300
 
301
+ ## 锁定章纲(本批各章已确认的章纲,最高优先级;为空则按前情自然规划)
302
+ __LOCKED_OUTLINE__
303
+
298
304
  ## 最新一章结尾(衔接锚点)
299
305
  __PREV_TAIL__
300
306
 
@@ -480,6 +486,86 @@ def book_volume_plan(task, outline, upto):
480
486
  return volumes.merge_titles(plan, named)
481
487
 
482
488
 
489
+ _CH_OUTLINE_FILE_RE = re.compile(r"ch-(\d{1,4})\.md$", re.IGNORECASE)
490
+
491
+
492
+ def _read_workdir_text(p, cap):
493
+ """章纲/卷纲文件读文本:UTF-8 → GBK → replace(同 _prev_serial_story 纪律,
494
+ CLI 子代理在中文 Windows 上可能把文件落成 GBK)。"""
495
+ try:
496
+ b = p.read_bytes()
497
+ except OSError:
498
+ return ""
499
+ for enc in ("utf-8", "gbk"):
500
+ try:
501
+ return b.decode(enc)[:cap].strip()
502
+ except UnicodeDecodeError:
503
+ continue
504
+ return b.decode("utf-8", "replace")[:cap].strip()
505
+
506
+
507
+ def locked_chapter_outline_text(workdir, chapter, cap=3000):
508
+ """工作目录里本章的锁定章纲全文(章纲/*ch-<N>.md,按文件名章号精确匹配)。
509
+ 逐章章纲是作者逐章确认过的事实标准——大纲生成、起草、修订三面都应以它为
510
+ 最高优先级,否则「起草跟批次大纲、评审跟章纲」会永久互斥(2026-10-03
511
+ 不周山 21-28 章 rogue 大纲实案)。无匹配文件返回 ""(老书零噪音)。"""
512
+ if not workdir or chapter is None:
513
+ return ""
514
+ try:
515
+ base = Path(str(workdir)) / "章纲"
516
+ if not base.is_dir():
517
+ return ""
518
+ hits = []
519
+ for p in base.iterdir():
520
+ if not p.is_file():
521
+ continue
522
+ m = _CH_OUTLINE_FILE_RE.search(p.name)
523
+ if m and int(m.group(1)) == int(chapter):
524
+ hits.append(p)
525
+ except OSError:
526
+ return ""
527
+ if not hits:
528
+ return ""
529
+ hits.sort(key=lambda p: p.name) # 同章号多份时取文件名序最大(卷号更高者)
530
+ return _read_workdir_text(hits[-1], cap)
531
+
532
+
533
+ def locked_serial_outline_block(task, start, end, per_cap=2500, total_cap=16000):
534
+ """续写大纲生成的锁定章纲注入块:本批 [start, end] 逐章汇总章纲全文。
535
+ 一份章纲都没有时回退 大纲/*.md(截断兜底,至少锚住卷纲方向);两者皆无
536
+ 返回 ""(零噪音)。"""
537
+ workdir = str(task.get("workdir") or "")
538
+ parts, used = [], 0
539
+ if workdir:
540
+ for n in range(int(start), int(end) + 1):
541
+ if used >= total_cap:
542
+ break
543
+ t = locked_chapter_outline_text(workdir, n, cap=per_cap)
544
+ if t:
545
+ head = "### 第 %d 章锁定章纲" % n
546
+ parts.append(head + "\n" + t)
547
+ used += len(head) + len(t) + 2
548
+ if parts:
549
+ return "\n\n".join(parts)[:total_cap]
550
+ if workdir:
551
+ try:
552
+ base = Path(workdir) / "大纲"
553
+ if base.is_dir():
554
+ buf, used = [], 0
555
+ for p in sorted(base.glob("*.md")):
556
+ if used >= total_cap:
557
+ break
558
+ t = _read_workdir_text(p, min(4000, total_cap - used))
559
+ if t:
560
+ buf.append("### %s\n%s" % (p.name, t))
561
+ used += len(t) + len(p.name) + 8
562
+ if buf:
563
+ return "\n\n".join(buf)[:total_cap]
564
+ except OSError:
565
+ pass
566
+ return ""
567
+
568
+
483
569
  def make_serial_outline(task, author_agent=None, workdir=None, ev=None, log_path=None,
484
570
  deadline=None, run_id=None):
485
571
  """连载大纲:编排者 API 优先 → 作者 CLI → 模板。返回 {book_title, chapters:[{title,beats,hook}]}。
@@ -520,6 +606,7 @@ def make_serial_outline(task, author_agent=None, workdir=None, ev=None, log_path
520
606
  else:
521
607
  vol_block, vol_key = "", ""
522
608
  if start > 1:
609
+ locked_block = locked_serial_outline_block(task, start, start + n - 1)
523
610
  prompt = (SERIAL_CONTINUE_OUTLINE_PROMPT
524
611
  .replace("__SKILLS__", sk_block)
525
612
  .replace("__DONE__", str(done))
@@ -532,6 +619,7 @@ def make_serial_outline(task, author_agent=None, workdir=None, ev=None, log_path
532
619
  or "(前文未记录文风锚,请依据前情大纲与最新一章结尾自行归纳,"
533
620
  "归纳后全批保持一致)")
534
621
  .replace("__PREV_OUTLINE__", prev_lines or "(无大纲记录,请依据下方最新一章结尾与小说目标衔接)")
622
+ .replace("__LOCKED_OUTLINE__", locked_block or "(无)")
535
623
  .replace("__PREV_TAIL__", prev_tail or "(无)")
536
624
  .replace("__GOAL__", task["goal"])
537
625
  .replace("__CONTEXT__", task.get("context") or "(无)"))