codebee 0.1.103 → 0.1.105
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 +12 -0
- package/LICENSE +21 -21
- package/README.md +5 -3
- package/app/core/acceptance.py +100 -100
- package/app/core/acp_client.py +457 -457
- 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/backend_profiles.py +188 -188
- package/app/core/backup.py +470 -470
- package/app/core/beekeeper.py +183 -183
- package/app/core/benchstore.py +102 -102
- 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/capability.py +144 -144
- package/app/core/chaptersafe.py +32 -32
- package/app/core/checkpoints.py +48 -5
- package/app/core/cleanup.py +303 -303
- package/app/core/command_auth.py +302 -302
- 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_log.py +128 -128
- package/app/core/env_scrub.py +85 -85
- package/app/core/errorlog.py +137 -137
- package/app/core/eval_matrix.py +91 -91
- package/app/core/evaluation.py +174 -174
- package/app/core/failure_notify.py +290 -290
- package/app/core/flow_graph.py +33 -33
- 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/knowledge_pipeline.py +86 -86
- package/app/core/manager.py +2386 -2385
- package/app/core/mocks.py +64 -64
- package/app/core/notify.py +201 -201
- package/app/core/novel_quality.py +285 -181
- package/app/core/openapi.py +246 -217
- package/app/core/operations.py +228 -228
- package/app/core/paihang.py +145 -145
- package/app/core/pipeline.py +81 -11
- package/app/core/planner.py +7 -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/project_memory.py +252 -206
- package/app/core/prompt_guard.py +78 -78
- package/app/core/publish/__init__.py +7 -7
- package/app/core/publish/auto.py +659 -659
- package/app/core/publish/chaprows.py +138 -138
- package/app/core/publish/flows-fanqie-calibrated.json +442 -442
- package/app/core/publish/manager.py +1185 -1185
- package/app/core/publish/qimao.py +193 -193
- package/app/core/publish/volumes.py +108 -108
- package/app/core/publish/ws.py +139 -139
- package/app/core/quality_gate.py +201 -201
- package/app/core/redact.py +38 -38
- package/app/core/registry.py +35 -0
- package/app/core/repeat_guard.py +129 -129
- package/app/core/retrieval.py +137 -137
- package/app/core/review_requests.py +119 -0
- package/app/core/runner.py +5 -1
- package/app/core/sandbox_runtime.py +95 -95
- 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.py +215 -215
- package/app/core/settings_schema.py +282 -282
- 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/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/tool_outputs.py +107 -107
- package/app/core/tracing.py +207 -207
- package/app/core/upstream.py +41 -41
- package/app/core/usage.py +1038 -1038
- package/app/core/volumes.py +389 -389
- package/app/core/webhooks.py +89 -89
- package/app/core/wxdigest.py +865 -865
- package/app/core/zentao.py +2692 -2692
- package/app/main.py +43 -0
- package/app/mcp_server.py +244 -244
- package/app/pick_dialog.py +110 -110
- package/app/ui/app.js +21 -2
- package/app/ui/artdiff.js +74 -74
- package/app/ui/hive3d.js +54 -31
- package/app/ui/i18n.js +32 -1
- package/app/ui/icons/bee.svg +79 -79
- package/app/ui/index.html +1 -0
- package/app/ui/manifest.json +16 -16
- package/app/ui/qrcode.js +2297 -2297
- package/bin/tutti.js +147 -147
- package/package.json +1 -1
- package/tools/wx_reader.py +261 -261
- package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +0 -88
- package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +0 -126
- 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 +0 -210
- 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/output/_createbook_body.json +0 -1
- package/app/output/_probe_fq_click.py +0 -50
- package/app/output/_probe_fq_form.py +0 -42
- package/app/output/_probe_fq_modal.py +0 -55
- package/app/output/_probe_fq_points.py +0 -71
- package/app/output/_probe_fq_submit.py +0 -34
- package/app/output/_probe_fq_watch.py +0 -62
- package/app/output/workbench-clean-preview.jpg +0 -0
- package/app/output/workbench-reference-render.png +0 -0
- package/app/output/workbench-shell.png +0 -0
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
|
@@ -445,11 +445,20 @@ def _run_step(run_id, role, agent, prompt, workdir, readonly, ev, timeout=runner
|
|
|
445
445
|
model=agent.get("model") or "",
|
|
446
446
|
provider=_binding_brief(agent))
|
|
447
447
|
try:
|
|
448
|
+
checkpoint_effects = [] if readonly else ["workspace_files"]
|
|
449
|
+
if agent.get("mode") != "mock":
|
|
450
|
+
checkpoint_effects.append("external_agent")
|
|
451
|
+
if agent.get("kind") in ("acp", "mcp"):
|
|
452
|
+
checkpoint_effects.append("external_protocol")
|
|
448
453
|
checkpoints.start(
|
|
449
454
|
run_id, step["n"], role, prompt,
|
|
450
455
|
input_data={"workdir": workdir, "readonly": readonly,
|
|
451
456
|
"agent": agent.get("id")},
|
|
452
|
-
attempt=len((store.get_run(run_id) or {}).get("steps") or [])
|
|
457
|
+
attempt=len((store.get_run(run_id) or {}).get("steps") or []),
|
|
458
|
+
tool_call={"name": "agent_step", "agent": agent.get("id"),
|
|
459
|
+
"role": role, "readonly": bool(readonly)},
|
|
460
|
+
conversation_ref=resume or "",
|
|
461
|
+
side_effects=checkpoint_effects)
|
|
453
462
|
except Exception:
|
|
454
463
|
pass
|
|
455
464
|
start = time.time()
|
|
@@ -475,7 +484,8 @@ def _run_step(run_id, role, agent, prompt, workdir, readonly, ev, timeout=runner
|
|
|
475
484
|
"tokens": 0, "usage": None, "error": sandbox_error,
|
|
476
485
|
"raw": {"exit_code": None, "sandbox_blocked": True},
|
|
477
486
|
"kind": agent.get("kind", "generic"), "model": agent.get("model")}
|
|
478
|
-
_finish_step_result(run_id, step, res, role, agent, start
|
|
487
|
+
_finish_step_result(run_id, step, res, role, agent, start,
|
|
488
|
+
readonly=readonly)
|
|
479
489
|
return res
|
|
480
490
|
if dead_binding:
|
|
481
491
|
from .error_codes import ErrorCode
|
|
@@ -484,7 +494,8 @@ def _run_step(run_id, role, agent, prompt, workdir, readonly, ev, timeout=runner
|
|
|
484
494
|
"error_code": ErrorCode.ENV_BLOCK,
|
|
485
495
|
"raw": {"exit_code": None}, "kind": agent.get("kind", "generic"),
|
|
486
496
|
"model": agent.get("model")}
|
|
487
|
-
_finish_step_result(run_id, step, res, role, agent, start
|
|
497
|
+
_finish_step_result(run_id, step, res, role, agent, start,
|
|
498
|
+
readonly=readonly)
|
|
488
499
|
return res
|
|
489
500
|
if agent.get("mode") == "mock":
|
|
490
501
|
time.sleep(0.3)
|
|
@@ -507,7 +518,8 @@ def _run_step(run_id, role, agent, prompt, workdir, readonly, ev, timeout=runner
|
|
|
507
518
|
"raw": {"exit_code": None, "repeat_stop": True},
|
|
508
519
|
"kind": agent.get("kind", "generic"),
|
|
509
520
|
"model": agent.get("model")}
|
|
510
|
-
_finish_step_result(run_id, step, res, role, agent, start
|
|
521
|
+
_finish_step_result(run_id, step, res, role, agent, start,
|
|
522
|
+
readonly=readonly)
|
|
511
523
|
return res
|
|
512
524
|
# 运行中指挥:drain 用户追加的指令/附件,注入本步(守门拦截时不 drain,
|
|
513
525
|
# 消息留给下一个真实步骤,不空耗);role/step_n 作送达回执
|
|
@@ -529,7 +541,8 @@ def _run_step(run_id, role, agent, prompt, workdir, readonly, ev, timeout=runner
|
|
|
529
541
|
"error": git_checkpoint.get("error") or "文件检查点准备失败",
|
|
530
542
|
"raw": {"exit_code": None, "checkpoint_blocked": True},
|
|
531
543
|
"kind": agent.get("kind", "generic"), "model": agent.get("model")}
|
|
532
|
-
_finish_step_result(run_id, step, res, role, agent, start
|
|
544
|
+
_finish_step_result(run_id, step, res, role, agent, start,
|
|
545
|
+
readonly=readonly)
|
|
533
546
|
return res
|
|
534
547
|
res = _spawn_step(session_run_id=run_id, role=role, agent=agent,
|
|
535
548
|
prompt=effective_prompt, workdir=workdir, readonly=readonly,
|
|
@@ -561,7 +574,8 @@ def _run_step(run_id, role, agent, prompt, workdir, readonly, ev, timeout=runner
|
|
|
561
574
|
pass
|
|
562
575
|
# 先收尾再查取消:取消时进程已被 run_process 杀停,若先抛 Cancelled,
|
|
563
576
|
# 步骤记录会永远停在「运行中」变僵尸(与 _run_verify 的顺序对齐)
|
|
564
|
-
_finish_step_result(run_id, step, res, role, agent, start
|
|
577
|
+
_finish_step_result(run_id, step, res, role, agent, start,
|
|
578
|
+
readonly=readonly)
|
|
565
579
|
if (res.get("raw") or {}).get("deadline_exceeded"):
|
|
566
580
|
store.update_run(run_id, expected_status="running", status="timeout",
|
|
567
581
|
ended_at=_now(), error="任务总时限已到")
|
|
@@ -883,7 +897,7 @@ def _spawn_step(session_run_id, role, agent, prompt, workdir, readonly, ev,
|
|
|
883
897
|
return res
|
|
884
898
|
|
|
885
899
|
|
|
886
|
-
def _finish_step_result(run_id, step, res, role, agent, start):
|
|
900
|
+
def _finish_step_result(run_id, step, res, role, agent, start, *, readonly=None):
|
|
887
901
|
"""step 收尾:记录 + 用量 + 运行时断言(5F)。"""
|
|
888
902
|
# 被取消杀停的步骤如实记「已取消」;超时被杀记「超时」——都只是步骤
|
|
889
903
|
# 显示层的细分,run 级仍是 failed,善后路径(重试/自动续跑)不变
|
|
@@ -924,7 +938,10 @@ def _finish_step_result(run_id, step, res, role, agent, start):
|
|
|
924
938
|
checkpoints.finish(
|
|
925
939
|
run_id, step["n"], checkpoint_status,
|
|
926
940
|
error=res.get("error") or "", output=res.get("text") or "",
|
|
927
|
-
metadata={"role": role, "agent": agent.get("id") or ""
|
|
941
|
+
metadata={"role": role, "agent": agent.get("id") or "",
|
|
942
|
+
"repropose_required": True,
|
|
943
|
+
"side_effects": ([] if readonly is True else ["workspace_files"]) +
|
|
944
|
+
(["external_agent"] if agent.get("mode") != "mock" else [])})
|
|
928
945
|
except Exception:
|
|
929
946
|
pass
|
|
930
947
|
# 错误台账:失败/超时各记一条结构化记录(遥测与诊断包的数据源)。
|
|
@@ -1040,6 +1057,7 @@ __FILES__
|
|
|
1040
1057
|
__CONTEXT__
|
|
1041
1058
|
|
|
1042
1059
|
## 要求
|
|
1060
|
+
- 思考先行:动手前先读懂相关代码、明确验收标准再改;不读懂就动手是大改与返工的主因。
|
|
1043
1061
|
- 修改要最小化、聚焦任务目标;不要无关重构。
|
|
1044
1062
|
- 反过度防御(借鉴 HERO:只约束你「提出」的方案,不约束你「检查」的范围)——
|
|
1045
1063
|
不要给没要求的数据加哈希/校验和;不要为内部脚本铺层层边界分支;
|
|
@@ -2919,6 +2937,7 @@ __VOLUME__
|
|
|
2919
2937
|
__LOCKED_CH__
|
|
2920
2938
|
__STALE_NOTE__
|
|
2921
2939
|
__REDO_ISSUES__
|
|
2940
|
+
__REUSE_GUARD__
|
|
2922
2941
|
- 正文约 __WORDS__ 字,中文,直接开写正文(可含本章标题行)。
|
|
2923
2942
|
|
|
2924
2943
|
## 前情提要(此前各章结尾摘录,衔接用)
|
|
@@ -3085,6 +3104,33 @@ def _wc(text):
|
|
|
3085
3104
|
return len(re.sub(r"\s", "", text or ""))
|
|
3086
3105
|
|
|
3087
3106
|
|
|
3107
|
+
def _reuse_guard_samples(workdir, upto, window=4, min_words=200):
|
|
3108
|
+
"""收集此前各章的开笔/收束摘录(起草负样例,防跨章句式同款——105-112 批
|
|
3109
|
+
「落笔入册」收束 8 章用 4 次被判 major 的根治手段之一:评审事后抓 → 起草
|
|
3110
|
+
事前给反面样例)。只看近 window 章(更早的句式影响衰减);过短/读不到的
|
|
3111
|
+
章静默跳过。"""
|
|
3112
|
+
samples = []
|
|
3113
|
+
for j in range(max(1, int(upto) - int(window)), int(upto)):
|
|
3114
|
+
t = _read_chapter(workdir, j)
|
|
3115
|
+
if not t or _wc(t) < int(min_words):
|
|
3116
|
+
continue
|
|
3117
|
+
body = re.sub(r"\s+", " ", t.strip())
|
|
3118
|
+
samples.append({"chapter": j, "head": body[:80], "tail": body[-100:]})
|
|
3119
|
+
return samples
|
|
3120
|
+
|
|
3121
|
+
|
|
3122
|
+
def _dup_reject_note(notes):
|
|
3123
|
+
"""查重打回后的重写指令块:把上一稿被拦截的重复证据逐条下发。"""
|
|
3124
|
+
lines = novel_quality.format_duplicate_notes(notes)
|
|
3125
|
+
if not lines:
|
|
3126
|
+
return ""
|
|
3127
|
+
return ("\n\n## 确定性查重拦截(必读)\n"
|
|
3128
|
+
"上一稿因整段/整句逐字重复未通过落盘验收,已被打回重写。重写时必须"
|
|
3129
|
+
"消除以下重复——删并重复内容、保留信息量最高的一处;若为有意的复诵"
|
|
3130
|
+
"或复沓,必须有动作/归属铺垫并改写措辞,不得逐字再现:\n- "
|
|
3131
|
+
+ "\n- ".join(lines) + "\n")
|
|
3132
|
+
|
|
3133
|
+
|
|
3088
3134
|
_ANCHOR_EDGE_PUNCT = "。!?,、;:…—·,.!?;:\"'“”‘’()()[]【】《》<>*#-"
|
|
3089
3135
|
|
|
3090
3136
|
|
|
@@ -3901,6 +3947,11 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
3901
3947
|
redo_lines += _major_lines(book_lvl)
|
|
3902
3948
|
redo_note = ("## 上一轮评审未达标原因(必读)\n" + "\n".join(redo_lines) + "\n"
|
|
3903
3949
|
) if redo_lines else ""
|
|
3950
|
+
# 跨章复用红线负样例:此前各章实际的开笔/收束摘录随起草下发,把
|
|
3951
|
+
# 「评审事后抓句式同款」前移成「起草事前看反面样例」(105-112 批
|
|
3952
|
+
# 章末收束句式 4 连用被判 major 的根治;首批第 1 章无样例为空块)
|
|
3953
|
+
reuse_guard = novel_quality.reuse_guard_block(
|
|
3954
|
+
_reuse_guard_samples(workdir, i))
|
|
3904
3955
|
# stable_order:同一任务 8 个章节的技能块必须字节级一致(§07 T1.2' 前缀缓存)
|
|
3905
3956
|
# 三块分开留底:起草重试的分层降级(_serial_shrunk_block)要按
|
|
3906
3957
|
# 「经验库/圣经」各自的身份收缩,折成一整块就只剩 4K 硬截一层了
|
|
@@ -3940,6 +3991,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
3940
3991
|
.replace("__LOCKED_CH__", serial_locked_chapter_block(workdir, i))
|
|
3941
3992
|
.replace("__STALE_NOTE__", stale_note)
|
|
3942
3993
|
.replace("__REDO_ISSUES__", redo_note)
|
|
3994
|
+
.replace("__REUSE_GUARD__", reuse_guard)
|
|
3943
3995
|
.replace("__REVIEW_RUBRIC__", rubric_block)
|
|
3944
3996
|
.replace("__HIGHLIGHT__", ch.get("highlight") or "按剧情要点自然铺设一处小冲突/小反转")
|
|
3945
3997
|
.replace("__STYLE__", style_tpl)
|
|
@@ -3956,9 +4008,14 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
3956
4008
|
# ——2026-09-17 七猫连载实测:多 run 并行推进时后位章节必撞限流,
|
|
3957
4009
|
# 一章失败即整 run 作废,太贵。
|
|
3958
4010
|
def _chapter_state():
|
|
3959
|
-
"""
|
|
4011
|
+
"""草稿验收:成品是章稿文件本身,不是退出码——文件够长且无
|
|
4012
|
+
逐字重复才算好。整段/整句/完整对白逐字重复是成稿事故(113/
|
|
4013
|
+
118 章实案两处整段连排、对白复现,评审 major 一票否决+白烧
|
|
4014
|
+
一轮评审),落盘时确定性拦截打回,省下评审再抓的往返。"""
|
|
3960
4015
|
txt = _read_chapter(workdir, i)
|
|
3961
|
-
|
|
4016
|
+
if not txt or _wc(txt) < int(wpc * 0.6):
|
|
4017
|
+
return False, (txt or "")
|
|
4018
|
+
return not novel_quality.duplicate_notes(txt), txt
|
|
3962
4019
|
|
|
3963
4020
|
res = None
|
|
3964
4021
|
good = False
|
|
@@ -3971,6 +4028,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
3971
4028
|
prev_prompt = None # 上一次实际下发的提示词
|
|
3972
4029
|
prev_timed_out = False # 上一次是否「超时/停滞」收场
|
|
3973
4030
|
shrink_note = "" # 本次重试的分层降级说明(进 step note 供对账)
|
|
4031
|
+
dup_fix_note = "" # 查重打回后的消重指令(重试/换将随提示词下发)
|
|
3974
4032
|
for draft_attempt in range(3):
|
|
3975
4033
|
if draft_attempt:
|
|
3976
4034
|
# 30s / 60s 退避;ev.wait 睡等可被取消即刻唤醒
|
|
@@ -3986,7 +4044,11 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
3986
4044
|
# 30KB 对讯飞必挂)
|
|
3987
4045
|
shrunk_block, shrink_note = _serial_shrunk_block(
|
|
3988
4046
|
sk_lessons, bible, kb_block)
|
|
3989
|
-
use_prompt = prompt.replace(sk_block, shrunk_block)
|
|
4047
|
+
use_prompt = prompt.replace(sk_block, shrunk_block) + dup_fix_note
|
|
4048
|
+
elif draft_attempt and dup_fix_note:
|
|
4049
|
+
# 同作者重试带上一稿的重复证据(消重指令)——同提示重跑
|
|
4050
|
+
# 只会长出同族事故,新变量才值得烧(同 2026-09-22 超时教训)
|
|
4051
|
+
use_prompt = prompt + dup_fix_note
|
|
3990
4052
|
# 超时重试必须有新变量才值得做(2026-09-22):提示词与上次逐字节
|
|
3991
4053
|
# 相同、上次又是超时/停滞收场时,重试只是把同一个超时再烧一遍
|
|
3992
4054
|
# (2400s × N)。此时直接跳出交替换将——换 CLI/模型才是新机会。
|
|
@@ -4014,6 +4076,10 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
4014
4076
|
good, txt = _chapter_state()
|
|
4015
4077
|
if good:
|
|
4016
4078
|
break
|
|
4079
|
+
if txt:
|
|
4080
|
+
_dups = novel_quality.duplicate_notes(txt)
|
|
4081
|
+
if _dups:
|
|
4082
|
+
dup_fix_note = _dup_reject_note(_dups)
|
|
4017
4083
|
if res["ok"] and _wc(res.get("text") or "") >= int(wpc * 0.6):
|
|
4018
4084
|
# 回复正文就是完整章稿(kimi 35B 实测:正文当消息回而不
|
|
4019
4085
|
# 落盘)→ 代为落盘救回成品
|
|
@@ -4024,6 +4090,10 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
4024
4090
|
pass
|
|
4025
4091
|
if good:
|
|
4026
4092
|
break
|
|
4093
|
+
if txt:
|
|
4094
|
+
_dups = novel_quality.duplicate_notes(txt)
|
|
4095
|
+
if _dups:
|
|
4096
|
+
dup_fix_note = _dup_reject_note(_dups)
|
|
4027
4097
|
if ev is not None and ev.is_set():
|
|
4028
4098
|
break
|
|
4029
4099
|
# 同作者重试穷尽 → 起草换将:按路由分序逐个试备选(最多 2 个,
|
package/app/core/planner.py
CHANGED
|
@@ -266,6 +266,10 @@ __SKILLS__
|
|
|
266
266
|
不强制每章都打脸、反转或以悬念断章;
|
|
267
267
|
- 章末可自然收束或留白;大纲中的 highlight 描述主要戏剧/情绪变化或回报,无显著爆点时写清叙事功能,不要强造爆点。
|
|
268
268
|
- 大纲中的情感转折必须给出触发事件和人物选择,不能只写“误会加深/感情升温”;
|
|
269
|
+
- 开笔方式与收束形态批内轮换:相邻章不得用同型句子开笔(如连续多章以「第X日/
|
|
270
|
+
清晨」时间戳起笔),收束形态(落笔定格、悬置问句、场景切换、对话收拍、动作
|
|
271
|
+
停顿等)同一形态全批至多两次,第二次必须换载体或升级信息量——章尾节奏趋同
|
|
272
|
+
是批量生成最易露馅的指纹;
|
|
269
273
|
- style_anchor 是全书唯一的文风锚,每章起草/修订/评审都会注入:必须写明视角人称与句长基调等可执行约束,不要空话;
|
|
270
274
|
- highlight 是本章主要情绪/戏剧变化或读者回报,可为爽、暖、痛、惊、趣味、亲密、发现、安定等;
|
|
271
275
|
平静或过渡章节可承担必要功能,不为填字段伪造高潮;
|
|
@@ -293,6 +297,9 @@ __SKILLS__
|
|
|
293
297
|
- 在关键章节写清人物选择、阻碍与后果;不要求每章都有完整冲突闭环或悬念钩子,避免无功能重复;
|
|
294
298
|
- 复杂情感必须落到触发事件、动作/生理反应和潜台词,不能用一句概括代替;
|
|
295
299
|
- 延续既定叙述视角和语言锚点(见下方既定文风锚),删掉重复复述,避免节奏变慢;
|
|
300
|
+
- 开笔方式与收束形态批内轮换:相邻章不得用同型句子开笔(如连续多章以「第X日/
|
|
301
|
+
清晨」时间戳起笔),收束形态同一形态全批至多两次,第二次必须换载体或升级
|
|
302
|
+
信息量——章尾节奏趋同是批量生成最易露馅的指纹;
|
|
296
303
|
- 题材健康,无违规内容,符合平台签约调性。
|
|
297
304
|
__VOLUMES__
|
|
298
305
|
|