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.
Files changed (138) hide show
  1. package/CHANGELOG.md +725 -720
  2. package/LICENSE +21 -21
  3. package/README.md +515 -509
  4. package/app/core/acceptance.py +100 -100
  5. package/app/core/agent_context.py +100 -0
  6. package/app/core/aiflavor.py +197 -197
  7. package/app/core/analytics.py +38 -38
  8. package/app/core/artifacts.py +148 -148
  9. package/app/core/assets.py +110 -110
  10. package/app/core/attachments.py +623 -623
  11. package/app/core/audit.py +30 -30
  12. package/app/core/backup.py +470 -470
  13. package/app/core/beekeeper.py +183 -183
  14. package/app/core/benchstore.py +102 -95
  15. package/app/core/board.py +209 -209
  16. package/app/core/bookmeta.py +1036 -1036
  17. package/app/core/bookmeta_catalog.py +180 -180
  18. package/app/core/branching.py +137 -137
  19. package/app/core/builtin_agent.py +1680 -1680
  20. package/app/core/capability.py +144 -144
  21. package/app/core/chaptersafe.py +32 -32
  22. package/app/core/checkpoints.py +143 -0
  23. package/app/core/cleanup.py +303 -303
  24. package/app/core/compaction.py +201 -201
  25. package/app/core/contracts.py +255 -255
  26. package/app/core/covergen.py +356 -356
  27. package/app/core/defectretro.py +43 -43
  28. package/app/core/diagnostics.py +114 -114
  29. package/app/core/dispatch.py +198 -191
  30. package/app/core/dispatch_log.py +128 -128
  31. package/app/core/env_scrub.py +85 -85
  32. package/app/core/errorlog.py +137 -137
  33. package/app/core/eval_matrix.py +82 -0
  34. package/app/core/evalbench.py +184 -91
  35. package/app/core/evaluation.py +174 -174
  36. package/app/core/failure_notify.py +290 -290
  37. package/app/core/flow_graph.py +33 -0
  38. package/app/core/goal_service.py +167 -167
  39. package/app/core/health.py +389 -389
  40. package/app/core/history.py +38 -38
  41. package/app/core/hooks.py +162 -162
  42. package/app/core/jobs.py +25 -4
  43. package/app/core/knowledge_pipeline.py +86 -0
  44. package/app/core/manager.py +2232 -2232
  45. package/app/core/mocks.py +64 -64
  46. package/app/core/notify.py +201 -201
  47. package/app/core/novel_quality.py +181 -189
  48. package/app/core/openapi.py +177 -154
  49. package/app/core/operations.py +228 -228
  50. package/app/core/paihang.py +145 -145
  51. package/app/core/pipeline.py +43 -19
  52. package/app/core/planner.py +17 -11
  53. package/app/core/policy.py +52 -0
  54. package/app/core/portguard.py +111 -111
  55. package/app/core/presence.py +77 -77
  56. package/app/core/preview.py +293 -293
  57. package/app/core/prompt_guard.py +78 -78
  58. package/app/core/publish/__init__.py +7 -7
  59. package/app/core/publish/auto.py +26 -3
  60. package/app/core/publish/browser.py +655 -655
  61. package/app/core/publish/chaprows.py +138 -0
  62. package/app/core/publish/fanqie.py +210 -165
  63. package/app/core/publish/flow.py +753 -640
  64. package/app/core/publish/flows-fanqie-calibrated.json +349 -336
  65. package/app/core/publish/ledger.py +13 -1
  66. package/app/core/publish/manager.py +1150 -1118
  67. package/app/core/publish/qimao.py +176 -176
  68. package/app/core/publish/volumes.py +108 -0
  69. package/app/core/publish/ws.py +139 -139
  70. package/app/core/quality_gate.py +194 -194
  71. package/app/core/redact.py +38 -38
  72. package/app/core/repeat_guard.py +129 -129
  73. package/app/core/retrieval.py +137 -137
  74. package/app/core/router.py +398 -398
  75. package/app/core/runner.py +2238 -2238
  76. package/app/core/selfupdate.py +384 -384
  77. package/app/core/session_log.py +161 -161
  78. package/app/core/sessions.py +312 -312
  79. package/app/core/settings.py +20 -0
  80. package/app/core/settings_schema.py +270 -270
  81. package/app/core/share_page.py +114 -114
  82. package/app/core/skill_scan.py +139 -139
  83. package/app/core/skillpacks/fanqie-novel.md +8 -6
  84. package/app/core/skillpacks/market/character-bible.md +66 -66
  85. package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
  86. package/app/core/skillpacks/market/git-workflow.md +57 -57
  87. package/app/core/skillpacks/market/release-notes.md +72 -72
  88. package/app/core/skillpacks/market/weekly-report.md +71 -71
  89. package/app/core/skillpacks/market/worldview-consistency.md +70 -70
  90. package/app/core/skillpacks/qimao-signing.md +14 -11
  91. package/app/core/skills.py +32 -21
  92. package/app/core/step_runner.py +132 -132
  93. package/app/core/store.py +9 -0
  94. package/app/core/story_tracking.py +275 -275
  95. package/app/core/task_compile.py +199 -199
  96. package/app/core/telemetry.py +292 -292
  97. package/app/core/tlsctx.py +64 -64
  98. package/app/core/token_meter.py +147 -147
  99. package/app/core/tracing.py +207 -144
  100. package/app/core/upstream.py +41 -41
  101. package/app/core/usage.py +1038 -1038
  102. package/app/core/volumes.py +389 -389
  103. package/app/core/webhooks.py +89 -0
  104. package/app/core/wxdigest.py +858 -858
  105. package/app/core/zentao.py +2692 -2692
  106. package/app/main.py +104 -9
  107. package/app/mcp_server.py +244 -244
  108. package/app/pick_dialog.py +110 -110
  109. package/app/ui/app.js +286 -18
  110. package/app/ui/artdiff.js +74 -74
  111. package/app/ui/assets/community-qr.png +0 -0
  112. package/app/ui/assets/donate-alipay.jpg +0 -0
  113. package/app/ui/assets/donate-qr.jpg +0 -0
  114. package/app/ui/board.html +162 -162
  115. package/app/ui/board.js +305 -305
  116. package/app/ui/i18n.js +33 -0
  117. package/app/ui/icons/bee.svg +79 -79
  118. package/app/ui/index.html +32 -3
  119. package/app/ui/manifest.json +16 -16
  120. package/app/ui/qrcode.js +2297 -2297
  121. package/app/ui/style.css +196 -2
  122. package/bin/tutti.js +147 -147
  123. package/package.json +48 -48
  124. package/tools/wx_reader.py +261 -261
  125. package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +0 -88
  126. package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +0 -126
  127. package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
  128. package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +0 -210
  129. package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
  130. package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
  131. package/app/.playwright-cli/run-detail-after.png +0 -0
  132. package/app/output/_createbook_body.json +0 -1
  133. package/app/output/_probe_fq_click.py +0 -50
  134. package/app/output/_probe_fq_form.py +0 -42
  135. package/app/output/_probe_fq_modal.py +0 -55
  136. package/app/output/_probe_fq_points.py +0 -71
  137. package/app/output/_probe_fq_submit.py +0 -34
  138. package/app/output/_probe_fq_watch.py +0 -62
@@ -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 "(用户未指定方向,按大盘热门分析)"))
@@ -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
- - 章末钩子:__HOOK__
2696
- - 本章爽点/情绪爆点:__HIGHLIGHT__(必须按特写镜头写厚:铺垫在前、放大在中、余波在后,用动作/五感/生理反应/环境反馈呈现,禁止一句话带过)
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
- 若是前 3 章,必须核对黄金一章/黄金三章的切入速度;若发现拖沓、硬推或情绪概括,给出可定位的 major 问题与原文引文。
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
- # 独立签约评估(开篇闸门 + 2万字检查点共用):评分维度保持作者任务口径,
2763
- # 平台五维在这里按平台真实尺度单独核——驳回模板的高频话术逐条要判定。
2764
- SERIAL_SIGNING_EVAL_PROMPT = """你是签约评估编辑(不要修改任何文件)。请按免费阅读平台(番茄/七猫)签约评估的口径评审下面的稿件__NOTE__。
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
- 每个维度打 1-10 分(可为小数),按平台真实尺度从严。五个维度固定为:开篇吸引力、情节推进、文风统一、情感细腻度、节奏控制。
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 的模型调度决策。
@@ -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
- - 第 1-3 章是黄金三章:第 1 章前 100 字出现主角、前 200 字出现异常或欲望、前 600 字进入具体困境,
263
- 第 3 章末留大钩子;不要用世界观或环境描写拖延切入;
264
- - 每章都写清“主角主动选择 → 阻碍 → 局面变化 → 章末钩子”,禁止水字数的日常流水账和硬旁白推剧情;
263
+ - 把开篇字数位置当阅读观察点而非硬门槛:尽早让读者辨认视角人物、故事承诺和正在发生的事,
264
+ 但按题材、叙述策略与实际效果决定节奏,不为数字机械塞冲突;
265
+ - 每章要有清楚的叙事功能(推进事件、人物/关系、信息、氛围或情绪之一或多项),并检查因果与局面变化;
266
+ 不强制每章都打脸、反转或以悬念断章;
267
+ - 章末可自然收束或留白;大纲中的 highlight 描述主要戏剧/情绪变化或回报,无显著爆点时写清叙事功能,不要强造爆点。
265
268
  - 大纲中的情感转折必须给出触发事件和人物选择,不能只写“误会加深/感情升温”;
266
269
  - style_anchor 是全书唯一的文风锚,每章起草/修订/评审都会注入:必须写明视角人称与句长基调等可执行约束,不要空话;
267
- - 每章必须给出 highlight(爽点/情绪爆点):没有爆点的章节要重新设计,过渡章也要有小冲突小反转;
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
- 这本书的第 1 章在签约预评估中「开篇吸引力」未达线,评审意见如下:
756
+ SERIAL_OPENING_REBUILD_PROMPT = """你是小说结构编辑,不代表任何平台,也不能预测签约结果。
757
+ 这本书的开篇评审发现吸引力风险,实际意见如下:
754
758
  __CRITIQUE__
755
759
 
756
- 请重设计前 __COUNT__ 章的大纲节拍(只动这 __COUNT__ 章,不碰后续章节),让开篇符合黄金一章标准:
757
- 第 1 章前 100 字出现主角、前 200 字出现异常或冲突、前 600 字进入具体困境、章末留可兑现的钩子;
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}