codebee 0.1.71 → 0.1.72
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 +595 -589
- package/LICENSE +21 -21
- package/README.md +508 -509
- package/app/core/acceptance.py +100 -100
- package/app/core/aiflavor.py +197 -197
- 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 +935 -935
- package/app/core/bookmeta_catalog.py +180 -180
- package/app/core/branching.py +137 -137
- package/app/core/builtin_agent.py +1680 -1673
- 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/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/evalbench.py +836 -720
- package/app/core/evaluation.py +174 -174
- package/app/core/failure_notify.py +290 -290
- package/app/core/flows.py +7 -7
- package/app/core/goal_service.py +167 -167
- package/app/core/health.py +374 -374
- package/app/core/history.py +38 -38
- package/app/core/hooks.py +162 -162
- package/app/core/jobs.py +21 -0
- package/app/core/manager.py +2143 -2143
- package/app/core/market.py +88 -2
- package/app/core/market_remote.py +16 -0
- package/app/core/mocks.py +64 -64
- package/app/core/modelhub.py +3 -2
- package/app/core/notify.py +201 -201
- package/app/core/novel_quality.py +189 -189
- package/app/core/operations.py +228 -228
- package/app/core/paihang.py +138 -138
- package/app/core/pipeline.py +102 -30
- package/app/core/portguard.py +111 -111
- package/app/core/preview.py +293 -293
- package/app/core/prompt_guard.py +78 -0
- package/app/core/publish/__init__.py +7 -7
- package/app/core/publish/auto.py +396 -367
- package/app/core/publish/browser.py +655 -655
- package/app/core/publish/fanqie.py +165 -165
- package/app/core/publish/flow.py +589 -589
- package/app/core/publish/flows-fanqie-calibrated.json +336 -336
- package/app/core/publish/ledger.py +307 -229
- package/app/core/publish/manager.py +931 -824
- package/app/core/publish/qimao.py +146 -146
- package/app/core/publish/ws.py +139 -139
- package/app/core/quality_gate.py +194 -0
- package/app/core/redact.py +38 -38
- package/app/core/repeat_guard.py +129 -129
- package/app/core/router.py +398 -398
- package/app/core/runner.py +2142 -2142
- 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 -122
- package/app/core/skillpacks/fanqie-novel.md +80 -80
- 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 +58 -8
- package/app/core/step_runner.py +132 -132
- package/app/core/store.py +51 -0
- 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 -2614
- package/app/main.py +86 -11
- package/app/mcp_server.py +175 -175
- package/app/pick_dialog.py +110 -110
- package/app/ui/app.js +258 -54
- 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 +24 -0
- package/app/ui/icons/bee.svg +79 -79
- package/app/ui/index.html +5 -0
- package/app/ui/manifest.json +16 -16
- package/app/ui/qrcode.js +2297 -2297
- package/app/ui/style.css +7051 -7012
- package/bin/tutti.js +147 -147
- package/package.json +48 -47
- 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//344/275/234/345/223/201/344/277/241/346/201/257-/344/270/203/347/214/253.md +0 -18
package/app/core/paihang.py
CHANGED
|
@@ -1,138 +1,138 @@
|
|
|
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 "(用户未指定方向,按大盘热门分析)"))
|
|
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/pipeline.py
CHANGED
|
@@ -19,7 +19,7 @@ import re
|
|
|
19
19
|
import threading
|
|
20
20
|
import time
|
|
21
21
|
|
|
22
|
-
from . import aiflavor, attachments, branching, catalog, chaptersafe, defectretro, dispatch_log, flows, history, hooks, jobs, knowledge, manager, modelhub, mocks, novel_quality, 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, novel_quality, paihang, planner, quality_gate, 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
|
|
@@ -2636,16 +2636,34 @@ def _critique_json(res, dims):
|
|
|
2636
2636
|
任何一道出分即算有效评审——「无法解析」绝不能把正常出分的评审吞掉
|
|
2637
2637
|
(2026-09-18 七猫案:kimi 内嵌引号病连烧三轮自动续跑全判评审全挂)。"""
|
|
2638
2638
|
text = res.get("text") or ""
|
|
2639
|
+
def validated(value):
|
|
2640
|
+
value = value if isinstance(value, dict) else {}
|
|
2641
|
+
raw_scores = value.get("scores") if isinstance(value.get("scores"), dict) else {}
|
|
2642
|
+
scores, errors = quality_gate.normalize_scores(raw_scores, dims)
|
|
2643
|
+
if errors:
|
|
2644
|
+
# Keep individually valid dimensions for repair/polish diagnostics,
|
|
2645
|
+
# but mark the set incomplete so release consensus cannot use it.
|
|
2646
|
+
partial = {}
|
|
2647
|
+
for dim in dims:
|
|
2648
|
+
one, one_errors = quality_gate.normalize_scores({dim: raw_scores.get(dim)}, [dim])
|
|
2649
|
+
if not one_errors:
|
|
2650
|
+
partial[dim] = one[dim]
|
|
2651
|
+
scores = partial
|
|
2652
|
+
value["scores"] = scores
|
|
2653
|
+
if errors:
|
|
2654
|
+
value["score_errors"] = errors
|
|
2655
|
+
return value
|
|
2656
|
+
|
|
2639
2657
|
gj = runner.as_scores(runner.extract_json(text))
|
|
2640
2658
|
if isinstance(gj, dict) and isinstance(gj.get("scores"), dict) and gj.get("scores"):
|
|
2641
|
-
return gj
|
|
2659
|
+
return validated(gj)
|
|
2642
2660
|
prose = runner.scores_from_prose(text, dims)
|
|
2643
2661
|
if not prose:
|
|
2644
2662
|
# 第四道网(BAML 借鉴):维度名没命中时按「X:N 分」模式泛化抓取——
|
|
2645
2663
|
# 自定义 rubric 改了维度措辞而模型用了自己的说法时仍能救回
|
|
2646
2664
|
prose = runner.extract_scores_from_text(text)
|
|
2647
2665
|
if prose:
|
|
2648
|
-
return {"scores": prose, "issues": [], "summary": text[:400]}
|
|
2666
|
+
return validated({"scores": prose, "issues": [], "summary": text[:400]})
|
|
2649
2667
|
return {"scores": {}, "issues": [],
|
|
2650
2668
|
"summary": "评审输出无法解析:%s" % (text or res.get("error") or "")[:150]}
|
|
2651
2669
|
|
|
@@ -3025,6 +3043,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
3025
3043
|
|
|
3026
3044
|
chapter_scores = [] # [{chapter,title,means,passed,rounds,words}]
|
|
3027
3045
|
issues_all = []
|
|
3046
|
+
signing_review_meta = {}
|
|
3028
3047
|
|
|
3029
3048
|
# ---- 独立签约评估设施(开篇闸门 + 2万字检查点共用)----
|
|
3030
3049
|
def _signing_eval(upto_chapter, note):
|
|
@@ -3048,23 +3067,30 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
3048
3067
|
.replace("__DIMKEYS__", signing_dimkey)
|
|
3049
3068
|
.replace("__NOTE__", note or "")
|
|
3050
3069
|
.replace("__MANUSCRIPT__", "\n\n".join(texts)[:26000]))
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3070
|
+
reviewers = critics or [impl]
|
|
3071
|
+
reviews, summaries, issues = [], [], []
|
|
3072
|
+
for idx, agent in enumerate(reviewers):
|
|
3073
|
+
res = _run_step(run_id, "signing-eval", modelhub.bind_agent(agent, difficulty),
|
|
3074
|
+
prompt, workdir, readonly=True, ev=ev, timeout=2400,
|
|
3075
|
+
note=("签约评估 %s" % (note or "")).strip())
|
|
3076
|
+
cj = _critique_json(res, novel_quality.SIGNING_RUBRIC)
|
|
3077
|
+
if cj.get("scores"):
|
|
3078
|
+
reviews.append({"id": agent.get("id") or "signing-%d" % idx,
|
|
3079
|
+
"scores": cj.get("scores")})
|
|
3080
|
+
summaries.append(str(cj.get("summary") or ""))
|
|
3081
|
+
issues.extend((cj.get("issues") or [])[:6])
|
|
3082
|
+
agg = quality_gate.aggregate_reviews(
|
|
3083
|
+
reviews, novel_quality.SIGNING_RUBRIC, threshold=threshold,
|
|
3084
|
+
min_reviewers=1)
|
|
3085
|
+
signing_review_meta.clear()
|
|
3086
|
+
signing_review_meta.update({"reviewer_count": agg.get("reviewer_count", 0),
|
|
3087
|
+
"consensus": agg.get("consensus", False),
|
|
3088
|
+
"spreads": agg.get("spreads", {})})
|
|
3089
|
+
means = agg.get("means") or {}
|
|
3090
|
+
if not agg.get("eligible") or not means:
|
|
3064
3091
|
return None, [], ""
|
|
3065
|
-
issues = (cj.get("issues") or [])[:6]
|
|
3066
3092
|
issues_all.extend({"chapter": upto_chapter, **it} for it in issues)
|
|
3067
|
-
return means, issues,
|
|
3093
|
+
return means, issues, next((s for s in summaries if s), "")
|
|
3068
3094
|
|
|
3069
3095
|
def _signing_record(kind, payload):
|
|
3070
3096
|
"""签约评估结果入 run(signing_evals.<kind>),审计与 UI 证据。"""
|
|
@@ -3265,9 +3291,9 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
3265
3291
|
note="小说第 %d 章" % i,
|
|
3266
3292
|
event_check=event_check, chapter=i) + note_extra,
|
|
3267
3293
|
workdir, readonly=True, ev=ev)
|
|
3268
|
-
cj =
|
|
3294
|
+
cj = _critique_json(res, dims)
|
|
3269
3295
|
if isinstance(cj, dict) and isinstance(cj.get("scores"), dict) \
|
|
3270
|
-
and cj.get("scores"):
|
|
3296
|
+
and len(cj.get("scores") or {}) == len(dims):
|
|
3271
3297
|
cj_map[spare["id"]] = cj
|
|
3272
3298
|
scored += 1
|
|
3273
3299
|
remember_agent(actual_critics, spare)
|
|
@@ -3283,11 +3309,20 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
3283
3309
|
def means_of(cj_map):
|
|
3284
3310
|
vals = {}
|
|
3285
3311
|
for d in dims:
|
|
3286
|
-
xs = [
|
|
3287
|
-
|
|
3312
|
+
xs = []
|
|
3313
|
+
for cj in cj_map.values():
|
|
3314
|
+
scores, errors = quality_gate.normalize_scores(cj.get("scores"), dims)
|
|
3315
|
+
if not errors and d in scores:
|
|
3316
|
+
xs.append(scores[d])
|
|
3288
3317
|
vals[d] = round(sum(xs) / len(xs), 1) if xs else 0.0
|
|
3289
3318
|
return vals
|
|
3290
3319
|
|
|
3320
|
+
def review_stats(cj_map, threshold_value):
|
|
3321
|
+
reviews = [{"id": aid, "scores": cj.get("scores")}
|
|
3322
|
+
for aid, cj in cj_map.items()]
|
|
3323
|
+
return quality_gate.aggregate_reviews(
|
|
3324
|
+
reviews, dims, threshold=threshold_value)
|
|
3325
|
+
|
|
3291
3326
|
critic_sids = {} # §07 T1.1:每评审的会话 id(第 2 轮复用,前缀走缓存读)
|
|
3292
3327
|
race_cj = None # 变体赛马已评审胜者:直接作为第 1 轮结果,不重评
|
|
3293
3328
|
race_scored = 0
|
|
@@ -3664,6 +3699,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
3664
3699
|
"已中止以免以 0 分误判质量" % i, ended_at=_now())
|
|
3665
3700
|
return
|
|
3666
3701
|
means = means_of(cj_by_agent)
|
|
3702
|
+
chapter_review = review_stats(cj_by_agent, threshold_ch)
|
|
3667
3703
|
passed = bool(means) and all(v >= threshold_ch for v in means.values())
|
|
3668
3704
|
if rnd == 1 and gate_live:
|
|
3669
3705
|
s_means, s_issues, s_summary = _signing_eval(
|
|
@@ -3787,6 +3823,9 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
3787
3823
|
return
|
|
3788
3824
|
cs_new = {"chapter": i, "title": ch["title"], "means": means,
|
|
3789
3825
|
"passed": bool(means) and all(v >= threshold_ch for v in means.values()),
|
|
3826
|
+
"reviewer_count": chapter_review.get("reviewer_count", 0),
|
|
3827
|
+
"reviewer_consensus": chapter_review.get("consensus", False),
|
|
3828
|
+
"reviewer_spreads": chapter_review.get("spreads", {}),
|
|
3790
3829
|
"rounds": rounds_used,
|
|
3791
3830
|
"words": _wc(_read_chapter(workdir, i)),
|
|
3792
3831
|
"quality_signals": novel_quality.signal_summary(
|
|
@@ -3839,6 +3878,9 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
3839
3878
|
i, "(2万字签约预评估检查点)")
|
|
3840
3879
|
cp = {"at_words": total_words, "chapter": i,
|
|
3841
3880
|
"means": cp_means or {},
|
|
3881
|
+
"reviewer_count": signing_review_meta.get("reviewer_count", 0),
|
|
3882
|
+
"reviewer_consensus": signing_review_meta.get("consensus", False),
|
|
3883
|
+
"reviewer_spreads": signing_review_meta.get("spreads", {}),
|
|
3842
3884
|
"passed": bool(cp_means) and all(
|
|
3843
3885
|
v >= threshold for v in cp_means.values()) if cp_means else None,
|
|
3844
3886
|
"summary": (cp_summary or "")[:200]}
|
|
@@ -3882,6 +3924,9 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
3882
3924
|
i, "(2万字检查点复核)")
|
|
3883
3925
|
if cp_means2:
|
|
3884
3926
|
cp["means"] = cp_means2
|
|
3927
|
+
cp["reviewer_count"] = signing_review_meta.get("reviewer_count", 0)
|
|
3928
|
+
cp["reviewer_consensus"] = signing_review_meta.get("consensus", False)
|
|
3929
|
+
cp["reviewer_spreads"] = signing_review_meta.get("spreads", {})
|
|
3885
3930
|
cp["summary"] = (cp_sum2 or "")[:200]
|
|
3886
3931
|
cp["passed"] = all(v >= threshold for v in cp_means2.values())
|
|
3887
3932
|
store.update_run(run_id, signing_checkpoint=cp)
|
|
@@ -3889,13 +3934,14 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
3889
3934
|
# ---- 3) 全局一致性评审(覆盖 1..end 全书:续写批次必须连同旧章一起查一致性)
|
|
3890
3935
|
full_text = "\n\n".join(_read_chapter(workdir, i) for i in range(1, end + 1))
|
|
3891
3936
|
global_issues = []
|
|
3937
|
+
global_review_results = []
|
|
3892
3938
|
|
|
3893
3939
|
def run_global_round(agent_list):
|
|
3894
3940
|
"""一轮全局评审:返回 (出分评审数, 按维累计分)。失败/不可解析不得当成低分计入。
|
|
3895
3941
|
|
|
3896
3942
|
多评审并发(2026-09-22,同章级评审并发):各自读同一份全书文本、互不依赖,
|
|
3897
3943
|
串行只是把等待时间叠起来。共享结构仍在 join 后按原顺序合并,结果与串行一致。"""
|
|
3898
|
-
gmeans_acc, scored = {}, 0
|
|
3944
|
+
gmeans_acc, scored, valid_reviews = {}, 0, []
|
|
3899
3945
|
results = {}
|
|
3900
3946
|
# 绑定解析提前到主线程(线程内不做会写盘的 bind_agent)
|
|
3901
3947
|
bound = [modelhub.bind_agent(a, difficulty) for a in agent_list]
|
|
@@ -3944,16 +3990,21 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
3944
3990
|
continue
|
|
3945
3991
|
if "error" in gj:
|
|
3946
3992
|
raise gj["error"] # 还原串行版的异常上抛语义
|
|
3947
|
-
|
|
3993
|
+
scores, errors = quality_gate.normalize_scores(gj.get("scores"), dims)
|
|
3994
|
+
partial_scores = gj.get("scores") if isinstance(gj.get("scores"), dict) else {}
|
|
3995
|
+
if scores or partial_scores:
|
|
3948
3996
|
scored += 1
|
|
3997
|
+
if not errors:
|
|
3998
|
+
valid_reviews.append({"id": agent_list[idx].get("id") or "reviewer-%d" % idx,
|
|
3999
|
+
"scores": scores})
|
|
3949
4000
|
global_issues.extend({"chapter": "全书", **it} for it in (gj.get("issues") or [])[:8])
|
|
3950
4001
|
for d in dims:
|
|
3951
|
-
v =
|
|
4002
|
+
v = scores.get(d) if not errors else partial_scores.get(d)
|
|
3952
4003
|
if v is not None:
|
|
3953
4004
|
gmeans_acc.setdefault(d, []).append(float(v))
|
|
3954
|
-
return scored, gmeans_acc
|
|
4005
|
+
return scored, gmeans_acc, valid_reviews
|
|
3955
4006
|
|
|
3956
|
-
gscored, gmeans_acc = run_global_round(critics)
|
|
4007
|
+
gscored, gmeans_acc, global_review_results = run_global_round(critics)
|
|
3957
4008
|
# 「评不上」≠「评了低分」:全局评审全挂时先从其它真实智能体补位(对齐章级
|
|
3958
4009
|
# 评审者级 fallback);补位后仍零分则判 run 失败——「无法评审」绝不能当成
|
|
3959
4010
|
# 「全局评审未通过」去盖「未达标」章(2026-09-18 假未达标案:codex 绑定链
|
|
@@ -3963,7 +4014,8 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
3963
4014
|
tried = {a.get("id") for a in critics}
|
|
3964
4015
|
for spare in [a for a in (agents or [])
|
|
3965
4016
|
if a.get("mode") == "real" and a.get("id") not in tried][:2]:
|
|
3966
|
-
sc, acc = run_global_round([spare])
|
|
4017
|
+
sc, acc, spare_reviews = run_global_round([spare])
|
|
4018
|
+
global_review_results.extend(spare_reviews)
|
|
3967
4019
|
for d, xs in acc.items():
|
|
3968
4020
|
gmeans_acc.setdefault(d, []).extend(xs)
|
|
3969
4021
|
gscored += sc
|
|
@@ -3982,6 +4034,8 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
3982
4034
|
return
|
|
3983
4035
|
global_means = {d: round(sum(xs) / len(xs), 1) for d, xs in gmeans_acc.items()}
|
|
3984
4036
|
global_pass = _all_ge(global_means, threshold)
|
|
4037
|
+
global_review = quality_gate.aggregate_reviews(
|
|
4038
|
+
global_review_results, dims, threshold=threshold)
|
|
3985
4039
|
|
|
3986
4040
|
# ---- 3.5) 自驱打磨:全局评审不过 → 自动重改最弱章并重评(至多 2 轮,无需人工)
|
|
3987
4041
|
polish_rounds = 0
|
|
@@ -4079,14 +4133,17 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
4079
4133
|
# 不代表书变差,不能拿「无法评审」覆盖真实分数)
|
|
4080
4134
|
try:
|
|
4081
4135
|
full_text = "\n\n".join(_read_chapter(workdir, i2) for i2 in range(1, end + 1))
|
|
4082
|
-
gscored2, gmeans_acc2 = run_global_round(critics)
|
|
4136
|
+
gscored2, gmeans_acc2, global_review_results2 = run_global_round(critics)
|
|
4083
4137
|
except BaseException:
|
|
4084
4138
|
store.finish_step(run_id, pstep["n"], "failed",
|
|
4085
4139
|
summary="打磨后全书重评异常中止,已落盘章稿不受影响")
|
|
4086
4140
|
raise
|
|
4087
4141
|
if gscored2:
|
|
4142
|
+
global_review_results = global_review_results2
|
|
4088
4143
|
global_means = {d: round(sum(xs) / len(xs), 1) for d, xs in gmeans_acc2.items()}
|
|
4089
4144
|
global_pass = _all_ge(global_means, threshold)
|
|
4145
|
+
global_review = quality_gate.aggregate_reviews(
|
|
4146
|
+
global_review_results, dims, threshold=threshold)
|
|
4090
4147
|
store.finish_step(run_id, pstep["n"], "done" if global_pass else "failed",
|
|
4091
4148
|
summary="重改 %s;打磨后全局 %s(%s)" % (
|
|
4092
4149
|
"、".join("第 %d 章" % x for x in fixed),
|
|
@@ -4125,7 +4182,16 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
4125
4182
|
duration_s=0.1)
|
|
4126
4183
|
|
|
4127
4184
|
chapters_pass = all(c["passed"] for c in chapter_scores)
|
|
4128
|
-
|
|
4185
|
+
major_issues = [x for x in issues_all + global_issues
|
|
4186
|
+
if isinstance(x, dict) and x.get("severity") == "major"]
|
|
4187
|
+
publishable = bool(chapters_pass and global_pass and not major_issues)
|
|
4188
|
+
signing_checkpoint = (store.get_run(run_id) or {}).get("signing_checkpoint") or {}
|
|
4189
|
+
quality_status = "continue_only"
|
|
4190
|
+
if publishable and global_review.get("consensus") and all(
|
|
4191
|
+
c.get("reviewer_count", 0) >= quality_gate.MIN_REVIEWERS and
|
|
4192
|
+
c.get("reviewer_consensus") is True for c in chapter_scores):
|
|
4193
|
+
quality_status = ("signing_ready" if signing_checkpoint.get("passed") is True
|
|
4194
|
+
else "publish_ready")
|
|
4129
4195
|
overall = round(sum(sum(c["means"].values()) / max(1, len(c["means"]))
|
|
4130
4196
|
for c in chapter_scores) / max(1, len(chapter_scores)), 1)
|
|
4131
4197
|
verdict = {
|
|
@@ -4135,6 +4201,12 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
4135
4201
|
"start_chapter": start, "end_chapter": end, "total_words": total_words,
|
|
4136
4202
|
"chapter_scores": chapter_scores, "global_scores": global_means,
|
|
4137
4203
|
"global_pass": global_pass, "route": route,
|
|
4204
|
+
"global_reviewer_count": global_review.get("reviewer_count", 0),
|
|
4205
|
+
"global_reviewer_consensus": global_review.get("consensus", False),
|
|
4206
|
+
"global_reviewer_spreads": global_review.get("spreads", {}),
|
|
4207
|
+
"major_issues": major_issues[:20],
|
|
4208
|
+
"signing_checkpoint": signing_checkpoint,
|
|
4209
|
+
"quality_status": quality_status,
|
|
4138
4210
|
}
|
|
4139
4211
|
if vol_plan:
|
|
4140
4212
|
verdict["volumes"] = vol_plan
|