@jackwener/opencli 1.5.5 → 1.5.6

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 (231) hide show
  1. package/README.md +27 -2
  2. package/README.zh-CN.md +36 -4
  3. package/dist/browser/daemon-client.d.ts +5 -1
  4. package/dist/browser/page.d.ts +6 -0
  5. package/dist/browser/page.js +15 -0
  6. package/dist/cli-manifest.json +1229 -67
  7. package/dist/clis/band/bands.d.ts +1 -0
  8. package/dist/clis/band/bands.js +72 -0
  9. package/dist/clis/band/mentions.d.ts +1 -0
  10. package/dist/clis/band/mentions.js +127 -0
  11. package/dist/clis/band/post.d.ts +1 -0
  12. package/dist/clis/band/post.js +175 -0
  13. package/dist/clis/band/posts.d.ts +1 -0
  14. package/dist/clis/band/posts.js +94 -0
  15. package/dist/clis/doubao/detail.d.ts +1 -0
  16. package/dist/clis/doubao/detail.js +33 -0
  17. package/dist/clis/doubao/detail.test.d.ts +1 -0
  18. package/dist/clis/doubao/detail.test.js +42 -0
  19. package/dist/clis/doubao/history.d.ts +1 -0
  20. package/dist/clis/doubao/history.js +28 -0
  21. package/dist/clis/doubao/history.test.d.ts +1 -0
  22. package/dist/clis/doubao/history.test.js +37 -0
  23. package/dist/clis/doubao/meeting-summary.d.ts +1 -0
  24. package/dist/clis/doubao/meeting-summary.js +39 -0
  25. package/dist/clis/doubao/meeting-transcript.d.ts +1 -0
  26. package/dist/clis/doubao/meeting-transcript.js +36 -0
  27. package/dist/clis/doubao/utils.d.ts +27 -0
  28. package/dist/clis/doubao/utils.js +317 -0
  29. package/dist/clis/doubao/utils.test.d.ts +1 -0
  30. package/dist/clis/doubao/utils.test.js +24 -0
  31. package/dist/clis/douyin/_shared/public-api.d.ts +33 -0
  32. package/dist/clis/douyin/_shared/public-api.js +29 -0
  33. package/dist/clis/douyin/user-videos.d.ts +5 -0
  34. package/dist/clis/douyin/user-videos.js +74 -0
  35. package/dist/clis/douyin/user-videos.test.d.ts +1 -0
  36. package/dist/clis/douyin/user-videos.test.js +108 -0
  37. package/dist/clis/ones/common.d.ts +32 -0
  38. package/dist/clis/ones/common.js +144 -0
  39. package/dist/clis/ones/enrich-tasks.d.ts +5 -0
  40. package/dist/clis/ones/enrich-tasks.js +37 -0
  41. package/dist/clis/ones/login.d.ts +1 -0
  42. package/dist/clis/ones/login.js +80 -0
  43. package/dist/clis/ones/logout.d.ts +1 -0
  44. package/dist/clis/ones/logout.js +17 -0
  45. package/dist/clis/ones/me.d.ts +1 -0
  46. package/dist/clis/ones/me.js +30 -0
  47. package/dist/clis/ones/my-tasks.d.ts +1 -0
  48. package/dist/clis/ones/my-tasks.js +120 -0
  49. package/dist/clis/ones/resolve-labels.d.ts +10 -0
  50. package/dist/clis/ones/resolve-labels.js +64 -0
  51. package/dist/clis/ones/task-helpers.d.ts +29 -0
  52. package/dist/clis/ones/task-helpers.js +212 -0
  53. package/dist/clis/ones/task-helpers.test.d.ts +1 -0
  54. package/dist/clis/ones/task-helpers.test.js +12 -0
  55. package/dist/clis/ones/task.d.ts +1 -0
  56. package/dist/clis/ones/task.js +66 -0
  57. package/dist/clis/ones/tasks.d.ts +1 -0
  58. package/dist/clis/ones/tasks.js +79 -0
  59. package/dist/clis/ones/token-info.d.ts +1 -0
  60. package/dist/clis/ones/token-info.js +42 -0
  61. package/dist/clis/ones/worklog.d.ts +11 -0
  62. package/dist/clis/ones/worklog.js +267 -0
  63. package/dist/clis/ones/worklog.test.d.ts +1 -0
  64. package/dist/clis/ones/worklog.test.js +20 -0
  65. package/dist/clis/spotify/spotify.d.ts +1 -0
  66. package/dist/clis/spotify/spotify.js +316 -0
  67. package/dist/clis/spotify/utils.d.ts +21 -0
  68. package/dist/clis/spotify/utils.js +66 -0
  69. package/dist/clis/spotify/utils.test.d.ts +1 -0
  70. package/dist/clis/spotify/utils.test.js +67 -0
  71. package/dist/clis/tieba/commands.test.d.ts +4 -0
  72. package/dist/clis/tieba/commands.test.js +79 -0
  73. package/dist/clis/tieba/hot.d.ts +1 -0
  74. package/dist/clis/tieba/hot.js +48 -0
  75. package/dist/clis/tieba/posts.d.ts +1 -0
  76. package/dist/clis/tieba/posts.js +85 -0
  77. package/dist/clis/tieba/read.d.ts +1 -0
  78. package/dist/clis/tieba/read.js +140 -0
  79. package/dist/clis/tieba/search.d.ts +1 -0
  80. package/dist/clis/tieba/search.js +108 -0
  81. package/dist/clis/tieba/utils.d.ts +101 -0
  82. package/dist/clis/tieba/utils.js +240 -0
  83. package/dist/clis/tieba/utils.test.d.ts +1 -0
  84. package/dist/clis/tieba/utils.test.js +290 -0
  85. package/dist/clis/weread/book.js +100 -13
  86. package/dist/clis/weread/commands.test.js +221 -0
  87. package/dist/clis/weread/private-api-regression.test.d.ts +1 -0
  88. package/dist/{weread-private-api-regression.test.js → clis/weread/private-api-regression.test.js} +92 -30
  89. package/dist/clis/weread/search-regression.test.d.ts +1 -0
  90. package/dist/clis/weread/search-regression.test.js +407 -0
  91. package/dist/clis/weread/search.js +143 -7
  92. package/dist/clis/weread/shelf.js +13 -95
  93. package/dist/clis/weread/utils.d.ts +46 -0
  94. package/dist/clis/weread/utils.js +214 -7
  95. package/dist/clis/weread/utils.test.js +71 -1
  96. package/dist/clis/xiaohongshu/publish.d.ts +1 -1
  97. package/dist/clis/xiaohongshu/publish.js +78 -31
  98. package/dist/clis/xiaohongshu/publish.test.js +66 -1
  99. package/dist/clis/xiaohongshu/user-helpers.d.ts +1 -0
  100. package/dist/clis/xiaohongshu/user-helpers.js +2 -0
  101. package/dist/clis/xiaohongshu/user-helpers.test.js +18 -0
  102. package/dist/clis/xueqiu/comments.d.ts +118 -0
  103. package/dist/clis/xueqiu/comments.js +354 -0
  104. package/dist/clis/xueqiu/comments.test.d.ts +1 -0
  105. package/dist/clis/xueqiu/comments.test.js +696 -0
  106. package/dist/clis/youtube/transcript.js +2 -4
  107. package/dist/clis/youtube/utils.d.ts +9 -0
  108. package/dist/clis/youtube/utils.js +67 -3
  109. package/dist/clis/youtube/utils.test.d.ts +1 -0
  110. package/dist/clis/youtube/utils.test.js +37 -0
  111. package/dist/clis/youtube/video.js +16 -15
  112. package/dist/clis/zsxq/dynamics.d.ts +1 -0
  113. package/dist/clis/zsxq/dynamics.js +47 -0
  114. package/dist/clis/zsxq/groups.d.ts +1 -0
  115. package/dist/clis/zsxq/groups.js +32 -0
  116. package/dist/clis/zsxq/search.d.ts +1 -0
  117. package/dist/clis/zsxq/search.js +43 -0
  118. package/dist/clis/zsxq/search.test.d.ts +1 -0
  119. package/dist/clis/zsxq/search.test.js +24 -0
  120. package/dist/clis/zsxq/topic.d.ts +1 -0
  121. package/dist/clis/zsxq/topic.js +47 -0
  122. package/dist/clis/zsxq/topic.test.d.ts +1 -0
  123. package/dist/clis/zsxq/topic.test.js +29 -0
  124. package/dist/clis/zsxq/topics.d.ts +1 -0
  125. package/dist/clis/zsxq/topics.js +25 -0
  126. package/dist/clis/zsxq/topics.test.d.ts +1 -0
  127. package/dist/clis/zsxq/topics.test.js +24 -0
  128. package/dist/clis/zsxq/utils.d.ts +97 -0
  129. package/dist/clis/zsxq/utils.js +230 -0
  130. package/dist/commanderAdapter.js +1 -1
  131. package/dist/commanderAdapter.test.js +39 -0
  132. package/dist/external-clis.yaml +17 -0
  133. package/dist/types.d.ts +5 -0
  134. package/docs/.vitepress/config.mts +3 -0
  135. package/docs/adapters/browser/band.md +63 -0
  136. package/docs/adapters/browser/ones.md +59 -0
  137. package/docs/adapters/browser/spotify.md +62 -0
  138. package/docs/adapters/browser/tieba.md +45 -0
  139. package/docs/adapters/browser/xueqiu.md +5 -0
  140. package/docs/adapters/browser/zsxq.md +49 -0
  141. package/docs/adapters/index.md +5 -2
  142. package/docs/adapters-doc/ones.md +32 -0
  143. package/extension/src/background.ts +15 -0
  144. package/extension/src/cdp.ts +42 -0
  145. package/extension/src/protocol.ts +5 -1
  146. package/package.json +1 -1
  147. package/scripts/postinstall.js +16 -0
  148. package/src/browser/daemon-client.ts +5 -1
  149. package/src/browser/page.ts +16 -0
  150. package/src/clis/band/bands.ts +76 -0
  151. package/src/clis/band/mentions.ts +134 -0
  152. package/src/clis/band/post.ts +187 -0
  153. package/src/clis/band/posts.ts +106 -0
  154. package/src/clis/doubao/detail.test.ts +53 -0
  155. package/src/clis/doubao/detail.ts +41 -0
  156. package/src/clis/doubao/history.test.ts +45 -0
  157. package/src/clis/doubao/history.ts +32 -0
  158. package/src/clis/doubao/meeting-summary.ts +53 -0
  159. package/src/clis/doubao/meeting-transcript.ts +48 -0
  160. package/src/clis/doubao/utils.test.ts +45 -0
  161. package/src/clis/doubao/utils.ts +371 -0
  162. package/src/clis/douyin/_shared/public-api.ts +84 -0
  163. package/src/clis/douyin/user-videos.test.ts +122 -0
  164. package/src/clis/douyin/user-videos.ts +101 -0
  165. package/src/clis/ones/common.ts +187 -0
  166. package/src/clis/ones/enrich-tasks.ts +47 -0
  167. package/src/clis/ones/login.ts +103 -0
  168. package/src/clis/ones/logout.ts +19 -0
  169. package/src/clis/ones/me.ts +34 -0
  170. package/src/clis/ones/my-tasks.ts +148 -0
  171. package/src/clis/ones/resolve-labels.ts +80 -0
  172. package/src/clis/ones/task-helpers.test.ts +14 -0
  173. package/src/clis/ones/task-helpers.ts +214 -0
  174. package/src/clis/ones/task.ts +79 -0
  175. package/src/clis/ones/tasks.ts +92 -0
  176. package/src/clis/ones/token-info.ts +46 -0
  177. package/src/clis/ones/worklog.test.ts +24 -0
  178. package/src/clis/ones/worklog.ts +306 -0
  179. package/src/clis/spotify/spotify.ts +328 -0
  180. package/src/clis/spotify/utils.test.ts +87 -0
  181. package/src/clis/spotify/utils.ts +92 -0
  182. package/src/clis/tieba/commands.test.ts +86 -0
  183. package/src/clis/tieba/hot.ts +52 -0
  184. package/src/clis/tieba/posts.ts +108 -0
  185. package/src/clis/tieba/read.ts +158 -0
  186. package/src/clis/tieba/search.ts +119 -0
  187. package/src/clis/tieba/utils.test.ts +322 -0
  188. package/src/clis/tieba/utils.ts +348 -0
  189. package/src/clis/weread/book.ts +116 -13
  190. package/src/clis/weread/commands.test.ts +249 -0
  191. package/src/{weread-private-api-regression.test.ts → clis/weread/private-api-regression.test.ts} +108 -30
  192. package/src/clis/weread/search-regression.test.ts +440 -0
  193. package/src/clis/weread/search.ts +189 -9
  194. package/src/clis/weread/shelf.ts +20 -122
  195. package/src/clis/weread/utils.test.ts +81 -1
  196. package/src/clis/weread/utils.ts +264 -7
  197. package/src/clis/xiaohongshu/publish.test.ts +79 -1
  198. package/src/clis/xiaohongshu/publish.ts +84 -30
  199. package/src/clis/xiaohongshu/user-helpers.test.ts +23 -0
  200. package/src/clis/xiaohongshu/user-helpers.ts +4 -0
  201. package/src/clis/xueqiu/comments.test.ts +823 -0
  202. package/src/clis/xueqiu/comments.ts +461 -0
  203. package/src/clis/youtube/transcript.ts +2 -4
  204. package/src/clis/youtube/utils.test.ts +43 -0
  205. package/src/clis/youtube/utils.ts +69 -0
  206. package/src/clis/youtube/video.ts +16 -15
  207. package/src/clis/zsxq/dynamics.ts +60 -0
  208. package/src/clis/zsxq/groups.ts +41 -0
  209. package/src/clis/zsxq/search.test.ts +29 -0
  210. package/src/clis/zsxq/search.ts +54 -0
  211. package/src/clis/zsxq/topic.test.ts +34 -0
  212. package/src/clis/zsxq/topic.ts +68 -0
  213. package/src/clis/zsxq/topics.test.ts +29 -0
  214. package/src/clis/zsxq/topics.ts +36 -0
  215. package/src/clis/zsxq/utils.ts +351 -0
  216. package/src/commanderAdapter.test.ts +47 -0
  217. package/src/commanderAdapter.ts +1 -1
  218. package/src/external-clis.yaml +17 -0
  219. package/src/types.ts +5 -0
  220. package/tests/e2e/band-auth.test.ts +20 -0
  221. package/tests/e2e/browser-auth-helpers.ts +18 -0
  222. package/tests/e2e/browser-auth.test.ts +35 -47
  223. package/tests/e2e/browser-public.test.ts +288 -0
  224. package/tests/e2e/management.test.ts +1 -1
  225. package/tests/e2e/plugin-management.test.ts +1 -1
  226. package/vitest.config.ts +1 -0
  227. package/SKILL.md +0 -879
  228. package/dist/weread-private-api-regression.test.d.ts +0 -1
  229. package/dist/weread-search-regression.test.d.ts +0 -1
  230. package/dist/weread-search-regression.test.js +0 -39
  231. package/src/weread-search-regression.test.ts +0 -44
@@ -1,5 +1,68 @@
1
1
  import { cli, Strategy } from '../../registry.js';
2
- import { fetchPrivateApi } from './utils.js';
2
+ import { CliError } from '../../errors.js';
3
+ import { fetchPrivateApi, resolveShelfReaderUrl } from './utils.js';
4
+ /**
5
+ * Read visible book metadata from the web reader cover/flyleaf page.
6
+ * This path is used as a fallback when the private API session has expired.
7
+ */
8
+ async function loadReaderFallbackResult(page, readerUrl) {
9
+ await page.goto(readerUrl);
10
+ await page.wait({ selector: '.horizontalReaderCoverPage_content_bookTitle, .wr_flyleaf_page_bookInfo_bookTitle', timeout: 10 });
11
+ const result = await page.evaluate(`
12
+ (() => {
13
+ const text = (node) => node?.textContent?.trim() || '';
14
+ const bodyText = document.body?.innerText?.replace(/\\s+/g, ' ').trim() || '';
15
+ const titleSelector = '.horizontalReaderCoverPage_content_bookTitle, .wr_flyleaf_page_bookInfo_bookTitle';
16
+ const authorSelector = '.horizontalReaderCoverPage_content_author, .wr_flyleaf_page_bookInfo_author';
17
+ const extractRating = () => {
18
+ const match = bodyText.match(/微信读书推荐值\\s*([0-9.]+%)/);
19
+ return match ? match[1] : '';
20
+ };
21
+ const extractPublisher = () => {
22
+ const direct = text(document.querySelector('.introDialog_content_pub_line'));
23
+ return direct.startsWith('出版社') ? direct.replace(/^出版社\\s*/, '').trim() : '';
24
+ };
25
+ const extractIntro = () => {
26
+ const selectors = [
27
+ '.horizontalReaderCoverPage_content_bookInfo_intro',
28
+ '.wr_flyleaf_page_bookIntro_content',
29
+ '.introDialog_content_intro_para',
30
+ ];
31
+ for (const selector of selectors) {
32
+ const value = text(document.querySelector(selector));
33
+ if (value) return value;
34
+ }
35
+ return '';
36
+ };
37
+
38
+ const categorySource = Array.from(document.scripts)
39
+ .map((script) => script.textContent || '')
40
+ .find((scriptText) => scriptText.includes('"category"')) || '';
41
+ const categoryMatch = categorySource.match(/"category"\\s*:\\s*"([^"]+)"/);
42
+ const title = text(document.querySelector(titleSelector));
43
+ const author = text(document.querySelector(authorSelector));
44
+
45
+ return {
46
+ title,
47
+ author,
48
+ publisher: extractPublisher(),
49
+ intro: extractIntro(),
50
+ category: categoryMatch ? categoryMatch[1].trim() : '',
51
+ rating: extractRating(),
52
+ metadataReady: Boolean(title || author),
53
+ };
54
+ })()
55
+ `);
56
+ return {
57
+ title: String(result?.title || '').trim(),
58
+ author: String(result?.author || '').trim(),
59
+ publisher: String(result?.publisher || '').trim(),
60
+ intro: String(result?.intro || '').trim(),
61
+ category: String(result?.category || '').trim(),
62
+ rating: String(result?.rating || '').trim(),
63
+ metadataReady: result?.metadataReady === true,
64
+ };
65
+ }
3
66
  cli({
4
67
  site: 'weread',
5
68
  name: 'book',
@@ -7,20 +70,44 @@ cli({
7
70
  domain: 'weread.qq.com',
8
71
  strategy: Strategy.COOKIE,
9
72
  args: [
10
- { name: 'book-id', positional: true, required: true, help: 'Book ID (numeric, from search or shelf results)' },
73
+ { name: 'book-id', positional: true, required: true, help: 'Book ID from search or shelf results' },
11
74
  ],
12
75
  columns: ['title', 'author', 'publisher', 'intro', 'category', 'rating'],
13
76
  func: async (page, args) => {
14
- const data = await fetchPrivateApi(page, '/book/info', { bookId: args['book-id'] });
15
- // newRating is 0-1000 scale per community docs; needs runtime verification
16
- const rating = data.newRating ? `${(data.newRating / 10).toFixed(1)}%` : '-';
17
- return [{
18
- title: data.title ?? '',
19
- author: data.author ?? '',
20
- publisher: data.publisher ?? '',
21
- intro: data.intro ?? '',
22
- category: data.category ?? '',
23
- rating,
24
- }];
77
+ const bookId = String(args['book-id'] || '').trim();
78
+ try {
79
+ const data = await fetchPrivateApi(page, '/book/info', { bookId });
80
+ // newRating is 0-1000 scale per community docs; needs runtime verification
81
+ const rating = data.newRating ? `${(data.newRating / 10).toFixed(1)}%` : '-';
82
+ return [{
83
+ title: data.title ?? '',
84
+ author: data.author ?? '',
85
+ publisher: data.publisher ?? '',
86
+ intro: data.intro ?? '',
87
+ category: data.category ?? '',
88
+ rating,
89
+ }];
90
+ }
91
+ catch (error) {
92
+ if (!(error instanceof CliError) || error.code !== 'AUTH_REQUIRED') {
93
+ throw error;
94
+ }
95
+ const readerUrl = await resolveShelfReaderUrl(page, bookId);
96
+ if (!readerUrl) {
97
+ throw error;
98
+ }
99
+ const data = await loadReaderFallbackResult(page, readerUrl);
100
+ if (!data.metadataReady || !data.title) {
101
+ throw error;
102
+ }
103
+ return [{
104
+ title: data.title,
105
+ author: data.author,
106
+ publisher: data.publisher,
107
+ intro: data.intro,
108
+ category: data.category,
109
+ rating: data.rating,
110
+ }];
111
+ }
25
112
  },
26
113
  });
@@ -1,4 +1,5 @@
1
1
  import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { CliError } from '../../errors.js';
2
3
  const { mockFetchPrivateApi } = vi.hoisted(() => ({
3
4
  mockFetchPrivateApi: vi.fn(),
4
5
  }));
@@ -25,6 +26,226 @@ describe('weread book-id positional args', () => {
25
26
  await book.func({}, { 'book-id': '12345' });
26
27
  expect(mockFetchPrivateApi).toHaveBeenCalledWith({}, '/book/info', { bookId: '12345' });
27
28
  });
29
+ it('falls back to the shelf reader page when private API auth has expired', async () => {
30
+ mockFetchPrivateApi.mockRejectedValue(new CliError('AUTH_REQUIRED', 'Not logged in to WeRead', 'Please log in to weread.qq.com in Chrome first'));
31
+ const page = {
32
+ goto: vi.fn().mockResolvedValue(undefined),
33
+ evaluate: vi.fn()
34
+ .mockResolvedValueOnce({
35
+ cacheFound: true,
36
+ rawBooks: [
37
+ { bookId: 'MP_WXS_3634777637', title: '文明、现代化、价值投资与中国', author: '李录' },
38
+ ],
39
+ shelfIndexes: [
40
+ { bookId: 'MP_WXS_3634777637', idx: 0, role: 'book' },
41
+ ],
42
+ })
43
+ .mockResolvedValueOnce(['https://weread.qq.com/web/reader/6f5323f071bd7f7b6f521e8'])
44
+ .mockResolvedValueOnce({
45
+ title: '文明、现代化、价值投资与中国',
46
+ author: '李录',
47
+ publisher: '中信出版集团',
48
+ intro: '对中国未来几十年的预测。',
49
+ category: '',
50
+ rating: '84.1%',
51
+ metadataReady: true,
52
+ }),
53
+ getCookies: vi.fn().mockResolvedValue([
54
+ { name: 'wr_vid', value: '70486028', domain: '.weread.qq.com' },
55
+ ]),
56
+ wait: vi.fn().mockResolvedValue(undefined),
57
+ };
58
+ const result = await book.func(page, { 'book-id': 'MP_WXS_3634777637' });
59
+ expect(page.goto).toHaveBeenNthCalledWith(1, 'https://weread.qq.com/web/shelf');
60
+ expect(page.goto).toHaveBeenNthCalledWith(2, 'https://weread.qq.com/web/reader/6f5323f071bd7f7b6f521e8');
61
+ expect(page.evaluate).toHaveBeenCalledTimes(3);
62
+ expect(result).toEqual([
63
+ {
64
+ title: '文明、现代化、价值投资与中国',
65
+ author: '李录',
66
+ publisher: '中信出版集团',
67
+ intro: '对中国未来几十年的预测。',
68
+ category: '',
69
+ rating: '84.1%',
70
+ },
71
+ ]);
72
+ });
73
+ it('keeps mixed shelf entries aligned when resolving MP_WXS reader urls', async () => {
74
+ mockFetchPrivateApi.mockRejectedValue(new CliError('AUTH_REQUIRED', 'Not logged in to WeRead', 'Please log in to weread.qq.com in Chrome first'));
75
+ const page = {
76
+ goto: vi.fn().mockResolvedValue(undefined),
77
+ evaluate: vi.fn()
78
+ .mockResolvedValueOnce({
79
+ cacheFound: true,
80
+ rawBooks: [
81
+ { bookId: 'MP_WXS_1', title: '公众号文章一', author: '作者甲' },
82
+ { bookId: 'BOOK_2', title: '普通书二', author: '作者乙' },
83
+ { bookId: 'MP_WXS_3', title: '公众号文章三', author: '作者丙' },
84
+ ],
85
+ shelfIndexes: [
86
+ { bookId: 'MP_WXS_1', idx: 0, role: 'mp' },
87
+ { bookId: 'BOOK_2', idx: 1, role: 'book' },
88
+ { bookId: 'MP_WXS_3', idx: 2, role: 'mp' },
89
+ ],
90
+ })
91
+ .mockResolvedValueOnce([
92
+ 'https://weread.qq.com/web/reader/mp1',
93
+ 'https://weread.qq.com/web/reader/book2',
94
+ 'https://weread.qq.com/web/reader/mp3',
95
+ ])
96
+ .mockResolvedValueOnce({
97
+ title: '公众号文章一',
98
+ author: '作者甲',
99
+ publisher: '微信读书',
100
+ intro: '第一篇文章。',
101
+ category: '',
102
+ rating: '',
103
+ metadataReady: true,
104
+ }),
105
+ getCookies: vi.fn().mockResolvedValue([
106
+ { name: 'wr_vid', value: '70486028', domain: '.weread.qq.com' },
107
+ ]),
108
+ wait: vi.fn().mockResolvedValue(undefined),
109
+ };
110
+ const result = await book.func(page, { 'book-id': 'MP_WXS_1' });
111
+ expect(page.goto).toHaveBeenNthCalledWith(1, 'https://weread.qq.com/web/shelf');
112
+ expect(page.goto).toHaveBeenNthCalledWith(2, 'https://weread.qq.com/web/reader/mp1');
113
+ expect(result).toEqual([
114
+ {
115
+ title: '公众号文章一',
116
+ author: '作者甲',
117
+ publisher: '微信读书',
118
+ intro: '第一篇文章。',
119
+ category: '',
120
+ rating: '',
121
+ },
122
+ ]);
123
+ });
124
+ it('rethrows AUTH_REQUIRED when shelf ordering is incomplete and reader urls cannot be trusted', async () => {
125
+ mockFetchPrivateApi.mockRejectedValue(new CliError('AUTH_REQUIRED', 'Not logged in to WeRead', 'Please log in to weread.qq.com in Chrome first'));
126
+ const page = {
127
+ goto: vi.fn().mockResolvedValue(undefined),
128
+ evaluate: vi.fn()
129
+ .mockResolvedValueOnce({
130
+ cacheFound: true,
131
+ rawBooks: [
132
+ { bookId: 'BOOK_1', title: '第一本', author: '作者甲' },
133
+ { bookId: 'BOOK_2', title: '第二本', author: '作者乙' },
134
+ ],
135
+ shelfIndexes: [
136
+ { bookId: 'BOOK_2', idx: 0, role: 'book' },
137
+ ],
138
+ })
139
+ .mockResolvedValueOnce([
140
+ 'https://weread.qq.com/web/reader/book2',
141
+ 'https://weread.qq.com/web/reader/book1',
142
+ ]),
143
+ getCookies: vi.fn().mockResolvedValue([
144
+ { name: 'wr_vid', value: '70486028', domain: '.weread.qq.com' },
145
+ ]),
146
+ wait: vi.fn().mockResolvedValue(undefined),
147
+ };
148
+ await expect(book.func(page, { 'book-id': 'BOOK_1' })).rejects.toMatchObject({
149
+ code: 'AUTH_REQUIRED',
150
+ message: 'Not logged in to WeRead',
151
+ });
152
+ expect(page.goto).toHaveBeenCalledTimes(1);
153
+ expect(page.goto).toHaveBeenCalledWith('https://weread.qq.com/web/shelf');
154
+ });
155
+ it('waits for shelf indexes to hydrate before resolving a trusted reader url', async () => {
156
+ mockFetchPrivateApi.mockRejectedValue(new CliError('AUTH_REQUIRED', 'Not logged in to WeRead', 'Please log in to weread.qq.com in Chrome first'));
157
+ const page = {
158
+ goto: vi.fn().mockResolvedValue(undefined),
159
+ evaluate: vi.fn()
160
+ .mockResolvedValueOnce({
161
+ cacheFound: true,
162
+ rawBooks: [
163
+ { bookId: 'BOOK_1', title: '第一本', author: '作者甲' },
164
+ { bookId: 'BOOK_2', title: '第二本', author: '作者乙' },
165
+ ],
166
+ shelfIndexes: [
167
+ { bookId: 'BOOK_2', idx: 0, role: 'book' },
168
+ ],
169
+ })
170
+ .mockResolvedValueOnce({
171
+ cacheFound: true,
172
+ rawBooks: [
173
+ { bookId: 'BOOK_1', title: '第一本', author: '作者甲' },
174
+ { bookId: 'BOOK_2', title: '第二本', author: '作者乙' },
175
+ ],
176
+ shelfIndexes: [
177
+ { bookId: 'BOOK_2', idx: 0, role: 'book' },
178
+ { bookId: 'BOOK_1', idx: 1, role: 'book' },
179
+ ],
180
+ })
181
+ .mockResolvedValueOnce([
182
+ 'https://weread.qq.com/web/reader/book2',
183
+ 'https://weread.qq.com/web/reader/book1',
184
+ ])
185
+ .mockResolvedValueOnce({
186
+ title: '第一本',
187
+ author: '作者甲',
188
+ publisher: '出版社甲',
189
+ intro: '简介甲',
190
+ category: '',
191
+ rating: '',
192
+ metadataReady: true,
193
+ }),
194
+ getCookies: vi.fn().mockResolvedValue([
195
+ { name: 'wr_vid', value: '70486028', domain: '.weread.qq.com' },
196
+ ]),
197
+ wait: vi.fn().mockResolvedValue(undefined),
198
+ };
199
+ const result = await book.func(page, { 'book-id': 'BOOK_1' });
200
+ expect(page.goto).toHaveBeenNthCalledWith(1, 'https://weread.qq.com/web/shelf');
201
+ expect(page.goto).toHaveBeenNthCalledWith(2, 'https://weread.qq.com/web/reader/book1');
202
+ expect(result).toEqual([
203
+ {
204
+ title: '第一本',
205
+ author: '作者甲',
206
+ publisher: '出版社甲',
207
+ intro: '简介甲',
208
+ category: '',
209
+ rating: '',
210
+ },
211
+ ]);
212
+ });
213
+ it('rethrows AUTH_REQUIRED when the reader page lacks stable cover metadata', async () => {
214
+ mockFetchPrivateApi.mockRejectedValue(new CliError('AUTH_REQUIRED', 'Not logged in to WeRead', 'Please log in to weread.qq.com in Chrome first'));
215
+ const page = {
216
+ goto: vi.fn().mockResolvedValue(undefined),
217
+ evaluate: vi.fn()
218
+ .mockResolvedValueOnce({
219
+ cacheFound: true,
220
+ rawBooks: [
221
+ { bookId: 'BOOK_1', title: '第一本', author: '作者甲' },
222
+ ],
223
+ shelfIndexes: [
224
+ { bookId: 'BOOK_1', idx: 0, role: 'book' },
225
+ ],
226
+ })
227
+ .mockResolvedValueOnce([
228
+ 'https://weread.qq.com/web/reader/book1',
229
+ ])
230
+ .mockResolvedValueOnce({
231
+ title: '',
232
+ author: '',
233
+ publisher: '',
234
+ intro: '这是正文第一段,不应该被当成简介。',
235
+ category: '',
236
+ rating: '',
237
+ metadataReady: false,
238
+ }),
239
+ getCookies: vi.fn().mockResolvedValue([
240
+ { name: 'wr_vid', value: '70486028', domain: '.weread.qq.com' },
241
+ ]),
242
+ wait: vi.fn().mockResolvedValue(undefined),
243
+ };
244
+ await expect(book.func(page, { 'book-id': 'BOOK_1' })).rejects.toMatchObject({
245
+ code: 'AUTH_REQUIRED',
246
+ message: 'Not logged in to WeRead',
247
+ });
248
+ });
28
249
  it('passes the positional book-id to highlights', async () => {
29
250
  mockFetchPrivateApi.mockResolvedValue({ updated: [] });
30
251
  await highlights.func({}, { 'book-id': 'abc', limit: 5 });
@@ -0,0 +1 @@
1
+ import './shelf.js';
@@ -1,7 +1,8 @@
1
1
  import { beforeEach, describe, expect, it, vi } from 'vitest';
2
- import { getRegistry } from './registry.js';
3
- import { fetchPrivateApi } from './clis/weread/utils.js';
4
- import './clis/weread/shelf.js';
2
+ import { getRegistry } from '../../registry.js';
3
+ import { log } from '../../logger.js';
4
+ import { fetchPrivateApi } from './utils.js';
5
+ import './shelf.js';
5
6
  describe('weread private API regression', () => {
6
7
  beforeEach(() => {
7
8
  vi.restoreAllMocks();
@@ -10,8 +11,10 @@ describe('weread private API regression', () => {
10
11
  const mockPage = {
11
12
  getCookies: vi.fn()
12
13
  .mockResolvedValueOnce([
13
- { name: 'wr_name', value: 'alice', domain: 'weread.qq.com' },
14
14
  { name: 'wr_vid', value: 'vid123', domain: 'i.weread.qq.com' },
15
+ ])
16
+ .mockResolvedValueOnce([
17
+ { name: 'wr_name', value: 'alice', domain: 'weread.qq.com' },
15
18
  ]),
16
19
  evaluate: vi.fn(),
17
20
  };
@@ -23,8 +26,9 @@ describe('weread private API regression', () => {
23
26
  vi.stubGlobal('fetch', fetchMock);
24
27
  const result = await fetchPrivateApi(mockPage, '/book/info', { bookId: '123' });
25
28
  expect(result.title).toBe('Test Book');
26
- expect(mockPage.getCookies).toHaveBeenCalledTimes(1);
29
+ expect(mockPage.getCookies).toHaveBeenCalledTimes(2);
27
30
  expect(mockPage.getCookies).toHaveBeenCalledWith({ url: 'https://i.weread.qq.com/book/info?bookId=123' });
31
+ expect(mockPage.getCookies).toHaveBeenCalledWith({ domain: 'weread.qq.com' });
28
32
  expect(mockPage.evaluate).not.toHaveBeenCalled();
29
33
  expect(fetchMock).toHaveBeenCalledWith('https://i.weread.qq.com/book/info?bookId=123', expect.objectContaining({
30
34
  headers: expect.objectContaining({
@@ -32,6 +36,58 @@ describe('weread private API regression', () => {
32
36
  }),
33
37
  }));
34
38
  });
39
+ it('merges host-only main-domain cookies into private API requests', async () => {
40
+ // Simulates host-only cookies on weread.qq.com that don't match i.weread.qq.com by URL
41
+ const mockPage = {
42
+ getCookies: vi.fn()
43
+ .mockResolvedValueOnce([]) // URL lookup returns nothing for i.weread.qq.com
44
+ .mockResolvedValueOnce([
45
+ { name: 'wr_skey', value: 'skey-host', domain: 'weread.qq.com' },
46
+ { name: 'wr_vid', value: 'vid-host', domain: 'weread.qq.com' },
47
+ ]),
48
+ evaluate: vi.fn(),
49
+ };
50
+ const fetchMock = vi.fn().mockResolvedValue({
51
+ ok: true,
52
+ status: 200,
53
+ json: () => Promise.resolve({ title: 'Book', errcode: 0 }),
54
+ });
55
+ vi.stubGlobal('fetch', fetchMock);
56
+ await fetchPrivateApi(mockPage, '/book/info', { bookId: '42' });
57
+ expect(mockPage.getCookies).toHaveBeenCalledTimes(2);
58
+ expect(mockPage.getCookies).toHaveBeenCalledWith({ url: 'https://i.weread.qq.com/book/info?bookId=42' });
59
+ expect(mockPage.getCookies).toHaveBeenCalledWith({ domain: 'weread.qq.com' });
60
+ expect(fetchMock).toHaveBeenCalledWith('https://i.weread.qq.com/book/info?bookId=42', expect.objectContaining({
61
+ headers: expect.objectContaining({
62
+ Cookie: 'wr_skey=skey-host; wr_vid=vid-host',
63
+ }),
64
+ }));
65
+ });
66
+ it('prefers API-subdomain cookies over main-domain cookies on name collision', async () => {
67
+ const mockPage = {
68
+ getCookies: vi.fn()
69
+ .mockResolvedValueOnce([
70
+ { name: 'wr_skey', value: 'from-api', domain: 'i.weread.qq.com' },
71
+ ])
72
+ .mockResolvedValueOnce([
73
+ { name: 'wr_skey', value: 'from-main', domain: 'weread.qq.com' },
74
+ { name: 'wr_vid', value: 'vid-main', domain: 'weread.qq.com' },
75
+ ]),
76
+ evaluate: vi.fn(),
77
+ };
78
+ const fetchMock = vi.fn().mockResolvedValue({
79
+ ok: true,
80
+ status: 200,
81
+ json: () => Promise.resolve({ title: 'Book', errcode: 0 }),
82
+ });
83
+ vi.stubGlobal('fetch', fetchMock);
84
+ await fetchPrivateApi(mockPage, '/book/info', { bookId: '99' });
85
+ expect(fetchMock).toHaveBeenCalledWith('https://i.weread.qq.com/book/info?bookId=99', expect.objectContaining({
86
+ headers: expect.objectContaining({
87
+ Cookie: 'wr_skey=from-api; wr_vid=vid-main',
88
+ }),
89
+ }));
90
+ });
35
91
  it('maps unauthenticated private API responses to AUTH_REQUIRED', async () => {
36
92
  const mockPage = {
37
93
  getCookies: vi.fn().mockResolvedValue([]),
@@ -101,8 +157,10 @@ describe('weread private API regression', () => {
101
157
  const mockPage = {
102
158
  getCookies: vi.fn()
103
159
  .mockResolvedValueOnce([
104
- { name: 'wr_name', value: 'alice', domain: 'weread.qq.com' },
105
160
  { name: 'wr_vid', value: 'vid123', domain: 'i.weread.qq.com' },
161
+ ])
162
+ .mockResolvedValueOnce([
163
+ { name: 'wr_name', value: 'alice', domain: 'weread.qq.com' },
106
164
  ]),
107
165
  evaluate: vi.fn(),
108
166
  };
@@ -122,9 +180,11 @@ describe('weread private API regression', () => {
122
180
  const result = await command.func(mockPage, { limit: 1 });
123
181
  expect(mockPage.evaluate).not.toHaveBeenCalled();
124
182
  expect(fetchMock).toHaveBeenCalledWith('https://i.weread.qq.com/shelf/sync?synckey=0&lectureSynckey=0', expect.any(Object));
183
+ expect(mockPage.getCookies).toHaveBeenCalledTimes(2);
125
184
  expect(mockPage.getCookies).toHaveBeenCalledWith({
126
185
  url: 'https://i.weread.qq.com/shelf/sync?synckey=0&lectureSynckey=0',
127
186
  });
187
+ expect(mockPage.getCookies).toHaveBeenCalledWith({ domain: 'weread.qq.com' });
128
188
  expect(result).toEqual([
129
189
  {
130
190
  title: 'Deep Work',
@@ -137,14 +197,15 @@ describe('weread private API regression', () => {
137
197
  it('falls back to structured shelf cache when the private API reports AUTH_REQUIRED', async () => {
138
198
  const command = getRegistry().get('weread/shelf');
139
199
  expect(command?.func).toBeTypeOf('function');
200
+ const warnSpy = vi.spyOn(log, 'warn').mockImplementation(() => { });
140
201
  const mockPage = {
141
202
  getCookies: vi.fn()
142
- .mockResolvedValueOnce([
143
- { name: 'wr_skey', value: 'skey123', domain: '.weread.qq.com' },
144
- ])
145
- .mockResolvedValueOnce([
146
- { name: 'wr_vid', value: 'vid-current', domain: '.weread.qq.com' },
147
- ]),
203
+ // fetchPrivateApi: URL lookup (i.weread.qq.com)
204
+ .mockResolvedValueOnce([{ name: 'wr_skey', value: 'skey123', domain: '.weread.qq.com' }])
205
+ // fetchPrivateApi: domain lookup (weread.qq.com)
206
+ .mockResolvedValueOnce([{ name: 'wr_skey', value: 'skey123', domain: '.weread.qq.com' }])
207
+ // loadWebShelfSnapshot: domain lookup for wr_vid
208
+ .mockResolvedValueOnce([{ name: 'wr_vid', value: 'vid-current', domain: '.weread.qq.com' }]),
148
209
  goto: vi.fn().mockResolvedValue(undefined),
149
210
  evaluate: vi.fn().mockImplementation(async (source) => {
150
211
  expect(source).toContain('shelf:rawBooks:vid-current');
@@ -183,6 +244,7 @@ describe('weread private API regression', () => {
183
244
  expect(mockPage.goto).toHaveBeenCalledWith('https://weread.qq.com/web/shelf');
184
245
  expect(mockPage.getCookies).toHaveBeenCalledWith({ domain: 'weread.qq.com' });
185
246
  expect(mockPage.evaluate).toHaveBeenCalledTimes(1);
247
+ expect(warnSpy).toHaveBeenCalledWith('WeRead private API auth expired; showing cached shelf data from localStorage. Results may be stale, and detail commands may still require re-login.');
186
248
  expect(result).toEqual([
187
249
  {
188
250
  title: '文明、现代化、价值投资与中国',
@@ -197,12 +259,12 @@ describe('weread private API regression', () => {
197
259
  expect(command?.func).toBeTypeOf('function');
198
260
  const mockPage = {
199
261
  getCookies: vi.fn()
200
- .mockResolvedValueOnce([
201
- { name: 'wr_skey', value: 'skey123', domain: '.weread.qq.com' },
202
- ])
203
- .mockResolvedValueOnce([
204
- { name: 'wr_vid', value: 'vid-current', domain: '.weread.qq.com' },
205
- ]),
262
+ // fetchPrivateApi: URL lookup (i.weread.qq.com)
263
+ .mockResolvedValueOnce([{ name: 'wr_skey', value: 'skey123', domain: '.weread.qq.com' }])
264
+ // fetchPrivateApi: domain lookup (weread.qq.com)
265
+ .mockResolvedValueOnce([{ name: 'wr_skey', value: 'skey123', domain: '.weread.qq.com' }])
266
+ // loadWebShelfSnapshot: domain lookup for wr_vid
267
+ .mockResolvedValueOnce([{ name: 'wr_vid', value: 'vid-current', domain: '.weread.qq.com' }]),
206
268
  goto: vi.fn().mockResolvedValue(undefined),
207
269
  evaluate: vi.fn().mockResolvedValue({
208
270
  cacheFound: false,
@@ -228,12 +290,12 @@ describe('weread private API regression', () => {
228
290
  expect(command?.func).toBeTypeOf('function');
229
291
  const mockPage = {
230
292
  getCookies: vi.fn()
231
- .mockResolvedValueOnce([
232
- { name: 'wr_skey', value: 'skey123', domain: '.weread.qq.com' },
233
- ])
234
- .mockResolvedValueOnce([
235
- { name: 'wr_vid', value: 'vid-current', domain: '.weread.qq.com' },
236
- ]),
293
+ // fetchPrivateApi: URL lookup (i.weread.qq.com)
294
+ .mockResolvedValueOnce([{ name: 'wr_skey', value: 'skey123', domain: '.weread.qq.com' }])
295
+ // fetchPrivateApi: domain lookup (weread.qq.com)
296
+ .mockResolvedValueOnce([{ name: 'wr_skey', value: 'skey123', domain: '.weread.qq.com' }])
297
+ // loadWebShelfSnapshot: domain lookup for wr_vid
298
+ .mockResolvedValueOnce([{ name: 'wr_vid', value: 'vid-current', domain: '.weread.qq.com' }]),
237
299
  goto: vi.fn().mockResolvedValue(undefined),
238
300
  evaluate: vi.fn().mockResolvedValue({
239
301
  cacheFound: true,
@@ -257,12 +319,12 @@ describe('weread private API regression', () => {
257
319
  expect(command?.func).toBeTypeOf('function');
258
320
  const mockPage = {
259
321
  getCookies: vi.fn()
260
- .mockResolvedValueOnce([
261
- { name: 'wr_skey', value: 'skey123', domain: '.weread.qq.com' },
262
- ])
263
- .mockResolvedValueOnce([
264
- { name: 'wr_vid', value: 'vid-current', domain: '.weread.qq.com' },
265
- ]),
322
+ // fetchPrivateApi: URL lookup (i.weread.qq.com)
323
+ .mockResolvedValueOnce([{ name: 'wr_skey', value: 'skey123', domain: '.weread.qq.com' }])
324
+ // fetchPrivateApi: domain lookup (weread.qq.com)
325
+ .mockResolvedValueOnce([{ name: 'wr_skey', value: 'skey123', domain: '.weread.qq.com' }])
326
+ // loadWebShelfSnapshot: domain lookup for wr_vid
327
+ .mockResolvedValueOnce([{ name: 'wr_vid', value: 'vid-current', domain: '.weread.qq.com' }]),
266
328
  goto: vi.fn().mockResolvedValue(undefined),
267
329
  evaluate: vi.fn().mockResolvedValue({
268
330
  cacheFound: true,
@@ -0,0 +1 @@
1
+ import './search.js';