codebee 0.1.101 → 0.1.103

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 (134) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/LICENSE +21 -21
  3. package/README.md +3 -6
  4. package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +88 -0
  5. package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +126 -0
  6. package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
  7. package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +210 -0
  8. package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
  9. package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
  10. package/app/.playwright-cli/run-detail-after.png +0 -0
  11. package/app/core/acceptance.py +100 -100
  12. package/app/core/acp_client.py +457 -426
  13. package/app/core/aiflavor.py +197 -197
  14. package/app/core/analytics.py +38 -38
  15. package/app/core/artifacts.py +148 -148
  16. package/app/core/assets.py +110 -110
  17. package/app/core/attachments.py +623 -623
  18. package/app/core/audit.py +30 -30
  19. package/app/core/backend_profiles.py +188 -188
  20. package/app/core/backup.py +470 -470
  21. package/app/core/beekeeper.py +183 -183
  22. package/app/core/benchstore.py +102 -102
  23. package/app/core/board.py +21 -17
  24. package/app/core/bookmeta.py +1036 -1036
  25. package/app/core/bookmeta_catalog.py +180 -180
  26. package/app/core/branching.py +137 -137
  27. package/app/core/builtin_agent.py +13 -4
  28. package/app/core/capability.py +144 -144
  29. package/app/core/catalog.py +28 -6
  30. package/app/core/chaptersafe.py +32 -32
  31. package/app/core/checkpoints.py +117 -31
  32. package/app/core/cleanup.py +303 -303
  33. package/app/core/command_auth.py +302 -302
  34. package/app/core/compaction.py +201 -201
  35. package/app/core/contracts.py +255 -255
  36. package/app/core/covergen.py +356 -356
  37. package/app/core/defectretro.py +43 -43
  38. package/app/core/diagnostics.py +114 -114
  39. package/app/core/dispatch.py +1 -1
  40. package/app/core/dispatch_log.py +128 -128
  41. package/app/core/env_scrub.py +85 -85
  42. package/app/core/errorlog.py +137 -137
  43. package/app/core/eval_matrix.py +91 -82
  44. package/app/core/evaluation.py +174 -174
  45. package/app/core/failure_notify.py +290 -290
  46. package/app/core/flow_graph.py +33 -33
  47. package/app/core/goal_service.py +167 -167
  48. package/app/core/health.py +389 -389
  49. package/app/core/history.py +38 -38
  50. package/app/core/hooks.py +162 -162
  51. package/app/core/knowledge_pipeline.py +86 -86
  52. package/app/core/manager.py +2385 -2385
  53. package/app/core/mcp_client.py +7 -2
  54. package/app/core/mocks.py +64 -64
  55. package/app/core/notify.py +201 -201
  56. package/app/core/novel_quality.py +181 -181
  57. package/app/core/openapi.py +217 -209
  58. package/app/core/operations.py +228 -228
  59. package/app/core/paihang.py +145 -145
  60. package/app/core/pipeline.py +6 -3
  61. package/app/core/policy.py +9 -0
  62. package/app/core/portguard.py +111 -111
  63. package/app/core/presence.py +77 -77
  64. package/app/core/preview.py +293 -293
  65. package/app/core/project_memory.py +206 -206
  66. package/app/core/prompt_guard.py +78 -78
  67. package/app/core/publish/__init__.py +7 -7
  68. package/app/core/publish/auto.py +659 -562
  69. package/app/core/publish/chaprows.py +138 -138
  70. package/app/core/publish/fanqie.py +16 -10
  71. package/app/core/publish/flow.py +29 -1
  72. package/app/core/publish/flows-fanqie-calibrated.json +442 -442
  73. package/app/core/publish/manager.py +1185 -1177
  74. package/app/core/publish/qimao.py +193 -193
  75. package/app/core/publish/volumes.py +108 -108
  76. package/app/core/publish/ws.py +139 -139
  77. package/app/core/quality_gate.py +201 -201
  78. package/app/core/redact.py +38 -38
  79. package/app/core/repeat_guard.py +129 -129
  80. package/app/core/retrieval.py +137 -137
  81. package/app/core/sandbox_runtime.py +95 -0
  82. package/app/core/selfupdate.py +384 -384
  83. package/app/core/session_log.py +161 -161
  84. package/app/core/sessions.py +312 -312
  85. package/app/core/settings.py +215 -215
  86. package/app/core/settings_schema.py +282 -282
  87. package/app/core/share_page.py +114 -114
  88. package/app/core/skill_scan.py +139 -139
  89. package/app/core/skillpacks/market/character-bible.md +66 -66
  90. package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
  91. package/app/core/skillpacks/market/git-workflow.md +57 -57
  92. package/app/core/skillpacks/market/release-notes.md +72 -72
  93. package/app/core/skillpacks/market/weekly-report.md +71 -71
  94. package/app/core/skillpacks/market/worldview-consistency.md +70 -70
  95. package/app/core/step_runner.py +132 -132
  96. package/app/core/store.py +13 -3
  97. package/app/core/story_tracking.py +275 -275
  98. package/app/core/task_compile.py +199 -199
  99. package/app/core/telemetry.py +292 -292
  100. package/app/core/tlsctx.py +64 -64
  101. package/app/core/token_meter.py +147 -147
  102. package/app/core/tool_outputs.py +107 -107
  103. package/app/core/tracing.py +207 -207
  104. package/app/core/upstream.py +41 -41
  105. package/app/core/usage.py +1038 -1038
  106. package/app/core/volumes.py +389 -389
  107. package/app/core/webhooks.py +89 -89
  108. package/app/core/wxdigest.py +865 -865
  109. package/app/core/zentao.py +2692 -2692
  110. package/app/main.py +1 -0
  111. package/app/mcp_server.py +244 -244
  112. package/app/output/_createbook_body.json +1 -0
  113. package/app/output/_probe_fq_click.py +50 -0
  114. package/app/output/_probe_fq_form.py +42 -0
  115. package/app/output/_probe_fq_modal.py +55 -0
  116. package/app/output/_probe_fq_points.py +71 -0
  117. package/app/output/_probe_fq_submit.py +34 -0
  118. package/app/output/_probe_fq_watch.py +62 -0
  119. package/app/output/workbench-clean-preview.jpg +0 -0
  120. package/app/output/workbench-reference-render.png +0 -0
  121. package/app/output/workbench-shell.png +0 -0
  122. package/app/pick_dialog.py +110 -110
  123. package/app/ui/app.js +165 -29
  124. package/app/ui/artdiff.js +74 -74
  125. package/app/ui/hive3d.js +491 -223
  126. package/app/ui/i18n.js +18 -1
  127. package/app/ui/icons/bee.svg +79 -79
  128. package/app/ui/index.html +24 -4
  129. package/app/ui/manifest.json +16 -16
  130. package/app/ui/qrcode.js +2297 -2297
  131. package/app/ui/style.css +548 -18
  132. package/bin/tutti.js +147 -147
  133. package/package.json +1 -1
  134. package/tools/wx_reader.py +261 -261
@@ -1,138 +1,138 @@
1
- # -*- coding: utf-8 -*-
2
- """章节管理页的行收集与分桶(校准的数据面,manager 只做编排)。
3
-
4
- 从 manager.py 拆出(manager 超 1200 行行数基线,2026-10-08):收集器只吃
5
- page + 平台 CONFIG,不依赖任何内部模块——放 L0 层无依赖。
6
- 状态关键词口径与台账一致:已发布 / 待审核·审核中·排队 / 未通过·驳回。
7
- """
8
- from __future__ import annotations
9
-
10
- import re
11
- import time
12
-
13
- _STATUS_PUB = "已发布"
14
- _STATUS_REVIEW = ("待审核", "审核中", "排队")
15
- _STATUS_BAD = ("未通过", "驳回")
16
-
17
- _CH_NO_RE = re.compile(r"第\s*(\d+)\s*章")
18
-
19
-
20
- def bucket_chapter_rows(rows):
21
- """章节管理页的行文本 → {total, published, review, rejected, published_nos}。
22
-
23
- 行里含状态关键词才计入对应桶;total=识别出的章节数据行数。
24
- published_nos=已发布行里解析出的章号(第N章,阿拉伯数字)——本地台账
25
- 只记得 CodeBee 自己发的章,用户手工补交的只有平台知道,校准把它带
26
- 回来供幂等/待发清单对账。"""
27
- out = {"total": 0, "published": 0, "review": 0, "rejected": 0,
28
- "published_nos": []}
29
- nos = set()
30
- for t in rows or []:
31
- t = str(t)
32
- if not t:
33
- continue
34
- if "章节名称" in t: # 表头兜底(正常到不了这)
35
- continue
36
- out["total"] += 1
37
- if any(k in t for k in _STATUS_BAD):
38
- out["rejected"] += 1
39
- elif _STATUS_PUB in t:
40
- out["published"] += 1
41
- m = _CH_NO_RE.search(t)
42
- if m:
43
- nos.add(int(m.group(1)))
44
- elif any(k in t for k in _STATUS_REVIEW):
45
- out["review"] += 1
46
- out["published_nos"] = sorted(nos)
47
- return out
48
-
49
-
50
- def collect_manage_rows(page, mod):
51
- """读章节管理页的全部行:跨页翻页收齐(番茄表每页 15 行,只数当前页
52
- 会把 30 章已发报成 15——2026-10-08 校准少计实案)。平台没配翻页 JS
53
- (七猫)或翻页无进展时按单页收。行文本去重保序,重复页不会翻倍。"""
54
- js_rows = mod.CONFIG.get("count_rows_js")
55
- next_js = mod.CONFIG.get("pager_next_js")
56
- settle = float(mod.CONFIG.get("count_page_settle") or 1.2)
57
- rows, seen = [], set()
58
- for _ in range(int(mod.CONFIG.get("count_max_pages") or 20)):
59
- fresh = 0
60
- for t in (page.call(js_rows) or []):
61
- t = str(t)
62
- if t and t not in seen:
63
- seen.add(t)
64
- rows.append(t)
65
- fresh += 1
66
- if not next_js or fresh == 0:
67
- break
68
- r = page.call(next_js)
69
- if not isinstance(r, dict) or r.get("done"):
70
- break
71
- time.sleep(settle)
72
- return rows
73
-
74
-
75
- def collect_all_volumes(page, mod):
76
- """跨分卷收行:章节管理页带分卷筛选(番茄「第一卷:…」下拉,无「全部」
77
- 项),列表只显示选中卷的章节——卷二发布后只数卷一会继续少计。弹层在
78
- click 后下一帧才渲染,open→读/点之间必须隔 sleep(同步 JS 一口气查必
79
- 落空,2026-10-08 真机实案)。收完恢复进入时的卷;一卷都没选成时按原
80
- 筛选兜底收一次,不让分卷自动化失败拖死整次校准。
81
-
82
- 返回 (rows, vols):vols=读到的平台分卷显示名清单(读不到为 None),
83
- 供 books.json 缓存平台卷表。"""
84
- vj = mod.CONFIG.get("volume_js") or {}
85
- if not all(vj.get(k) for k in ("open", "options", "pick")):
86
- return collect_manage_rows(page, mod), None
87
- settle = float(mod.CONFIG.get("count_page_settle") or 1.2)
88
-
89
- def open_popup():
90
- try:
91
- page.call(vj["open"])
92
- except Exception:
93
- pass
94
- time.sleep(0.8)
95
-
96
- orig = ""
97
- try:
98
- orig = str(page.call(vj.get("current")) or "").strip()
99
- except Exception:
100
- pass
101
- open_popup()
102
- try:
103
- vols = page.call(vj["options"]) or []
104
- except Exception:
105
- vols = []
106
- if not isinstance(vols, list) or len(vols) < 2:
107
- return collect_manage_rows(page, mod), None # 单卷:当前筛选就是全量
108
- rows, seen, picked_any, misses = [], set(), False, 0
109
- for v in vols:
110
- v = str(v or "").strip()
111
- if not v:
112
- continue
113
- open_popup()
114
- try:
115
- r = page.call(vj["pick"], v)
116
- except Exception:
117
- r = None
118
- if not isinstance(r, dict) or not r.get("ok"):
119
- misses += 1
120
- if misses >= 3:
121
- break # 弹层压根点不动:别把选项清单烧完
122
- continue
123
- misses = 0
124
- picked_any = True
125
- time.sleep(settle)
126
- for t in collect_manage_rows(page, mod):
127
- if t not in seen:
128
- seen.add(t)
129
- rows.append(t)
130
- if orig: # 还原进入时的卷筛选
131
- open_popup()
132
- try:
133
- page.call(vj["pick"], orig)
134
- except Exception:
135
- pass
136
- if not picked_any:
137
- return collect_manage_rows(page, mod), list(vols)
138
- return rows, [str(v) for v in vols if str(v or "").strip()]
1
+ # -*- coding: utf-8 -*-
2
+ """章节管理页的行收集与分桶(校准的数据面,manager 只做编排)。
3
+
4
+ 从 manager.py 拆出(manager 超 1200 行行数基线,2026-10-08):收集器只吃
5
+ page + 平台 CONFIG,不依赖任何内部模块——放 L0 层无依赖。
6
+ 状态关键词口径与台账一致:已发布 / 待审核·审核中·排队 / 未通过·驳回。
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ import time
12
+
13
+ _STATUS_PUB = "已发布"
14
+ _STATUS_REVIEW = ("待审核", "审核中", "排队")
15
+ _STATUS_BAD = ("未通过", "驳回")
16
+
17
+ _CH_NO_RE = re.compile(r"第\s*(\d+)\s*章")
18
+
19
+
20
+ def bucket_chapter_rows(rows):
21
+ """章节管理页的行文本 → {total, published, review, rejected, published_nos}。
22
+
23
+ 行里含状态关键词才计入对应桶;total=识别出的章节数据行数。
24
+ published_nos=已发布行里解析出的章号(第N章,阿拉伯数字)——本地台账
25
+ 只记得 CodeBee 自己发的章,用户手工补交的只有平台知道,校准把它带
26
+ 回来供幂等/待发清单对账。"""
27
+ out = {"total": 0, "published": 0, "review": 0, "rejected": 0,
28
+ "published_nos": []}
29
+ nos = set()
30
+ for t in rows or []:
31
+ t = str(t)
32
+ if not t:
33
+ continue
34
+ if "章节名称" in t: # 表头兜底(正常到不了这)
35
+ continue
36
+ out["total"] += 1
37
+ if any(k in t for k in _STATUS_BAD):
38
+ out["rejected"] += 1
39
+ elif _STATUS_PUB in t:
40
+ out["published"] += 1
41
+ m = _CH_NO_RE.search(t)
42
+ if m:
43
+ nos.add(int(m.group(1)))
44
+ elif any(k in t for k in _STATUS_REVIEW):
45
+ out["review"] += 1
46
+ out["published_nos"] = sorted(nos)
47
+ return out
48
+
49
+
50
+ def collect_manage_rows(page, mod):
51
+ """读章节管理页的全部行:跨页翻页收齐(番茄表每页 15 行,只数当前页
52
+ 会把 30 章已发报成 15——2026-10-08 校准少计实案)。平台没配翻页 JS
53
+ (七猫)或翻页无进展时按单页收。行文本去重保序,重复页不会翻倍。"""
54
+ js_rows = mod.CONFIG.get("count_rows_js")
55
+ next_js = mod.CONFIG.get("pager_next_js")
56
+ settle = float(mod.CONFIG.get("count_page_settle") or 1.2)
57
+ rows, seen = [], set()
58
+ for _ in range(int(mod.CONFIG.get("count_max_pages") or 20)):
59
+ fresh = 0
60
+ for t in (page.call(js_rows) or []):
61
+ t = str(t)
62
+ if t and t not in seen:
63
+ seen.add(t)
64
+ rows.append(t)
65
+ fresh += 1
66
+ if not next_js or fresh == 0:
67
+ break
68
+ r = page.call(next_js)
69
+ if not isinstance(r, dict) or r.get("done"):
70
+ break
71
+ time.sleep(settle)
72
+ return rows
73
+
74
+
75
+ def collect_all_volumes(page, mod):
76
+ """跨分卷收行:章节管理页带分卷筛选(番茄「第一卷:…」下拉,无「全部」
77
+ 项),列表只显示选中卷的章节——卷二发布后只数卷一会继续少计。弹层在
78
+ click 后下一帧才渲染,open→读/点之间必须隔 sleep(同步 JS 一口气查必
79
+ 落空,2026-10-08 真机实案)。收完恢复进入时的卷;一卷都没选成时按原
80
+ 筛选兜底收一次,不让分卷自动化失败拖死整次校准。
81
+
82
+ 返回 (rows, vols):vols=读到的平台分卷显示名清单(读不到为 None),
83
+ 供 books.json 缓存平台卷表。"""
84
+ vj = mod.CONFIG.get("volume_js") or {}
85
+ if not all(vj.get(k) for k in ("open", "options", "pick")):
86
+ return collect_manage_rows(page, mod), None
87
+ settle = float(mod.CONFIG.get("count_page_settle") or 1.2)
88
+
89
+ def open_popup():
90
+ try:
91
+ page.call(vj["open"])
92
+ except Exception:
93
+ pass
94
+ time.sleep(0.8)
95
+
96
+ orig = ""
97
+ try:
98
+ orig = str(page.call(vj.get("current")) or "").strip()
99
+ except Exception:
100
+ pass
101
+ open_popup()
102
+ try:
103
+ vols = page.call(vj["options"]) or []
104
+ except Exception:
105
+ vols = []
106
+ if not isinstance(vols, list) or len(vols) < 2:
107
+ return collect_manage_rows(page, mod), None # 单卷:当前筛选就是全量
108
+ rows, seen, picked_any, misses = [], set(), False, 0
109
+ for v in vols:
110
+ v = str(v or "").strip()
111
+ if not v:
112
+ continue
113
+ open_popup()
114
+ try:
115
+ r = page.call(vj["pick"], v)
116
+ except Exception:
117
+ r = None
118
+ if not isinstance(r, dict) or not r.get("ok"):
119
+ misses += 1
120
+ if misses >= 3:
121
+ break # 弹层压根点不动:别把选项清单烧完
122
+ continue
123
+ misses = 0
124
+ picked_any = True
125
+ time.sleep(settle)
126
+ for t in collect_manage_rows(page, mod):
127
+ if t not in seen:
128
+ seen.add(t)
129
+ rows.append(t)
130
+ if orig: # 还原进入时的卷筛选
131
+ open_popup()
132
+ try:
133
+ page.call(vj["pick"], orig)
134
+ except Exception:
135
+ pass
136
+ if not picked_any:
137
+ return collect_manage_rows(page, mod), list(vols)
138
+ return rows, [str(v) for v in vols if str(v or "").strip()]
@@ -110,12 +110,17 @@ UPLOAD_CHAPTER = [
110
110
  "optional": True},
111
111
  ]
112
112
 
113
- # 发章存草稿(2026-10-09 全部发草稿):与 UPLOAD_CHAPTER 同一张填表前段,
114
- # 终点不同——点编辑器头部「存草稿」直接落草稿箱,不走「下一步→内容检测→
115
- # 发布提示→提交」发布链。草稿不上线,批量连跑无需人工确认;提交发布由
116
- # 用户到章节管理/草稿箱手工完成。verify 沿用章节管理页断言(草稿行同样
117
- # 出现在列表里,标题/章号标记可命中);若平台改版把草稿挪去独立草稿箱页,
118
- # 首轮失败的 step 截图会指出来,照 flows-fanqie.json 校准即可。
113
+ # 发章存草稿(2026-10-09 全部发草稿;2026-10-10 假成功案重构):
114
+ # 与 UPLOAD_CHAPTER 同一张填表前段,终点不同——点编辑器头部「存草稿」直接
115
+ # 落草稿箱,不走「下一步→内容检测→发布提示→提交」发布链。三点教训入表:
116
+ # 1) submit 带 always=true:草稿流程跑在 auto_submit=false 语义下,而这里的
117
+ # 「存草稿」点击本身就是保存动作——被人工闸吞掉时 manager 把「填好未存」
118
+ # 记成成功(44 章假成功实案)。
119
+ # 2) expect_text 等编辑器页保存反馈为主凭据(标记文案以真机校准为准,失败
120
+ # 自动落 fail 截图);「已保存」这类自动保存字样刻意不入标记——编辑器
121
+ # 自动保存不可依赖,不能当本次点击的证据。
122
+ # 3) verify 仍走章节管理页兜底(新章草稿行同样在列表里);对「已发布章重复
123
+ # 起草」它必然假通过——该情形由起草前对账(auto 层)剔除,不靠本表。
119
124
  UPLOAD_CHAPTER_DRAFT = [
120
125
  {"do": "navigate", "url": "{editor_url}"},
121
126
  {"do": "url_any", "any": ["fanqienovel.com"]},
@@ -131,10 +136,11 @@ UPLOAD_CHAPTER_DRAFT = [
131
136
  {"do": "wait", "sel": "[contenteditable=true],textarea[class*=content],div[class*=editor]", "timeout": 10},
132
137
  {"do": "fill", "sel": "[contenteditable=true],textarea[class*=content]", "key": "chapter_body"},
133
138
  {"do": "shot", "name": "chapter-filled-draft"},
134
- {"do": "submit", "text": "存草稿", "scope": "button", "tries": 15},
135
- {"do": "js_click", "text": "存草稿", "scope": "button", "tries": 10,
136
- "optional": True, "skip_if_modal": True},
137
- {"do": "sleep", "s": 2.5},
139
+ {"do": "submit", "text": "存草稿", "scope": "button", "tries": 15, "always": True},
140
+ {"do": "sleep", "s": 1.2},
141
+ {"do": "shot", "name": "chapter-draft-clicked"},
142
+ {"do": "expect_text",
143
+ "any": ["保存成功", "存草稿成功", "草稿已保存", "已存草稿"], "timeout": 12},
138
144
  {"do": "verify", "url": "{chapter_manage_url}",
139
145
  "any": ["第{chapter_no}章", "{chapter_name}"], "settle": 6, "new_tab": True},
140
146
  ]
@@ -558,7 +558,10 @@ def run_flow(page, steps, values=None, config=None, auto_submit=False,
558
558
  if log:
559
559
  log("PROBE " + json.dumps(info, ensure_ascii=False)[:4000])
560
560
  elif act == "submit":
561
- if not auto_submit:
561
+ # always=true 的提交步(草稿流的「存草稿」)本身就是保存动作,
562
+ # 不吃 auto_submit 人工闸——被闸吞掉时流程停在「填好未存」,
563
+ # manager 却照记成功(2026-10-10 44 章假成功实案)
564
+ if not auto_submit and not st.get("always"):
562
565
  note(i, "已填好未提交——请人工检查后提交(auto_submit=false)")
563
566
  if shot:
564
567
  shot("ready-manual-submit")
@@ -686,6 +689,31 @@ def run_flow(page, steps, values=None, config=None, auto_submit=False,
686
689
  finally:
687
690
  if new_tab:
688
691
  vpage.close_tab()
692
+ elif act == "expect_text":
693
+ # 页面反馈等待(草稿保存凭据,2026-10-10):轮询页面文本直到
694
+ # 出现任一成功标记(toast/状态字样)——verify 的章节管理页对
695
+ # 「已发布章重复起草」必然假通过,编辑器保存反馈才是本次动作
696
+ # 自己的证据。超时即失败:宁可带 fail 截图拦下,不可把「没
697
+ # 存上」记成成功。标记文案以真机校准为准。
698
+ marks = [str(m) for m in (st.get("any") or [])]
699
+ if not marks:
700
+ continue
701
+ deadline = time.time() + float(st.get("timeout") or 12)
702
+ hit = ""
703
+ while time.time() < deadline:
704
+ body_txt = str(page.call(
705
+ "()=>(document.body.innerText||'')", timeout=15) or "")
706
+ hit = next((m for m in marks if m in body_txt), "")
707
+ if hit:
708
+ break
709
+ time.sleep(0.8)
710
+ if not hit:
711
+ raise FlowError("未见保存成功反馈(等了 %ds,标记:%s)——"
712
+ "若截图里实际已保存成功,请把真实反馈文案"
713
+ "校准进流程表"
714
+ % (int(float(st.get("timeout") or 12)),
715
+ "、".join(marks)[:60]))
716
+ note(i, "反馈命中「%s」" % hit)
689
717
  elif act == "volume":
690
718
  # 发章带卷(2026-10-08 批量自动发布·自动分卷):确保目标卷
691
719
  # 在平台上存在(缺则走「新建分卷」)。平台新章按卷自动归类、