codebee 0.1.71 → 0.1.73
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 +601 -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 +8 -8
- 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 +25 -1
- package/app/core/manager.py +2208 -2143
- package/app/core/market.py +90 -4
- 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 +145 -138
- package/app/core/pipeline.py +102 -30
- package/app/core/planner.py +2 -0
- package/app/core/plugins.py +34 -0
- 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 +640 -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 +1118 -824
- package/app/core/publish/qimao.py +176 -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 +98 -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/skillpacks/qimao-signing.md +70 -59
- 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 +110 -39
- package/app/mcp_server.py +175 -175
- package/app/pick_dialog.py +110 -110
- package/app/ui/app.js +509 -114
- 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 +41 -2
- package/app/ui/icons/bee.svg +79 -79
- package/app/ui/index.html +8 -3
- package/app/ui/manifest.json +16 -16
- package/app/ui/qrcode.js +2297 -2297
- package/app/ui/style.css +74 -26
- 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,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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
"
|
|
134
|
-
"
|
|
135
|
-
"
|
|
136
|
-
"
|
|
137
|
-
"
|
|
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
|
+
|
|
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
|
@@ -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
|
package/app/core/planner.py
CHANGED
|
@@ -256,6 +256,8 @@ __SKILLS__
|
|
|
256
256
|
只输出一个 ```json 代码块,不要输出其他内容。JSON 结构:
|
|
257
257
|
{"book_title": "书名", "style_anchor": "全书文风锚(80字内,具体可执行:叙述视角与人称、句长基调、语域、对白风格、禁用表达)", "chapters": [{"title": "章节标题", "beats": "本章剧情要点(50-120字:事件/冲突/推进)", "hook": "章末钩子(一句话)", "highlight": "本章爽点/情绪爆点(一句话:谁在哪里完成什么反转或释放什么情绪)"}]__VOLUMES_KEY__}
|
|
258
258
|
硬性要求:
|
|
259
|
+
- book_title 4-15 字,必须考虑平台书名全站唯一(撞名建不了书):避开「我的XX藏不住」
|
|
260
|
+
「开局XX」等高频套路组合,书名里带本书独有的专名(人名/金手指/设定词),独有专名越多越不易撞名;
|
|
259
261
|
- 第 1-3 章是黄金三章:第 1 章前 100 字出现主角、前 200 字出现异常或欲望、前 600 字进入具体困境,
|
|
260
262
|
第 3 章末留大钩子;不要用世界观或环境描写拖延切入;
|
|
261
263
|
- 每章都写清“主角主动选择 → 阻碍 → 局面变化 → 章末钩子”,禁止水字数的日常流水账和硬旁白推剧情;
|
package/app/core/plugins.py
CHANGED
|
@@ -9,6 +9,7 @@ bundles are never executed by this module.
|
|
|
9
9
|
"""
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
+
import hashlib
|
|
12
13
|
import json
|
|
13
14
|
import os
|
|
14
15
|
import re
|
|
@@ -523,3 +524,36 @@ def active_mcp_servers():
|
|
|
523
524
|
continue
|
|
524
525
|
return [x for x in out if isinstance(x, dict) and x.get("command")]
|
|
525
526
|
|
|
527
|
+
|
|
528
|
+
def merged_servers_text(user_text):
|
|
529
|
+
"""用户显式 MCP 清单(JSON 文本)+ 已启用插件的声明 → 合成后的 JSON 文本。
|
|
530
|
+
|
|
531
|
+
插件服务器名加 ``plg_`` 前缀+短哈希隔离,防与用户配置撞名;用户清单
|
|
532
|
+
非法或非数组时原样返回,坏插件绝不连累用户显式配置的服务器。
|
|
533
|
+
逻辑原先长在 main() 启动闭包里、测试够不着(ae9bcb8 半交付时两个
|
|
534
|
+
用例即因此恒错),2026-09-30 随 /api/plugins 路由补齐一并下沉。"""
|
|
535
|
+
raw = str(user_text or "")
|
|
536
|
+
try:
|
|
537
|
+
configured = json.loads(raw) if raw.strip() else []
|
|
538
|
+
except (ValueError, TypeError):
|
|
539
|
+
return raw
|
|
540
|
+
if not isinstance(configured, list):
|
|
541
|
+
return raw
|
|
542
|
+
try:
|
|
543
|
+
plugin_servers = active_mcp_servers()
|
|
544
|
+
except Exception:
|
|
545
|
+
plugin_servers = []
|
|
546
|
+
for item in plugin_servers or []:
|
|
547
|
+
if not isinstance(item, dict):
|
|
548
|
+
continue
|
|
549
|
+
plugin_id = str(item.pop("plugin_id", "plugin"))
|
|
550
|
+
server_name = str(item.get("name") or "server")
|
|
551
|
+
digest = hashlib.sha256(
|
|
552
|
+
(plugin_id + ":" + server_name).encode("utf-8")).hexdigest()[:6]
|
|
553
|
+
safe = re.sub(r"[^a-z0-9_-]", "-", plugin_id.lower())
|
|
554
|
+
item["name"] = ("plg_" + safe[:11] + "_" + digest)[:24]
|
|
555
|
+
if not any(x.get("name") == item["name"] for x in configured
|
|
556
|
+
if isinstance(x, dict)):
|
|
557
|
+
configured.append(item)
|
|
558
|
+
return json.dumps(configured, ensure_ascii=False)
|
|
559
|
+
|