@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
@@ -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)
@@ -43,44 +43,98 @@ const TITLE_SELECTORS = [
43
43
  'input[maxlength]',
44
44
  ];
45
45
 
46
- type ImagePayload = { name: string; mimeType: string; base64: string };
46
+ const SUPPORTED_EXTENSIONS: Record<string, string> = {
47
+ '.jpg': 'image/jpeg',
48
+ '.jpeg': 'image/jpeg',
49
+ '.png': 'image/png',
50
+ '.gif': 'image/gif',
51
+ '.webp': 'image/webp',
52
+ };
47
53
 
48
54
  /**
49
- * Read a local image and return the name, MIME type, and base64 content.
50
- * Throws if the file does not exist or the extension is unsupported.
55
+ * Validate image paths: check existence and extension.
56
+ * Returns resolved absolute paths.
51
57
  */
52
- function readImageFile(filePath: string): ImagePayload {
53
- const absPath = path.resolve(filePath);
54
- if (!fs.existsSync(absPath)) throw new Error(`Image file not found: ${absPath}`);
55
- const ext = path.extname(absPath).toLowerCase();
56
- const mimeMap: Record<string, string> = {
57
- '.jpg': 'image/jpeg',
58
- '.jpeg': 'image/jpeg',
59
- '.png': 'image/png',
60
- '.gif': 'image/gif',
61
- '.webp': 'image/webp',
62
- };
63
- const mimeType = mimeMap[ext];
64
- if (!mimeType) throw new Error(`Unsupported image format "${ext}". Supported: jpg, png, gif, webp`);
65
- const base64 = fs.readFileSync(absPath).toString('base64');
66
- return { name: path.basename(absPath), mimeType, base64 };
58
+ function validateImagePaths(filePaths: string[]): string[] {
59
+ return filePaths.map((filePath) => {
60
+ const absPath = path.resolve(filePath);
61
+ if (!fs.existsSync(absPath)) throw new Error(`Image file not found: ${absPath}`);
62
+ const ext = path.extname(absPath).toLowerCase();
63
+ if (!SUPPORTED_EXTENSIONS[ext]) {
64
+ throw new Error(`Unsupported image format "${ext}". Supported: jpg, png, gif, webp`);
65
+ }
66
+ return absPath;
67
+ });
67
68
  }
68
69
 
70
+ /** CSS selector for image-accepting file inputs. */
71
+ const IMAGE_INPUT_SELECTOR = 'input[type="file"][accept*="image"],'
72
+ + 'input[type="file"][accept*=".jpg"],'
73
+ + 'input[type="file"][accept*=".jpeg"],'
74
+ + 'input[type="file"][accept*=".png"],'
75
+ + 'input[type="file"][accept*=".gif"],'
76
+ + 'input[type="file"][accept*=".webp"]';
77
+
69
78
  /**
70
- * Inject images into the page's file input using DataTransfer.
71
- * Converts base64 payloads to File objects in the browser context, then dispatches
72
- * a synthetic 'change' event on the input element.
79
+ * Upload images via CDP DOM.setFileInputFiles Chrome reads files directly
80
+ * from the local filesystem, avoiding base64 payload size limits.
73
81
  *
74
- * Returns { ok, count, error }.
82
+ * Falls back to the legacy base64 DataTransfer approach if the extension
83
+ * does not support set-file-input (e.g. older extension version).
75
84
  */
76
- async function injectImages(page: IPage, images: ImagePayload[]): Promise<{ ok: boolean; count: number; error?: string }> {
85
+ async function uploadImages(
86
+ page: IPage,
87
+ absPaths: string[],
88
+ ): Promise<{ ok: boolean; count: number; error?: string }> {
89
+ // ── Primary: CDP DOM.setFileInputFiles ──────────────────────────────
90
+ if (page.setFileInput) {
91
+ try {
92
+ // Find image-accepting file input on the page
93
+ const selector: string | null = await page.evaluate(`
94
+ (() => {
95
+ const sels = ${JSON.stringify(IMAGE_INPUT_SELECTOR)};
96
+ const el = document.querySelector(sels);
97
+ return el ? sels : null;
98
+ })()
99
+ `);
100
+ if (!selector) {
101
+ return { ok: false, count: 0, error: 'No file input found on page' };
102
+ }
103
+ await page.setFileInput(absPaths, selector);
104
+ return { ok: true, count: absPaths.length };
105
+ } catch (err) {
106
+ // If set-file-input action is not supported by extension, fall through to legacy
107
+ const msg = err instanceof Error ? err.message : String(err);
108
+ if (msg.includes('Unknown action') || msg.includes('not supported')) {
109
+ // Extension too old — fall through to legacy base64 method
110
+ } else {
111
+ return { ok: false, count: 0, error: msg };
112
+ }
113
+ }
114
+ }
115
+
116
+ // ── Fallback: legacy base64 DataTransfer injection ─────────────────
117
+ const images = absPaths.map((absPath) => {
118
+ const base64 = fs.readFileSync(absPath).toString('base64');
119
+ const ext = path.extname(absPath).toLowerCase();
120
+ return { name: path.basename(absPath), mimeType: SUPPORTED_EXTENSIONS[ext], base64 };
121
+ });
122
+
123
+ // Warn if total payload is large — this may fail with older extensions
124
+ const totalBytes = images.reduce((sum, img) => sum + img.base64.length, 0);
125
+ if (totalBytes > 500_000) {
126
+ console.warn(
127
+ `[warn] Total image payload is ${(totalBytes / 1024 / 1024).toFixed(1)}MB (base64). ` +
128
+ 'This may fail with the browser bridge. Update the extension to v1.6+ for CDP-based upload, ' +
129
+ 'or compress images before publishing.'
130
+ );
131
+ }
132
+
77
133
  const payload = JSON.stringify(images);
78
134
  return page.evaluate(`
79
135
  (async () => {
80
136
  const images = ${payload};
81
137
 
82
- // Only use image-capable file inputs. Do not fall back to a generic uploader,
83
- // otherwise we can accidentally feed images into the video upload flow.
84
138
  const inputs = Array.from(document.querySelectorAll('input[type="file"]'));
85
139
  const input = inputs.find(el => {
86
140
  const accept = el.getAttribute('accept') || '';
@@ -346,8 +400,8 @@ cli({
346
400
  if (imagePaths.length > MAX_IMAGES)
347
401
  throw new Error(`Too many images: ${imagePaths.length} (max ${MAX_IMAGES})`);
348
402
 
349
- // Read images in Node.js context before navigating (fast-fail on bad paths)
350
- const imageData: ImagePayload[] = imagePaths.map(readImageFile);
403
+ // Validate image paths before navigating (fast-fail on bad paths / unsupported formats)
404
+ const absImagePaths = validateImagePaths(imagePaths);
351
405
 
352
406
  // ── Step 1: Navigate to publish page ──────────────────────────────────────
353
407
  await page.goto(PUBLISH_URL);
@@ -377,7 +431,7 @@ cli({
377
431
  }
378
432
 
379
433
  // ── Step 3: Upload images ──────────────────────────────────────────────────
380
- const upload = await injectImages(page, imageData);
434
+ const upload = await uploadImages(page, absImagePaths);
381
435
  if (!upload.ok) {
382
436
  await page.screenshot({ path: '/tmp/xhs_publish_upload_debug.png' });
383
437
  throw new Error(
@@ -532,7 +586,7 @@ cli({
532
586
  status: isSuccess ? `✅ ${verb}` : '⚠️ 操作完成,请在浏览器中确认',
533
587
  detail: [
534
588
  `"${title}"`,
535
- `${imageData.length}张图片`,
589
+ `${absImagePaths.length}张图片`,
536
590
  topics.length ? `话题: ${topics.join(' ')}` : '',
537
591
  successMsg || finalUrl || '',
538
592
  ]
@@ -75,6 +75,7 @@ describe('extractXhsUserNotes', () => {
75
75
  title: 'First note',
76
76
  type: 'video',
77
77
  likes: '4.6万',
78
+ cover: '',
78
79
  url: 'https://www.xiaohongshu.com/user/profile/user-1/note-1?xsec_token=abc&xsec_source=pc_user',
79
80
  },
80
81
  {
@@ -82,11 +83,33 @@ describe('extractXhsUserNotes', () => {
82
83
  title: 'Second note',
83
84
  type: 'normal',
84
85
  likes: '42',
86
+ cover: '',
85
87
  url: 'https://www.xiaohongshu.com/user/profile/fallback-user/note-2',
86
88
  },
87
89
  ]);
88
90
  });
89
91
 
92
+ it('extracts cover urls with fallback priority urlDefault -> urlPre -> url', () => {
93
+ const rows = extractXhsUserNotes(
94
+ {
95
+ noteGroups: [
96
+ [
97
+ { noteCard: { noteId: 'cover-1', cover: { urlDefault: 'https://img.example/default.jpg', urlPre: 'https://img.example/pre.jpg', url: 'https://img.example/raw.jpg' } } },
98
+ { noteCard: { noteId: 'cover-2', cover: { urlPre: 'https://img.example/pre-only.jpg', url: 'https://img.example/raw-only.jpg' } } },
99
+ { noteCard: { noteId: 'cover-3', cover: { url: 'https://img.example/raw-fallback.jpg' } } },
100
+ ],
101
+ ],
102
+ },
103
+ 'fallback-user'
104
+ );
105
+
106
+ expect(rows.map(row => row.cover)).toEqual([
107
+ 'https://img.example/default.jpg',
108
+ 'https://img.example/pre-only.jpg',
109
+ 'https://img.example/raw-fallback.jpg',
110
+ ]);
111
+ });
112
+
90
113
  it('deduplicates repeated notes by note id', () => {
91
114
  const rows = extractXhsUserNotes(
92
115
  {
@@ -8,6 +8,7 @@ export interface XhsUserNoteRow {
8
8
  title: string;
9
9
  type: string;
10
10
  likes: string;
11
+ cover: string;
11
12
  url: string;
12
13
  }
13
14
 
@@ -72,11 +73,14 @@ export function extractXhsUserNotes(snapshot: XhsUserPageSnapshot, fallbackUserI
72
73
  const xsecToken = toCleanString(entry?.xsecToken ?? entry?.xsec_token ?? noteCard.xsecToken ?? noteCard.xsec_token);
73
74
  const likes = toCleanString(noteCard.interactInfo?.likedCount ?? noteCard.interact_info?.liked_count ?? 0) || '0';
74
75
 
76
+ const cover = toCleanString(noteCard.cover?.urlDefault ?? noteCard.cover?.urlPre ?? noteCard.cover?.url ?? '');
77
+
75
78
  rows.push({
76
79
  id: noteId,
77
80
  title: toCleanString(noteCard.displayTitle ?? noteCard.display_title ?? noteCard.title),
78
81
  type: toCleanString(noteCard.type),
79
82
  likes,
83
+ cover,
80
84
  url: buildXhsNoteUrl(userId || fallbackUserId, noteId, xsecToken),
81
85
  });
82
86
  }