codebee 0.1.68 → 0.1.70

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 (133) hide show
  1. package/CHANGELOG.md +580 -543
  2. package/LICENSE +21 -21
  3. package/README.md +11 -0
  4. package/app/browser-final-1440.png +0 -0
  5. package/app/browser-final-390.png +0 -0
  6. package/app/browser-google-fallback.png +0 -0
  7. package/app/browser-google-test.png +0 -0
  8. package/app/browser-workbench-1440.png +0 -0
  9. package/app/browser-workbench-390.png +0 -0
  10. package/app/browser-workspace-google.png +0 -0
  11. package/app/browser-workspace-smoke.png +0 -0
  12. package/app/browser-workspace.png +0 -0
  13. package/app/core/acceptance.py +100 -100
  14. package/app/core/aiflavor.py +197 -197
  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 -0
  19. package/app/core/automation.py +853 -758
  20. package/app/core/backup.py +470 -470
  21. package/app/core/beekeeper.py +183 -183
  22. package/app/core/benchstore.py +95 -0
  23. package/app/core/board.py +209 -209
  24. package/app/core/bookmeta.py +935 -930
  25. package/app/core/bookmeta_catalog.py +180 -180
  26. package/app/core/branching.py +137 -137
  27. package/app/core/builtin_agent.py +1673 -1234
  28. package/app/core/capability.py +144 -144
  29. package/app/core/catalog.py +7 -1
  30. package/app/core/chaptersafe.py +32 -32
  31. package/app/core/cleanup.py +303 -303
  32. package/app/core/compaction.py +201 -201
  33. package/app/core/covergen.py +356 -356
  34. package/app/core/defectretro.py +43 -43
  35. package/app/core/diagnostics.py +114 -114
  36. package/app/core/dispatch.py +190 -173
  37. package/app/core/dispatch_log.py +128 -128
  38. package/app/core/env_scrub.py +85 -85
  39. package/app/core/error_codes.py +33 -24
  40. package/app/core/errorlog.py +137 -137
  41. package/app/core/evalbench.py +720 -0
  42. package/app/core/evaluation.py +174 -174
  43. package/app/core/failure_notify.py +290 -290
  44. package/app/core/flows.py +228 -3
  45. package/app/core/goal_service.py +167 -167
  46. package/app/core/health.py +374 -374
  47. package/app/core/history.py +38 -38
  48. package/app/core/hooks.py +162 -162
  49. package/app/core/jobs.py +51 -30
  50. package/app/core/manager.py +2143 -2122
  51. package/app/core/market.py +362 -362
  52. package/app/core/market_remote.py +10 -0
  53. package/app/core/mcp_client.py +287 -0
  54. package/app/core/mocks.py +64 -64
  55. package/app/core/modelhub.py +193 -10
  56. package/app/core/notify.py +201 -106
  57. package/app/core/novel_quality.py +189 -0
  58. package/app/core/operations.py +228 -228
  59. package/app/core/paihang.py +138 -138
  60. package/app/core/paths.py +6 -2
  61. package/app/core/pipeline.py +426 -28
  62. package/app/core/planner.py +243 -22
  63. package/app/core/plugins.py +525 -0
  64. package/app/core/portguard.py +111 -111
  65. package/app/core/preview.py +293 -293
  66. package/app/core/publish/__init__.py +7 -7
  67. package/app/core/publish/auto.py +367 -367
  68. package/app/core/publish/browser.py +655 -539
  69. package/app/core/publish/fanqie.py +165 -159
  70. package/app/core/publish/flow.py +589 -440
  71. package/app/core/publish/flows-fanqie-calibrated.json +336 -332
  72. package/app/core/publish/ledger.py +229 -203
  73. package/app/core/publish/manager.py +824 -692
  74. package/app/core/publish/qimao.py +146 -141
  75. package/app/core/publish/ws.py +139 -139
  76. package/app/core/redact.py +38 -38
  77. package/app/core/remotesync.py +121 -0
  78. package/app/core/repeat_guard.py +129 -129
  79. package/app/core/router.py +398 -398
  80. package/app/core/runner.py +2142 -2067
  81. package/app/core/selfupdate.py +384 -384
  82. package/app/core/session_log.py +161 -161
  83. package/app/core/sessions.py +312 -312
  84. package/app/core/settings.py +186 -149
  85. package/app/core/settings_schema.py +270 -262
  86. package/app/core/share_page.py +114 -114
  87. package/app/core/skill_scan.py +122 -122
  88. package/app/core/skillpacks/fanqie-novel.md +80 -80
  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 +104 -82
  97. package/app/core/task_compile.py +199 -199
  98. package/app/core/telemetry.py +292 -292
  99. package/app/core/tlsctx.py +64 -64
  100. package/app/core/token_meter.py +147 -147
  101. package/app/core/tracing.py +144 -144
  102. package/app/core/upstream.py +41 -41
  103. package/app/core/usage.py +1035 -862
  104. package/app/core/volumes.py +389 -389
  105. package/app/core/wxdigest.py +858 -858
  106. package/app/core/zentao.py +2614 -2465
  107. package/app/current-browser.png +0 -0
  108. package/app/main.py +3245 -2981
  109. package/app/mcp_server.py +175 -0
  110. package/app/pet.py +82 -21
  111. package/app/pick_dialog.py +110 -110
  112. package/app/plugins/code-review-helper/.codex-plugin/plugin.json +18 -0
  113. package/app/plugins/code-review-helper/skills/review.md +16 -0
  114. package/app/ui/app.js +15706 -14399
  115. package/app/ui/artdiff.js +74 -74
  116. package/app/ui/board.html +162 -162
  117. package/app/ui/board.js +305 -305
  118. package/app/ui/i18n.js +3012 -2770
  119. package/app/ui/icons/bee.svg +79 -79
  120. package/app/ui/index.html +277 -13
  121. package/app/ui/manifest.json +16 -16
  122. package/app/ui/qrcode.js +2297 -2297
  123. package/app/ui/style.css +802 -16
  124. package/bin/tutti.js +147 -147
  125. package/package.json +47 -46
  126. package/tools/wx_reader.py +261 -261
  127. package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +0 -88
  128. package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +0 -126
  129. package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
  130. package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +0 -210
  131. package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
  132. package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
  133. package/app/.playwright-cli/run-detail-after.png +0 -0
@@ -1,159 +1,165 @@
1
- # -*- coding: utf-8 -*-
2
- """番茄作家后台的发布流程定义(默认表:选择器为合理推测,待实测校准)。
3
-
4
- 校准方式(不改代码):把真实步骤写进 data/publish/flows-fanqie.json:
5
- {"create_book": [ {"do":"navigate","url":"..."}, {"do":"fill","sel":"...","key":"title"} ... ],
6
- "upload_chapter": [ ... ]}
7
- 存在即整体覆盖本文件的 FLOWS;用「探测表单」按钮(probe 流程)dump 出
8
- 页面真实元素清单后照着写即可。每次失败的截图也会指出卡在哪一步。
9
-
10
- URL 依据:tools/fanqie_bookmeta.json 的 source 记录了 2026-09-17 实抓
11
- fanqienovel.com/main/writer/create 建书弹层;入口走作家后台首页点
12
- 「创建作品」,不硬编码深链(深链随改版漂移,入口按钮最稳)。
13
- """
14
- from __future__ import annotations
15
-
16
- CONFIG = {
17
- "id": "fanqie",
18
- "label": "番茄",
19
- "home": "https://fanqienovel.com/main/writer/", # 实测 writer. 子域不存在(DNS 000);快照实抓为主站路径
20
- # 导航后 URL 含任一标记 → 未登录(跳到了登录/通行证页)
21
- "login_url_marks": ["login", "passport", "sso", "account/signin"],
22
- }
23
-
24
- # 建书:入口 → 弹层/页面 → 文本字段直填;分类/签约模式/标签走文本点击。
25
- # 文本输入类的选择器优先用 placeholder 模糊匹配(改版后 id/class 易变,
26
- # 「书名」「简介」这类 placeholder 语料最稳定)。
27
- CREATE_BOOK = [
28
- {"do": "navigate", "url": "{home}"},
29
- {"do": "url_any", "any": ["fanqienovel.com"]},
30
- {"do": "click_text", "text": "创建作品", "contains": True, "scope": "button,a,[role=button],span"},
31
- {"do": "probe", "note": "建书表单"},
32
- {"do": "wait", "sel": "input[placeholder*='书名'],input[placeholder*='作品名'],input[maxlength]", "timeout": 10},
33
- {"do": "fill", "sel": "input[placeholder*='书名'],input[placeholder*='作品名']", "key": "title"},
34
- {"do": "fill", "sel": "textarea[placeholder*='简介'],textarea", "key": "summary"},
35
- {"do": "fill", "sel": "input[placeholder*='主角']", "key": "protagonist"},
36
- {"do": "click_text", "text": "{signing_mode}", "contains": False,
37
- "scope": "[class*=mode] label,label,span,div"},
38
- {"do": "click_text", "text": "{category}", "contains": False,
39
- "scope": "[class*=categor] li,span,div"},
40
- {"do": "shot", "name": "create-book-filled"},
41
- {"do": "submit", "sel": "button[class*=submit],button[class*=primary]"},
42
- ]
43
-
44
- # 发章:后台首页 → 按书名点进作品 → 新建章节 → 填标题与正文。
45
- # book_name 由 manager 从作品登记(books.json)带入 values,click_text 复用。
46
- UPLOAD_CHAPTER = [
47
- {"do": "navigate", "url": "{home}"},
48
- {"do": "url_any", "any": ["fanqienovel.com"]},
49
- {"do": "click_text", "text": "{book_name}", "contains": True,
50
- "scope": "a,span,div[class*=title],div[class*=book]"},
51
- {"do": "click_text", "text": "新建章节", "contains": True, "scope": "button,a,[role=button],span"},
52
- {"do": "probe", "note": "章节编辑器"},
53
- {"do": "fill", "sel": "input[placeholder*='章节'],input[placeholder*='标题']", "key": "chapter_title"},
54
- {"do": "wait", "sel": "[contenteditable=true],textarea[class*=content],div[class*=editor]", "timeout": 10},
55
- {"do": "fill", "sel": "[contenteditable=true],textarea[class*=content]", "key": "chapter_body"},
56
- {"do": "shot", "name": "chapter-filled"},
57
- {"do": "submit", "sel": "button[class*=publish],button[class*=submit]"},
58
- ]
59
-
60
- # 登录态探测:打开后台首页,URL 被踢到登录页 → 未登录
61
- CHECK_LOGIN = [
62
- {"do": "navigate", "url": "{home}"},
63
- {"do": "url_any", "any": ["fanqienovel.com"]},
64
- ]
65
-
66
- # 表单探测(校准辅助):开建书入口后 dump 全部可交互元素
67
- PROBE_FORM = [
68
- {"do": "navigate", "url": "{home}"},
69
- {"do": "url_any", "any": ["fanqienovel.com"]},
70
- {"do": "click_text", "text": "创建作品", "contains": True, "scope": "button,a,[role=button],span"},
71
- {"do": "probe", "note": "建书表单"},
72
- ]
73
-
74
- FLOWS = {"create_book": CREATE_BOOK, "upload_chapter": UPLOAD_CHAPTER,
75
- "check_login": CHECK_LOGIN, "probe_form": PROBE_FORM}
76
-
77
-
78
- def _first(meta, key):
79
- v = meta.get(key)
80
- return str(v[0]).strip() if isinstance(v, list) and v else str(v or "").strip()
81
-
82
-
83
- def values_create_book(meta):
84
- """bookmeta 字段 → 建书表单值。标签弹层的分区点选用单值字段
85
- (每组选 1 个最稳:番茄主题/角色/情节各≤2、内容组各有上限),
86
- 数组全量点选由 tag_groups 路径兼容。"""
87
- return {
88
- "title": (meta.get("book_name") or "").strip(),
89
- "summary": (meta.get("summary") or "").strip(),
90
- "protagonist": (meta.get("protagonist_1") or "").strip(),
91
- "protagonist2": (meta.get("protagonist_2") or "").strip(),
92
- "signing_mode": (meta.get("signing_mode") or "连载模式").strip(),
93
- "category": (meta.get("category") or "").strip(),
94
- "target_reader": (meta.get("target_reader") or "男频").strip(),
95
- "tag_theme": _first(meta, "tags_theme"),
96
- "tag_role": _first(meta, "tags_role"),
97
- "tag_plot": _first(meta, "tags_plot"),
98
- "tag_content_emotion": _first(meta, "content_emotion"),
99
- "tag_content_character": _first(meta, "content_character"),
100
- "tag_content_world": _first(meta, "content_world"),
101
- }
102
-
103
-
104
- def tag_groups(meta):
105
- """标签字段名 → 值列表(manager 用来生成逐个 click_text 步骤)。"""
106
- out = []
107
- for key in ("tags_theme", "tags_role", "tags_plot",
108
- "content_plot", "content_emotion", "content_character", "content_world"):
109
- v = meta.get(key)
110
- if isinstance(v, list) and v:
111
- out.append((key, [str(x) for x in v]))
112
- return out
113
-
114
-
115
- def editor_url(book):
116
- """章节编辑器直达(真机实测):/main/writer/<book_id>/publish/。"""
117
- bid = str((book or {}).get("book_id") or "")
118
- if bid:
119
- return "https://fanqienovel.com/main/writer/%s/publish/" % bid
120
- return CONFIG["home"]
121
-
122
-
123
- def chapter_manage_url(book):
124
- """章节管理页(上线验证用)。"""
125
- bid = str((book or {}).get("book_id") or "")
126
- if bid:
127
- return "https://fanqienovel.com/main/writer/chapter-manage/" + bid
128
- return CONFIG["home"] + "book-manage"
129
-
130
-
131
- def resolve_book_id(page, book):
132
- """登记缺 book_id 时按书名在作家后台找回:点书卡进详情,从 URL 提取。
133
-
134
- 建书成功但 id 提取落空(跳转链没走完/auto_submit=false 人工提交)的书,
135
- 发章前经 manager._resolve_book_id 调到这里补账;找不回返回空串不拦死。"""
136
- import re
137
- import time
138
- from . import flow as _flow
139
- title = str((book or {}).get("title") or "").strip()
140
- if not title:
141
- return ""
142
- try:
143
- page.navigate(CONFIG["home"], timeout=30)
144
- except Exception:
145
- return ""
146
- r = None
147
- for _try in range(6): # 书列表慢渲染
148
- r = page.call(_flow._click_match_js(), [title], 300)
149
- if (r or {}).get("ok"):
150
- break
151
- time.sleep(1.0)
152
- if not (r or {}).get("ok"):
153
- return ""
154
- time.sleep(2.5) # 详情页跳转收尾
155
- m = re.search(r"book-info/(\d+)|chapter-manage/(\d+)|writer/(\d+)/publish",
156
- str(page.url() or ""))
157
- if not m:
158
- return ""
159
- return m.group(1) or m.group(2) or m.group(3) or ""
1
+ # -*- coding: utf-8 -*-
2
+ """番茄作家后台的发布流程定义(默认表:选择器为合理推测,待实测校准)。
3
+
4
+ 校准方式(不改代码):把真实步骤写进 data/publish/flows-fanqie.json:
5
+ {"create_book": [ {"do":"navigate","url":"..."}, {"do":"fill","sel":"...","key":"title"} ... ],
6
+ "upload_chapter": [ ... ]}
7
+ 存在即整体覆盖本文件的 FLOWS;用「探测表单」按钮(probe 流程)dump 出
8
+ 页面真实元素清单后照着写即可。每次失败的截图也会指出卡在哪一步。
9
+
10
+ URL 依据:tools/fanqie_bookmeta.json 的 source 记录了 2026-09-17 实抓
11
+ fanqienovel.com/main/writer/create 建书弹层;入口走作家后台首页点
12
+ 「创建作品」,不硬编码深链(深链随改版漂移,入口按钮最稳)。
13
+ """
14
+ from __future__ import annotations
15
+
16
+ CONFIG = {
17
+ "id": "fanqie",
18
+ "label": "番茄",
19
+ "home": "https://fanqienovel.com/main/writer/", # 实测 writer. 子域不存在(DNS 000);快照实抓为主站路径
20
+ # 导航后 URL 含任一标记 → 未登录(跳到了登录/通行证页)
21
+ "login_url_marks": ["login", "passport", "sso", "account/signin"],
22
+ # 章节管理页计数(2026-09-28 实机校准):Arco 表格,数据行=含 ≥4 个 td 的 tr
23
+ # (表头行是 th,天然排除)。逐行 innerText 交 manager 按状态关键词分桶。
24
+ "count_rows_js":
25
+ "()=>[...document.querySelectorAll('tr')]"
26
+ ".filter(tr => tr.querySelectorAll(':scope > td').length >= 4)"
27
+ ".map(tr => tr.innerText.replace(/\\s+/g, ' ').trim()).filter(Boolean)",
28
+ }
29
+
30
+ # 建书:入口 → 弹层/页面 → 文本字段直填;分类/签约模式/标签走文本点击。
31
+ # 文本输入类的选择器优先用 placeholder 模糊匹配(改版后 id/class 易变,
32
+ # 「书名」「简介」这类 placeholder 语料最稳定)。
33
+ CREATE_BOOK = [
34
+ {"do": "navigate", "url": "{home}"},
35
+ {"do": "url_any", "any": ["fanqienovel.com"]},
36
+ {"do": "click_text", "text": "创建作品", "contains": True, "scope": "button,a,[role=button],span"},
37
+ {"do": "probe", "note": "建书表单"},
38
+ {"do": "wait", "sel": "input[placeholder*='书名'],input[placeholder*='作品名'],input[maxlength]", "timeout": 10},
39
+ {"do": "fill", "sel": "input[placeholder*='书名'],input[placeholder*='作品名']", "key": "title"},
40
+ {"do": "fill", "sel": "textarea[placeholder*='简介'],textarea", "key": "summary"},
41
+ {"do": "fill", "sel": "input[placeholder*='主角']", "key": "protagonist"},
42
+ {"do": "click_text", "text": "{signing_mode}", "contains": False,
43
+ "scope": "[class*=mode] label,label,span,div"},
44
+ {"do": "click_text", "text": "{category}", "contains": False,
45
+ "scope": "[class*=categor] li,span,div"},
46
+ {"do": "shot", "name": "create-book-filled"},
47
+ {"do": "submit", "sel": "button[class*=submit],button[class*=primary]"},
48
+ ]
49
+
50
+ # 发章:后台首页 → 按书名点进作品 → 新建章节 → 填标题与正文。
51
+ # book_name 由 manager 从作品登记(books.json)带入 values,click_text 复用。
52
+ UPLOAD_CHAPTER = [
53
+ {"do": "navigate", "url": "{home}"},
54
+ {"do": "url_any", "any": ["fanqienovel.com"]},
55
+ {"do": "click_text", "text": "{book_name}", "contains": True,
56
+ "scope": "a,span,div[class*=title],div[class*=book]"},
57
+ {"do": "click_text", "text": "新建章节", "contains": True, "scope": "button,a,[role=button],span"},
58
+ {"do": "probe", "note": "章节编辑器"},
59
+ {"do": "fill", "sel": "input[placeholder*='章节'],input[placeholder*='标题']", "key": "chapter_title"},
60
+ {"do": "wait", "sel": "[contenteditable=true],textarea[class*=content],div[class*=editor]", "timeout": 10},
61
+ {"do": "fill", "sel": "[contenteditable=true],textarea[class*=content]", "key": "chapter_body"},
62
+ {"do": "shot", "name": "chapter-filled"},
63
+ {"do": "submit", "sel": "button[class*=publish],button[class*=submit]"},
64
+ ]
65
+
66
+ # 登录态探测:打开后台首页,URL 被踢到登录页 → 未登录
67
+ CHECK_LOGIN = [
68
+ {"do": "navigate", "url": "{home}"},
69
+ {"do": "url_any", "any": ["fanqienovel.com"]},
70
+ ]
71
+
72
+ # 表单探测(校准辅助):开建书入口后 dump 全部可交互元素
73
+ PROBE_FORM = [
74
+ {"do": "navigate", "url": "{home}"},
75
+ {"do": "url_any", "any": ["fanqienovel.com"]},
76
+ {"do": "click_text", "text": "创建作品", "contains": True, "scope": "button,a,[role=button],span"},
77
+ {"do": "probe", "note": "建书表单"},
78
+ ]
79
+
80
+ FLOWS = {"create_book": CREATE_BOOK, "upload_chapter": UPLOAD_CHAPTER,
81
+ "check_login": CHECK_LOGIN, "probe_form": PROBE_FORM}
82
+
83
+
84
+ def _first(meta, key):
85
+ v = meta.get(key)
86
+ return str(v[0]).strip() if isinstance(v, list) and v else str(v or "").strip()
87
+
88
+
89
+ def values_create_book(meta):
90
+ """bookmeta 字段 → 建书表单值。标签弹层的分区点选用单值字段
91
+ (每组选 1 个最稳:番茄主题/角色/情节各≤2、内容组各有上限),
92
+ 数组全量点选由 tag_groups 路径兼容。"""
93
+ return {
94
+ "title": (meta.get("book_name") or "").strip(),
95
+ "summary": (meta.get("summary") or "").strip(),
96
+ "protagonist": (meta.get("protagonist_1") or "").strip(),
97
+ "protagonist2": (meta.get("protagonist_2") or "").strip(),
98
+ "signing_mode": (meta.get("signing_mode") or "连载模式").strip(),
99
+ "category": (meta.get("category") or "").strip(),
100
+ "target_reader": (meta.get("target_reader") or "男频").strip(),
101
+ "tag_theme": _first(meta, "tags_theme"),
102
+ "tag_role": _first(meta, "tags_role"),
103
+ "tag_plot": _first(meta, "tags_plot"),
104
+ "tag_content_emotion": _first(meta, "content_emotion"),
105
+ "tag_content_character": _first(meta, "content_character"),
106
+ "tag_content_world": _first(meta, "content_world"),
107
+ }
108
+
109
+
110
+ def tag_groups(meta):
111
+ """标签字段名 → 值列表(manager 用来生成逐个 click_text 步骤)。"""
112
+ out = []
113
+ for key in ("tags_theme", "tags_role", "tags_plot",
114
+ "content_plot", "content_emotion", "content_character", "content_world"):
115
+ v = meta.get(key)
116
+ if isinstance(v, list) and v:
117
+ out.append((key, [str(x) for x in v]))
118
+ return out
119
+
120
+
121
+ def editor_url(book):
122
+ """章节编辑器直达(真机实测):/main/writer/<book_id>/publish/。"""
123
+ bid = str((book or {}).get("book_id") or "")
124
+ if bid:
125
+ return "https://fanqienovel.com/main/writer/%s/publish/" % bid
126
+ return CONFIG["home"]
127
+
128
+
129
+ def chapter_manage_url(book):
130
+ """章节管理页(上线验证用)。"""
131
+ bid = str((book or {}).get("book_id") or "")
132
+ if bid:
133
+ return "https://fanqienovel.com/main/writer/chapter-manage/" + bid
134
+ return CONFIG["home"] + "book-manage"
135
+
136
+
137
+ def resolve_book_id(page, book):
138
+ """登记缺 book_id 时按书名在作家后台找回:点书卡进详情,从 URL 提取。
139
+
140
+ 建书成功但 id 提取落空(跳转链没走完/auto_submit=false 人工提交)的书,
141
+ 发章前经 manager._resolve_book_id 调到这里补账;找不回返回空串不拦死。"""
142
+ import re
143
+ import time
144
+ from . import flow as _flow
145
+ title = str((book or {}).get("title") or "").strip()
146
+ if not title:
147
+ return ""
148
+ try:
149
+ page.navigate(CONFIG["home"], timeout=30)
150
+ except Exception:
151
+ return ""
152
+ r = None
153
+ for _try in range(6): # 书列表慢渲染
154
+ r = page.call(_flow._click_match_js(), [title], 300)
155
+ if (r or {}).get("ok"):
156
+ break
157
+ time.sleep(1.0)
158
+ if not (r or {}).get("ok"):
159
+ return ""
160
+ time.sleep(2.5) # 详情页跳转收尾
161
+ m = re.search(r"book-info/(\d+)|chapter-manage/(\d+)|writer/(\d+)/publish",
162
+ str(page.url() or ""))
163
+ if not m:
164
+ return ""
165
+ return m.group(1) or m.group(2) or m.group(3) or ""