codebee 0.1.95 → 0.1.96
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 +725 -720
- package/LICENSE +21 -21
- package/README.md +515 -509
- package/app/core/acceptance.py +100 -100
- package/app/core/agent_context.py +100 -0
- package/app/core/aiflavor.py +197 -197
- package/app/core/analytics.py +38 -38
- package/app/core/artifacts.py +148 -148
- package/app/core/assets.py +110 -110
- package/app/core/attachments.py +623 -623
- package/app/core/audit.py +30 -30
- package/app/core/backup.py +470 -470
- package/app/core/beekeeper.py +183 -183
- package/app/core/benchstore.py +102 -95
- package/app/core/board.py +209 -209
- package/app/core/bookmeta.py +1036 -1036
- package/app/core/bookmeta_catalog.py +180 -180
- package/app/core/branching.py +137 -137
- package/app/core/builtin_agent.py +1680 -1680
- package/app/core/capability.py +144 -144
- package/app/core/chaptersafe.py +32 -32
- package/app/core/checkpoints.py +143 -0
- package/app/core/cleanup.py +303 -303
- package/app/core/compaction.py +201 -201
- package/app/core/contracts.py +255 -255
- package/app/core/covergen.py +356 -356
- package/app/core/defectretro.py +43 -43
- package/app/core/diagnostics.py +114 -114
- package/app/core/dispatch.py +198 -191
- 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 +82 -0
- package/app/core/evalbench.py +184 -91
- package/app/core/evaluation.py +174 -174
- package/app/core/failure_notify.py +290 -290
- package/app/core/flow_graph.py +33 -0
- 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/jobs.py +25 -4
- package/app/core/knowledge_pipeline.py +86 -0
- package/app/core/manager.py +2232 -2232
- package/app/core/mocks.py +64 -64
- package/app/core/notify.py +201 -201
- package/app/core/novel_quality.py +181 -189
- package/app/core/openapi.py +177 -154
- package/app/core/operations.py +228 -228
- package/app/core/paihang.py +145 -145
- package/app/core/pipeline.py +43 -19
- package/app/core/planner.py +17 -11
- package/app/core/policy.py +52 -0
- package/app/core/portguard.py +111 -111
- package/app/core/presence.py +77 -77
- package/app/core/preview.py +293 -293
- package/app/core/prompt_guard.py +78 -78
- package/app/core/publish/__init__.py +7 -7
- package/app/core/publish/auto.py +26 -3
- package/app/core/publish/browser.py +655 -655
- package/app/core/publish/chaprows.py +138 -0
- package/app/core/publish/fanqie.py +210 -165
- package/app/core/publish/flow.py +753 -640
- package/app/core/publish/flows-fanqie-calibrated.json +349 -336
- package/app/core/publish/ledger.py +13 -1
- package/app/core/publish/manager.py +1150 -1118
- package/app/core/publish/qimao.py +176 -176
- package/app/core/publish/volumes.py +108 -0
- package/app/core/publish/ws.py +139 -139
- package/app/core/quality_gate.py +194 -194
- package/app/core/redact.py +38 -38
- package/app/core/repeat_guard.py +129 -129
- package/app/core/retrieval.py +137 -137
- package/app/core/router.py +398 -398
- package/app/core/runner.py +2238 -2238
- package/app/core/selfupdate.py +384 -384
- package/app/core/session_log.py +161 -161
- package/app/core/sessions.py +312 -312
- package/app/core/settings.py +20 -0
- package/app/core/settings_schema.py +270 -270
- package/app/core/share_page.py +114 -114
- package/app/core/skill_scan.py +139 -139
- package/app/core/skillpacks/fanqie-novel.md +8 -6
- 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 +14 -11
- package/app/core/skills.py +32 -21
- package/app/core/step_runner.py +132 -132
- package/app/core/store.py +9 -0
- package/app/core/story_tracking.py +275 -275
- package/app/core/task_compile.py +199 -199
- package/app/core/telemetry.py +292 -292
- package/app/core/tlsctx.py +64 -64
- package/app/core/token_meter.py +147 -147
- package/app/core/tracing.py +207 -144
- 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 -0
- package/app/core/wxdigest.py +858 -858
- package/app/core/zentao.py +2692 -2692
- package/app/main.py +104 -9
- package/app/mcp_server.py +244 -244
- package/app/pick_dialog.py +110 -110
- package/app/ui/app.js +286 -18
- package/app/ui/artdiff.js +74 -74
- package/app/ui/assets/community-qr.png +0 -0
- package/app/ui/assets/donate-alipay.jpg +0 -0
- package/app/ui/assets/donate-qr.jpg +0 -0
- package/app/ui/board.html +162 -162
- package/app/ui/board.js +305 -305
- package/app/ui/i18n.js +33 -0
- package/app/ui/icons/bee.svg +79 -79
- package/app/ui/index.html +32 -3
- package/app/ui/manifest.json +16 -16
- package/app/ui/qrcode.js +2297 -2297
- package/app/ui/style.css +196 -2
- package/bin/tutti.js +147 -147
- package/package.json +48 -48
- 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/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
|
@@ -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, quality_gate, registry, router, runner, skills, store, task_compile, usage, volumes
|
|
22
|
+
from . import aiflavor, agent_context, attachments, branching, catalog, chaptersafe, checkpoints, 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
|
|
@@ -442,6 +442,14 @@ def _run_step(run_id, role, agent, prompt, workdir, readonly, ev, timeout=runner
|
|
|
442
442
|
agent.get("label", agent["id"]), note=note,
|
|
443
443
|
model=agent.get("model") or "",
|
|
444
444
|
provider=_binding_brief(agent))
|
|
445
|
+
try:
|
|
446
|
+
checkpoints.start(
|
|
447
|
+
run_id, step["n"], role, prompt,
|
|
448
|
+
input_data={"workdir": workdir, "readonly": readonly,
|
|
449
|
+
"agent": agent.get("id")},
|
|
450
|
+
attempt=len((store.get_run(run_id) or {}).get("steps") or []))
|
|
451
|
+
except Exception:
|
|
452
|
+
pass
|
|
445
453
|
start = time.time()
|
|
446
454
|
if dead_binding:
|
|
447
455
|
from .error_codes import ErrorCode
|
|
@@ -779,6 +787,17 @@ def _finish_step_result(run_id, step, res, role, agent, start):
|
|
|
779
787
|
# 思考过程(内置智能体流式抓取):落进步骤记录,对话气泡折叠展示
|
|
780
788
|
thinking=res.get("reasoning") or None,
|
|
781
789
|
partial=bool(res.get("partial")))
|
|
790
|
+
try:
|
|
791
|
+
checkpoint_status = ("unknown" if raw.get("unknown") else
|
|
792
|
+
"timeout" if status == "timeout" else
|
|
793
|
+
"cancelled" if status == "cancelled" else
|
|
794
|
+
"done" if status == "done" else "failed")
|
|
795
|
+
checkpoints.finish(
|
|
796
|
+
run_id, step["n"], checkpoint_status,
|
|
797
|
+
error=res.get("error") or "", output=res.get("text") or "",
|
|
798
|
+
metadata={"role": role, "agent": agent.get("id") or ""})
|
|
799
|
+
except Exception:
|
|
800
|
+
pass
|
|
782
801
|
# 错误台账:失败/超时各记一条结构化记录(遥测与诊断包的数据源)。
|
|
783
802
|
# 用户主动取消不入账——那不是产品问题;detail 只存脱敏后的失败摘录。
|
|
784
803
|
if status in ("failed", "timeout"):
|
|
@@ -2692,8 +2711,8 @@ __VOLUME__
|
|
|
2692
2711
|
- 撰写本书第 __I__ 章,把本章正文写入文件 `__FILE__`(直接写入该文件,只写本章)。文件必须以 UTF-8 编码保存:PowerShell 一律显式加 `-Encoding UTF8`(如 `Set-Content -Path __FILE__ -Encoding UTF8`),禁止依赖系统默认编码,否则中文会乱码。
|
|
2693
2712
|
- 章节标题:__TITLE__
|
|
2694
2713
|
- 剧情要点:__BEATS__
|
|
2695
|
-
-
|
|
2696
|
-
-
|
|
2714
|
+
- 章末收束/继续阅读理由(可自然收束或留白):__HOOK__
|
|
2715
|
+
- 本章主要情绪/戏剧变化:__HIGHLIGHT__(按其重要性决定篇幅和表现方式;不强制放大,不为满足标签伪造高潮)
|
|
2697
2716
|
__LOCKED_CH__
|
|
2698
2717
|
__STALE_NOTE__
|
|
2699
2718
|
- 正文约 __WORDS__ 字,中文,直接开写正文(可含本章标题行)。
|
|
@@ -2732,23 +2751,23 @@ __STYLE__
|
|
|
2732
2751
|
|
|
2733
2752
|
## 签约返修原则
|
|
2734
2753
|
- 统一叙述语域、句式和视角距离,不在白描、半文半白和网络梗之间跳变。
|
|
2735
|
-
-
|
|
2736
|
-
-
|
|
2754
|
+
- 复杂情感需按其叙事分量呈现;动作、感官、对话、内心直述、环境或留白均可,避免与本稿风格不合的机械扩写。
|
|
2755
|
+
- 重要情绪/戏剧变化要有与其分量相称的呈现;避免概括带过,但不把每个变化都扩写成高潮。
|
|
2737
2756
|
- 删除不改变局面的环境、解释和重复叙述;每段至少完成冲突、信息或人物选择之一。
|
|
2738
2757
|
- 让主角主动做出选择并推动下一步,补足因果桥,不用旁白硬推剧情。
|
|
2739
2758
|
|
|
2740
2759
|
## 要求
|
|
2741
2760
|
- 针对性解决所有 major 问题(含确定性文本信号指出的问题),保持与前后的剧情衔接;字数仍约 __WORDS__ 字。"""
|
|
2742
2761
|
|
|
2743
|
-
SERIAL_GLOBAL_PROMPT = """
|
|
2762
|
+
SERIAL_GLOBAL_PROMPT = """你是独立小说质量评审(不要修改任何文件),不代表任何签约平台。全书各章已完稿,请从**全书整体**视角评审。
|
|
2744
2763
|
请输出一个 ```json 代码块,不要输出其他内容。JSON 结构:
|
|
2745
2764
|
{
|
|
2746
2765
|
"scores": {"__DIMKEYS__"},
|
|
2747
2766
|
"issues": [{"dim": "维度名", "severity": "major|minor", "note": "具体问题(指明哪一章)", "quote": "支撑该问题的稿件原文连续片段(≥8字,逐字摘录不许改写)"}],
|
|
2748
|
-
"summary": "
|
|
2767
|
+
"summary": "一句话总评:所读范围内的文本优势、主要风险与未验证项;不得预测签约结果"
|
|
2749
2768
|
}
|
|
2750
|
-
每个维度打 1-10
|
|
2751
|
-
|
|
2769
|
+
每个维度打 1-10 分,按统一文本 rubric 与所读证据评估,不以“宁严勿宽”压分。重点关注:主线一致性、人物弧光、开篇吸引力、文风表现、情感表达、情节推进与节奏控制。
|
|
2770
|
+
若是全书最初的前 3 章,可观察视角人物、故事承诺、开篇兑现和读者理解成本;固定字数点、钩子/爽点密度仅作参考,不是扣分门槛。发现问题须给可定位原文引文,并区分文本事实、解释和建议。
|
|
2752
2771
|
major 问题必须给 quote(系统会逐条校验引文是否真在稿件中——编造的引文会被降档标记)。
|
|
2753
2772
|
|
|
2754
2773
|
## 全书目标
|
|
@@ -2759,19 +2778,19 @@ __GOAL__
|
|
|
2759
2778
|
__MANUSCRIPT__
|
|
2760
2779
|
---"""
|
|
2761
2780
|
|
|
2762
|
-
#
|
|
2763
|
-
#
|
|
2764
|
-
SERIAL_SIGNING_EVAL_PROMPT = """
|
|
2781
|
+
# 独立文本质量评估(开篇闸门 + 项目检查点共用):沿用稳定 rubric,
|
|
2782
|
+
# 不代表平台决定;按原文证据检查常见拒稿问题,不将经验观察点变成硬门槛。
|
|
2783
|
+
SERIAL_SIGNING_EVAL_PROMPT = """你是独立小说质量评审(不要修改任何文件),不是番茄或七猫的签约编辑,不能代表平台结论。
|
|
2784
|
+
评审稿件__NOTE__,按下方统一文本质量 rubric 评估;如附当前平台规则或编辑原话,可单独核对适配,不得臆测平台内部标准。
|
|
2765
2785
|
请输出一个 ```json 代码块,不要输出其他内容。JSON 结构:
|
|
2766
2786
|
{
|
|
2767
2787
|
"scores": {"__DIMKEYS__"},
|
|
2768
2788
|
"issues": [{"dim": "维度名", "severity": "major|minor", "note": "具体问题(指明哪一章)", "quote": "支撑该问题的稿件原文连续片段(≥8字,逐字摘录不许改写)"}],
|
|
2769
|
-
"summary": "
|
|
2789
|
+
"summary": "一句话内部文本诊断:优势、主要风险和未验证项;不得表述为平台结论或过签预测"
|
|
2770
2790
|
}
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
行文拖沓节奏缓慢(重复铺垫、与主线无关的描写)——每一条都要给出明确判定与证据。
|
|
2791
|
+
统一评分 rubric:1-3 为明显影响理解或阅读的严重问题;4-6 为有具体可定位的弱项;7-8 为基本有效、仍有局部提升空间;9-10 为在本稿目标下表现突出。分数必须由所读文本支撑,不把分数映射成过签概率。
|
|
2792
|
+
五个维度固定为:开篇吸引力、情节推进、文风统一、情感细腻度、节奏控制。每项给证据与未读范围;不能因不用某种套路(如黄金三章、打脸、单一视角)扣分。
|
|
2793
|
+
可检查文风是否突变、情绪是否被概括、开篇承诺是否兑现、因果桥是否成立、重复是否损害节奏;这些是检查问题而不是预设缺陷。没有当前平台规则或明确编辑反馈时,不把质量评审包装成平台审核结论。
|
|
2775
2794
|
|
|
2776
2795
|
## 待评审稿件
|
|
2777
2796
|
---
|
|
@@ -4309,7 +4328,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
4309
4328
|
agent.get("label"))
|
|
4310
4329
|
time.sleep(0.15)
|
|
4311
4330
|
gj = {"scores": {d: 8.0 for d in dims},
|
|
4312
|
-
"issues": [], "summary": "(mock
|
|
4331
|
+
"issues": [], "summary": "(mock)内部结构评估:整体结构完整;此结果不代表平台签约结论"}
|
|
4313
4332
|
store.finish_step(run_id, step["n"], "done", summary="均分 8.0:(mock)全书达标",
|
|
4314
4333
|
duration_s=0.15)
|
|
4315
4334
|
results[idx] = gj
|
|
@@ -4326,7 +4345,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
4326
4345
|
"- 本次送审批次为全书第 %d—%d 章(连载中段批次),文本中的前文节选仅供衔接核对;\n"
|
|
4327
4346
|
"- 「黄金一章/黄金三章」口径仅适用于全书第 1—3 章,不得据此扣减本批批首章的开篇分;\n"
|
|
4328
4347
|
"- 本批各章的既定事件以工作目录「章纲/」下的锁定章纲为准(可自行查阅):承段、"
|
|
4329
|
-
"
|
|
4348
|
+
"过渡章的节奏分应对照章纲既定事件与叙事功能是否落实;自然收束、留白均可,不以悬念钩子作为硬门槛。"
|
|
4330
4349
|
"不以高潮段的事件密度为统一标准;\n"
|
|
4331
4350
|
"- 「伏笔悬置」类 major 须先核对章纲的揭示位安排:揭示位写在后批章纲中的伏笔"
|
|
4332
4351
|
"不算悬置,无任何安排的悬置才成立;\n"
|
|
@@ -5505,6 +5524,11 @@ def execute_run(run_id):
|
|
|
5505
5524
|
difficulty=task_spec["difficulty"])
|
|
5506
5525
|
task = dict(task)
|
|
5507
5526
|
task = attachments.refresh_task(task, task.get("workdir") or "")
|
|
5527
|
+
try:
|
|
5528
|
+
task, _agent_context = agent_context.inject(task, task.get("workdir") or "")
|
|
5529
|
+
store.update_run(run_id, agent_context=_agent_context)
|
|
5530
|
+
except Exception:
|
|
5531
|
+
pass
|
|
5508
5532
|
task["_compiled_spec"] = task_spec
|
|
5509
5533
|
# 运行内统一使用编译后的难度;store 中历史任务常带 difficulty=auto,
|
|
5510
5534
|
# 不能让这个兼容值覆盖 easy/default/hard 的模型调度决策。
|
package/app/core/planner.py
CHANGED
|
@@ -250,21 +250,25 @@ __GOAL__
|
|
|
250
250
|
## 背景与上下文
|
|
251
251
|
__CONTEXT__"""
|
|
252
252
|
|
|
253
|
-
SERIAL_OUTLINE_PROMPT = """
|
|
253
|
+
SERIAL_OUTLINE_PROMPT = """你是小说结构编辑。你不代表番茄、七猫或其他平台,也不能预测签约或流量结果。
|
|
254
254
|
|
|
255
255
|
__SKILLS__
|
|
256
256
|
请为下面的小说目标设计一份连载大纲:共 __N__ 章,每章约 __W__ 字。
|
|
257
257
|
只输出一个 ```json 代码块,不要输出其他内容。JSON 结构:
|
|
258
258
|
{"book_title": "书名", "style_anchor": "全书文风锚(80字内,具体可执行:叙述视角与人称、句长基调、语域、对白风格、禁用表达)", "chapters": [{"title": "章节标题", "beats": "本章剧情要点(50-120字:事件/冲突/推进)", "hook": "章末钩子(一句话)", "highlight": "本章爽点/情绪爆点(一句话:谁在哪里完成什么反转或释放什么情绪)"}]__VOLUMES_KEY__}
|
|
259
|
+
字段说明:hook 可为自然收束、留白或继续阅读理由,不强制悬念;highlight 表示主要戏剧/情绪变化或回报,无显著爆点时说明叙事功能,不要强造爆点。
|
|
259
260
|
硬性要求:
|
|
260
261
|
- book_title 4-15 字,必须考虑平台书名全站唯一(撞名建不了书):避开「我的XX藏不住」
|
|
261
262
|
「开局XX」等高频套路组合,书名里带本书独有的专名(人名/金手指/设定词),独有专名越多越不易撞名;
|
|
262
|
-
-
|
|
263
|
-
|
|
264
|
-
-
|
|
263
|
+
- 把开篇字数位置当阅读观察点而非硬门槛:尽早让读者辨认视角人物、故事承诺和正在发生的事,
|
|
264
|
+
但按题材、叙述策略与实际效果决定节奏,不为数字机械塞冲突;
|
|
265
|
+
- 每章要有清楚的叙事功能(推进事件、人物/关系、信息、氛围或情绪之一或多项),并检查因果与局面变化;
|
|
266
|
+
不强制每章都打脸、反转或以悬念断章;
|
|
267
|
+
- 章末可自然收束或留白;大纲中的 highlight 描述主要戏剧/情绪变化或回报,无显著爆点时写清叙事功能,不要强造爆点。
|
|
265
268
|
- 大纲中的情感转折必须给出触发事件和人物选择,不能只写“误会加深/感情升温”;
|
|
266
269
|
- style_anchor 是全书唯一的文风锚,每章起草/修订/评审都会注入:必须写明视角人称与句长基调等可执行约束,不要空话;
|
|
267
|
-
-
|
|
270
|
+
- highlight 是本章主要情绪/戏剧变化或读者回报,可为爽、暖、痛、惊、趣味、亲密、发现、安定等;
|
|
271
|
+
平静或过渡章节可承担必要功能,不为填字段伪造高潮;
|
|
268
272
|
- 结局必须闭环(完本感),主角有成长弧光;
|
|
269
273
|
- 题材健康,无违规内容,符合平台签约调性。
|
|
270
274
|
__VOLUMES__
|
|
@@ -286,7 +290,7 @@ __SKILLS__
|
|
|
286
290
|
- 主线沿既有脉络推进,新冲突尽量从已埋伏笔中生长,人物性格与前文一致;
|
|
287
291
|
- 若下方给出「锁定章纲」:本批各章 title/beats/hook 必须逐章从对应章纲提炼对齐
|
|
288
292
|
(标题沿用、视角不换、节拍不裁并、伏笔揭示位不提前),不得虚构章纲之外的主线事件;
|
|
289
|
-
-
|
|
293
|
+
- 在关键章节写清人物选择、阻碍与后果;不要求每章都有完整冲突闭环或悬念钩子,避免无功能重复;
|
|
290
294
|
- 复杂情感必须落到触发事件、动作/生理反应和潜台词,不能用一句概括代替;
|
|
291
295
|
- 延续既定叙述视角和语言锚点(见下方既定文风锚),删掉重复复述,避免节奏变慢;
|
|
292
296
|
- 题材健康,无违规内容,符合平台签约调性。
|
|
@@ -749,15 +753,17 @@ def make_serial_outline(task, author_agent=None, workdir=None, ev=None, log_path
|
|
|
749
753
|
|
|
750
754
|
# 开篇重设计提示词(开篇闸门专用):第 1 章签约评估的开篇吸引力未达线时,
|
|
751
755
|
# 回大纲层重设计前三章节拍而不是只修稿——切入点不行是结构病,修文案救不了。
|
|
752
|
-
SERIAL_OPENING_REBUILD_PROMPT = """
|
|
753
|
-
|
|
756
|
+
SERIAL_OPENING_REBUILD_PROMPT = """你是小说结构编辑,不代表任何平台,也不能预测签约结果。
|
|
757
|
+
这本书的开篇评审发现吸引力风险,实际意见如下:
|
|
754
758
|
__CRITIQUE__
|
|
755
759
|
|
|
756
|
-
请重设计前 __COUNT__ 章的大纲节拍(只动这 __COUNT__
|
|
757
|
-
|
|
758
|
-
|
|
760
|
+
请重设计前 __COUNT__ 章的大纲节拍(只动这 __COUNT__ 章,不碰后续章节),针对反馈解决具体问题。
|
|
761
|
+
先保留原稿有效之处,再提出能改变事件、人物选择或后果的改动;不能只换词、加怪声或添加钩子。
|
|
762
|
+
前 100/200/600 字只是阅读观察点,不是硬指标;不预设危机、金手指、背叛或打脸。
|
|
763
|
+
判断开篇是否成立,要看题材承诺是否兑现、事件是否具体、读者能否理解人物处境和下一步,尊重本稿叙述方式。
|
|
759
764
|
只输出一个 ```json 代码块,不要输出其他内容。JSON 结构:
|
|
760
765
|
{"chapters": [{"title": "章节标题", "beats": "本章剧情要点(50-120字:事件/冲突/推进)", "hook": "章末钩子(一句话)", "highlight": "本章爽点/情绪爆点(一句话)"}]}
|
|
766
|
+
其中 hook 可为自然收束、留白或继续阅读理由,不强制悬念;highlight 无显著爆点时说明叙事功能,不要强造爆点。
|
|
761
767
|
章序与原大纲一致(从第 1 章起),必须保持书名、主角与核心设定不变,只强化切入与钩子设计。
|
|
762
768
|
|
|
763
769
|
## 全书目标
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Descriptive sandbox policy primitives shared by API and runners."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def normalize_sandbox(spec=None, workdir=None):
|
|
10
|
+
spec = spec if isinstance(spec, dict) else {}
|
|
11
|
+
root = Path(workdir or os.getcwd()).expanduser().resolve()
|
|
12
|
+
roots = []
|
|
13
|
+
for value in spec.get("allowed_roots") or [str(root)]:
|
|
14
|
+
try:
|
|
15
|
+
candidate = Path(value).expanduser().resolve()
|
|
16
|
+
except (TypeError, OSError, ValueError):
|
|
17
|
+
continue
|
|
18
|
+
if candidate not in roots:
|
|
19
|
+
roots.append(candidate)
|
|
20
|
+
if not roots:
|
|
21
|
+
roots = [root]
|
|
22
|
+
env = []
|
|
23
|
+
for value in spec.get("env_allowlist") or []:
|
|
24
|
+
name = str(value or "").strip()
|
|
25
|
+
if name and name.replace("_", "a").isalnum() and name not in env:
|
|
26
|
+
env.append(name)
|
|
27
|
+
try:
|
|
28
|
+
timeout = max(1, min(7 * 24 * 3600, int(spec.get("timeout_s") or 3600)))
|
|
29
|
+
except (TypeError, ValueError):
|
|
30
|
+
timeout = 3600
|
|
31
|
+
try:
|
|
32
|
+
output = max(1024, min(100 * 1024 * 1024, int(spec.get("max_output_bytes") or 10 * 1024 * 1024)))
|
|
33
|
+
except (TypeError, ValueError):
|
|
34
|
+
output = 10 * 1024 * 1024
|
|
35
|
+
return {"allowed_roots": [str(x) for x in roots], "env_allowlist": env,
|
|
36
|
+
"network": bool(spec.get("network", True)), "timeout_s": timeout,
|
|
37
|
+
"max_output_bytes": output}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def path_allowed(path, sandbox):
|
|
41
|
+
try:
|
|
42
|
+
candidate = Path(path).expanduser().resolve()
|
|
43
|
+
return any(candidate == Path(root) or Path(root) in candidate.parents
|
|
44
|
+
for root in (sandbox or {}).get("allowed_roots") or [])
|
|
45
|
+
except (TypeError, OSError, ValueError):
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def filter_env(env, sandbox):
|
|
50
|
+
values = env if isinstance(env, dict) else {}
|
|
51
|
+
allow = (sandbox or {}).get("env_allowlist") or []
|
|
52
|
+
return {str(k): str(v) for k, v in values.items() if str(k) in allow}
|