@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,4 +1,5 @@
1
1
  import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { CliError } from '../../errors.js';
2
3
 
3
4
  const { mockFetchPrivateApi } = vi.hoisted(() => ({
4
5
  mockFetchPrivateApi: vi.fn(),
@@ -34,6 +35,254 @@ describe('weread book-id positional args', () => {
34
35
  expect(mockFetchPrivateApi).toHaveBeenCalledWith({}, '/book/info', { bookId: '12345' });
35
36
  });
36
37
 
38
+ it('falls back to the shelf reader page when private API auth has expired', async () => {
39
+ mockFetchPrivateApi.mockRejectedValue(
40
+ new CliError('AUTH_REQUIRED', 'Not logged in to WeRead', 'Please log in to weread.qq.com in Chrome first'),
41
+ );
42
+
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;
70
+
71
+ const result = await book!.func!(page, { 'book-id': 'MP_WXS_3634777637' });
72
+
73
+ expect(page.goto).toHaveBeenNthCalledWith(1, 'https://weread.qq.com/web/shelf');
74
+ expect(page.goto).toHaveBeenNthCalledWith(2, 'https://weread.qq.com/web/reader/6f5323f071bd7f7b6f521e8');
75
+ expect(page.evaluate).toHaveBeenCalledTimes(3);
76
+ expect(result).toEqual([
77
+ {
78
+ title: '文明、现代化、价值投资与中国',
79
+ author: '李录',
80
+ publisher: '中信出版集团',
81
+ intro: '对中国未来几十年的预测。',
82
+ category: '',
83
+ rating: '84.1%',
84
+ },
85
+ ]);
86
+ });
87
+
88
+ it('keeps mixed shelf entries aligned when resolving MP_WXS reader urls', async () => {
89
+ mockFetchPrivateApi.mockRejectedValue(
90
+ new CliError('AUTH_REQUIRED', 'Not logged in to WeRead', 'Please log in to weread.qq.com in Chrome first'),
91
+ );
92
+
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;
128
+
129
+ const result = await book!.func!(page, { 'book-id': 'MP_WXS_1' });
130
+
131
+ expect(page.goto).toHaveBeenNthCalledWith(1, 'https://weread.qq.com/web/shelf');
132
+ expect(page.goto).toHaveBeenNthCalledWith(2, 'https://weread.qq.com/web/reader/mp1');
133
+ expect(result).toEqual([
134
+ {
135
+ title: '公众号文章一',
136
+ author: '作者甲',
137
+ publisher: '微信读书',
138
+ intro: '第一篇文章。',
139
+ category: '',
140
+ rating: '',
141
+ },
142
+ ]);
143
+ });
144
+
145
+ it('rethrows AUTH_REQUIRED when shelf ordering is incomplete and reader urls cannot be trusted', async () => {
146
+ mockFetchPrivateApi.mockRejectedValue(
147
+ new CliError('AUTH_REQUIRED', 'Not logged in to WeRead', 'Please log in to weread.qq.com in Chrome first'),
148
+ );
149
+
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;
172
+
173
+ await expect(book!.func!(page, { 'book-id': 'BOOK_1' })).rejects.toMatchObject({
174
+ code: 'AUTH_REQUIRED',
175
+ message: 'Not logged in to WeRead',
176
+ });
177
+ expect(page.goto).toHaveBeenCalledTimes(1);
178
+ expect(page.goto).toHaveBeenCalledWith('https://weread.qq.com/web/shelf');
179
+ });
180
+
181
+ it('waits for shelf indexes to hydrate before resolving a trusted reader url', async () => {
182
+ mockFetchPrivateApi.mockRejectedValue(
183
+ new CliError('AUTH_REQUIRED', 'Not logged in to WeRead', 'Please log in to weread.qq.com in Chrome first'),
184
+ );
185
+
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' },
197
+ ],
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' },
208
+ ],
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
+ }),
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;
228
+
229
+ const result = await book!.func!(page, { 'book-id': 'BOOK_1' });
230
+
231
+ expect(page.goto).toHaveBeenNthCalledWith(1, 'https://weread.qq.com/web/shelf');
232
+ expect(page.goto).toHaveBeenNthCalledWith(2, 'https://weread.qq.com/web/reader/book1');
233
+ expect(result).toEqual([
234
+ {
235
+ title: '第一本',
236
+ author: '作者甲',
237
+ publisher: '出版社甲',
238
+ intro: '简介甲',
239
+ category: '',
240
+ rating: '',
241
+ },
242
+ ]);
243
+ });
244
+
245
+ it('rethrows AUTH_REQUIRED when the reader page lacks stable cover metadata', async () => {
246
+ mockFetchPrivateApi.mockRejectedValue(
247
+ new CliError('AUTH_REQUIRED', 'Not logged in to WeRead', 'Please log in to weread.qq.com in Chrome first'),
248
+ );
249
+
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;
279
+
280
+ await expect(book!.func!(page, { 'book-id': 'BOOK_1' })).rejects.toMatchObject({
281
+ code: 'AUTH_REQUIRED',
282
+ message: 'Not logged in to WeRead',
283
+ });
284
+ });
285
+
37
286
  it('passes the positional book-id to highlights', async () => {
38
287
  mockFetchPrivateApi.mockResolvedValue({ updated: [] });
39
288
 
@@ -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
 
6
7
  describe('weread private API regression', () => {
7
8
  beforeEach(() => {
@@ -12,8 +13,10 @@ describe('weread private API regression', () => {
12
13
  const mockPage = {
13
14
  getCookies: vi.fn()
14
15
  .mockResolvedValueOnce([
15
- { name: 'wr_name', value: 'alice', domain: 'weread.qq.com' },
16
16
  { name: 'wr_vid', value: 'vid123', domain: 'i.weread.qq.com' },
17
+ ])
18
+ .mockResolvedValueOnce([
19
+ { name: 'wr_name', value: 'alice', domain: 'weread.qq.com' },
17
20
  ]),
18
21
  evaluate: vi.fn(),
19
22
  } as any;
@@ -28,8 +31,9 @@ describe('weread private API regression', () => {
28
31
  const result = await fetchPrivateApi(mockPage, '/book/info', { bookId: '123' });
29
32
 
30
33
  expect(result.title).toBe('Test Book');
31
- expect(mockPage.getCookies).toHaveBeenCalledTimes(1);
34
+ expect(mockPage.getCookies).toHaveBeenCalledTimes(2);
32
35
  expect(mockPage.getCookies).toHaveBeenCalledWith({ url: 'https://i.weread.qq.com/book/info?bookId=123' });
36
+ expect(mockPage.getCookies).toHaveBeenCalledWith({ domain: 'weread.qq.com' });
33
37
  expect(mockPage.evaluate).not.toHaveBeenCalled();
34
38
  expect(fetchMock).toHaveBeenCalledWith(
35
39
  'https://i.weread.qq.com/book/info?bookId=123',
@@ -41,6 +45,72 @@ describe('weread private API regression', () => {
41
45
  );
42
46
  });
43
47
 
48
+ it('merges host-only main-domain cookies into private API requests', async () => {
49
+ // Simulates host-only cookies on weread.qq.com that don't match i.weread.qq.com by URL
50
+ const mockPage = {
51
+ getCookies: vi.fn()
52
+ .mockResolvedValueOnce([]) // URL lookup returns nothing for i.weread.qq.com
53
+ .mockResolvedValueOnce([
54
+ { name: 'wr_skey', value: 'skey-host', domain: 'weread.qq.com' },
55
+ { name: 'wr_vid', value: 'vid-host', domain: 'weread.qq.com' },
56
+ ]),
57
+ evaluate: vi.fn(),
58
+ } as any;
59
+
60
+ const fetchMock = vi.fn().mockResolvedValue({
61
+ ok: true,
62
+ status: 200,
63
+ json: () => Promise.resolve({ title: 'Book', errcode: 0 }),
64
+ });
65
+ vi.stubGlobal('fetch', fetchMock);
66
+
67
+ await fetchPrivateApi(mockPage, '/book/info', { bookId: '42' });
68
+
69
+ expect(mockPage.getCookies).toHaveBeenCalledTimes(2);
70
+ expect(mockPage.getCookies).toHaveBeenCalledWith({ url: 'https://i.weread.qq.com/book/info?bookId=42' });
71
+ expect(mockPage.getCookies).toHaveBeenCalledWith({ domain: 'weread.qq.com' });
72
+ expect(fetchMock).toHaveBeenCalledWith(
73
+ 'https://i.weread.qq.com/book/info?bookId=42',
74
+ expect.objectContaining({
75
+ headers: expect.objectContaining({
76
+ Cookie: 'wr_skey=skey-host; wr_vid=vid-host',
77
+ }),
78
+ }),
79
+ );
80
+ });
81
+
82
+ it('prefers API-subdomain cookies over main-domain cookies on name collision', async () => {
83
+ const mockPage = {
84
+ getCookies: vi.fn()
85
+ .mockResolvedValueOnce([
86
+ { name: 'wr_skey', value: 'from-api', domain: 'i.weread.qq.com' },
87
+ ])
88
+ .mockResolvedValueOnce([
89
+ { name: 'wr_skey', value: 'from-main', domain: 'weread.qq.com' },
90
+ { name: 'wr_vid', value: 'vid-main', domain: 'weread.qq.com' },
91
+ ]),
92
+ evaluate: vi.fn(),
93
+ } as any;
94
+
95
+ const fetchMock = vi.fn().mockResolvedValue({
96
+ ok: true,
97
+ status: 200,
98
+ json: () => Promise.resolve({ title: 'Book', errcode: 0 }),
99
+ });
100
+ vi.stubGlobal('fetch', fetchMock);
101
+
102
+ await fetchPrivateApi(mockPage, '/book/info', { bookId: '99' });
103
+
104
+ expect(fetchMock).toHaveBeenCalledWith(
105
+ 'https://i.weread.qq.com/book/info?bookId=99',
106
+ expect.objectContaining({
107
+ headers: expect.objectContaining({
108
+ Cookie: 'wr_skey=from-api; wr_vid=vid-main',
109
+ }),
110
+ }),
111
+ );
112
+ });
113
+
44
114
  it('maps unauthenticated private API responses to AUTH_REQUIRED', async () => {
45
115
  const mockPage = {
46
116
  getCookies: vi.fn().mockResolvedValue([]),
@@ -126,8 +196,10 @@ describe('weread private API regression', () => {
126
196
  const mockPage = {
127
197
  getCookies: vi.fn()
128
198
  .mockResolvedValueOnce([
129
- { name: 'wr_name', value: 'alice', domain: 'weread.qq.com' },
130
199
  { name: 'wr_vid', value: 'vid123', domain: 'i.weread.qq.com' },
200
+ ])
201
+ .mockResolvedValueOnce([
202
+ { name: 'wr_name', value: 'alice', domain: 'weread.qq.com' },
131
203
  ]),
132
204
  evaluate: vi.fn(),
133
205
  } as any;
@@ -153,9 +225,11 @@ describe('weread private API regression', () => {
153
225
  'https://i.weread.qq.com/shelf/sync?synckey=0&lectureSynckey=0',
154
226
  expect.any(Object),
155
227
  );
228
+ expect(mockPage.getCookies).toHaveBeenCalledTimes(2);
156
229
  expect(mockPage.getCookies).toHaveBeenCalledWith({
157
230
  url: 'https://i.weread.qq.com/shelf/sync?synckey=0&lectureSynckey=0',
158
231
  });
232
+ expect(mockPage.getCookies).toHaveBeenCalledWith({ domain: 'weread.qq.com' });
159
233
  expect(result).toEqual([
160
234
  {
161
235
  title: 'Deep Work',
@@ -169,15 +243,16 @@ describe('weread private API regression', () => {
169
243
  it('falls back to structured shelf cache when the private API reports AUTH_REQUIRED', async () => {
170
244
  const command = getRegistry().get('weread/shelf');
171
245
  expect(command?.func).toBeTypeOf('function');
246
+ const warnSpy = vi.spyOn(log, 'warn').mockImplementation(() => {});
172
247
 
173
248
  const mockPage = {
174
249
  getCookies: vi.fn()
175
- .mockResolvedValueOnce([
176
- { name: 'wr_skey', value: 'skey123', domain: '.weread.qq.com' },
177
- ])
178
- .mockResolvedValueOnce([
179
- { name: 'wr_vid', value: 'vid-current', domain: '.weread.qq.com' },
180
- ]),
250
+ // fetchPrivateApi: URL lookup (i.weread.qq.com)
251
+ .mockResolvedValueOnce([{ name: 'wr_skey', value: 'skey123', domain: '.weread.qq.com' }])
252
+ // fetchPrivateApi: domain lookup (weread.qq.com)
253
+ .mockResolvedValueOnce([{ name: 'wr_skey', value: 'skey123', domain: '.weread.qq.com' }])
254
+ // loadWebShelfSnapshot: domain lookup for wr_vid
255
+ .mockResolvedValueOnce([{ name: 'wr_vid', value: 'vid-current', domain: '.weread.qq.com' }]),
181
256
  goto: vi.fn().mockResolvedValue(undefined),
182
257
  evaluate: vi.fn().mockImplementation(async (source: string) => {
183
258
  expect(source).toContain('shelf:rawBooks:vid-current');
@@ -219,6 +294,9 @@ describe('weread private API regression', () => {
219
294
  expect(mockPage.goto).toHaveBeenCalledWith('https://weread.qq.com/web/shelf');
220
295
  expect(mockPage.getCookies).toHaveBeenCalledWith({ domain: 'weread.qq.com' });
221
296
  expect(mockPage.evaluate).toHaveBeenCalledTimes(1);
297
+ expect(warnSpy).toHaveBeenCalledWith(
298
+ 'WeRead private API auth expired; showing cached shelf data from localStorage. Results may be stale, and detail commands may still require re-login.',
299
+ );
222
300
  expect(result).toEqual([
223
301
  {
224
302
  title: '文明、现代化、价值投资与中国',
@@ -235,12 +313,12 @@ describe('weread private API regression', () => {
235
313
 
236
314
  const mockPage = {
237
315
  getCookies: vi.fn()
238
- .mockResolvedValueOnce([
239
- { name: 'wr_skey', value: 'skey123', domain: '.weread.qq.com' },
240
- ])
241
- .mockResolvedValueOnce([
242
- { name: 'wr_vid', value: 'vid-current', domain: '.weread.qq.com' },
243
- ]),
316
+ // fetchPrivateApi: URL lookup (i.weread.qq.com)
317
+ .mockResolvedValueOnce([{ name: 'wr_skey', value: 'skey123', domain: '.weread.qq.com' }])
318
+ // fetchPrivateApi: domain lookup (weread.qq.com)
319
+ .mockResolvedValueOnce([{ name: 'wr_skey', value: 'skey123', domain: '.weread.qq.com' }])
320
+ // loadWebShelfSnapshot: domain lookup for wr_vid
321
+ .mockResolvedValueOnce([{ name: 'wr_vid', value: 'vid-current', domain: '.weread.qq.com' }]),
244
322
  goto: vi.fn().mockResolvedValue(undefined),
245
323
  evaluate: vi.fn().mockResolvedValue({
246
324
  cacheFound: false,
@@ -270,12 +348,12 @@ describe('weread private API regression', () => {
270
348
 
271
349
  const mockPage = {
272
350
  getCookies: vi.fn()
273
- .mockResolvedValueOnce([
274
- { name: 'wr_skey', value: 'skey123', domain: '.weread.qq.com' },
275
- ])
276
- .mockResolvedValueOnce([
277
- { name: 'wr_vid', value: 'vid-current', domain: '.weread.qq.com' },
278
- ]),
351
+ // fetchPrivateApi: URL lookup (i.weread.qq.com)
352
+ .mockResolvedValueOnce([{ name: 'wr_skey', value: 'skey123', domain: '.weread.qq.com' }])
353
+ // fetchPrivateApi: domain lookup (weread.qq.com)
354
+ .mockResolvedValueOnce([{ name: 'wr_skey', value: 'skey123', domain: '.weread.qq.com' }])
355
+ // loadWebShelfSnapshot: domain lookup for wr_vid
356
+ .mockResolvedValueOnce([{ name: 'wr_vid', value: 'vid-current', domain: '.weread.qq.com' }]),
279
357
  goto: vi.fn().mockResolvedValue(undefined),
280
358
  evaluate: vi.fn().mockResolvedValue({
281
359
  cacheFound: true,
@@ -304,12 +382,12 @@ describe('weread private API regression', () => {
304
382
 
305
383
  const mockPage = {
306
384
  getCookies: vi.fn()
307
- .mockResolvedValueOnce([
308
- { name: 'wr_skey', value: 'skey123', domain: '.weread.qq.com' },
309
- ])
310
- .mockResolvedValueOnce([
311
- { name: 'wr_vid', value: 'vid-current', domain: '.weread.qq.com' },
312
- ]),
385
+ // fetchPrivateApi: URL lookup (i.weread.qq.com)
386
+ .mockResolvedValueOnce([{ name: 'wr_skey', value: 'skey123', domain: '.weread.qq.com' }])
387
+ // fetchPrivateApi: domain lookup (weread.qq.com)
388
+ .mockResolvedValueOnce([{ name: 'wr_skey', value: 'skey123', domain: '.weread.qq.com' }])
389
+ // loadWebShelfSnapshot: domain lookup for wr_vid
390
+ .mockResolvedValueOnce([{ name: 'wr_vid', value: 'vid-current', domain: '.weread.qq.com' }]),
313
391
  goto: vi.fn().mockResolvedValue(undefined),
314
392
  evaluate: vi.fn().mockResolvedValue({
315
393
  cacheFound: true,