@jackwener/opencli 1.5.4 β†’ 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 (256) 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 +1284 -67
  7. package/dist/cli.js +14 -14
  8. package/dist/clis/antigravity/serve.js +2 -2
  9. package/dist/clis/band/bands.d.ts +1 -0
  10. package/dist/clis/band/bands.js +72 -0
  11. package/dist/clis/band/mentions.d.ts +1 -0
  12. package/dist/clis/band/mentions.js +127 -0
  13. package/dist/clis/band/post.d.ts +1 -0
  14. package/dist/clis/band/post.js +175 -0
  15. package/dist/clis/band/posts.d.ts +1 -0
  16. package/dist/clis/band/posts.js +94 -0
  17. package/dist/clis/doubao/detail.d.ts +1 -0
  18. package/dist/clis/doubao/detail.js +33 -0
  19. package/dist/clis/doubao/detail.test.d.ts +1 -0
  20. package/dist/clis/doubao/detail.test.js +42 -0
  21. package/dist/clis/doubao/history.d.ts +1 -0
  22. package/dist/clis/doubao/history.js +28 -0
  23. package/dist/clis/doubao/history.test.d.ts +1 -0
  24. package/dist/clis/doubao/history.test.js +37 -0
  25. package/dist/clis/doubao/meeting-summary.d.ts +1 -0
  26. package/dist/clis/doubao/meeting-summary.js +39 -0
  27. package/dist/clis/doubao/meeting-transcript.d.ts +1 -0
  28. package/dist/clis/doubao/meeting-transcript.js +36 -0
  29. package/dist/clis/doubao/utils.d.ts +27 -0
  30. package/dist/clis/doubao/utils.js +317 -0
  31. package/dist/clis/doubao/utils.test.d.ts +1 -0
  32. package/dist/clis/doubao/utils.test.js +24 -0
  33. package/dist/clis/douyin/_shared/public-api.d.ts +33 -0
  34. package/dist/clis/douyin/_shared/public-api.js +29 -0
  35. package/dist/clis/douyin/user-videos.d.ts +5 -0
  36. package/dist/clis/douyin/user-videos.js +74 -0
  37. package/dist/clis/douyin/user-videos.test.d.ts +1 -0
  38. package/dist/clis/douyin/user-videos.test.js +108 -0
  39. package/dist/clis/ones/common.d.ts +32 -0
  40. package/dist/clis/ones/common.js +144 -0
  41. package/dist/clis/ones/enrich-tasks.d.ts +5 -0
  42. package/dist/clis/ones/enrich-tasks.js +37 -0
  43. package/dist/clis/ones/login.d.ts +1 -0
  44. package/dist/clis/ones/login.js +80 -0
  45. package/dist/clis/ones/logout.d.ts +1 -0
  46. package/dist/clis/ones/logout.js +17 -0
  47. package/dist/clis/ones/me.d.ts +1 -0
  48. package/dist/clis/ones/me.js +30 -0
  49. package/dist/clis/ones/my-tasks.d.ts +1 -0
  50. package/dist/clis/ones/my-tasks.js +120 -0
  51. package/dist/clis/ones/resolve-labels.d.ts +10 -0
  52. package/dist/clis/ones/resolve-labels.js +64 -0
  53. package/dist/clis/ones/task-helpers.d.ts +29 -0
  54. package/dist/clis/ones/task-helpers.js +212 -0
  55. package/dist/clis/ones/task-helpers.test.d.ts +1 -0
  56. package/dist/clis/ones/task-helpers.test.js +12 -0
  57. package/dist/clis/ones/task.d.ts +1 -0
  58. package/dist/clis/ones/task.js +66 -0
  59. package/dist/clis/ones/tasks.d.ts +1 -0
  60. package/dist/clis/ones/tasks.js +79 -0
  61. package/dist/clis/ones/token-info.d.ts +1 -0
  62. package/dist/clis/ones/token-info.js +42 -0
  63. package/dist/clis/ones/worklog.d.ts +11 -0
  64. package/dist/clis/ones/worklog.js +267 -0
  65. package/dist/clis/ones/worklog.test.d.ts +1 -0
  66. package/dist/clis/ones/worklog.test.js +20 -0
  67. package/dist/clis/sinafinance/rolling-news.d.ts +4 -0
  68. package/dist/clis/sinafinance/rolling-news.js +40 -0
  69. package/dist/clis/sinafinance/stock.d.ts +8 -0
  70. package/dist/clis/sinafinance/stock.js +117 -0
  71. package/dist/clis/spotify/spotify.d.ts +1 -0
  72. package/dist/clis/spotify/spotify.js +316 -0
  73. package/dist/clis/spotify/utils.d.ts +21 -0
  74. package/dist/clis/spotify/utils.js +66 -0
  75. package/dist/clis/spotify/utils.test.d.ts +1 -0
  76. package/dist/clis/spotify/utils.test.js +67 -0
  77. package/dist/clis/tieba/commands.test.d.ts +4 -0
  78. package/dist/clis/tieba/commands.test.js +79 -0
  79. package/dist/clis/tieba/hot.d.ts +1 -0
  80. package/dist/clis/tieba/hot.js +48 -0
  81. package/dist/clis/tieba/posts.d.ts +1 -0
  82. package/dist/clis/tieba/posts.js +85 -0
  83. package/dist/clis/tieba/read.d.ts +1 -0
  84. package/dist/clis/tieba/read.js +140 -0
  85. package/dist/clis/tieba/search.d.ts +1 -0
  86. package/dist/clis/tieba/search.js +108 -0
  87. package/dist/clis/tieba/utils.d.ts +101 -0
  88. package/dist/clis/tieba/utils.js +240 -0
  89. package/dist/clis/tieba/utils.test.d.ts +1 -0
  90. package/dist/clis/tieba/utils.test.js +290 -0
  91. package/dist/clis/weread/book.js +100 -13
  92. package/dist/clis/weread/commands.test.js +221 -0
  93. package/dist/clis/weread/private-api-regression.test.d.ts +1 -0
  94. package/dist/{weread-private-api-regression.test.js β†’ clis/weread/private-api-regression.test.js} +92 -30
  95. package/dist/clis/weread/search-regression.test.d.ts +1 -0
  96. package/dist/clis/weread/search-regression.test.js +407 -0
  97. package/dist/clis/weread/search.js +143 -7
  98. package/dist/clis/weread/shelf.js +13 -95
  99. package/dist/clis/weread/utils.d.ts +46 -0
  100. package/dist/clis/weread/utils.js +214 -7
  101. package/dist/clis/weread/utils.test.js +71 -1
  102. package/dist/clis/xiaohongshu/publish.d.ts +1 -1
  103. package/dist/clis/xiaohongshu/publish.js +78 -31
  104. package/dist/clis/xiaohongshu/publish.test.js +66 -1
  105. package/dist/clis/xiaohongshu/user-helpers.d.ts +1 -0
  106. package/dist/clis/xiaohongshu/user-helpers.js +2 -0
  107. package/dist/clis/xiaohongshu/user-helpers.test.js +18 -0
  108. package/dist/clis/xueqiu/comments.d.ts +118 -0
  109. package/dist/clis/xueqiu/comments.js +354 -0
  110. package/dist/clis/xueqiu/comments.test.d.ts +1 -0
  111. package/dist/clis/xueqiu/comments.test.js +696 -0
  112. package/dist/clis/youtube/transcript.js +2 -4
  113. package/dist/clis/youtube/utils.d.ts +9 -0
  114. package/dist/clis/youtube/utils.js +67 -3
  115. package/dist/clis/youtube/utils.test.d.ts +1 -0
  116. package/dist/clis/youtube/utils.test.js +37 -0
  117. package/dist/clis/youtube/video.js +16 -15
  118. package/dist/clis/zsxq/dynamics.d.ts +1 -0
  119. package/dist/clis/zsxq/dynamics.js +47 -0
  120. package/dist/clis/zsxq/groups.d.ts +1 -0
  121. package/dist/clis/zsxq/groups.js +32 -0
  122. package/dist/clis/zsxq/search.d.ts +1 -0
  123. package/dist/clis/zsxq/search.js +43 -0
  124. package/dist/clis/zsxq/search.test.d.ts +1 -0
  125. package/dist/clis/zsxq/search.test.js +24 -0
  126. package/dist/clis/zsxq/topic.d.ts +1 -0
  127. package/dist/clis/zsxq/topic.js +47 -0
  128. package/dist/clis/zsxq/topic.test.d.ts +1 -0
  129. package/dist/clis/zsxq/topic.test.js +29 -0
  130. package/dist/clis/zsxq/topics.d.ts +1 -0
  131. package/dist/clis/zsxq/topics.js +25 -0
  132. package/dist/clis/zsxq/topics.test.d.ts +1 -0
  133. package/dist/clis/zsxq/topics.test.js +24 -0
  134. package/dist/clis/zsxq/utils.d.ts +97 -0
  135. package/dist/clis/zsxq/utils.js +230 -0
  136. package/dist/commanderAdapter.js +27 -4
  137. package/dist/commanderAdapter.test.js +39 -0
  138. package/dist/daemon.js +5 -4
  139. package/dist/errors.d.ts +29 -1
  140. package/dist/errors.js +49 -11
  141. package/dist/external-clis.yaml +17 -0
  142. package/dist/external.js +3 -3
  143. package/dist/main.js +2 -1
  144. package/dist/tui.js +2 -1
  145. package/dist/types.d.ts +5 -0
  146. package/docs/.vitepress/config.mts +3 -0
  147. package/docs/adapters/browser/band.md +63 -0
  148. package/docs/adapters/browser/ones.md +59 -0
  149. package/docs/adapters/browser/sinafinance.md +56 -6
  150. package/docs/adapters/browser/spotify.md +62 -0
  151. package/docs/adapters/browser/tieba.md +45 -0
  152. package/docs/adapters/browser/xueqiu.md +5 -0
  153. package/docs/adapters/browser/zsxq.md +49 -0
  154. package/docs/adapters/index.md +5 -2
  155. package/docs/adapters-doc/ones.md +32 -0
  156. package/extension/dist/background.js +1 -2
  157. package/extension/manifest.json +1 -1
  158. package/extension/package.json +1 -1
  159. package/extension/src/background.ts +17 -1
  160. package/extension/src/cdp.ts +42 -0
  161. package/extension/src/protocol.ts +5 -1
  162. package/package.json +1 -1
  163. package/scripts/postinstall.js +16 -0
  164. package/src/browser/daemon-client.ts +5 -1
  165. package/src/browser/page.ts +16 -0
  166. package/src/cli.ts +14 -14
  167. package/src/clis/antigravity/serve.ts +2 -2
  168. package/src/clis/band/bands.ts +76 -0
  169. package/src/clis/band/mentions.ts +134 -0
  170. package/src/clis/band/post.ts +187 -0
  171. package/src/clis/band/posts.ts +106 -0
  172. package/src/clis/doubao/detail.test.ts +53 -0
  173. package/src/clis/doubao/detail.ts +41 -0
  174. package/src/clis/doubao/history.test.ts +45 -0
  175. package/src/clis/doubao/history.ts +32 -0
  176. package/src/clis/doubao/meeting-summary.ts +53 -0
  177. package/src/clis/doubao/meeting-transcript.ts +48 -0
  178. package/src/clis/doubao/utils.test.ts +45 -0
  179. package/src/clis/doubao/utils.ts +371 -0
  180. package/src/clis/douyin/_shared/public-api.ts +84 -0
  181. package/src/clis/douyin/user-videos.test.ts +122 -0
  182. package/src/clis/douyin/user-videos.ts +101 -0
  183. package/src/clis/ones/common.ts +187 -0
  184. package/src/clis/ones/enrich-tasks.ts +47 -0
  185. package/src/clis/ones/login.ts +103 -0
  186. package/src/clis/ones/logout.ts +19 -0
  187. package/src/clis/ones/me.ts +34 -0
  188. package/src/clis/ones/my-tasks.ts +148 -0
  189. package/src/clis/ones/resolve-labels.ts +80 -0
  190. package/src/clis/ones/task-helpers.test.ts +14 -0
  191. package/src/clis/ones/task-helpers.ts +214 -0
  192. package/src/clis/ones/task.ts +79 -0
  193. package/src/clis/ones/tasks.ts +92 -0
  194. package/src/clis/ones/token-info.ts +46 -0
  195. package/src/clis/ones/worklog.test.ts +24 -0
  196. package/src/clis/ones/worklog.ts +306 -0
  197. package/src/clis/sinafinance/rolling-news.ts +42 -0
  198. package/src/clis/sinafinance/stock.ts +127 -0
  199. package/src/clis/spotify/spotify.ts +328 -0
  200. package/src/clis/spotify/utils.test.ts +87 -0
  201. package/src/clis/spotify/utils.ts +92 -0
  202. package/src/clis/tieba/commands.test.ts +86 -0
  203. package/src/clis/tieba/hot.ts +52 -0
  204. package/src/clis/tieba/posts.ts +108 -0
  205. package/src/clis/tieba/read.ts +158 -0
  206. package/src/clis/tieba/search.ts +119 -0
  207. package/src/clis/tieba/utils.test.ts +322 -0
  208. package/src/clis/tieba/utils.ts +348 -0
  209. package/src/clis/weread/book.ts +116 -13
  210. package/src/clis/weread/commands.test.ts +249 -0
  211. package/src/{weread-private-api-regression.test.ts β†’ clis/weread/private-api-regression.test.ts} +108 -30
  212. package/src/clis/weread/search-regression.test.ts +440 -0
  213. package/src/clis/weread/search.ts +189 -9
  214. package/src/clis/weread/shelf.ts +20 -122
  215. package/src/clis/weread/utils.test.ts +81 -1
  216. package/src/clis/weread/utils.ts +264 -7
  217. package/src/clis/xiaohongshu/publish.test.ts +79 -1
  218. package/src/clis/xiaohongshu/publish.ts +84 -30
  219. package/src/clis/xiaohongshu/user-helpers.test.ts +23 -0
  220. package/src/clis/xiaohongshu/user-helpers.ts +4 -0
  221. package/src/clis/xueqiu/comments.test.ts +823 -0
  222. package/src/clis/xueqiu/comments.ts +461 -0
  223. package/src/clis/youtube/transcript.ts +2 -4
  224. package/src/clis/youtube/utils.test.ts +43 -0
  225. package/src/clis/youtube/utils.ts +69 -0
  226. package/src/clis/youtube/video.ts +16 -15
  227. package/src/clis/zsxq/dynamics.ts +60 -0
  228. package/src/clis/zsxq/groups.ts +41 -0
  229. package/src/clis/zsxq/search.test.ts +29 -0
  230. package/src/clis/zsxq/search.ts +54 -0
  231. package/src/clis/zsxq/topic.test.ts +34 -0
  232. package/src/clis/zsxq/topic.ts +68 -0
  233. package/src/clis/zsxq/topics.test.ts +29 -0
  234. package/src/clis/zsxq/topics.ts +36 -0
  235. package/src/clis/zsxq/utils.ts +351 -0
  236. package/src/commanderAdapter.test.ts +47 -0
  237. package/src/commanderAdapter.ts +26 -3
  238. package/src/daemon.ts +5 -4
  239. package/src/errors.ts +71 -10
  240. package/src/external-clis.yaml +17 -0
  241. package/src/external.ts +3 -3
  242. package/src/main.ts +2 -1
  243. package/src/tui.ts +2 -1
  244. package/src/types.ts +5 -0
  245. package/tests/e2e/band-auth.test.ts +20 -0
  246. package/tests/e2e/browser-auth-helpers.ts +18 -0
  247. package/tests/e2e/browser-auth.test.ts +35 -47
  248. package/tests/e2e/browser-public.test.ts +288 -0
  249. package/tests/e2e/management.test.ts +1 -1
  250. package/tests/e2e/plugin-management.test.ts +1 -1
  251. package/vitest.config.ts +1 -0
  252. package/SKILL.md +0 -879
  253. package/dist/weread-private-api-regression.test.d.ts +0 -1
  254. package/dist/weread-search-regression.test.d.ts +0 -1
  255. package/dist/weread-search-regression.test.js +0 -39
  256. package/src/weread-search-regression.test.ts +0 -44
@@ -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';
@@ -0,0 +1,407 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { getRegistry } from '../../registry.js';
3
+ import './search.js';
4
+ describe('weread/search regression', () => {
5
+ beforeEach(() => {
6
+ vi.restoreAllMocks();
7
+ });
8
+ it('uses the query argument for the search API and returns reader urls from search html', async () => {
9
+ const command = getRegistry().get('weread/search');
10
+ expect(command?.func).toBeTypeOf('function');
11
+ const fetchMock = vi.fn()
12
+ .mockResolvedValueOnce({
13
+ ok: true,
14
+ json: () => Promise.resolve({
15
+ books: [
16
+ {
17
+ bookInfo: {
18
+ title: 'Deep Work',
19
+ author: 'Cal Newport',
20
+ bookId: 'abc123',
21
+ },
22
+ },
23
+ ],
24
+ }),
25
+ })
26
+ .mockResolvedValueOnce({
27
+ ok: true,
28
+ text: () => Promise.resolve(`
29
+ <ul class="search_bookDetail_list">
30
+ <li class="wr_bookList_item">
31
+ <a class="wr_bookList_item_link" href="/web/reader/reader123"></a>
32
+ <p class="wr_bookList_item_title">Deep Work</p>
33
+ </li>
34
+ </ul>
35
+ `),
36
+ });
37
+ vi.stubGlobal('fetch', fetchMock);
38
+ const result = await command.func(null, { query: 'deep work', limit: 5 });
39
+ expect(fetchMock).toHaveBeenCalledTimes(2);
40
+ expect(String(fetchMock.mock.calls[0][0])).toContain('keyword=deep+work');
41
+ expect(String(fetchMock.mock.calls[1][0])).toContain('/web/search/books?keyword=deep+work');
42
+ expect(result).toEqual([
43
+ {
44
+ rank: 1,
45
+ title: 'Deep Work',
46
+ author: 'Cal Newport',
47
+ bookId: 'abc123',
48
+ url: 'https://weread.qq.com/web/reader/reader123',
49
+ },
50
+ ]);
51
+ });
52
+ it('does not emit stale bookDetail urls when the reader url is unavailable', async () => {
53
+ const command = getRegistry().get('weread/search');
54
+ expect(command?.func).toBeTypeOf('function');
55
+ const fetchMock = vi.fn()
56
+ .mockResolvedValueOnce({
57
+ ok: true,
58
+ json: () => Promise.resolve({
59
+ books: [
60
+ {
61
+ bookInfo: {
62
+ title: 'Deep Work',
63
+ author: 'Cal Newport',
64
+ bookId: 'abc123',
65
+ },
66
+ },
67
+ ],
68
+ }),
69
+ })
70
+ .mockResolvedValueOnce({
71
+ ok: true,
72
+ text: () => Promise.resolve('<html><body><p>no search cards</p></body></html>'),
73
+ });
74
+ vi.stubGlobal('fetch', fetchMock);
75
+ const result = await command.func(null, { query: 'deep work', limit: 5 });
76
+ expect(result).toEqual([
77
+ {
78
+ rank: 1,
79
+ title: 'Deep Work',
80
+ author: 'Cal Newport',
81
+ bookId: 'abc123',
82
+ url: '',
83
+ },
84
+ ]);
85
+ });
86
+ it('matches reader urls by title queue instead of assuming identical result order', async () => {
87
+ const command = getRegistry().get('weread/search');
88
+ expect(command?.func).toBeTypeOf('function');
89
+ const fetchMock = vi.fn()
90
+ .mockResolvedValueOnce({
91
+ ok: true,
92
+ json: () => Promise.resolve({
93
+ books: [
94
+ {
95
+ bookInfo: {
96
+ title: 'Deep Work',
97
+ author: 'Cal Newport',
98
+ bookId: 'abc123',
99
+ },
100
+ },
101
+ {
102
+ bookInfo: {
103
+ title: 'Digital Minimalism',
104
+ author: 'Cal Newport',
105
+ bookId: 'xyz789',
106
+ },
107
+ },
108
+ ],
109
+ }),
110
+ })
111
+ .mockResolvedValueOnce({
112
+ ok: true,
113
+ text: () => Promise.resolve(`
114
+ <ul class="search_bookDetail_list">
115
+ <li class="wr_bookList_item">
116
+ <a class="wr_bookList_item_link" href="/web/reader/unrelated111"></a>
117
+ <p class="wr_bookList_item_title">Atomic Habits</p>
118
+ </li>
119
+ <li class="wr_bookList_item">
120
+ <a class="wr_bookList_item_link" href="/web/reader/digital222"></a>
121
+ <p class="wr_bookList_item_title">Digital Minimalism</p>
122
+ </li>
123
+ <li class="wr_bookList_item">
124
+ <a class="wr_bookList_item_link" href="/web/reader/deep333"></a>
125
+ <p class="wr_bookList_item_title">Deep Work</p>
126
+ </li>
127
+ </ul>
128
+ `),
129
+ });
130
+ vi.stubGlobal('fetch', fetchMock);
131
+ const result = await command.func(null, { query: 'cal newport', limit: 5 });
132
+ expect(result).toEqual([
133
+ {
134
+ rank: 1,
135
+ title: 'Deep Work',
136
+ author: 'Cal Newport',
137
+ bookId: 'abc123',
138
+ url: 'https://weread.qq.com/web/reader/deep333',
139
+ },
140
+ {
141
+ rank: 2,
142
+ title: 'Digital Minimalism',
143
+ author: 'Cal Newport',
144
+ bookId: 'xyz789',
145
+ url: 'https://weread.qq.com/web/reader/digital222',
146
+ },
147
+ ]);
148
+ });
149
+ it('falls back to empty urls when the search html request fails', async () => {
150
+ const command = getRegistry().get('weread/search');
151
+ expect(command?.func).toBeTypeOf('function');
152
+ const fetchMock = vi.fn()
153
+ .mockResolvedValueOnce({
154
+ ok: true,
155
+ json: () => Promise.resolve({
156
+ books: [
157
+ {
158
+ bookInfo: {
159
+ title: 'Deep Work',
160
+ author: 'Cal Newport',
161
+ bookId: 'abc123',
162
+ },
163
+ },
164
+ ],
165
+ }),
166
+ })
167
+ .mockRejectedValueOnce(new Error('network timeout'));
168
+ vi.stubGlobal('fetch', fetchMock);
169
+ const result = await command.func(null, { query: 'deep work', limit: 5 });
170
+ expect(result).toEqual([
171
+ {
172
+ rank: 1,
173
+ title: 'Deep Work',
174
+ author: 'Cal Newport',
175
+ bookId: 'abc123',
176
+ url: '',
177
+ },
178
+ ]);
179
+ });
180
+ it('binds reader urls with title and author instead of title alone', async () => {
181
+ const command = getRegistry().get('weread/search');
182
+ expect(command?.func).toBeTypeOf('function');
183
+ const fetchMock = vi.fn()
184
+ .mockResolvedValueOnce({
185
+ ok: true,
186
+ json: () => Promise.resolve({
187
+ books: [
188
+ {
189
+ bookInfo: {
190
+ title: 'ζ–‡ζ˜Ž',
191
+ author: 'δ½œθ€…η”²',
192
+ bookId: 'book-a',
193
+ },
194
+ },
195
+ {
196
+ bookInfo: {
197
+ title: 'ζ–‡ζ˜Ž',
198
+ author: 'δ½œθ€…δΉ™',
199
+ bookId: 'book-b',
200
+ },
201
+ },
202
+ ],
203
+ }),
204
+ })
205
+ .mockResolvedValueOnce({
206
+ ok: true,
207
+ text: () => Promise.resolve(`
208
+ <ul class="search_bookDetail_list">
209
+ <li class="wr_bookList_item">
210
+ <a class="wr_bookList_item_link" href="/web/reader/book-b-reader"></a>
211
+ <p class="wr_bookList_item_title">ζ–‡ζ˜Ž</p>
212
+ <p class="wr_bookList_item_author"><a href="/web/search/books?author=%E4%BD%9C%E8%80%85%E4%B9%99">δ½œθ€…δΉ™</a></p>
213
+ </li>
214
+ <li class="wr_bookList_item">
215
+ <a class="wr_bookList_item_link" href="/web/reader/book-a-reader"></a>
216
+ <p class="wr_bookList_item_title">ζ–‡ζ˜Ž</p>
217
+ <p class="wr_bookList_item_author"><a href="/web/search/books?author=%E4%BD%9C%E8%80%85%E7%94%B2">δ½œθ€…η”²</a></p>
218
+ </li>
219
+ </ul>
220
+ `),
221
+ });
222
+ vi.stubGlobal('fetch', fetchMock);
223
+ const result = await command.func(null, { query: 'ζ–‡ζ˜Ž', limit: 5 });
224
+ expect(result).toEqual([
225
+ {
226
+ rank: 1,
227
+ title: 'ζ–‡ζ˜Ž',
228
+ author: 'δ½œθ€…η”²',
229
+ bookId: 'book-a',
230
+ url: 'https://weread.qq.com/web/reader/book-a-reader',
231
+ },
232
+ {
233
+ rank: 2,
234
+ title: 'ζ–‡ζ˜Ž',
235
+ author: 'δ½œθ€…δΉ™',
236
+ bookId: 'book-b',
237
+ url: 'https://weread.qq.com/web/reader/book-b-reader',
238
+ },
239
+ ]);
240
+ });
241
+ it('leaves urls empty when same-title results are ambiguous and html cards have no author', async () => {
242
+ const command = getRegistry().get('weread/search');
243
+ expect(command?.func).toBeTypeOf('function');
244
+ const fetchMock = vi.fn()
245
+ .mockResolvedValueOnce({
246
+ ok: true,
247
+ json: () => Promise.resolve({
248
+ books: [
249
+ {
250
+ bookInfo: {
251
+ title: 'ζ–‡ζ˜Ž',
252
+ author: 'δ½œθ€…η”²',
253
+ bookId: 'book-a',
254
+ },
255
+ },
256
+ {
257
+ bookInfo: {
258
+ title: 'ζ–‡ζ˜Ž',
259
+ author: 'δ½œθ€…δΉ™',
260
+ bookId: 'book-b',
261
+ },
262
+ },
263
+ ],
264
+ }),
265
+ })
266
+ .mockResolvedValueOnce({
267
+ ok: true,
268
+ text: () => Promise.resolve(`
269
+ <ul class="search_bookDetail_list">
270
+ <li class="wr_bookList_item">
271
+ <a class="wr_bookList_item_link" href="/web/reader/book-b-reader"></a>
272
+ <p class="wr_bookList_item_title">ζ–‡ζ˜Ž</p>
273
+ </li>
274
+ <li class="wr_bookList_item">
275
+ <a class="wr_bookList_item_link" href="/web/reader/book-a-reader"></a>
276
+ <p class="wr_bookList_item_title">ζ–‡ζ˜Ž</p>
277
+ </li>
278
+ </ul>
279
+ `),
280
+ });
281
+ vi.stubGlobal('fetch', fetchMock);
282
+ const result = await command.func(null, { query: 'ζ–‡ζ˜Ž', limit: 5 });
283
+ expect(result).toEqual([
284
+ {
285
+ rank: 1,
286
+ title: 'ζ–‡ζ˜Ž',
287
+ author: 'δ½œθ€…η”²',
288
+ bookId: 'book-a',
289
+ url: '',
290
+ },
291
+ {
292
+ rank: 2,
293
+ title: 'ζ–‡ζ˜Ž',
294
+ author: 'δ½œθ€…δΉ™',
295
+ bookId: 'book-b',
296
+ url: '',
297
+ },
298
+ ]);
299
+ });
300
+ it('leaves urls empty when exact author matching fails and multiple html cards share the same title', async () => {
301
+ const command = getRegistry().get('weread/search');
302
+ expect(command?.func).toBeTypeOf('function');
303
+ const fetchMock = vi.fn()
304
+ .mockResolvedValueOnce({
305
+ ok: true,
306
+ json: () => Promise.resolve({
307
+ books: [
308
+ {
309
+ bookInfo: {
310
+ title: 'ζ–‡ζ˜Ž',
311
+ author: 'δ½œθ€…η”²',
312
+ bookId: 'book-a',
313
+ },
314
+ },
315
+ ],
316
+ }),
317
+ })
318
+ .mockResolvedValueOnce({
319
+ ok: true,
320
+ text: () => Promise.resolve(`
321
+ <ul class="search_bookDetail_list">
322
+ <li class="wr_bookList_item">
323
+ <a class="wr_bookList_item_link" href="/web/reader/book-a-reader"></a>
324
+ <p class="wr_bookList_item_title">ζ–‡ζ˜Ž</p>
325
+ <p class="wr_bookList_item_author"><a href="/web/search/books?author=%E4%BD%9C%E8%80%85%E4%B9%99">δ½œθ€…δΉ™</a></p>
326
+ </li>
327
+ <li class="wr_bookList_item">
328
+ <a class="wr_bookList_item_link" href="/web/reader/book-a-reader-2"></a>
329
+ <p class="wr_bookList_item_title">ζ–‡ζ˜Ž</p>
330
+ </li>
331
+ </ul>
332
+ `),
333
+ });
334
+ vi.stubGlobal('fetch', fetchMock);
335
+ const result = await command.func(null, { query: 'ζ–‡ζ˜Ž', limit: 5 });
336
+ expect(result).toEqual([
337
+ {
338
+ rank: 1,
339
+ title: 'ζ–‡ζ˜Ž',
340
+ author: 'δ½œθ€…η”²',
341
+ bookId: 'book-a',
342
+ url: '',
343
+ },
344
+ ]);
345
+ });
346
+ it('leaves urls empty when multiple results share the same title and author identity', async () => {
347
+ const command = getRegistry().get('weread/search');
348
+ expect(command?.func).toBeTypeOf('function');
349
+ const fetchMock = vi.fn()
350
+ .mockResolvedValueOnce({
351
+ ok: true,
352
+ json: () => Promise.resolve({
353
+ books: [
354
+ {
355
+ bookInfo: {
356
+ title: 'ζ–‡ζ˜Ž',
357
+ author: 'δ½œθ€…η”²',
358
+ bookId: 'book-a',
359
+ },
360
+ },
361
+ {
362
+ bookInfo: {
363
+ title: 'ζ–‡ζ˜Ž',
364
+ author: 'δ½œθ€…η”²',
365
+ bookId: 'book-b',
366
+ },
367
+ },
368
+ ],
369
+ }),
370
+ })
371
+ .mockResolvedValueOnce({
372
+ ok: true,
373
+ text: () => Promise.resolve(`
374
+ <ul class="search_bookDetail_list">
375
+ <li class="wr_bookList_item">
376
+ <a class="wr_bookList_item_link" href="/web/reader/book-b-reader"></a>
377
+ <p class="wr_bookList_item_title">ζ–‡ζ˜Ž</p>
378
+ <p class="wr_bookList_item_author"><a href="/web/search/books?author=%E4%BD%9C%E8%80%85%E7%94%B2">δ½œθ€…η”²</a></p>
379
+ </li>
380
+ <li class="wr_bookList_item">
381
+ <a class="wr_bookList_item_link" href="/web/reader/book-a-reader"></a>
382
+ <p class="wr_bookList_item_title">ζ–‡ζ˜Ž</p>
383
+ <p class="wr_bookList_item_author"><a href="/web/search/books?author=%E4%BD%9C%E8%80%85%E7%94%B2">δ½œθ€…η”²</a></p>
384
+ </li>
385
+ </ul>
386
+ `),
387
+ });
388
+ vi.stubGlobal('fetch', fetchMock);
389
+ const result = await command.func(null, { query: 'ζ–‡ζ˜Ž', limit: 5 });
390
+ expect(result).toEqual([
391
+ {
392
+ rank: 1,
393
+ title: 'ζ–‡ζ˜Ž',
394
+ author: 'δ½œθ€…η”²',
395
+ bookId: 'book-a',
396
+ url: '',
397
+ },
398
+ {
399
+ rank: 2,
400
+ title: 'ζ–‡ζ˜Ž',
401
+ author: 'δ½œθ€…η”²',
402
+ bookId: 'book-b',
403
+ url: '',
404
+ },
405
+ ]);
406
+ });
407
+ });