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.
- package/CHANGELOG.md +633 -624
- package/LICENSE +21 -21
- package/README.md +508 -509
- package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +88 -0
- package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +126 -0
- package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
- package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +210 -0
- package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
- package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
- package/app/.playwright-cli/run-detail-after.png +0 -0
- package/app/core/acceptance.py +100 -100
- package/app/core/aiflavor.py +197 -197
- package/app/core/analytics.py +38 -38
- package/app/core/artifacts.py +148 -148
- package/app/core/assets.py +110 -110
- package/app/core/attachments.py +623 -623
- package/app/core/audit.py +30 -30
- package/app/core/backup.py +470 -470
- package/app/core/beekeeper.py +183 -183
- package/app/core/benchstore.py +95 -95
- package/app/core/board.py +209 -209
- package/app/core/bookmeta.py +1036 -1036
- package/app/core/bookmeta_catalog.py +180 -180
- package/app/core/branching.py +137 -137
- package/app/core/builtin_agent.py +1680 -1680
- package/app/core/capability.py +144 -144
- package/app/core/chaptersafe.py +32 -32
- package/app/core/cleanup.py +303 -303
- package/app/core/compaction.py +201 -201
- package/app/core/contracts.py +255 -255
- package/app/core/covergen.py +356 -356
- package/app/core/defectretro.py +43 -43
- package/app/core/diagnostics.py +114 -114
- package/app/core/dispatch.py +190 -190
- package/app/core/dispatch_log.py +128 -128
- package/app/core/env_scrub.py +85 -85
- package/app/core/errorlog.py +137 -137
- package/app/core/evaluation.py +174 -174
- package/app/core/failure_notify.py +290 -290
- package/app/core/goal_service.py +167 -167
- package/app/core/health.py +389 -389
- package/app/core/history.py +38 -38
- package/app/core/hooks.py +162 -162
- package/app/core/manager.py +2232 -2232
- package/app/core/mocks.py +64 -64
- package/app/core/notify.py +201 -201
- package/app/core/novel_quality.py +189 -189
- package/app/core/openapi.py +154 -154
- package/app/core/operations.py +228 -228
- package/app/core/paihang.py +145 -145
- package/app/core/pipeline.py +29 -0
- package/app/core/planner.py +88 -0
- package/app/core/portguard.py +111 -111
- package/app/core/presence.py +77 -77
- package/app/core/preview.py +293 -293
- package/app/core/prompt_guard.py +78 -78
- package/app/core/publish/__init__.py +7 -7
- package/app/core/publish/browser.py +655 -655
- package/app/core/publish/fanqie.py +165 -165
- package/app/core/publish/flow.py +640 -640
- package/app/core/publish/flows-fanqie-calibrated.json +336 -336
- package/app/core/publish/manager.py +1118 -1118
- package/app/core/publish/qimao.py +176 -176
- package/app/core/publish/ws.py +139 -139
- package/app/core/quality_gate.py +194 -194
- package/app/core/redact.py +38 -38
- package/app/core/repeat_guard.py +129 -129
- package/app/core/retrieval.py +137 -137
- package/app/core/router.py +398 -398
- package/app/core/runner.py +2238 -2238
- package/app/core/selfupdate.py +384 -384
- package/app/core/session_log.py +161 -161
- package/app/core/sessions.py +312 -312
- package/app/core/settings_schema.py +270 -270
- package/app/core/share_page.py +114 -114
- package/app/core/skill_scan.py +139 -139
- package/app/core/skillpacks/market/character-bible.md +66 -66
- package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
- package/app/core/skillpacks/market/git-workflow.md +57 -57
- package/app/core/skillpacks/market/release-notes.md +72 -72
- package/app/core/skillpacks/market/weekly-report.md +71 -71
- package/app/core/skillpacks/market/worldview-consistency.md +70 -70
- package/app/core/skills.py +1 -1
- package/app/core/step_runner.py +132 -132
- package/app/core/story_tracking.py +275 -275
- package/app/core/task_compile.py +199 -199
- package/app/core/telemetry.py +292 -292
- package/app/core/tlsctx.py +64 -64
- package/app/core/token_meter.py +147 -147
- package/app/core/tracing.py +144 -144
- package/app/core/upstream.py +41 -41
- package/app/core/usage.py +1035 -1035
- package/app/core/volumes.py +389 -389
- package/app/core/wxdigest.py +858 -858
- package/app/core/zentao.py +2692 -2692
- package/app/mcp_server.py +244 -244
- package/app/output/_createbook_body.json +1 -0
- package/app/output/_probe_fq_click.py +50 -0
- package/app/output/_probe_fq_form.py +42 -0
- package/app/output/_probe_fq_modal.py +55 -0
- package/app/output/_probe_fq_points.py +71 -0
- package/app/output/_probe_fq_submit.py +34 -0
- package/app/output/_probe_fq_watch.py +62 -0
- package/app/pick_dialog.py +110 -110
- package/app/ui/artdiff.js +74 -74
- package/app/ui/board.html +162 -162
- package/app/ui/board.js +305 -305
- package/app/ui/i18n.js +2 -2
- package/app/ui/icons/bee.svg +79 -79
- package/app/ui/manifest.json +16 -16
- package/app/ui/qrcode.js +2297 -2297
- package/app/ui/style.css +7086 -7086
- package/bin/tutti.js +147 -147
- package/package.json +48 -48
- package/tools/wx_reader.py +261 -261
package/app/core/paihang.py
CHANGED
|
@@ -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 "(用户未指定方向,按大盘热门分析)"))
|
package/app/core/pipeline.py
CHANGED
|
@@ -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)
|
package/app/core/planner.py
CHANGED
|
@@ -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 "(无)"))
|