@jackwener/opencli 1.5.6 → 1.5.8

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 (338) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +4 -2
  3. package/README.zh-CN.md +4 -1
  4. package/SKILL.md +879 -0
  5. package/dist/browser/cdp.d.ts +1 -0
  6. package/dist/browser/cdp.js +30 -27
  7. package/dist/browser/daemon-client.d.ts +7 -1
  8. package/dist/browser/daemon-client.js +3 -0
  9. package/dist/browser/dom-helpers.js +1 -0
  10. package/dist/browser/dom-helpers.test.js +14 -1
  11. package/dist/browser/mcp.js +18 -13
  12. package/dist/browser/page.js +22 -2
  13. package/dist/browser/page.test.d.ts +1 -0
  14. package/dist/browser/page.test.js +44 -0
  15. package/dist/browser/stealth.js +198 -0
  16. package/dist/browser/stealth.test.d.ts +1 -0
  17. package/dist/browser/stealth.test.js +134 -0
  18. package/dist/browser.test.js +1 -1
  19. package/dist/build-manifest.d.ts +1 -0
  20. package/dist/build-manifest.js +5 -1
  21. package/dist/build-manifest.test.js +2 -0
  22. package/dist/cli-manifest.json +544 -137
  23. package/dist/cli.js +20 -3
  24. package/dist/clis/antigravity/serve.d.ts +1 -1
  25. package/dist/clis/antigravity/serve.js +5 -8
  26. package/dist/clis/bilibili/subtitle.js +4 -0
  27. package/dist/clis/bilibili/subtitle.test.d.ts +1 -0
  28. package/dist/clis/bilibili/subtitle.test.js +48 -0
  29. package/dist/clis/chatwise/ask.js +0 -2
  30. package/dist/clis/chatwise/export.js +0 -2
  31. package/dist/clis/chatwise/history.js +0 -2
  32. package/dist/clis/chatwise/model.js +0 -2
  33. package/dist/clis/chatwise/new.js +1 -2
  34. package/dist/clis/chatwise/read.js +0 -2
  35. package/dist/clis/chatwise/screenshot.js +1 -2
  36. package/dist/clis/chatwise/send.js +0 -2
  37. package/dist/clis/chatwise/status.js +1 -2
  38. package/dist/clis/ctrip/search.d.ts +13 -0
  39. package/dist/clis/ctrip/search.js +73 -48
  40. package/dist/clis/ctrip/search.test.d.ts +1 -0
  41. package/dist/clis/ctrip/search.test.js +64 -0
  42. package/dist/clis/douyin/_shared/sts2.js +8 -2
  43. package/dist/clis/douyin/_shared/sts2.test.d.ts +1 -0
  44. package/dist/clis/douyin/_shared/sts2.test.js +27 -0
  45. package/dist/clis/douyin/activities.js +4 -2
  46. package/dist/clis/douyin/activities.test.js +34 -1
  47. package/dist/clis/douyin/collections.js +1 -1
  48. package/dist/clis/douyin/collections.test.js +24 -2
  49. package/dist/clis/douyin/draft.d.ts +8 -11
  50. package/dist/clis/douyin/draft.js +302 -185
  51. package/dist/clis/douyin/draft.test.d.ts +1 -1
  52. package/dist/clis/douyin/draft.test.js +357 -2
  53. package/dist/clis/douyin/hashtag.js +9 -2
  54. package/dist/clis/douyin/hashtag.test.js +35 -2
  55. package/dist/clis/douyin/profile.js +1 -1
  56. package/dist/clis/douyin/profile.test.js +36 -1
  57. package/dist/clis/douyin/videos.js +22 -5
  58. package/dist/clis/douyin/videos.test.js +45 -2
  59. package/dist/clis/facebook/search.test.d.ts +5 -0
  60. package/dist/clis/facebook/search.test.js +60 -0
  61. package/dist/clis/facebook/search.yaml +4 -3
  62. package/dist/clis/instagram/download.d.ts +16 -0
  63. package/dist/clis/instagram/download.js +225 -0
  64. package/dist/clis/instagram/download.test.d.ts +1 -0
  65. package/dist/clis/instagram/download.test.js +118 -0
  66. package/dist/clis/notebooklm/bind-current.d.ts +1 -0
  67. package/dist/clis/notebooklm/bind-current.js +29 -0
  68. package/dist/clis/notebooklm/bind-current.test.d.ts +1 -0
  69. package/dist/clis/notebooklm/bind-current.test.js +35 -0
  70. package/dist/clis/notebooklm/binding.test.d.ts +1 -0
  71. package/dist/clis/notebooklm/binding.test.js +44 -0
  72. package/dist/clis/notebooklm/compat.test.d.ts +3 -0
  73. package/dist/clis/notebooklm/compat.test.js +16 -0
  74. package/dist/clis/notebooklm/current.d.ts +1 -0
  75. package/dist/clis/notebooklm/current.js +28 -0
  76. package/dist/clis/notebooklm/get.d.ts +1 -0
  77. package/dist/clis/notebooklm/get.js +37 -0
  78. package/dist/clis/notebooklm/history.d.ts +1 -0
  79. package/dist/clis/notebooklm/history.js +25 -0
  80. package/dist/clis/notebooklm/history.test.d.ts +1 -0
  81. package/dist/clis/notebooklm/history.test.js +58 -0
  82. package/dist/clis/notebooklm/list.d.ts +1 -0
  83. package/dist/clis/notebooklm/list.js +35 -0
  84. package/dist/clis/notebooklm/note-list.d.ts +1 -0
  85. package/dist/clis/notebooklm/note-list.js +28 -0
  86. package/dist/clis/notebooklm/note-list.test.d.ts +1 -0
  87. package/dist/clis/notebooklm/note-list.test.js +56 -0
  88. package/dist/clis/notebooklm/notes-get.d.ts +1 -0
  89. package/dist/clis/notebooklm/notes-get.js +47 -0
  90. package/dist/clis/notebooklm/notes-get.test.d.ts +1 -0
  91. package/dist/clis/notebooklm/notes-get.test.js +72 -0
  92. package/dist/clis/notebooklm/rpc.d.ts +36 -0
  93. package/dist/clis/notebooklm/rpc.js +189 -0
  94. package/dist/clis/notebooklm/rpc.test.d.ts +1 -0
  95. package/dist/clis/notebooklm/rpc.test.js +105 -0
  96. package/dist/clis/notebooklm/shared.d.ts +87 -0
  97. package/dist/clis/notebooklm/shared.js +3 -0
  98. package/dist/clis/notebooklm/source-fulltext.d.ts +1 -0
  99. package/dist/clis/notebooklm/source-fulltext.js +44 -0
  100. package/dist/clis/notebooklm/source-fulltext.test.d.ts +1 -0
  101. package/dist/clis/notebooklm/source-fulltext.test.js +106 -0
  102. package/dist/clis/notebooklm/source-get.d.ts +1 -0
  103. package/dist/clis/notebooklm/source-get.js +40 -0
  104. package/dist/clis/notebooklm/source-get.test.d.ts +1 -0
  105. package/dist/clis/notebooklm/source-get.test.js +84 -0
  106. package/dist/clis/notebooklm/source-guide.d.ts +1 -0
  107. package/dist/clis/notebooklm/source-guide.js +44 -0
  108. package/dist/clis/notebooklm/source-guide.test.d.ts +1 -0
  109. package/dist/clis/notebooklm/source-guide.test.js +104 -0
  110. package/dist/clis/notebooklm/source-list.d.ts +1 -0
  111. package/dist/clis/notebooklm/source-list.js +30 -0
  112. package/dist/clis/notebooklm/status.d.ts +1 -0
  113. package/dist/clis/notebooklm/status.js +31 -0
  114. package/dist/clis/notebooklm/summary.d.ts +1 -0
  115. package/dist/clis/notebooklm/summary.js +30 -0
  116. package/dist/clis/notebooklm/summary.test.d.ts +1 -0
  117. package/dist/clis/notebooklm/summary.test.js +78 -0
  118. package/dist/clis/notebooklm/utils.d.ts +37 -0
  119. package/dist/clis/notebooklm/utils.js +739 -0
  120. package/dist/clis/notebooklm/utils.test.d.ts +1 -0
  121. package/dist/clis/notebooklm/utils.test.js +390 -0
  122. package/dist/clis/substack/utils.d.ts +4 -0
  123. package/dist/clis/substack/utils.js +8 -2
  124. package/dist/clis/substack/utils.test.d.ts +1 -0
  125. package/dist/clis/substack/utils.test.js +46 -0
  126. package/dist/clis/v2ex/hot.yaml +4 -1
  127. package/dist/clis/v2ex/latest.yaml +4 -1
  128. package/dist/clis/v2ex/topic.yaml +6 -1
  129. package/dist/clis/weixin/download.d.ts +9 -0
  130. package/dist/clis/weixin/download.js +76 -6
  131. package/dist/clis/weread/book.js +108 -2
  132. package/dist/clis/weread/commands.test.js +262 -152
  133. package/dist/clis/weread/utils.d.ts +10 -0
  134. package/dist/clis/weread/utils.js +27 -7
  135. package/dist/clis/xiaohongshu/comments.d.ts +3 -0
  136. package/dist/clis/xiaohongshu/comments.js +76 -17
  137. package/dist/clis/xiaohongshu/comments.test.js +70 -9
  138. package/dist/clis/xiaohongshu/download.d.ts +4 -1
  139. package/dist/clis/xiaohongshu/download.js +83 -22
  140. package/dist/clis/xiaohongshu/download.test.d.ts +1 -0
  141. package/dist/clis/xiaohongshu/download.test.js +75 -0
  142. package/dist/clis/xiaohongshu/note-helpers.d.ts +12 -0
  143. package/dist/clis/xiaohongshu/note-helpers.js +23 -0
  144. package/dist/clis/xiaohongshu/note.d.ts +7 -0
  145. package/dist/clis/xiaohongshu/note.js +76 -0
  146. package/dist/clis/xiaohongshu/note.test.d.ts +1 -0
  147. package/dist/clis/xiaohongshu/note.test.js +136 -0
  148. package/dist/clis/xiaohongshu/search.js +9 -0
  149. package/dist/clis/xiaohongshu/search.test.js +10 -4
  150. package/dist/clis/youtube/search.js +57 -17
  151. package/dist/clis/zhihu/question.js +19 -17
  152. package/dist/clis/zhihu/question.test.d.ts +1 -0
  153. package/dist/clis/zhihu/question.test.js +54 -0
  154. package/dist/commanderAdapter.js +9 -0
  155. package/dist/commanderAdapter.test.js +25 -0
  156. package/dist/commands/daemon.d.ts +9 -0
  157. package/dist/commands/daemon.js +124 -0
  158. package/dist/commands/daemon.test.d.ts +1 -0
  159. package/dist/commands/daemon.test.js +185 -0
  160. package/dist/completion.js +3 -1
  161. package/dist/constants.d.ts +2 -0
  162. package/dist/constants.js +2 -0
  163. package/dist/daemon.d.ts +1 -1
  164. package/dist/daemon.js +25 -14
  165. package/dist/daemon.test.d.ts +1 -0
  166. package/dist/daemon.test.js +65 -0
  167. package/dist/discovery.d.ts +9 -0
  168. package/dist/discovery.js +47 -2
  169. package/dist/electron-apps.d.ts +29 -0
  170. package/dist/electron-apps.js +65 -0
  171. package/dist/electron-apps.test.d.ts +1 -0
  172. package/dist/electron-apps.test.js +43 -0
  173. package/dist/engine.test.js +41 -9
  174. package/dist/execution.js +20 -16
  175. package/dist/extension-manifest-regression.test.js +1 -0
  176. package/dist/idle-manager.d.ts +19 -0
  177. package/dist/idle-manager.js +54 -0
  178. package/dist/launcher.d.ts +36 -0
  179. package/dist/launcher.js +152 -0
  180. package/dist/launcher.test.d.ts +1 -0
  181. package/dist/launcher.test.js +57 -0
  182. package/dist/main.js +3 -3
  183. package/dist/registry.d.ts +1 -0
  184. package/dist/registry.js +31 -3
  185. package/dist/registry.test.js +13 -0
  186. package/dist/runtime.d.ts +5 -3
  187. package/dist/runtime.js +12 -5
  188. package/dist/serialization.d.ts +1 -0
  189. package/dist/serialization.js +3 -0
  190. package/dist/serialization.test.js +17 -1
  191. package/dist/tui.d.ts +7 -0
  192. package/dist/tui.js +52 -0
  193. package/dist/tui.test.d.ts +1 -0
  194. package/dist/tui.test.js +19 -0
  195. package/dist/weixin-download.test.js +14 -0
  196. package/docs/.vitepress/config.mts +1 -0
  197. package/docs/adapters/browser/notebooklm.md +69 -0
  198. package/docs/adapters/browser/xiaohongshu.md +19 -10
  199. package/docs/adapters/index.md +67 -66
  200. package/docs/guide/browser-bridge.md +12 -0
  201. package/docs/guide/troubleshooting.md +9 -4
  202. package/docs/superpowers/plans/2026-03-31-daemon-lifecycle-redesign.md +857 -0
  203. package/docs/superpowers/specs/2026-03-31-daemon-lifecycle-redesign.md +208 -0
  204. package/docs/zh/guide/browser-bridge.md +12 -0
  205. package/extension/dist/background.js +250 -11
  206. package/extension/manifest.json +2 -1
  207. package/extension/src/background.test.ts +202 -2
  208. package/extension/src/background.ts +175 -10
  209. package/extension/src/cdp.test.ts +75 -0
  210. package/extension/src/cdp.ts +89 -3
  211. package/extension/src/protocol.ts +7 -5
  212. package/package.json +1 -1
  213. package/src/browser/cdp.ts +24 -17
  214. package/src/browser/daemon-client.ts +7 -1
  215. package/src/browser/dom-helpers.test.ts +15 -1
  216. package/src/browser/dom-helpers.ts +1 -0
  217. package/src/browser/mcp.ts +18 -13
  218. package/src/browser/page.test.ts +58 -0
  219. package/src/browser/page.ts +18 -2
  220. package/src/browser/stealth.test.ts +153 -0
  221. package/src/browser/stealth.ts +198 -0
  222. package/src/browser.test.ts +1 -1
  223. package/src/build-manifest.test.ts +2 -0
  224. package/src/build-manifest.ts +6 -1
  225. package/src/cli.ts +21 -3
  226. package/src/clis/antigravity/SKILL.md +3 -12
  227. package/src/clis/antigravity/serve.ts +5 -10
  228. package/src/clis/bilibili/subtitle.test.ts +60 -0
  229. package/src/clis/bilibili/subtitle.ts +4 -0
  230. package/src/clis/chatwise/ask.ts +0 -2
  231. package/src/clis/chatwise/export.ts +0 -2
  232. package/src/clis/chatwise/history.ts +0 -2
  233. package/src/clis/chatwise/model.ts +0 -2
  234. package/src/clis/chatwise/new.ts +1 -2
  235. package/src/clis/chatwise/read.ts +0 -2
  236. package/src/clis/chatwise/screenshot.ts +1 -2
  237. package/src/clis/chatwise/send.ts +0 -2
  238. package/src/clis/chatwise/status.ts +1 -2
  239. package/src/clis/ctrip/search.test.ts +73 -0
  240. package/src/clis/ctrip/search.ts +97 -47
  241. package/src/clis/douyin/_shared/sts2.test.ts +31 -0
  242. package/src/clis/douyin/_shared/sts2.ts +11 -3
  243. package/src/clis/douyin/activities.test.ts +41 -1
  244. package/src/clis/douyin/activities.ts +12 -3
  245. package/src/clis/douyin/collections.test.ts +35 -2
  246. package/src/clis/douyin/collections.ts +1 -1
  247. package/src/clis/douyin/draft.test.ts +444 -2
  248. package/src/clis/douyin/draft.ts +382 -218
  249. package/src/clis/douyin/hashtag.test.ts +42 -2
  250. package/src/clis/douyin/hashtag.ts +11 -3
  251. package/src/clis/douyin/profile.test.ts +43 -1
  252. package/src/clis/douyin/profile.ts +9 -2
  253. package/src/clis/douyin/videos.test.ts +52 -2
  254. package/src/clis/douyin/videos.ts +49 -15
  255. package/src/clis/facebook/search.test.ts +70 -0
  256. package/src/clis/facebook/search.yaml +4 -3
  257. package/src/clis/instagram/download.test.ts +159 -0
  258. package/src/clis/instagram/download.ts +286 -0
  259. package/src/clis/notebooklm/bind-current.test.ts +43 -0
  260. package/src/clis/notebooklm/bind-current.ts +36 -0
  261. package/src/clis/notebooklm/binding.test.ts +53 -0
  262. package/src/clis/notebooklm/compat.test.ts +19 -0
  263. package/src/clis/notebooklm/current.ts +38 -0
  264. package/src/clis/notebooklm/get.ts +53 -0
  265. package/src/clis/notebooklm/history.test.ts +70 -0
  266. package/src/clis/notebooklm/history.ts +36 -0
  267. package/src/clis/notebooklm/list.ts +40 -0
  268. package/src/clis/notebooklm/note-list.test.ts +64 -0
  269. package/src/clis/notebooklm/note-list.ts +42 -0
  270. package/src/clis/notebooklm/notes-get.test.ts +88 -0
  271. package/src/clis/notebooklm/notes-get.ts +67 -0
  272. package/src/clis/notebooklm/rpc.test.ts +126 -0
  273. package/src/clis/notebooklm/rpc.ts +286 -0
  274. package/src/clis/notebooklm/shared.ts +98 -0
  275. package/src/clis/notebooklm/source-fulltext.test.ts +123 -0
  276. package/src/clis/notebooklm/source-fulltext.ts +69 -0
  277. package/src/clis/notebooklm/source-get.test.ts +100 -0
  278. package/src/clis/notebooklm/source-get.ts +60 -0
  279. package/src/clis/notebooklm/source-guide.test.ts +121 -0
  280. package/src/clis/notebooklm/source-guide.ts +69 -0
  281. package/src/clis/notebooklm/source-list.ts +45 -0
  282. package/src/clis/notebooklm/status.ts +34 -0
  283. package/src/clis/notebooklm/summary.test.ts +94 -0
  284. package/src/clis/notebooklm/summary.ts +45 -0
  285. package/src/clis/notebooklm/utils.test.ts +446 -0
  286. package/src/clis/notebooklm/utils.ts +893 -0
  287. package/src/clis/substack/utils.test.ts +54 -0
  288. package/src/clis/substack/utils.ts +10 -2
  289. package/src/clis/v2ex/hot.yaml +4 -1
  290. package/src/clis/v2ex/latest.yaml +4 -1
  291. package/src/clis/v2ex/topic.yaml +6 -1
  292. package/src/clis/weixin/download.ts +95 -6
  293. package/src/clis/weread/book.ts +142 -2
  294. package/src/clis/weread/commands.test.ts +314 -154
  295. package/src/clis/weread/utils.ts +33 -4
  296. package/src/clis/xiaohongshu/comments.test.ts +85 -9
  297. package/src/clis/xiaohongshu/comments.ts +76 -17
  298. package/src/clis/xiaohongshu/download.test.ts +96 -0
  299. package/src/clis/xiaohongshu/download.ts +83 -22
  300. package/src/clis/xiaohongshu/note-helpers.ts +25 -0
  301. package/src/clis/xiaohongshu/note.test.ts +164 -0
  302. package/src/clis/xiaohongshu/note.ts +86 -0
  303. package/src/clis/xiaohongshu/search.test.ts +11 -4
  304. package/src/clis/xiaohongshu/search.ts +13 -0
  305. package/src/clis/youtube/search.ts +57 -17
  306. package/src/clis/zhihu/question.test.ts +71 -0
  307. package/src/clis/zhihu/question.ts +27 -15
  308. package/src/commanderAdapter.test.ts +30 -0
  309. package/src/commanderAdapter.ts +7 -0
  310. package/src/commands/daemon.test.ts +238 -0
  311. package/src/commands/daemon.ts +135 -0
  312. package/src/completion.ts +2 -1
  313. package/src/constants.ts +3 -0
  314. package/src/daemon.test.ts +88 -0
  315. package/src/daemon.ts +26 -14
  316. package/src/discovery.ts +52 -2
  317. package/src/electron-apps.test.ts +50 -0
  318. package/src/electron-apps.ts +89 -0
  319. package/src/engine.test.ts +45 -9
  320. package/src/execution.ts +24 -19
  321. package/src/extension-manifest-regression.test.ts +1 -0
  322. package/src/idle-manager.ts +60 -0
  323. package/src/launcher.test.ts +67 -0
  324. package/src/launcher.ts +185 -0
  325. package/src/main.ts +3 -2
  326. package/src/registry.test.ts +15 -0
  327. package/src/registry.ts +32 -3
  328. package/src/runtime.ts +13 -7
  329. package/src/serialization.test.ts +19 -1
  330. package/src/serialization.ts +2 -0
  331. package/src/tui.test.ts +23 -0
  332. package/src/tui.ts +65 -0
  333. package/src/weixin-download.test.ts +27 -0
  334. package/tests/e2e/browser-public-extended.test.ts +6 -2
  335. package/chatwise-opencli.ps1 +0 -82
  336. package/dist/clis/chatwise/shared.d.ts +0 -2
  337. package/dist/clis/chatwise/shared.js +0 -6
  338. package/src/clis/chatwise/shared.ts +0 -8
@@ -0,0 +1,54 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import type { IPage } from '../../types.js';
3
+ import { __test__, loadSubstackArchive, loadSubstackFeed } from './utils.js';
4
+
5
+ function createPageMock(evaluateResult: unknown): IPage {
6
+ return {
7
+ goto: vi.fn().mockResolvedValue(undefined),
8
+ evaluate: vi.fn().mockResolvedValue(evaluateResult),
9
+ snapshot: vi.fn().mockResolvedValue(undefined),
10
+ click: vi.fn().mockResolvedValue(undefined),
11
+ typeText: vi.fn().mockResolvedValue(undefined),
12
+ pressKey: vi.fn().mockResolvedValue(undefined),
13
+ scrollTo: vi.fn().mockResolvedValue(undefined),
14
+ getFormState: vi.fn().mockResolvedValue({}),
15
+ wait: vi.fn().mockResolvedValue(undefined),
16
+ tabs: vi.fn().mockResolvedValue([]),
17
+ closeTab: vi.fn().mockResolvedValue(undefined),
18
+ newTab: vi.fn().mockResolvedValue(undefined),
19
+ selectTab: vi.fn().mockResolvedValue(undefined),
20
+ networkRequests: vi.fn().mockResolvedValue([]),
21
+ consoleMessages: vi.fn().mockResolvedValue([]),
22
+ scroll: vi.fn().mockResolvedValue(undefined),
23
+ autoScroll: vi.fn().mockResolvedValue(undefined),
24
+ installInterceptor: vi.fn().mockResolvedValue(undefined),
25
+ getInterceptedRequests: vi.fn().mockResolvedValue([]),
26
+ getCookies: vi.fn().mockResolvedValue([]),
27
+ screenshot: vi.fn().mockResolvedValue(''),
28
+ waitForCapture: vi.fn().mockResolvedValue(undefined),
29
+ };
30
+ }
31
+
32
+ describe('substack utils wait selectors', () => {
33
+ it('waits for both feed link shapes before scraping the feed', async () => {
34
+ const page = createPageMock([]);
35
+
36
+ await loadSubstackFeed(page, 'https://substack.com/', 5);
37
+
38
+ expect(page.wait).toHaveBeenCalledWith({
39
+ selector: __test__.FEED_POST_LINK_SELECTOR,
40
+ timeout: 5,
41
+ });
42
+ });
43
+
44
+ it('waits for archive post links before scraping archive pages', async () => {
45
+ const page = createPageMock([]);
46
+
47
+ await loadSubstackArchive(page, 'https://example.substack.com', 5);
48
+
49
+ expect(page.wait).toHaveBeenCalledWith({
50
+ selector: __test__.ARCHIVE_POST_LINK_SELECTOR,
51
+ timeout: 5,
52
+ });
53
+ });
54
+ });
@@ -1,6 +1,9 @@
1
1
  import { CommandExecutionError } from '../../errors.js';
2
2
  import type { IPage } from '../../types.js';
3
3
 
4
+ const FEED_POST_LINK_SELECTOR = 'a[href*="/home/post/"], a[href*="/p/"]';
5
+ const ARCHIVE_POST_LINK_SELECTOR = 'a[href*="/p/"]';
6
+
4
7
  export function buildSubstackBrowseUrl(category?: string): string {
5
8
  if (!category || category === 'all') return 'https://substack.com/';
6
9
  const slug = category === 'tech' ? 'technology' : category;
@@ -10,7 +13,7 @@ export function buildSubstackBrowseUrl(category?: string): string {
10
13
  export async function loadSubstackFeed(page: IPage, url: string, limit: number): Promise<any[]> {
11
14
  if (!page) throw new CommandExecutionError('Browser session required for substack feed');
12
15
  await page.goto(url);
13
- await page.wait({ selector: 'article', timeout: 5 });
16
+ await page.wait({ selector: FEED_POST_LINK_SELECTOR, timeout: 5 });
14
17
  const data = await page.evaluate(`
15
18
  (async () => {
16
19
  await new Promise((resolve) => setTimeout(resolve, 3000));
@@ -79,7 +82,7 @@ export async function loadSubstackFeed(page: IPage, url: string, limit: number):
79
82
  export async function loadSubstackArchive(page: IPage, baseUrl: string, limit: number): Promise<any[]> {
80
83
  if (!page) throw new CommandExecutionError('Browser session required for substack archive');
81
84
  await page.goto(`${baseUrl}/archive`);
82
- await page.wait({ selector: 'article', timeout: 5 });
85
+ await page.wait({ selector: ARCHIVE_POST_LINK_SELECTOR, timeout: 5 });
83
86
  const data = await page.evaluate(`
84
87
  (async () => {
85
88
  await new Promise((resolve) => setTimeout(resolve, 3000));
@@ -131,3 +134,8 @@ export async function loadSubstackArchive(page: IPage, baseUrl: string, limit: n
131
134
 
132
135
  return Array.isArray(data) ? data : [];
133
136
  }
137
+
138
+ export const __test__ = {
139
+ FEED_POST_LINK_SELECTOR,
140
+ ARCHIVE_POST_LINK_SELECTOR,
141
+ };
@@ -16,10 +16,13 @@ pipeline:
16
16
  url: https://www.v2ex.com/api/topics/hot.json
17
17
 
18
18
  - map:
19
+ id: ${{ item.id }}
19
20
  rank: ${{ index + 1 }}
20
21
  title: ${{ item.title }}
22
+ node: ${{ item.node.title }}
21
23
  replies: ${{ item.replies }}
24
+ url: ${{ item.url }}
22
25
 
23
26
  - limit: ${{ args.limit }}
24
27
 
25
- columns: [rank, title, replies]
28
+ columns: [id, rank, title, node, replies, url]
@@ -16,10 +16,13 @@ pipeline:
16
16
  url: https://www.v2ex.com/api/topics/latest.json
17
17
 
18
18
  - map:
19
+ id: ${{ item.id }}
19
20
  rank: ${{ index + 1 }}
20
21
  title: ${{ item.title }}
22
+ node: ${{ item.node.title }}
21
23
  replies: ${{ item.replies }}
24
+ url: ${{ item.url }}
22
25
 
23
26
  - limit: ${{ args.limit }}
24
27
 
25
- columns: [rank, title, replies]
28
+ columns: [id, rank, title, node, replies, url]
@@ -19,10 +19,15 @@ pipeline:
19
19
  id: ${{ args.id }}
20
20
 
21
21
  - map:
22
+ id: ${{ item.id }}
22
23
  title: ${{ item.title }}
24
+ content: ${{ item.content }}
25
+ member: ${{ item.member.username }}
26
+ created: ${{ item.created }}
27
+ node: ${{ item.node.title }}
23
28
  replies: ${{ item.replies }}
24
29
  url: ${{ item.url }}
25
30
 
26
31
  - limit: 1
27
32
 
28
- columns: [title, replies, url]
33
+ columns: [id, title, content, member, created, node, replies, url]
@@ -110,6 +110,37 @@ export function extractWechatPublishTime(
110
110
  return formatWechatTimestamp(rawCreateTime);
111
111
  }
112
112
 
113
+ /**
114
+ * Detect WeChat anti-bot / verification gate pages before we try to parse the article.
115
+ */
116
+ export function detectWechatAccessIssue(
117
+ pageText: string | null | undefined,
118
+ htmlStr: string,
119
+ ): string {
120
+ const normalizedText = (pageText || '').replace(/\s+/g, ' ').trim();
121
+
122
+ if (
123
+ /环境异常/.test(normalizedText) &&
124
+ /(完成验证后即可继续访问|去验证)/.test(normalizedText)
125
+ ) {
126
+ return 'environment verification required';
127
+ }
128
+
129
+ if (/secitptpage\/verify\.html/.test(htmlStr) || /id=["']js_verify["']/.test(htmlStr)) {
130
+ return 'environment verification required';
131
+ }
132
+
133
+ return '';
134
+ }
135
+
136
+ export function pickFirstWechatMetaText(...candidates: Array<string | null | undefined>): string {
137
+ for (const candidate of candidates) {
138
+ const normalized = (candidate || '').replace(/\s+/g, ' ').trim();
139
+ if (normalized && normalized !== 'Name cleared') return normalized;
140
+ }
141
+ return '';
142
+ }
143
+
113
144
  /**
114
145
  * Build a self-contained helper for execution inside page.evaluate().
115
146
  */
@@ -161,6 +192,31 @@ export function buildExtractWechatPublishTimeJs(): string {
161
192
  }.toString()})`;
162
193
  }
163
194
 
195
+ /**
196
+ * Build a self-contained access-issue detector for execution inside page.evaluate().
197
+ */
198
+ export function buildDetectWechatAccessIssueJs(): string {
199
+ return `(${function detectWechatAccessIssueInPage(
200
+ pageText: string | null | undefined,
201
+ htmlStr: string,
202
+ ) {
203
+ const normalizedText = (pageText || '').replace(/\s+/g, ' ').trim();
204
+
205
+ if (
206
+ /环境异常/.test(normalizedText) &&
207
+ /(完成验证后即可继续访问|去验证)/.test(normalizedText)
208
+ ) {
209
+ return 'environment verification required';
210
+ }
211
+
212
+ if (/secitptpage\/verify\.html/.test(htmlStr) || /id=["']js_verify["']/.test(htmlStr)) {
213
+ return 'environment verification required';
214
+ }
215
+
216
+ return '';
217
+ }.toString()})`;
218
+ }
219
+
164
220
  // ============================================================
165
221
  // CLI Registration
166
222
  // ============================================================
@@ -196,18 +252,34 @@ cli({
196
252
  title: '',
197
253
  author: '',
198
254
  publishTime: '',
255
+ errorHint: '',
199
256
  contentHtml: '',
200
257
  codeBlocks: [],
201
258
  imageUrls: []
202
259
  };
203
260
 
204
- // Title: #activity-name
205
- const titleEl = document.querySelector('#activity-name');
206
- result.title = titleEl ? titleEl.textContent.trim() : '';
261
+ const pickFirstText = (...selectors) => {
262
+ for (const selector of selectors) {
263
+ const text = document.querySelector(selector)?.textContent?.replace(/\\s+/g, ' ').trim() || '';
264
+ if (text && text !== 'Name cleared') return text;
265
+ }
266
+ return '';
267
+ };
268
+
269
+ // WeChat has multiple article templates. Newer pages use #js_text_title.
270
+ result.title = pickFirstText(
271
+ '#activity-name',
272
+ '#js_text_title',
273
+ '.rich_media_title',
274
+ );
207
275
 
208
- // Author (WeChat Official Account name): #js_name
209
- const authorEl = document.querySelector('#js_name');
210
- result.author = authorEl ? authorEl.textContent.trim() : '';
276
+ result.author = pickFirstText(
277
+ '#js_name',
278
+ '.wx_follow_nickname',
279
+ '#profileBt .profile_nickname',
280
+ '.rich_media_meta.rich_media_meta_nickname',
281
+ '.rich_media_meta_nickname',
282
+ );
211
283
 
212
284
  // Publish time: prefer the rendered DOM text, then fall back to numeric create_time values.
213
285
  const publishTimeEl = document.querySelector('#publish_time');
@@ -217,6 +289,13 @@ cli({
217
289
  document.documentElement.innerHTML,
218
290
  );
219
291
 
292
+ const detectWechatAccessIssue = ${buildDetectWechatAccessIssueJs()};
293
+ result.errorHint = detectWechatAccessIssue(
294
+ document.body ? document.body.innerText : '',
295
+ document.documentElement.innerHTML,
296
+ );
297
+ if (result.errorHint) return result;
298
+
220
299
  // Content processing
221
300
  const contentEl = document.querySelector('#js_content');
222
301
  if (!contentEl) return result;
@@ -268,6 +347,16 @@ cli({
268
347
  })()
269
348
  `);
270
349
 
350
+ if (data?.errorHint === 'environment verification required') {
351
+ return [{
352
+ title: 'Error',
353
+ author: '-',
354
+ publish_time: '-',
355
+ status: 'failed — verification required in WeChat browser page',
356
+ size: '-',
357
+ }];
358
+ }
359
+
271
360
  return downloadArticle(
272
361
  {
273
362
  title: data?.title || '',
@@ -1,7 +1,13 @@
1
1
  import { cli, Strategy } from '../../registry.js';
2
2
  import { CliError } from '../../errors.js';
3
3
  import type { IPage } from '../../types.js';
4
- import { fetchPrivateApi, resolveShelfReaderUrl } from './utils.js';
4
+ import {
5
+ fetchPrivateApi,
6
+ fetchWebApi,
7
+ resolveShelfReader,
8
+ WEREAD_UA,
9
+ WEREAD_WEB_ORIGIN,
10
+ } from './utils.js';
5
11
 
6
12
  interface ReaderFallbackResult {
7
13
  title: string;
@@ -13,6 +19,132 @@ interface ReaderFallbackResult {
13
19
  metadataReady: boolean;
14
20
  }
15
21
 
22
+ interface SearchHtmlEntry {
23
+ title: string;
24
+ author: string;
25
+ url: string;
26
+ }
27
+
28
+ function decodeHtmlText(value: string): string {
29
+ return value
30
+ .replace(/<[^>]+>/g, '')
31
+ .replace(/&#x([0-9a-fA-F]+);/gi, (_, n) => String.fromCharCode(parseInt(n, 16)))
32
+ .replace(/&#(\d+);/g, (_, n) => String.fromCharCode(Number(n)))
33
+ .replace(/&nbsp;/g, ' ')
34
+ .replace(/&amp;/g, '&')
35
+ .replace(/&quot;/g, '"')
36
+ .trim();
37
+ }
38
+
39
+ function normalizeSearchText(value: string): string {
40
+ return value.replace(/\s+/g, ' ').trim();
41
+ }
42
+
43
+ function buildSearchIdentity(title: string, author: string): string {
44
+ return `${normalizeSearchText(title)}\u0000${normalizeSearchText(author)}`;
45
+ }
46
+
47
+ function countSearchTitles(entries: Array<{ title: string }>): Map<string, number> {
48
+ const counts = new Map<string, number>();
49
+ for (const entry of entries) {
50
+ const key = normalizeSearchText(entry.title);
51
+ if (!key) continue;
52
+ counts.set(key, (counts.get(key) || 0) + 1);
53
+ }
54
+ return counts;
55
+ }
56
+
57
+ function countSearchIdentities(entries: Array<{ title: string; author: string }>): Map<string, number> {
58
+ const counts = new Map<string, number>();
59
+ for (const entry of entries) {
60
+ const key = buildSearchIdentity(entry.title, entry.author);
61
+ if (!normalizeSearchText(entry.title) || !normalizeSearchText(entry.author)) continue;
62
+ counts.set(key, (counts.get(key) || 0) + 1);
63
+ }
64
+ return counts;
65
+ }
66
+
67
+ /**
68
+ * Reuse the public search page as a last-resort reader URL source when the
69
+ * cached shelf page cannot provide a trustworthy bookId-to-reader mapping.
70
+ */
71
+ async function resolveSearchReaderUrl(title: string, author: string): Promise<string> {
72
+ const normalizedTitle = normalizeSearchText(title);
73
+ const normalizedAuthor = normalizeSearchText(author);
74
+ if (!normalizedTitle) return '';
75
+
76
+ try {
77
+ const [data, htmlEntries] = await Promise.all([
78
+ fetchWebApi('/search/global', { keyword: normalizedTitle }),
79
+ (async (): Promise<SearchHtmlEntry[]> => {
80
+ const url = new URL('/web/search/books', WEREAD_WEB_ORIGIN);
81
+ url.searchParams.set('keyword', normalizedTitle);
82
+
83
+ const resp = await fetch(url.toString(), {
84
+ headers: { 'User-Agent': WEREAD_UA },
85
+ });
86
+ if (!resp.ok) return [];
87
+
88
+ const html = await resp.text();
89
+ const items = Array.from(
90
+ html.matchAll(/<li[^>]*class="wr_bookList_item"[^>]*>([\s\S]*?)<\/li>/g),
91
+ );
92
+
93
+ return items.map((match) => {
94
+ const chunk = match[1];
95
+ const hrefMatch = chunk.match(/<a[^>]*href="([^"]+)"[^>]*class="wr_bookList_item_link"[^>]*>|<a[^>]*class="wr_bookList_item_link"[^>]*href="([^"]+)"[^>]*>/);
96
+ const titleMatch = chunk.match(/<p[^>]*class="wr_bookList_item_title"[^>]*>([\s\S]*?)<\/p>/);
97
+ const authorMatch = chunk.match(/<p[^>]*class="wr_bookList_item_author"[^>]*>([\s\S]*?)<\/p>/);
98
+ const href = hrefMatch?.[1] || hrefMatch?.[2] || '';
99
+
100
+ return {
101
+ title: decodeHtmlText(titleMatch?.[1] || ''),
102
+ author: decodeHtmlText(authorMatch?.[1] || ''),
103
+ url: href ? new URL(href, WEREAD_WEB_ORIGIN).toString() : '',
104
+ };
105
+ }).filter((entry) => entry.title && entry.url);
106
+ })(),
107
+ ]);
108
+
109
+ const books: any[] = Array.isArray(data?.books) ? data.books : [];
110
+ const apiIdentityCounts = countSearchIdentities(
111
+ books.map((item: any) => ({
112
+ title: item.bookInfo?.title ?? '',
113
+ author: item.bookInfo?.author ?? '',
114
+ })),
115
+ );
116
+ const htmlIdentityCounts = countSearchIdentities(
117
+ htmlEntries.filter((entry) => entry.author),
118
+ );
119
+ const identityKey = buildSearchIdentity(normalizedTitle, normalizedAuthor);
120
+ if (
121
+ normalizedAuthor &&
122
+ (apiIdentityCounts.get(identityKey) || 0) === 1 &&
123
+ (htmlIdentityCounts.get(identityKey) || 0) === 1
124
+ ) {
125
+ const exactMatch = htmlEntries.find((entry) => buildSearchIdentity(entry.title, entry.author) === identityKey);
126
+ if (exactMatch?.url) return exactMatch.url;
127
+ }
128
+
129
+ const sameTitleHtmlEntries = htmlEntries.filter((entry) => normalizeSearchText(entry.title) === normalizedTitle);
130
+ if (normalizedAuthor && sameTitleHtmlEntries.some((entry) => normalizeSearchText(entry.author))) {
131
+ return '';
132
+ }
133
+
134
+ const apiTitleCounts = countSearchTitles(
135
+ books.map((item: any) => ({ title: item.bookInfo?.title ?? '' })),
136
+ );
137
+ const htmlTitleCounts = countSearchTitles(htmlEntries);
138
+ if ((apiTitleCounts.get(normalizedTitle) || 0) !== 1 || (htmlTitleCounts.get(normalizedTitle) || 0) !== 1) {
139
+ return '';
140
+ }
141
+
142
+ return htmlEntries.find((entry) => normalizeSearchText(entry.title) === normalizedTitle)?.url || '';
143
+ } catch {
144
+ return '';
145
+ }
146
+ }
147
+
16
148
  /**
17
149
  * Read visible book metadata from the web reader cover/flyleaf page.
18
150
  * This path is used as a fallback when the private API session has expired.
@@ -108,7 +240,15 @@ cli({
108
240
  throw error;
109
241
  }
110
242
 
111
- const readerUrl = await resolveShelfReaderUrl(page, bookId);
243
+ const { readerUrl: resolvedReaderUrl, snapshot } = await resolveShelfReader(page, bookId);
244
+ let readerUrl = resolvedReaderUrl;
245
+ if (!readerUrl) {
246
+ const cachedBook = snapshot.rawBooks.find((book) => String(book?.bookId || '').trim() === bookId);
247
+ readerUrl = await resolveSearchReaderUrl(
248
+ String(cachedBook?.title || ''),
249
+ String(cachedBook?.author || ''),
250
+ );
251
+ }
112
252
  if (!readerUrl) {
113
253
  throw error;
114
254
  }