@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,8 +1,7 @@
1
1
  import { cli, Strategy } from '../../registry.js';
2
2
  import { CliError } from '../../errors.js';
3
- import { fetchPrivateApi } from './utils.js';
4
- const WEREAD_DOMAIN = 'weread.qq.com';
5
- const WEREAD_SHELF_URL = `https://${WEREAD_DOMAIN}/web/shelf`;
3
+ import { log } from '../../logger.js';
4
+ import { buildWebShelfEntries, fetchPrivateApi, loadWebShelfSnapshot, } from './utils.js';
6
5
  function normalizeShelfLimit(limit) {
7
6
  if (!Number.isFinite(limit))
8
7
  return 0;
@@ -21,99 +20,15 @@ function normalizePrivateApiRows(data, limit) {
21
20
  function normalizeWebShelfRows(snapshot, limit) {
22
21
  if (limit <= 0)
23
22
  return [];
24
- const bookById = new Map();
25
- for (const book of snapshot.rawBooks) {
26
- const bookId = String(book?.bookId || '').trim();
27
- if (!bookId)
28
- continue;
29
- bookById.set(bookId, book);
30
- }
31
- const orderedBookIds = snapshot.shelfIndexes
32
- .filter((entry) => String(entry?.role || 'book') === 'book')
33
- .sort((left, right) => Number(left?.idx ?? Number.MAX_SAFE_INTEGER) - Number(right?.idx ?? Number.MAX_SAFE_INTEGER))
34
- .map((entry) => String(entry?.bookId || '').trim())
35
- .filter(Boolean);
36
- const fallbackOrder = snapshot.rawBooks
37
- .map((book) => String(book?.bookId || '').trim())
38
- .filter(Boolean);
39
- const orderedUniqueBookIds = Array.from(new Set([
40
- ...orderedBookIds,
41
- ...fallbackOrder,
42
- ]));
43
- return orderedUniqueBookIds
44
- .map((bookId) => {
45
- const book = bookById.get(bookId);
46
- if (!book)
47
- return null;
48
- return {
49
- title: String(book.title || '').trim(),
50
- author: String(book.author || '').trim(),
51
- progress: '-',
52
- bookId,
53
- };
54
- })
55
- .filter((item) => Boolean(item && (item.title || item.bookId)))
56
- .slice(0, limit);
57
- }
58
- /**
59
- * Read the structured shelf cache from the web shelf page.
60
- * The page hydrates localStorage with raw book data plus shelf ordering.
61
- */
62
- async function loadWebShelfSnapshot(page) {
63
- await page.goto(WEREAD_SHELF_URL);
64
- const cookies = await page.getCookies({ domain: WEREAD_DOMAIN });
65
- const currentVid = String(cookies.find((cookie) => cookie.name === 'wr_vid')?.value || '').trim();
66
- if (!currentVid) {
67
- return { cacheFound: false, rawBooks: [], shelfIndexes: [] };
68
- }
69
- const rawBooksKey = `shelf:rawBooks:${currentVid}`;
70
- const shelfIndexesKey = `shelf:shelfIndexes:${currentVid}`;
71
- const result = await page.evaluate(`
72
- (() => new Promise((resolve) => {
73
- const deadline = Date.now() + 5000;
74
- const rawBooksKey = ${JSON.stringify(rawBooksKey)};
75
- const shelfIndexesKey = ${JSON.stringify(shelfIndexesKey)};
76
-
77
- const readJson = (raw) => {
78
- if (typeof raw !== 'string') return null;
79
- try {
80
- return JSON.parse(raw);
81
- } catch {
82
- return null;
83
- }
84
- };
85
-
86
- const poll = () => {
87
- const rawBooksRaw = localStorage.getItem(rawBooksKey);
88
- const shelfIndexesRaw = localStorage.getItem(shelfIndexesKey);
89
- const rawBooks = readJson(rawBooksRaw);
90
- const shelfIndexes = readJson(shelfIndexesRaw);
91
- const cacheFound = Array.isArray(rawBooks);
92
-
93
- if (cacheFound || Date.now() >= deadline) {
94
- resolve({
95
- cacheFound,
96
- rawBooks: Array.isArray(rawBooks) ? rawBooks : [],
97
- shelfIndexes: Array.isArray(shelfIndexes) ? shelfIndexes : [],
98
- });
99
- return;
100
- }
101
-
102
- setTimeout(poll, 100);
103
- };
104
-
105
- poll();
23
+ return buildWebShelfEntries(snapshot)
24
+ .map((entry) => ({
25
+ title: entry.title,
26
+ author: entry.author,
27
+ progress: '-',
28
+ bookId: entry.bookId,
106
29
  }))
107
- `);
108
- if (!result || typeof result !== 'object') {
109
- return { cacheFound: false, rawBooks: [], shelfIndexes: [] };
110
- }
111
- const snapshot = result;
112
- return {
113
- cacheFound: snapshot.cacheFound === true,
114
- rawBooks: Array.isArray(snapshot.rawBooks) ? snapshot.rawBooks : [],
115
- shelfIndexes: Array.isArray(snapshot.shelfIndexes) ? snapshot.shelfIndexes : [],
116
- };
30
+ .filter((item) => Boolean(item.title || item.bookId))
31
+ .slice(0, limit);
117
32
  }
118
33
  cli({
119
34
  site: 'weread',
@@ -141,6 +56,9 @@ cli({
141
56
  if (!snapshot.cacheFound) {
142
57
  throw error;
143
58
  }
59
+ // Make the fallback explicit so users do not mistake cached shelf data
60
+ // for a valid private API session.
61
+ log.warn('WeRead private API auth expired; showing cached shelf data from localStorage. Results may be stale, and detail commands may still require re-login.');
144
62
  return normalizeWebShelfRows(snapshot, limit);
145
63
  }
146
64
  },
@@ -6,6 +6,31 @@
6
6
  * - API (i.weread.qq.com/*): private, Node.js fetch with cookies from browser
7
7
  */
8
8
  import type { IPage } from '../../types.js';
9
+ export declare const WEREAD_DOMAIN = "weread.qq.com";
10
+ export declare const WEREAD_WEB_ORIGIN = "https://weread.qq.com";
11
+ export declare const WEREAD_SHELF_URL = "https://weread.qq.com/web/shelf";
12
+ export declare const WEREAD_UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
13
+ export interface WebShelfRawBook {
14
+ bookId?: string;
15
+ title?: string;
16
+ author?: string;
17
+ }
18
+ export interface WebShelfIndexEntry {
19
+ bookId?: string;
20
+ idx?: number;
21
+ role?: string;
22
+ }
23
+ export interface WebShelfSnapshot {
24
+ cacheFound: boolean;
25
+ rawBooks: WebShelfRawBook[];
26
+ shelfIndexes: WebShelfIndexEntry[];
27
+ }
28
+ export interface WebShelfEntry {
29
+ bookId: string;
30
+ title: string;
31
+ author: string;
32
+ readerUrl: string;
33
+ }
9
34
  /**
10
35
  * Fetch a public WeRead web endpoint (Node.js direct fetch).
11
36
  * Used by search and ranking commands (browser: false).
@@ -14,7 +39,28 @@ export declare function fetchWebApi(path: string, params?: Record<string, string
14
39
  /**
15
40
  * Fetch a private WeRead API endpoint with cookies extracted from the browser.
16
41
  * The HTTP request itself runs in Node.js to avoid page-context CORS failures.
42
+ *
43
+ * Cookies are collected from both the API subdomain (i.weread.qq.com) and the
44
+ * main domain (weread.qq.com). WeRead may set auth cookies as host-only on
45
+ * weread.qq.com, which won't match i.weread.qq.com in a URL-based lookup.
17
46
  */
18
47
  export declare function fetchPrivateApi(page: IPage, path: string, params?: Record<string, string>): Promise<any>;
48
+ /**
49
+ * Build stable shelf records from the web cache plus optional rendered reader URLs.
50
+ * We only trust shelfIndexes when it fully covers the same bookId set as rawBooks;
51
+ * otherwise we keep rawBooks order to avoid partial hydration reordering entries.
52
+ */
53
+ export declare function buildWebShelfEntries(snapshot: WebShelfSnapshot, readerUrls?: string[]): WebShelfEntry[];
54
+ /**
55
+ * Read the structured shelf cache from the WeRead shelf page.
56
+ * The page hydrates localStorage asynchronously, so we poll briefly before
57
+ * giving up and treating the cache as unavailable for the current session.
58
+ */
59
+ export declare function loadWebShelfSnapshot(page: IPage): Promise<WebShelfSnapshot>;
60
+ /**
61
+ * Resolve a shelf bookId to the current web reader URL by pairing structured
62
+ * shelf cache order with the visible shelf links rendered on the page.
63
+ */
64
+ export declare function resolveShelfReaderUrl(page: IPage, bookId: string): Promise<string | null>;
19
65
  /** Format a Unix timestamp (seconds) to YYYY-MM-DD in UTC+8. Returns '-' for invalid input. */
20
66
  export declare function formatDate(ts: number | undefined | null): string;
@@ -6,9 +6,12 @@
6
6
  * - API (i.weread.qq.com/*): private, Node.js fetch with cookies from browser
7
7
  */
8
8
  import { CliError } from '../../errors.js';
9
- const WEB_API = 'https://weread.qq.com/web';
10
- const API = 'https://i.weread.qq.com';
11
- const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
9
+ export const WEREAD_DOMAIN = 'weread.qq.com';
10
+ export const WEREAD_WEB_ORIGIN = `https://${WEREAD_DOMAIN}`;
11
+ export const WEREAD_SHELF_URL = `${WEREAD_WEB_ORIGIN}/web/shelf`;
12
+ const WEB_API = `${WEREAD_WEB_ORIGIN}/web`;
13
+ const API = `https://i.${WEREAD_DOMAIN}`;
14
+ export const WEREAD_UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
12
15
  const WEREAD_AUTH_ERRCODES = new Set([-2010, -2012]);
13
16
  function buildCookieHeader(cookies) {
14
17
  return cookies.map((cookie) => `${cookie.name}=${cookie.value}`).join('; ');
@@ -16,6 +19,84 @@ function buildCookieHeader(cookies) {
16
19
  function isAuthErrorResponse(resp, data) {
17
20
  return resp.status === 401 || WEREAD_AUTH_ERRCODES.has(Number(data?.errcode));
18
21
  }
22
+ function getCurrentVid(cookies) {
23
+ return String(cookies.find((cookie) => cookie.name === 'wr_vid')?.value || '').trim();
24
+ }
25
+ function getWebShelfStorageKeys(currentVid) {
26
+ return {
27
+ rawBooksKey: `shelf:rawBooks:${currentVid}`,
28
+ shelfIndexesKey: `shelf:shelfIndexes:${currentVid}`,
29
+ };
30
+ }
31
+ function normalizeWebShelfSnapshot(value) {
32
+ return {
33
+ cacheFound: value?.cacheFound === true,
34
+ rawBooks: Array.isArray(value?.rawBooks) ? value.rawBooks : [],
35
+ shelfIndexes: Array.isArray(value?.shelfIndexes) ? value.shelfIndexes : [],
36
+ };
37
+ }
38
+ function buildShelfSnapshotPollScript(storageKeys, requireTrustedIndexes) {
39
+ return `
40
+ (() => new Promise((resolve) => {
41
+ const deadline = Date.now() + 5000;
42
+ const rawBooksKey = ${JSON.stringify(storageKeys.rawBooksKey)};
43
+ const shelfIndexesKey = ${JSON.stringify(storageKeys.shelfIndexesKey)};
44
+ const requireTrustedIndexes = ${JSON.stringify(requireTrustedIndexes)};
45
+
46
+ const readJson = (raw) => {
47
+ if (typeof raw !== 'string') return null;
48
+ try {
49
+ return JSON.parse(raw);
50
+ } catch {
51
+ return null;
52
+ }
53
+ };
54
+
55
+ const collectBookIds = (items) => Array.isArray(items)
56
+ ? Array.from(new Set(items.map((item) => String(item?.bookId || '').trim()).filter(Boolean)))
57
+ : [];
58
+
59
+ // Mirror of getTrustedIndexedBookIds in Node.js — keep in sync
60
+ const hasTrustedIndexes = (rawBooks, shelfIndexes) => {
61
+ const rawBookIds = collectBookIds(rawBooks);
62
+ if (rawBookIds.length === 0) return false;
63
+
64
+ const rawBookIdSet = new Set(rawBookIds);
65
+ const projectedIndexedBookIds = Array.isArray(shelfIndexes)
66
+ ? Array.from(new Set(
67
+ shelfIndexes
68
+ .filter((entry) => Number.isFinite(entry?.idx))
69
+ .sort((left, right) => Number(left?.idx ?? Number.MAX_SAFE_INTEGER) - Number(right?.idx ?? Number.MAX_SAFE_INTEGER))
70
+ .map((entry) => String(entry?.bookId || '').trim())
71
+ .filter((bookId) => rawBookIdSet.has(bookId)),
72
+ ))
73
+ : [];
74
+
75
+ return projectedIndexedBookIds.length === rawBookIds.length;
76
+ };
77
+
78
+ const poll = () => {
79
+ const rawBooks = readJson(localStorage.getItem(rawBooksKey));
80
+ const shelfIndexes = readJson(localStorage.getItem(shelfIndexesKey));
81
+ const cacheFound = Array.isArray(rawBooks);
82
+ const ready = cacheFound && (!requireTrustedIndexes || hasTrustedIndexes(rawBooks, shelfIndexes));
83
+
84
+ if (ready || Date.now() >= deadline) {
85
+ resolve({
86
+ cacheFound,
87
+ rawBooks: Array.isArray(rawBooks) ? rawBooks : [],
88
+ shelfIndexes: Array.isArray(shelfIndexes) ? shelfIndexes : [],
89
+ });
90
+ return;
91
+ }
92
+
93
+ setTimeout(poll, 100);
94
+ };
95
+
96
+ poll();
97
+ }))
98
+ `;
99
+ }
19
100
  /**
20
101
  * Fetch a public WeRead web endpoint (Node.js direct fetch).
21
102
  * Used by search and ranking commands (browser: false).
@@ -27,7 +108,7 @@ export async function fetchWebApi(path, params) {
27
108
  url.searchParams.set(k, v);
28
109
  }
29
110
  const resp = await fetch(url.toString(), {
30
- headers: { 'User-Agent': UA },
111
+ headers: { 'User-Agent': WEREAD_UA },
31
112
  });
32
113
  if (!resp.ok) {
33
114
  throw new CliError('FETCH_ERROR', `HTTP ${resp.status} for ${path}`, 'WeRead API may be temporarily unavailable');
@@ -42,6 +123,10 @@ export async function fetchWebApi(path, params) {
42
123
  /**
43
124
  * Fetch a private WeRead API endpoint with cookies extracted from the browser.
44
125
  * The HTTP request itself runs in Node.js to avoid page-context CORS failures.
126
+ *
127
+ * Cookies are collected from both the API subdomain (i.weread.qq.com) and the
128
+ * main domain (weread.qq.com). WeRead may set auth cookies as host-only on
129
+ * weread.qq.com, which won't match i.weread.qq.com in a URL-based lookup.
45
130
  */
46
131
  export async function fetchPrivateApi(page, path, params) {
47
132
  const url = new URL(`${API}${path}`);
@@ -50,13 +135,22 @@ export async function fetchPrivateApi(page, path, params) {
50
135
  url.searchParams.set(k, v);
51
136
  }
52
137
  const urlStr = url.toString();
53
- const cookies = await page.getCookies({ url: urlStr });
54
- const cookieHeader = buildCookieHeader(cookies);
138
+ // Merge cookies from both domains; API-domain cookies take precedence on name collision
139
+ const [apiCookies, domainCookies] = await Promise.all([
140
+ page.getCookies({ url: urlStr }),
141
+ page.getCookies({ domain: WEREAD_DOMAIN }),
142
+ ]);
143
+ const merged = new Map();
144
+ for (const c of domainCookies)
145
+ merged.set(c.name, c);
146
+ for (const c of apiCookies)
147
+ merged.set(c.name, c);
148
+ const cookieHeader = buildCookieHeader(Array.from(merged.values()));
55
149
  let resp;
56
150
  try {
57
151
  resp = await fetch(urlStr, {
58
152
  headers: {
59
- 'User-Agent': UA,
153
+ 'User-Agent': WEREAD_UA,
60
154
  'Origin': 'https://weread.qq.com',
61
155
  'Referer': 'https://weread.qq.com/',
62
156
  ...(cookieHeader ? { 'Cookie': cookieHeader } : {}),
@@ -84,6 +178,119 @@ export async function fetchPrivateApi(page, path, params) {
84
178
  }
85
179
  return data;
86
180
  }
181
+ function getUniqueRawBookIds(snapshot) {
182
+ return Array.from(new Set(snapshot.rawBooks
183
+ .map((book) => String(book?.bookId || '').trim())
184
+ .filter(Boolean)));
185
+ }
186
+ /** Mirror of hasTrustedIndexes in buildShelfSnapshotPollScript — keep in sync */
187
+ function getTrustedIndexedBookIds(snapshot) {
188
+ const rawBookIds = getUniqueRawBookIds(snapshot);
189
+ if (rawBookIds.length === 0)
190
+ return [];
191
+ const rawBookIdSet = new Set(rawBookIds);
192
+ const projectedIndexedBookIds = Array.from(new Set(snapshot.shelfIndexes
193
+ .filter((entry) => Number.isFinite(entry?.idx))
194
+ .sort((left, right) => Number(left?.idx ?? Number.MAX_SAFE_INTEGER) - Number(right?.idx ?? Number.MAX_SAFE_INTEGER))
195
+ .map((entry) => String(entry?.bookId || '').trim())
196
+ .filter((bookId) => rawBookIdSet.has(bookId))));
197
+ return projectedIndexedBookIds.length === rawBookIds.length ? projectedIndexedBookIds : [];
198
+ }
199
+ /**
200
+ * Build stable shelf records from the web cache plus optional rendered reader URLs.
201
+ * We only trust shelfIndexes when it fully covers the same bookId set as rawBooks;
202
+ * otherwise we keep rawBooks order to avoid partial hydration reordering entries.
203
+ */
204
+ export function buildWebShelfEntries(snapshot, readerUrls = []) {
205
+ const rawBookIds = getUniqueRawBookIds(snapshot);
206
+ const trustedIndexedBookIds = getTrustedIndexedBookIds(snapshot);
207
+ const orderedBookIds = trustedIndexedBookIds.length > 0 ? trustedIndexedBookIds : rawBookIds;
208
+ const rawBookById = new Map();
209
+ for (const book of snapshot.rawBooks) {
210
+ const bookId = String(book?.bookId || '').trim();
211
+ if (!bookId || rawBookById.has(bookId))
212
+ continue;
213
+ rawBookById.set(bookId, book);
214
+ }
215
+ return orderedBookIds.map((bookId, index) => {
216
+ const book = rawBookById.get(bookId);
217
+ return {
218
+ bookId,
219
+ title: String(book?.title || '').trim(),
220
+ author: String(book?.author || '').trim(),
221
+ readerUrl: String(readerUrls[index] || '').trim(),
222
+ };
223
+ });
224
+ }
225
+ /**
226
+ * Internal: load shelf snapshot and return the currentVid alongside it,
227
+ * so callers like resolveShelfReaderUrl can reuse it without a second getCookies.
228
+ */
229
+ async function loadWebShelfSnapshotWithVid(page) {
230
+ await page.goto(WEREAD_SHELF_URL);
231
+ const cookies = await page.getCookies({ domain: WEREAD_DOMAIN });
232
+ const currentVid = getCurrentVid(cookies);
233
+ if (!currentVid) {
234
+ return { snapshot: { cacheFound: false, rawBooks: [], shelfIndexes: [] }, currentVid: '' };
235
+ }
236
+ const result = await page.evaluate(buildShelfSnapshotPollScript(getWebShelfStorageKeys(currentVid), false));
237
+ return {
238
+ snapshot: normalizeWebShelfSnapshot(result),
239
+ currentVid,
240
+ };
241
+ }
242
+ /**
243
+ * Read the structured shelf cache from the WeRead shelf page.
244
+ * The page hydrates localStorage asynchronously, so we poll briefly before
245
+ * giving up and treating the cache as unavailable for the current session.
246
+ */
247
+ export async function loadWebShelfSnapshot(page) {
248
+ const { snapshot } = await loadWebShelfSnapshotWithVid(page);
249
+ return snapshot;
250
+ }
251
+ /**
252
+ * `book` needs a trustworthy `bookId -> readerUrl` mapping, which may lag behind
253
+ * the first rawBooks cache hydration. Keep the fast shelf fallback path separate
254
+ * and only wait here, with a bounded poll, when resolving reader URLs.
255
+ */
256
+ async function waitForTrustedWebShelfSnapshot(page, snapshot, currentVid) {
257
+ // Cache not available; nothing to wait for
258
+ if (!snapshot.cacheFound)
259
+ return snapshot;
260
+ // Indexes already fully cover rawBooks; no need to re-poll
261
+ if (getTrustedIndexedBookIds(snapshot).length > 0)
262
+ return snapshot;
263
+ if (!currentVid)
264
+ return snapshot;
265
+ const result = await page.evaluate(buildShelfSnapshotPollScript(getWebShelfStorageKeys(currentVid), true));
266
+ return normalizeWebShelfSnapshot(result);
267
+ }
268
+ /**
269
+ * Resolve a shelf bookId to the current web reader URL by pairing structured
270
+ * shelf cache order with the visible shelf links rendered on the page.
271
+ */
272
+ export async function resolveShelfReaderUrl(page, bookId) {
273
+ const { snapshot: initialSnapshot, currentVid } = await loadWebShelfSnapshotWithVid(page);
274
+ const snapshot = await waitForTrustedWebShelfSnapshot(page, initialSnapshot, currentVid);
275
+ if (!snapshot.cacheFound)
276
+ return null;
277
+ const trustedIndexedBookIds = getTrustedIndexedBookIds(snapshot);
278
+ if (trustedIndexedBookIds.length === 0)
279
+ return null;
280
+ const readerUrls = await page.evaluate(`
281
+ (() => Array.from(document.querySelectorAll('a.shelfBook[href]'))
282
+ .map((anchor) => {
283
+ const href = anchor.getAttribute('href') || '';
284
+ return href ? new URL(href, location.origin).toString() : '';
285
+ })
286
+ .filter(Boolean))
287
+ `);
288
+ if (readerUrls.length !== trustedIndexedBookIds.length)
289
+ return null;
290
+ const entries = buildWebShelfEntries(snapshot, readerUrls);
291
+ const entry = entries.find((candidate) => candidate.bookId === bookId);
292
+ return entry?.readerUrl || null;
293
+ }
87
294
  /** Format a Unix timestamp (seconds) to YYYY-MM-DD in UTC+8. Returns '-' for invalid input. */
88
295
  export function formatDate(ts) {
89
296
  if (!Number.isFinite(ts) || ts <= 0)
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { formatDate, fetchWebApi } from './utils.js';
2
+ import { buildWebShelfEntries, formatDate, fetchWebApi } from './utils.js';
3
3
  describe('formatDate', () => {
4
4
  it('formats a typical Unix timestamp in UTC+8', () => {
5
5
  // 1705276800 = 2024-01-15 00:00:00 UTC = 2024-01-15 08:00:00 Beijing
@@ -56,3 +56,73 @@ describe('fetchWebApi', () => {
56
56
  await expect(fetchWebApi('/search/global')).rejects.toThrow('Invalid JSON');
57
57
  });
58
58
  });
59
+ describe('buildWebShelfEntries', () => {
60
+ it('keeps mixed shelf item reader urls aligned when shelf indexes include non-book roles', () => {
61
+ const result = buildWebShelfEntries({
62
+ cacheFound: true,
63
+ rawBooks: [
64
+ { bookId: 'MP_WXS_1', title: '公众号文章一', author: '作者甲' },
65
+ { bookId: 'BOOK_2', title: '普通书二', author: '作者乙' },
66
+ { bookId: 'MP_WXS_3', title: '公众号文章三', author: '作者丙' },
67
+ ],
68
+ shelfIndexes: [
69
+ { bookId: 'MP_WXS_1', idx: 0, role: 'mp' },
70
+ { bookId: 'BOOK_2', idx: 1, role: 'book' },
71
+ { bookId: 'MP_WXS_3', idx: 2, role: 'mp' },
72
+ ],
73
+ }, [
74
+ 'https://weread.qq.com/web/reader/mp1',
75
+ 'https://weread.qq.com/web/reader/book2',
76
+ 'https://weread.qq.com/web/reader/mp3',
77
+ ]);
78
+ expect(result).toEqual([
79
+ {
80
+ bookId: 'MP_WXS_1',
81
+ title: '公众号文章一',
82
+ author: '作者甲',
83
+ readerUrl: 'https://weread.qq.com/web/reader/mp1',
84
+ },
85
+ {
86
+ bookId: 'BOOK_2',
87
+ title: '普通书二',
88
+ author: '作者乙',
89
+ readerUrl: 'https://weread.qq.com/web/reader/book2',
90
+ },
91
+ {
92
+ bookId: 'MP_WXS_3',
93
+ title: '公众号文章三',
94
+ author: '作者丙',
95
+ readerUrl: 'https://weread.qq.com/web/reader/mp3',
96
+ },
97
+ ]);
98
+ });
99
+ it('falls back to raw cache order when shelf indexes are incomplete', () => {
100
+ const result = buildWebShelfEntries({
101
+ cacheFound: true,
102
+ rawBooks: [
103
+ { bookId: 'BOOK_1', title: '第一本', author: '作者甲' },
104
+ { bookId: 'BOOK_2', title: '第二本', author: '作者乙' },
105
+ ],
106
+ shelfIndexes: [
107
+ { bookId: 'BOOK_2', idx: 0, role: 'book' },
108
+ ],
109
+ }, [
110
+ 'https://weread.qq.com/web/reader/book1',
111
+ 'https://weread.qq.com/web/reader/book2',
112
+ ]);
113
+ expect(result).toEqual([
114
+ {
115
+ bookId: 'BOOK_1',
116
+ title: '第一本',
117
+ author: '作者甲',
118
+ readerUrl: 'https://weread.qq.com/web/reader/book1',
119
+ },
120
+ {
121
+ bookId: 'BOOK_2',
122
+ title: '第二本',
123
+ author: '作者乙',
124
+ readerUrl: 'https://weread.qq.com/web/reader/book2',
125
+ },
126
+ ]);
127
+ });
128
+ });
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Flow:
5
5
  * 1. Navigate to creator publish page
6
- * 2. Upload images via DataTransfer injection into the file input
6
+ * 2. Upload images via CDP DOM.setFileInputFiles (with base64 fallback)
7
7
  * 3. Fill title and body text
8
8
  * 4. Add topic hashtags
9
9
  * 5. Publish (or save as draft)