@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
@@ -23,8 +23,20 @@ describe('weread book-id positional args', () => {
23
23
  const highlights = getRegistry().get('weread/highlights');
24
24
  const notes = getRegistry().get('weread/notes');
25
25
 
26
+ const repeatValue = <T,>(value: T, count: number): T[] => Array.from({ length: count }, () => value);
27
+
28
+ const createPageStub = (...evaluateResults: unknown[]) => ({
29
+ getCookies: vi.fn().mockResolvedValue([
30
+ { name: 'wr_vid', value: '70486028', domain: '.weread.qq.com' },
31
+ ]),
32
+ goto: vi.fn().mockResolvedValue(undefined),
33
+ wait: vi.fn().mockResolvedValue(undefined),
34
+ evaluate: vi.fn().mockImplementation(async () => evaluateResults.shift()),
35
+ });
36
+
26
37
  beforeEach(() => {
27
38
  mockFetchPrivateApi.mockReset();
39
+ vi.unstubAllGlobals();
28
40
  });
29
41
 
30
42
  it('passes the positional book-id to book details', async () => {
@@ -40,33 +52,27 @@ describe('weread book-id positional args', () => {
40
52
  new CliError('AUTH_REQUIRED', 'Not logged in to WeRead', 'Please log in to weread.qq.com in Chrome first'),
41
53
  );
42
54
 
43
- const page = {
44
- goto: vi.fn().mockResolvedValue(undefined),
45
- evaluate: vi.fn()
46
- .mockResolvedValueOnce({
47
- cacheFound: true,
48
- rawBooks: [
49
- { bookId: 'MP_WXS_3634777637', title: '文明、现代化、价值投资与中国', author: '李录' },
50
- ],
51
- shelfIndexes: [
52
- { bookId: 'MP_WXS_3634777637', idx: 0, role: 'book' },
53
- ],
54
- })
55
- .mockResolvedValueOnce(['https://weread.qq.com/web/reader/6f5323f071bd7f7b6f521e8'])
56
- .mockResolvedValueOnce({
57
- title: '文明、现代化、价值投资与中国',
58
- author: '李录',
59
- publisher: '中信出版集团',
60
- intro: '对中国未来几十年的预测。',
61
- category: '',
62
- rating: '84.1%',
63
- metadataReady: true,
64
- }),
65
- getCookies: vi.fn().mockResolvedValue([
66
- { name: 'wr_vid', value: '70486028', domain: '.weread.qq.com' },
67
- ]),
68
- wait: vi.fn().mockResolvedValue(undefined),
69
- } as any;
55
+ const page = createPageStub(
56
+ {
57
+ cacheFound: true,
58
+ rawBooks: [
59
+ { bookId: 'MP_WXS_3634777637', title: '文明、现代化、价值投资与中国', author: '李录' },
60
+ ],
61
+ shelfIndexes: [
62
+ { bookId: 'MP_WXS_3634777637', idx: 0, role: 'book' },
63
+ ],
64
+ },
65
+ ['https://weread.qq.com/web/reader/6f5323f071bd7f7b6f521e8'],
66
+ {
67
+ title: '文明、现代化、价值投资与中国',
68
+ author: '李录',
69
+ publisher: '中信出版集团',
70
+ intro: '对中国未来几十年的预测。',
71
+ category: '',
72
+ rating: '84.1%',
73
+ metadataReady: true,
74
+ },
75
+ ) as any;
70
76
 
71
77
  const result = await book!.func!(page, { 'book-id': 'MP_WXS_3634777637' });
72
78
 
@@ -90,41 +96,35 @@ describe('weread book-id positional args', () => {
90
96
  new CliError('AUTH_REQUIRED', 'Not logged in to WeRead', 'Please log in to weread.qq.com in Chrome first'),
91
97
  );
92
98
 
93
- const page = {
94
- goto: vi.fn().mockResolvedValue(undefined),
95
- evaluate: vi.fn()
96
- .mockResolvedValueOnce({
97
- cacheFound: true,
98
- rawBooks: [
99
- { bookId: 'MP_WXS_1', title: '公众号文章一', author: '作者甲' },
100
- { bookId: 'BOOK_2', title: '普通书二', author: '作者乙' },
101
- { bookId: 'MP_WXS_3', title: '公众号文章三', author: '作者丙' },
102
- ],
103
- shelfIndexes: [
104
- { bookId: 'MP_WXS_1', idx: 0, role: 'mp' },
105
- { bookId: 'BOOK_2', idx: 1, role: 'book' },
106
- { bookId: 'MP_WXS_3', idx: 2, role: 'mp' },
107
- ],
108
- })
109
- .mockResolvedValueOnce([
110
- 'https://weread.qq.com/web/reader/mp1',
111
- 'https://weread.qq.com/web/reader/book2',
112
- 'https://weread.qq.com/web/reader/mp3',
113
- ])
114
- .mockResolvedValueOnce({
115
- title: '公众号文章一',
116
- author: '作者甲',
117
- publisher: '微信读书',
118
- intro: '第一篇文章。',
119
- category: '',
120
- rating: '',
121
- metadataReady: true,
122
- }),
123
- getCookies: vi.fn().mockResolvedValue([
124
- { name: 'wr_vid', value: '70486028', domain: '.weread.qq.com' },
125
- ]),
126
- wait: vi.fn().mockResolvedValue(undefined),
127
- } as any;
99
+ const page = createPageStub(
100
+ {
101
+ cacheFound: true,
102
+ rawBooks: [
103
+ { bookId: 'MP_WXS_1', title: '公众号文章一', author: '作者甲' },
104
+ { bookId: 'BOOK_2', title: '普通书二', author: '作者乙' },
105
+ { bookId: 'MP_WXS_3', title: '公众号文章三', author: '作者丙' },
106
+ ],
107
+ shelfIndexes: [
108
+ { bookId: 'MP_WXS_1', idx: 0, role: 'mp' },
109
+ { bookId: 'BOOK_2', idx: 1, role: 'book' },
110
+ { bookId: 'MP_WXS_3', idx: 2, role: 'mp' },
111
+ ],
112
+ },
113
+ [
114
+ 'https://weread.qq.com/web/reader/mp1',
115
+ 'https://weread.qq.com/web/reader/book2',
116
+ 'https://weread.qq.com/web/reader/mp3',
117
+ ],
118
+ {
119
+ title: '公众号文章一',
120
+ author: '作者甲',
121
+ publisher: '微信读书',
122
+ intro: '第一篇文章。',
123
+ category: '',
124
+ rating: '',
125
+ metadataReady: true,
126
+ },
127
+ ) as any;
128
128
 
129
129
  const result = await book!.func!(page, { 'book-id': 'MP_WXS_1' });
130
130
 
@@ -146,85 +146,251 @@ describe('weread book-id positional args', () => {
146
146
  mockFetchPrivateApi.mockRejectedValue(
147
147
  new CliError('AUTH_REQUIRED', 'Not logged in to WeRead', 'Please log in to weread.qq.com in Chrome first'),
148
148
  );
149
+ vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('network disabled')));
149
150
 
150
- const page = {
151
- goto: vi.fn().mockResolvedValue(undefined),
152
- evaluate: vi.fn()
153
- .mockResolvedValueOnce({
154
- cacheFound: true,
155
- rawBooks: [
156
- { bookId: 'BOOK_1', title: '第一本', author: '作者甲' },
157
- { bookId: 'BOOK_2', title: '第二本', author: '作者乙' },
158
- ],
159
- shelfIndexes: [
160
- { bookId: 'BOOK_2', idx: 0, role: 'book' },
161
- ],
162
- })
163
- .mockResolvedValueOnce([
164
- 'https://weread.qq.com/web/reader/book2',
165
- 'https://weread.qq.com/web/reader/book1',
166
- ]),
167
- getCookies: vi.fn().mockResolvedValue([
168
- { name: 'wr_vid', value: '70486028', domain: '.weread.qq.com' },
169
- ]),
170
- wait: vi.fn().mockResolvedValue(undefined),
171
- } as any;
151
+ const page = createPageStub(
152
+ {
153
+ cacheFound: true,
154
+ rawBooks: [
155
+ { bookId: 'BOOK_1', title: '第一本', author: '作者甲' },
156
+ { bookId: 'BOOK_2', title: '第二本', author: '作者乙' },
157
+ ],
158
+ shelfIndexes: [
159
+ { bookId: 'BOOK_2', idx: 0, role: 'book' },
160
+ ],
161
+ },
162
+ [
163
+ 'https://weread.qq.com/web/reader/book2',
164
+ 'https://weread.qq.com/web/reader/book1',
165
+ ],
166
+ ) as any;
172
167
 
173
168
  await expect(book!.func!(page, { 'book-id': 'BOOK_1' })).rejects.toMatchObject({
174
169
  code: 'AUTH_REQUIRED',
175
170
  message: 'Not logged in to WeRead',
176
171
  });
177
172
  expect(page.goto).toHaveBeenCalledTimes(1);
178
- expect(page.goto).toHaveBeenCalledWith('https://weread.qq.com/web/shelf');
173
+ expect(page.goto).toHaveBeenNthCalledWith(1, 'https://weread.qq.com/web/shelf');
179
174
  });
180
175
 
181
- it('waits for shelf indexes to hydrate before resolving a trusted reader url', async () => {
176
+ it('falls back to the public search page when a cached ordinary book has no trusted shelf reader url', async () => {
182
177
  mockFetchPrivateApi.mockRejectedValue(
183
178
  new CliError('AUTH_REQUIRED', 'Not logged in to WeRead', 'Please log in to weread.qq.com in Chrome first'),
184
179
  );
185
180
 
186
- const page = {
187
- goto: vi.fn().mockResolvedValue(undefined),
188
- evaluate: vi.fn()
189
- .mockResolvedValueOnce({
190
- cacheFound: true,
191
- rawBooks: [
192
- { bookId: 'BOOK_1', title: '第一本', author: '作者甲' },
193
- { bookId: 'BOOK_2', title: '第二本', author: '作者乙' },
194
- ],
195
- shelfIndexes: [
196
- { bookId: 'BOOK_2', idx: 0, role: 'book' },
181
+ const fetchMock = vi.fn()
182
+ .mockResolvedValueOnce({
183
+ ok: true,
184
+ json: () => Promise.resolve({
185
+ books: [
186
+ {
187
+ bookInfo: {
188
+ title: '数据化运营:系统方法与实践案例',
189
+ author: '赵宏田 江丽萍 李宁',
190
+ bookId: '22920382',
191
+ },
192
+ },
197
193
  ],
198
- })
199
- .mockResolvedValueOnce({
200
- cacheFound: true,
201
- rawBooks: [
202
- { bookId: 'BOOK_1', title: '第一本', author: '作者甲' },
203
- { bookId: 'BOOK_2', title: '第二本', author: '作者乙' },
204
- ],
205
- shelfIndexes: [
206
- { bookId: 'BOOK_2', idx: 0, role: 'book' },
207
- { bookId: 'BOOK_1', idx: 1, role: 'book' },
194
+ }),
195
+ })
196
+ .mockResolvedValueOnce({
197
+ ok: true,
198
+ text: () => Promise.resolve(`
199
+ <ul class="search_bookDetail_list">
200
+ <li class="wr_bookList_item">
201
+ <a class="wr_bookList_item_link" href="/web/reader/book229"></a>
202
+ <p class="wr_bookList_item_title">数据化运营:系统方法与实践案例</p>
203
+ <p class="wr_bookList_item_author">赵宏田 江丽萍 李宁</p>
204
+ </li>
205
+ </ul>
206
+ `),
207
+ });
208
+ vi.stubGlobal('fetch', fetchMock);
209
+
210
+ const staleSnapshot = {
211
+ cacheFound: true,
212
+ rawBooks: [
213
+ { bookId: '22920382', title: '数据化运营:系统方法与实践案例', author: '赵宏田 江丽萍 李宁' },
214
+ ],
215
+ shelfIndexes: [
216
+ { bookId: 'stale-entry', idx: 0, role: 'book' },
217
+ ],
218
+ };
219
+ const page = createPageStub(
220
+ ...repeatValue(staleSnapshot, 2),
221
+ {
222
+ title: '数据化运营:系统方法与实践案例',
223
+ author: '赵宏田 江丽萍 李宁',
224
+ publisher: '电子工业出版社',
225
+ intro: '一本关于数据化运营的方法论书籍。',
226
+ category: '',
227
+ rating: '',
228
+ metadataReady: true,
229
+ },
230
+ ) as any;
231
+
232
+ const result = await book!.func!(page, { 'book-id': '22920382' });
233
+
234
+ expect(fetchMock).toHaveBeenCalledTimes(2);
235
+ expect(String(fetchMock.mock.calls[0][0])).toContain('/web/search/global?keyword=');
236
+ expect(String(fetchMock.mock.calls[1][0])).toContain('/web/search/books?keyword=');
237
+ expect(page.goto).toHaveBeenNthCalledWith(1, 'https://weread.qq.com/web/shelf');
238
+ expect(page.goto).toHaveBeenNthCalledWith(2, 'https://weread.qq.com/web/reader/book229');
239
+ expect(result).toEqual([
240
+ {
241
+ title: '数据化运营:系统方法与实践案例',
242
+ author: '赵宏田 江丽萍 李宁',
243
+ publisher: '电子工业出版社',
244
+ intro: '一本关于数据化运营的方法论书籍。',
245
+ category: '',
246
+ rating: '',
247
+ },
248
+ ]);
249
+ });
250
+
251
+ it('rethrows AUTH_REQUIRED when search fallback finds the same title with a different visible author', async () => {
252
+ mockFetchPrivateApi.mockRejectedValue(
253
+ new CliError('AUTH_REQUIRED', 'Not logged in to WeRead', 'Please log in to weread.qq.com in Chrome first'),
254
+ );
255
+
256
+ const fetchMock = vi.fn()
257
+ .mockResolvedValueOnce({
258
+ ok: true,
259
+ json: () => Promise.resolve({
260
+ books: [
261
+ {
262
+ bookInfo: {
263
+ title: '文明',
264
+ author: '作者乙',
265
+ bookId: 'wrong-book',
266
+ },
267
+ },
208
268
  ],
209
- })
210
- .mockResolvedValueOnce([
211
- 'https://weread.qq.com/web/reader/book2',
212
- 'https://weread.qq.com/web/reader/book1',
213
- ])
214
- .mockResolvedValueOnce({
215
- title: '第一本',
216
- author: '作者甲',
217
- publisher: '出版社甲',
218
- intro: '简介甲',
219
- category: '',
220
- rating: '',
221
- metadataReady: true,
222
269
  }),
223
- getCookies: vi.fn().mockResolvedValue([
224
- { name: 'wr_vid', value: '70486028', domain: '.weread.qq.com' },
225
- ]),
226
- wait: vi.fn().mockResolvedValue(undefined),
227
- } as any;
270
+ })
271
+ .mockResolvedValueOnce({
272
+ ok: true,
273
+ text: () => Promise.resolve(`
274
+ <ul class="search_bookDetail_list">
275
+ <li class="wr_bookList_item">
276
+ <a class="wr_bookList_item_link" href="/web/reader/wrong-reader"></a>
277
+ <p class="wr_bookList_item_title">文明</p>
278
+ <p class="wr_bookList_item_author">作者乙</p>
279
+ </li>
280
+ </ul>
281
+ `),
282
+ });
283
+ vi.stubGlobal('fetch', fetchMock);
284
+
285
+ const staleSnapshot = {
286
+ cacheFound: true,
287
+ rawBooks: [
288
+ { bookId: 'BOOK_1', title: '文明', author: '作者甲' },
289
+ ],
290
+ shelfIndexes: [
291
+ { bookId: 'stale-entry', idx: 0, role: 'book' },
292
+ ],
293
+ };
294
+ const page = createPageStub(
295
+ ...repeatValue(staleSnapshot, 2),
296
+ ) as any;
297
+
298
+ await expect(book!.func!(page, { 'book-id': 'BOOK_1' })).rejects.toMatchObject({
299
+ code: 'AUTH_REQUIRED',
300
+ message: 'Not logged in to WeRead',
301
+ });
302
+ expect(fetchMock).toHaveBeenCalledTimes(2);
303
+ expect(page.goto).toHaveBeenNthCalledWith(1, 'https://weread.qq.com/web/shelf');
304
+ });
305
+
306
+ it('falls back to raw cache order when shelf indexes never hydrate but rendered reader urls cover every cached entry', async () => {
307
+ mockFetchPrivateApi.mockRejectedValue(
308
+ new CliError('AUTH_REQUIRED', 'Not logged in to WeRead', 'Please log in to weread.qq.com in Chrome first'),
309
+ );
310
+ vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('network disabled')));
311
+
312
+ const emptyIndexSnapshot = {
313
+ cacheFound: true,
314
+ rawBooks: [
315
+ { bookId: '22920382', title: '数据化运营:系统方法与实践案例', author: '赵宏田 江丽萍 李宁' },
316
+ { bookId: 'MP_WXS_3634777637', title: '方伟看10年', author: '公众号' },
317
+ ],
318
+ shelfIndexes: [],
319
+ };
320
+ const page = createPageStub(
321
+ ...repeatValue(emptyIndexSnapshot, 2),
322
+ [
323
+ 'https://weread.qq.com/web/reader/book229',
324
+ 'https://weread.qq.com/web/reader/mp3634',
325
+ ],
326
+ {
327
+ title: '方伟看10年',
328
+ author: '公众号',
329
+ publisher: '',
330
+ intro: '公众号文章详情。',
331
+ category: '',
332
+ rating: '',
333
+ metadataReady: true,
334
+ },
335
+ ) as any;
336
+
337
+ const result = await book!.func!(page, { 'book-id': 'MP_WXS_3634777637' });
338
+
339
+ expect(page.goto).toHaveBeenNthCalledWith(1, 'https://weread.qq.com/web/shelf');
340
+ expect(page.goto).toHaveBeenNthCalledWith(2, 'https://weread.qq.com/web/reader/mp3634');
341
+ expect(result).toEqual([
342
+ {
343
+ title: '方伟看10年',
344
+ author: '公众号',
345
+ publisher: '',
346
+ intro: '公众号文章详情。',
347
+ category: '',
348
+ rating: '',
349
+ },
350
+ ]);
351
+ });
352
+
353
+ it('waits for shelf indexes to hydrate before resolving a trusted reader url', async () => {
354
+ mockFetchPrivateApi.mockRejectedValue(
355
+ new CliError('AUTH_REQUIRED', 'Not logged in to WeRead', 'Please log in to weread.qq.com in Chrome first'),
356
+ );
357
+
358
+ const page = createPageStub(
359
+ {
360
+ cacheFound: true,
361
+ rawBooks: [
362
+ { bookId: 'BOOK_1', title: '第一本', author: '作者甲' },
363
+ { bookId: 'BOOK_2', title: '第二本', author: '作者乙' },
364
+ ],
365
+ shelfIndexes: [
366
+ { bookId: 'BOOK_2', idx: 0, role: 'book' },
367
+ ],
368
+ },
369
+ {
370
+ cacheFound: true,
371
+ rawBooks: [
372
+ { bookId: 'BOOK_1', title: '第一本', author: '作者甲' },
373
+ { bookId: 'BOOK_2', title: '第二本', author: '作者乙' },
374
+ ],
375
+ shelfIndexes: [
376
+ { bookId: 'BOOK_2', idx: 0, role: 'book' },
377
+ { bookId: 'BOOK_1', idx: 1, role: 'book' },
378
+ ],
379
+ },
380
+ [
381
+ 'https://weread.qq.com/web/reader/book2',
382
+ 'https://weread.qq.com/web/reader/book1',
383
+ ],
384
+ {
385
+ title: '第一本',
386
+ author: '作者甲',
387
+ publisher: '出版社甲',
388
+ intro: '简介甲',
389
+ category: '',
390
+ rating: '',
391
+ metadataReady: true,
392
+ },
393
+ ) as any;
228
394
 
229
395
  const result = await book!.func!(page, { 'book-id': 'BOOK_1' });
230
396
 
@@ -247,35 +413,29 @@ describe('weread book-id positional args', () => {
247
413
  new CliError('AUTH_REQUIRED', 'Not logged in to WeRead', 'Please log in to weread.qq.com in Chrome first'),
248
414
  );
249
415
 
250
- const page = {
251
- goto: vi.fn().mockResolvedValue(undefined),
252
- evaluate: vi.fn()
253
- .mockResolvedValueOnce({
254
- cacheFound: true,
255
- rawBooks: [
256
- { bookId: 'BOOK_1', title: '第一本', author: '作者甲' },
257
- ],
258
- shelfIndexes: [
259
- { bookId: 'BOOK_1', idx: 0, role: 'book' },
260
- ],
261
- })
262
- .mockResolvedValueOnce([
263
- 'https://weread.qq.com/web/reader/book1',
264
- ])
265
- .mockResolvedValueOnce({
266
- title: '',
267
- author: '',
268
- publisher: '',
269
- intro: '这是正文第一段,不应该被当成简介。',
270
- category: '',
271
- rating: '',
272
- metadataReady: false,
273
- }),
274
- getCookies: vi.fn().mockResolvedValue([
275
- { name: 'wr_vid', value: '70486028', domain: '.weread.qq.com' },
276
- ]),
277
- wait: vi.fn().mockResolvedValue(undefined),
278
- } as any;
416
+ const page = createPageStub(
417
+ {
418
+ cacheFound: true,
419
+ rawBooks: [
420
+ { bookId: 'BOOK_1', title: '第一本', author: '作者甲' },
421
+ ],
422
+ shelfIndexes: [
423
+ { bookId: 'BOOK_1', idx: 0, role: 'book' },
424
+ ],
425
+ },
426
+ [
427
+ 'https://weread.qq.com/web/reader/book1',
428
+ ],
429
+ {
430
+ title: '',
431
+ author: '',
432
+ publisher: '',
433
+ intro: '这是正文第一段,不应该被当成简介。',
434
+ category: '',
435
+ rating: '',
436
+ metadataReady: false,
437
+ },
438
+ ) as any;
279
439
 
280
440
  await expect(book!.func!(page, { 'book-id': 'BOOK_1' })).rejects.toMatchObject({
281
441
  code: 'AUTH_REQUIRED',
@@ -42,6 +42,11 @@ export interface WebShelfEntry {
42
42
  readerUrl: string;
43
43
  }
44
44
 
45
+ export interface WebShelfReaderResolution {
46
+ snapshot: WebShelfSnapshot;
47
+ readerUrl: string | null;
48
+ }
49
+
45
50
  interface WebShelfStorageKeys {
46
51
  rawBooksKey: string;
47
52
  shelfIndexesKey: string;
@@ -331,11 +336,29 @@ async function waitForTrustedWebShelfSnapshot(page: IPage, snapshot: WebShelfSna
331
336
  * shelf cache order with the visible shelf links rendered on the page.
332
337
  */
333
338
  export async function resolveShelfReaderUrl(page: IPage, bookId: string): Promise<string | null> {
339
+ const resolution = await resolveShelfReader(page, bookId);
340
+ return resolution.readerUrl;
341
+ }
342
+
343
+ /**
344
+ * Resolve the current reader URL for a shelf entry and return the parsed shelf
345
+ * snapshot used during resolution, so callers can reuse cached title/author
346
+ * metadata without loading the shelf page twice.
347
+ */
348
+ export async function resolveShelfReader(page: IPage, bookId: string): Promise<WebShelfReaderResolution> {
334
349
  const { snapshot: initialSnapshot, currentVid } = await loadWebShelfSnapshotWithVid(page);
335
350
  const snapshot = await waitForTrustedWebShelfSnapshot(page, initialSnapshot, currentVid);
336
- if (!snapshot.cacheFound) return null;
351
+ if (!snapshot.cacheFound) {
352
+ return { snapshot, readerUrl: null };
353
+ }
354
+ const rawBookIds = getUniqueRawBookIds(snapshot);
337
355
  const trustedIndexedBookIds = getTrustedIndexedBookIds(snapshot);
338
- if (trustedIndexedBookIds.length === 0) return null;
356
+ const canUseRawOrderFallback = trustedIndexedBookIds.length === 0
357
+ && rawBookIds.length > 0
358
+ && snapshot.shelfIndexes.length === 0;
359
+ if (trustedIndexedBookIds.length === 0 && !canUseRawOrderFallback) {
360
+ return { snapshot, readerUrl: null };
361
+ }
339
362
 
340
363
  const readerUrls = await page.evaluate(`
341
364
  (() => Array.from(document.querySelectorAll('a.shelfBook[href]'))
@@ -345,11 +368,17 @@ export async function resolveShelfReaderUrl(page: IPage, bookId: string): Promis
345
368
  })
346
369
  .filter(Boolean))
347
370
  `) as string[];
348
- if (readerUrls.length !== trustedIndexedBookIds.length) return null;
371
+ const expectedEntryCount = trustedIndexedBookIds.length > 0 ? trustedIndexedBookIds.length : rawBookIds.length;
372
+ if (readerUrls.length !== expectedEntryCount) {
373
+ return { snapshot, readerUrl: null };
374
+ }
349
375
  const entries = buildWebShelfEntries(snapshot, readerUrls);
350
376
 
351
377
  const entry = entries.find((candidate) => candidate.bookId === bookId);
352
- return entry?.readerUrl || null;
378
+ return {
379
+ snapshot,
380
+ readerUrl: entry?.readerUrl || null,
381
+ };
353
382
  }
354
383
 
355
384
  /** Format a Unix timestamp (seconds) to YYYY-MM-DD in UTC+8. Returns '-' for invalid input. */