@jackwener/opencli 0.9.5 → 0.9.8

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 (270) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.yml +83 -0
  2. package/.github/ISSUE_TEMPLATE/config.yml +8 -0
  3. package/.github/ISSUE_TEMPLATE/feature_request.yml +42 -0
  4. package/.github/ISSUE_TEMPLATE/new_site_adapter.yml +57 -0
  5. package/.github/dependabot.yml +27 -0
  6. package/.github/pull_request_template.md +24 -0
  7. package/.github/workflows/ci.yml +14 -8
  8. package/.github/workflows/e2e-headed.yml +6 -2
  9. package/.github/workflows/pkg-pr-new.yml +2 -2
  10. package/.github/workflows/release-please.yml +25 -0
  11. package/.github/workflows/release.yml +2 -2
  12. package/.github/workflows/security.yml +36 -0
  13. package/CLI-ELECTRON.md +89 -36
  14. package/CONTRIBUTING.md +167 -0
  15. package/README.md +98 -32
  16. package/README.zh-CN.md +99 -33
  17. package/dist/browser/discover.js +22 -7
  18. package/dist/browser.test.js +23 -0
  19. package/dist/build-manifest.d.ts +26 -0
  20. package/dist/build-manifest.js +132 -60
  21. package/dist/build-manifest.test.d.ts +1 -0
  22. package/dist/build-manifest.test.js +26 -0
  23. package/dist/cli-manifest.json +1875 -271
  24. package/dist/clis/antigravity/model.js +2 -2
  25. package/dist/clis/antigravity/send.js +2 -2
  26. package/dist/clis/bilibili/download.d.ts +10 -0
  27. package/dist/clis/bilibili/download.js +135 -0
  28. package/dist/clis/chatgpt/ask.d.ts +1 -0
  29. package/dist/clis/chatgpt/ask.js +68 -0
  30. package/dist/clis/chatgpt/send.js +11 -0
  31. package/dist/clis/chatwise/ask.d.ts +1 -0
  32. package/dist/clis/chatwise/ask.js +76 -0
  33. package/dist/clis/chatwise/export.d.ts +1 -0
  34. package/dist/clis/chatwise/export.js +46 -0
  35. package/dist/clis/chatwise/history.d.ts +1 -0
  36. package/dist/clis/chatwise/history.js +43 -0
  37. package/dist/clis/chatwise/model.d.ts +1 -0
  38. package/dist/clis/chatwise/model.js +81 -0
  39. package/dist/clis/chatwise/new.d.ts +1 -0
  40. package/dist/clis/chatwise/new.js +18 -0
  41. package/dist/clis/chatwise/read.d.ts +1 -0
  42. package/dist/clis/chatwise/read.js +39 -0
  43. package/dist/clis/chatwise/screenshot.d.ts +1 -0
  44. package/dist/clis/chatwise/screenshot.js +27 -0
  45. package/dist/clis/chatwise/send.d.ts +1 -0
  46. package/dist/clis/chatwise/send.js +45 -0
  47. package/dist/clis/chatwise/status.d.ts +1 -0
  48. package/dist/clis/chatwise/status.js +22 -0
  49. package/dist/clis/codex/ask.d.ts +1 -0
  50. package/dist/clis/codex/ask.js +67 -0
  51. package/dist/clis/codex/export.d.ts +1 -0
  52. package/dist/clis/codex/export.js +37 -0
  53. package/dist/clis/codex/history.d.ts +1 -0
  54. package/dist/clis/codex/history.js +43 -0
  55. package/dist/clis/codex/read.js +3 -5
  56. package/dist/clis/codex/screenshot.d.ts +1 -0
  57. package/dist/clis/codex/screenshot.js +27 -0
  58. package/dist/clis/codex/send.js +3 -6
  59. package/dist/clis/codex/status.js +2 -1
  60. package/dist/clis/cursor/ask.d.ts +1 -0
  61. package/dist/clis/cursor/ask.js +69 -0
  62. package/dist/clis/cursor/composer.js +9 -28
  63. package/dist/clis/cursor/export.d.ts +1 -0
  64. package/dist/clis/cursor/export.js +51 -0
  65. package/dist/clis/cursor/history.d.ts +1 -0
  66. package/dist/clis/cursor/history.js +43 -0
  67. package/dist/clis/cursor/new.js +4 -13
  68. package/dist/clis/cursor/screenshot.d.ts +1 -0
  69. package/dist/clis/cursor/screenshot.js +31 -0
  70. package/dist/clis/discord-app/channels.d.ts +1 -0
  71. package/dist/clis/discord-app/channels.js +45 -0
  72. package/dist/clis/discord-app/members.d.ts +1 -0
  73. package/dist/clis/discord-app/members.js +38 -0
  74. package/dist/clis/discord-app/read.d.ts +1 -0
  75. package/dist/clis/discord-app/read.js +45 -0
  76. package/dist/clis/discord-app/search.d.ts +1 -0
  77. package/dist/clis/discord-app/search.js +56 -0
  78. package/dist/clis/discord-app/send.d.ts +1 -0
  79. package/dist/clis/discord-app/send.js +27 -0
  80. package/dist/clis/discord-app/servers.d.ts +1 -0
  81. package/dist/clis/discord-app/servers.js +36 -0
  82. package/dist/clis/discord-app/status.d.ts +1 -0
  83. package/dist/clis/discord-app/status.js +16 -0
  84. package/dist/clis/feishu/new.d.ts +1 -0
  85. package/dist/clis/feishu/new.js +27 -0
  86. package/dist/clis/feishu/read.d.ts +1 -0
  87. package/dist/clis/feishu/read.js +40 -0
  88. package/dist/clis/feishu/search.d.ts +1 -0
  89. package/dist/clis/feishu/search.js +30 -0
  90. package/dist/clis/feishu/send.d.ts +1 -0
  91. package/dist/clis/feishu/send.js +39 -0
  92. package/dist/clis/feishu/status.d.ts +1 -0
  93. package/dist/clis/feishu/status.js +28 -0
  94. package/dist/clis/grok/ask.d.ts +1 -0
  95. package/dist/clis/grok/ask.js +82 -0
  96. package/dist/clis/grok/debug.d.ts +1 -0
  97. package/dist/clis/grok/debug.js +45 -0
  98. package/dist/clis/jimeng/generate.yaml +84 -0
  99. package/dist/clis/jimeng/history.yaml +47 -0
  100. package/dist/clis/linux-do/categories.yaml +41 -0
  101. package/dist/clis/linux-do/category.yaml +49 -0
  102. package/dist/clis/linux-do/hot.yaml +50 -0
  103. package/dist/clis/linux-do/latest.yaml +40 -0
  104. package/dist/clis/linux-do/search.yaml +45 -0
  105. package/dist/clis/linux-do/topic.yaml +38 -0
  106. package/dist/clis/notion/export.d.ts +1 -0
  107. package/dist/clis/notion/export.js +31 -0
  108. package/dist/clis/notion/favorites.d.ts +1 -0
  109. package/dist/clis/notion/favorites.js +84 -0
  110. package/dist/clis/notion/new.d.ts +1 -0
  111. package/dist/clis/notion/new.js +34 -0
  112. package/dist/clis/notion/read.d.ts +1 -0
  113. package/dist/clis/notion/read.js +30 -0
  114. package/dist/clis/notion/search.d.ts +1 -0
  115. package/dist/clis/notion/search.js +46 -0
  116. package/dist/clis/notion/sidebar.d.ts +1 -0
  117. package/dist/clis/notion/sidebar.js +41 -0
  118. package/dist/clis/notion/status.d.ts +1 -0
  119. package/dist/clis/notion/status.js +16 -0
  120. package/dist/clis/notion/write.d.ts +1 -0
  121. package/dist/clis/notion/write.js +40 -0
  122. package/dist/clis/twitter/download.d.ts +8 -0
  123. package/dist/clis/twitter/download.js +204 -0
  124. package/dist/clis/wechat/chats.d.ts +1 -0
  125. package/dist/clis/wechat/chats.js +28 -0
  126. package/dist/clis/wechat/contacts.d.ts +1 -0
  127. package/dist/clis/wechat/contacts.js +28 -0
  128. package/dist/clis/wechat/read.d.ts +1 -0
  129. package/dist/clis/wechat/read.js +58 -0
  130. package/dist/clis/wechat/search.d.ts +1 -0
  131. package/dist/clis/wechat/search.js +31 -0
  132. package/dist/clis/wechat/send.d.ts +1 -0
  133. package/dist/clis/wechat/send.js +42 -0
  134. package/dist/clis/wechat/status.d.ts +1 -0
  135. package/dist/clis/wechat/status.js +29 -0
  136. package/dist/clis/xiaohongshu/creator-note-detail.d.ts +10 -0
  137. package/dist/clis/xiaohongshu/creator-note-detail.js +88 -0
  138. package/dist/clis/xiaohongshu/creator-notes.d.ts +11 -0
  139. package/dist/clis/xiaohongshu/creator-notes.js +109 -0
  140. package/dist/clis/xiaohongshu/creator-profile.d.ts +10 -0
  141. package/dist/clis/xiaohongshu/creator-profile.js +54 -0
  142. package/dist/clis/xiaohongshu/creator-stats.d.ts +10 -0
  143. package/dist/clis/xiaohongshu/creator-stats.js +74 -0
  144. package/dist/clis/xiaohongshu/download.d.ts +7 -0
  145. package/dist/clis/xiaohongshu/download.js +155 -0
  146. package/dist/clis/xiaohongshu/search.js +1 -1
  147. package/dist/clis/xiaohongshu/user-helpers.d.ts +15 -0
  148. package/dist/clis/xiaohongshu/user-helpers.js +67 -0
  149. package/dist/clis/xiaohongshu/user-helpers.test.d.ts +1 -0
  150. package/dist/clis/xiaohongshu/user-helpers.test.js +81 -0
  151. package/dist/clis/xiaohongshu/user.js +46 -29
  152. package/dist/clis/zhihu/download.d.ts +11 -0
  153. package/dist/clis/zhihu/download.js +186 -0
  154. package/dist/clis/zhihu/download.test.d.ts +1 -0
  155. package/dist/clis/zhihu/download.test.js +10 -0
  156. package/dist/download/index.d.ts +79 -0
  157. package/dist/download/index.js +325 -0
  158. package/dist/download/progress.d.ts +36 -0
  159. package/dist/download/progress.js +111 -0
  160. package/dist/engine.test.js +15 -0
  161. package/dist/main.js +16 -3
  162. package/dist/pipeline/registry.js +2 -0
  163. package/dist/pipeline/steps/download.d.ts +34 -0
  164. package/dist/pipeline/steps/download.js +251 -0
  165. package/dist/pipeline/template.js +28 -0
  166. package/package.json +4 -3
  167. package/scripts/test-site.mjs +70 -0
  168. package/src/browser/discover.ts +23 -7
  169. package/src/browser.test.ts +23 -0
  170. package/src/build-manifest.test.ts +28 -0
  171. package/src/build-manifest.ts +147 -57
  172. package/src/clis/antigravity/README.md +2 -3
  173. package/src/clis/antigravity/README.zh-CN.md +2 -3
  174. package/src/clis/antigravity/SKILL.md +1 -1
  175. package/src/clis/antigravity/model.ts +2 -2
  176. package/src/clis/antigravity/send.ts +2 -2
  177. package/src/clis/bilibili/download.ts +161 -0
  178. package/src/clis/chatgpt/README.md +25 -16
  179. package/src/clis/chatgpt/README.zh-CN.md +27 -18
  180. package/src/clis/chatgpt/ask.ts +77 -0
  181. package/src/clis/chatgpt/send.ts +12 -0
  182. package/src/clis/chatwise/README.md +38 -0
  183. package/src/clis/chatwise/README.zh-CN.md +38 -0
  184. package/src/clis/chatwise/ask.ts +87 -0
  185. package/src/clis/chatwise/export.ts +51 -0
  186. package/src/clis/chatwise/history.ts +47 -0
  187. package/src/clis/chatwise/model.ts +87 -0
  188. package/src/clis/chatwise/new.ts +21 -0
  189. package/src/clis/chatwise/read.ts +42 -0
  190. package/src/clis/chatwise/screenshot.ts +33 -0
  191. package/src/clis/chatwise/send.ts +50 -0
  192. package/src/clis/chatwise/status.ts +25 -0
  193. package/src/clis/codex/ask.ts +77 -0
  194. package/src/clis/codex/export.ts +42 -0
  195. package/src/clis/codex/extract-diff.ts +1 -0
  196. package/src/clis/codex/history.ts +47 -0
  197. package/src/clis/codex/read.ts +5 -6
  198. package/src/clis/codex/screenshot.ts +33 -0
  199. package/src/clis/codex/send.ts +6 -7
  200. package/src/clis/codex/status.ts +4 -2
  201. package/src/clis/cursor/ask.ts +81 -0
  202. package/src/clis/cursor/composer.ts +9 -30
  203. package/src/clis/cursor/export.ts +57 -0
  204. package/src/clis/cursor/history.ts +47 -0
  205. package/src/clis/cursor/new.ts +4 -15
  206. package/src/clis/cursor/screenshot.ts +38 -0
  207. package/src/clis/discord-app/README.md +28 -0
  208. package/src/clis/discord-app/README.zh-CN.md +28 -0
  209. package/src/clis/discord-app/channels.ts +48 -0
  210. package/src/clis/discord-app/members.ts +41 -0
  211. package/src/clis/discord-app/read.ts +49 -0
  212. package/src/clis/discord-app/search.ts +64 -0
  213. package/src/clis/discord-app/send.ts +32 -0
  214. package/src/clis/discord-app/servers.ts +39 -0
  215. package/src/clis/discord-app/status.ts +18 -0
  216. package/src/clis/feishu/README.md +20 -0
  217. package/src/clis/feishu/README.zh-CN.md +20 -0
  218. package/src/clis/feishu/new.ts +32 -0
  219. package/src/clis/feishu/read.ts +48 -0
  220. package/src/clis/feishu/search.ts +35 -0
  221. package/src/clis/feishu/send.ts +46 -0
  222. package/src/clis/feishu/status.ts +34 -0
  223. package/src/clis/grok/ask.ts +90 -0
  224. package/src/clis/grok/debug.ts +49 -0
  225. package/src/clis/jimeng/generate.yaml +84 -0
  226. package/src/clis/jimeng/history.yaml +47 -0
  227. package/src/clis/linux-do/categories.yaml +41 -0
  228. package/src/clis/linux-do/category.yaml +49 -0
  229. package/src/clis/linux-do/hot.yaml +50 -0
  230. package/src/clis/linux-do/latest.yaml +40 -0
  231. package/src/clis/linux-do/search.yaml +45 -0
  232. package/src/clis/linux-do/topic.yaml +38 -0
  233. package/src/clis/notion/README.md +29 -0
  234. package/src/clis/notion/README.zh-CN.md +29 -0
  235. package/src/clis/notion/export.ts +36 -0
  236. package/src/clis/notion/favorites.ts +87 -0
  237. package/src/clis/notion/new.ts +39 -0
  238. package/src/clis/notion/read.ts +33 -0
  239. package/src/clis/notion/search.ts +54 -0
  240. package/src/clis/notion/sidebar.ts +44 -0
  241. package/src/clis/notion/status.ts +18 -0
  242. package/src/clis/notion/write.ts +45 -0
  243. package/src/clis/twitter/download.ts +227 -0
  244. package/src/clis/wechat/README.md +28 -0
  245. package/src/clis/wechat/README.zh-CN.md +28 -0
  246. package/src/clis/wechat/chats.ts +33 -0
  247. package/src/clis/wechat/contacts.ts +33 -0
  248. package/src/clis/wechat/read.ts +72 -0
  249. package/src/clis/wechat/search.ts +36 -0
  250. package/src/clis/wechat/send.ts +49 -0
  251. package/src/clis/wechat/status.ts +35 -0
  252. package/src/clis/xiaohongshu/creator-note-detail.ts +95 -0
  253. package/src/clis/xiaohongshu/creator-notes.ts +116 -0
  254. package/src/clis/xiaohongshu/creator-profile.ts +60 -0
  255. package/src/clis/xiaohongshu/creator-stats.ts +81 -0
  256. package/src/clis/xiaohongshu/download.ts +173 -0
  257. package/src/clis/xiaohongshu/search.ts +1 -1
  258. package/src/clis/xiaohongshu/user-helpers.test.ts +106 -0
  259. package/src/clis/xiaohongshu/user-helpers.ts +85 -0
  260. package/src/clis/xiaohongshu/user.ts +52 -32
  261. package/src/clis/zhihu/download.test.ts +12 -0
  262. package/src/clis/zhihu/download.ts +223 -0
  263. package/src/download/index.ts +395 -0
  264. package/src/download/progress.ts +125 -0
  265. package/src/engine.test.ts +17 -0
  266. package/src/main.ts +12 -3
  267. package/src/pipeline/registry.ts +2 -0
  268. package/src/pipeline/steps/download.ts +310 -0
  269. package/src/pipeline/template.ts +26 -0
  270. package/tests/e2e/browser-auth.test.ts +25 -0
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Xiaohongshu Creator Note List — per-note metrics from the creator backend.
3
+ *
4
+ * Navigates to the note manager page and extracts per-note data from
5
+ * the rendered DOM. This approach bypasses the v2 API signature requirement.
6
+ *
7
+ * Returns: note title, publish date, views, likes, collects, comments.
8
+ *
9
+ * Requires: logged into creator.xiaohongshu.com in Chrome.
10
+ */
11
+ export {};
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Xiaohongshu Creator Note List — per-note metrics from the creator backend.
3
+ *
4
+ * Navigates to the note manager page and extracts per-note data from
5
+ * the rendered DOM. This approach bypasses the v2 API signature requirement.
6
+ *
7
+ * Returns: note title, publish date, views, likes, collects, comments.
8
+ *
9
+ * Requires: logged into creator.xiaohongshu.com in Chrome.
10
+ */
11
+ import { cli, Strategy } from '../../registry.js';
12
+ cli({
13
+ site: 'xiaohongshu',
14
+ name: 'creator-notes',
15
+ description: '小红书创作者笔记列表 + 每篇数据 (标题/日期/观看/点赞/收藏/评论)',
16
+ domain: 'creator.xiaohongshu.com',
17
+ strategy: Strategy.COOKIE,
18
+ browser: true,
19
+ args: [
20
+ { name: 'limit', type: 'int', default: 20, help: 'Number of notes to return' },
21
+ ],
22
+ columns: ['rank', 'id', 'title', 'date', 'views', 'likes', 'collects', 'comments', 'url'],
23
+ func: async (page, kwargs) => {
24
+ const limit = kwargs.limit || 20;
25
+ // Navigate to note manager
26
+ await page.goto('https://creator.xiaohongshu.com/new/note-manager');
27
+ await page.wait(4);
28
+ // Scroll to load more notes if needed
29
+ await page.autoScroll({ times: Math.ceil(limit / 10), delayMs: 1500 });
30
+ // Extract note data from rendered DOM
31
+ const notes = await page.evaluate(`
32
+ (() => {
33
+ const results = [];
34
+ // Note cards in the manager page contain title, date, and metric numbers
35
+ // Each note card has a consistent structure with the title, date line,
36
+ // and a row of 4 numbers (views, likes, collects, comments)
37
+ const cards = document.querySelectorAll('[class*="note-item"], [class*="noteItem"], [class*="card"]');
38
+
39
+ if (cards.length === 0) {
40
+ // Fallback: parse from any container with note-like content
41
+ const allText = document.body.innerText;
42
+ const notePattern = /(.+?)\\s+发布于\\s+(\\d{4}年\\d{2}月\\d{2}日\\s+\\d{2}:\\d{2})\\s*(\\d+)\\s*(\\d+)\\s*(\\d+)\\s*(\\d+)/g;
43
+ let match;
44
+ while ((match = notePattern.exec(allText)) !== null) {
45
+ results.push({
46
+ title: match[1].trim(),
47
+ date: match[2],
48
+ views: parseInt(match[3]) || 0,
49
+ likes: parseInt(match[4]) || 0,
50
+ collects: parseInt(match[5]) || 0,
51
+ comments: parseInt(match[6]) || 0,
52
+ });
53
+ }
54
+ return results;
55
+ }
56
+
57
+ cards.forEach(card => {
58
+ const text = card.innerText || '';
59
+ const linkEl = card.querySelector('a[href*="/publish/"], a[href*="/note/"], a[href*="/explore/"]');
60
+ const href = linkEl?.getAttribute('href') || '';
61
+ const idMatch = href.match(/\/(?:publish|explore|note)\/([a-zA-Z0-9]+)/);
62
+ // Try to extract structured data
63
+ const lines = text.split('\\n').map(l => l.trim()).filter(Boolean);
64
+ if (lines.length < 2) return;
65
+
66
+ const title = lines[0];
67
+ const dateLine = lines.find(l => l.includes('发布于'));
68
+ const dateMatch = dateLine?.match(/发布于\\s+(\\d{4}年\\d{2}月\\d{2}日\\s+\\d{2}:\\d{2})/);
69
+
70
+ // Remove the publish timestamp before collecting note metrics.
71
+ // Otherwise year/month/day/hour digits are picked up as views/likes/etc.
72
+ const metricText = dateLine ? text.replace(dateLine, ' ') : text;
73
+ const nums = metricText.match(/(?:^|\\s)(\\d+)(?:\\s|$)/g)?.map(n => parseInt(n.trim())) || [];
74
+
75
+ if (title && !title.includes('全部笔记')) {
76
+ results.push({
77
+ id: idMatch ? idMatch[1] : '',
78
+ title: title.replace(/\\s+/g, ' ').substring(0, 80),
79
+ date: dateMatch ? dateMatch[1] : '',
80
+ views: nums[0] || 0,
81
+ likes: nums[1] || 0,
82
+ collects: nums[2] || 0,
83
+ comments: nums[3] || 0,
84
+ url: href ? new URL(href, window.location.origin).toString() : '',
85
+ });
86
+ }
87
+ });
88
+
89
+ return results;
90
+ })()
91
+ `);
92
+ if (!Array.isArray(notes) || notes.length === 0) {
93
+ throw new Error('No notes found. Are you logged into creator.xiaohongshu.com?');
94
+ }
95
+ return notes
96
+ .slice(0, limit)
97
+ .map((n, i) => ({
98
+ rank: i + 1,
99
+ id: n.id,
100
+ title: n.title,
101
+ date: n.date,
102
+ views: n.views,
103
+ likes: n.likes,
104
+ collects: n.collects,
105
+ comments: n.comments,
106
+ url: n.url,
107
+ }));
108
+ },
109
+ });
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Xiaohongshu Creator Profile — creator account info and growth status.
3
+ *
4
+ * Uses the creator.xiaohongshu.com internal API (cookie auth).
5
+ * Returns follower/following counts, total likes+collects, and
6
+ * creator level growth info.
7
+ *
8
+ * Requires: logged into creator.xiaohongshu.com in Chrome.
9
+ */
10
+ export {};
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Xiaohongshu Creator Profile — creator account info and growth status.
3
+ *
4
+ * Uses the creator.xiaohongshu.com internal API (cookie auth).
5
+ * Returns follower/following counts, total likes+collects, and
6
+ * creator level growth info.
7
+ *
8
+ * Requires: logged into creator.xiaohongshu.com in Chrome.
9
+ */
10
+ import { cli, Strategy } from '../../registry.js';
11
+ cli({
12
+ site: 'xiaohongshu',
13
+ name: 'creator-profile',
14
+ description: '小红书创作者账号信息 (粉丝/关注/获赞/成长等级)',
15
+ domain: 'creator.xiaohongshu.com',
16
+ strategy: Strategy.COOKIE,
17
+ browser: true,
18
+ args: [],
19
+ columns: ['field', 'value'],
20
+ func: async (page, _kwargs) => {
21
+ await page.goto('https://creator.xiaohongshu.com/new/home');
22
+ await page.wait(3);
23
+ const data = await page.evaluate(`
24
+ async () => {
25
+ try {
26
+ const resp = await fetch('/api/galaxy/creator/home/personal_info', {
27
+ credentials: 'include',
28
+ });
29
+ if (!resp.ok) return { error: 'HTTP ' + resp.status };
30
+ return await resp.json();
31
+ } catch (e) {
32
+ return { error: e.message };
33
+ }
34
+ }
35
+ `);
36
+ if (data?.error) {
37
+ throw new Error(data.error + '. Are you logged into creator.xiaohongshu.com?');
38
+ }
39
+ if (!data?.data) {
40
+ throw new Error('Unexpected response structure');
41
+ }
42
+ const d = data.data;
43
+ const grow = d.grow_info || {};
44
+ return [
45
+ { field: 'Name', value: d.name ?? '' },
46
+ { field: 'Followers', value: d.fans_count ?? 0 },
47
+ { field: 'Following', value: d.follow_count ?? 0 },
48
+ { field: 'Likes & Collects', value: d.faved_count ?? 0 },
49
+ { field: 'Creator Level', value: grow.level ?? 0 },
50
+ { field: 'Level Progress', value: `${grow.fans_count ?? 0}/${grow.max_fans_count ?? 0} fans` },
51
+ { field: 'Bio', value: (d.personal_desc ?? '').replace(/\\n/g, ' | ') },
52
+ ];
53
+ },
54
+ });
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Xiaohongshu Creator Analytics — account-level metrics overview.
3
+ *
4
+ * Uses the creator.xiaohongshu.com internal API (cookie auth).
5
+ * Returns 7-day and 30-day aggregate stats: views, likes, collects,
6
+ * comments, shares, new followers, and daily trend data.
7
+ *
8
+ * Requires: logged into creator.xiaohongshu.com in Chrome.
9
+ */
10
+ export {};
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Xiaohongshu Creator Analytics — account-level metrics overview.
3
+ *
4
+ * Uses the creator.xiaohongshu.com internal API (cookie auth).
5
+ * Returns 7-day and 30-day aggregate stats: views, likes, collects,
6
+ * comments, shares, new followers, and daily trend data.
7
+ *
8
+ * Requires: logged into creator.xiaohongshu.com in Chrome.
9
+ */
10
+ import { cli, Strategy } from '../../registry.js';
11
+ cli({
12
+ site: 'xiaohongshu',
13
+ name: 'creator-stats',
14
+ description: '小红书创作者数据总览 (观看/点赞/收藏/评论/分享/涨粉,含每日趋势)',
15
+ domain: 'creator.xiaohongshu.com',
16
+ strategy: Strategy.COOKIE,
17
+ browser: true,
18
+ args: [
19
+ {
20
+ name: 'period',
21
+ type: 'string',
22
+ default: 'seven',
23
+ help: 'Stats period: seven or thirty',
24
+ choices: ['seven', 'thirty'],
25
+ },
26
+ ],
27
+ columns: ['metric', 'total', 'trend'],
28
+ func: async (page, kwargs) => {
29
+ const period = kwargs.period || 'seven';
30
+ // Navigate to creator center for cookie context
31
+ await page.goto('https://creator.xiaohongshu.com/new/home');
32
+ await page.wait(3);
33
+ const data = await page.evaluate(`
34
+ async () => {
35
+ try {
36
+ const resp = await fetch('/api/galaxy/creator/data/note_detail_new', {
37
+ credentials: 'include',
38
+ });
39
+ if (!resp.ok) return { error: 'HTTP ' + resp.status };
40
+ return await resp.json();
41
+ } catch (e) {
42
+ return { error: e.message };
43
+ }
44
+ }
45
+ `);
46
+ if (data?.error) {
47
+ throw new Error(data.error + '. Are you logged into creator.xiaohongshu.com?');
48
+ }
49
+ if (!data?.data) {
50
+ throw new Error('Unexpected response structure');
51
+ }
52
+ const stats = data.data[period];
53
+ if (!stats) {
54
+ throw new Error(`No data for period "${period}". Available: ${Object.keys(data.data).join(', ')}`);
55
+ }
56
+ // Format daily trend as sparkline-like summary
57
+ const formatTrend = (list) => {
58
+ if (!list || !list.length)
59
+ return '-';
60
+ return list.map((d) => d.count).join(' → ');
61
+ };
62
+ return [
63
+ { metric: '观看数 (views)', total: stats.view_count ?? 0, trend: formatTrend(stats.view_list) },
64
+ { metric: '平均观看时长 (avg view time ms)', total: stats.view_time_avg ?? 0, trend: formatTrend(stats.view_time_list) },
65
+ { metric: '主页访问 (home views)', total: stats.home_view_count ?? 0, trend: formatTrend(stats.home_view_list) },
66
+ { metric: '点赞数 (likes)', total: stats.like_count ?? 0, trend: formatTrend(stats.like_list) },
67
+ { metric: '收藏数 (collects)', total: stats.collect_count ?? 0, trend: formatTrend(stats.collect_list) },
68
+ { metric: '评论数 (comments)', total: stats.comment_count ?? 0, trend: formatTrend(stats.comment_list) },
69
+ { metric: '弹幕数 (danmaku)', total: stats.danmaku_count ?? 0, trend: '-' },
70
+ { metric: '分享数 (shares)', total: stats.share_count ?? 0, trend: formatTrend(stats.share_list) },
71
+ { metric: '涨粉数 (new followers)', total: stats.rise_fans_count ?? 0, trend: formatTrend(stats.rise_fans_list) },
72
+ ];
73
+ },
74
+ });
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Xiaohongshu download — download images and videos from a note.
3
+ *
4
+ * Usage:
5
+ * opencli xiaohongshu download --note-id abc123 --output ./xhs
6
+ */
7
+ export {};
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Xiaohongshu download — download images and videos from a note.
3
+ *
4
+ * Usage:
5
+ * opencli xiaohongshu download --note-id abc123 --output ./xhs
6
+ */
7
+ import * as fs from 'node:fs';
8
+ import * as path from 'node:path';
9
+ import { cli, Strategy } from '../../registry.js';
10
+ import { httpDownload, } from '../../download/index.js';
11
+ import { DownloadProgressTracker, formatBytes } from '../../download/progress.js';
12
+ cli({
13
+ site: 'xiaohongshu',
14
+ name: 'download',
15
+ description: '下载小红书笔记中的图片和视频',
16
+ domain: 'www.xiaohongshu.com',
17
+ strategy: Strategy.COOKIE,
18
+ args: [
19
+ { name: 'note_id', required: true, help: 'Note ID (from URL)' },
20
+ { name: 'output', default: './xiaohongshu-downloads', help: 'Output directory' },
21
+ ],
22
+ columns: ['index', 'type', 'status', 'size'],
23
+ func: async (page, kwargs) => {
24
+ const noteId = kwargs.note_id;
25
+ const output = kwargs.output;
26
+ // Navigate to note page
27
+ await page.goto(`https://www.xiaohongshu.com/explore/${noteId}`);
28
+ await page.wait(3);
29
+ // Extract note info and media URLs
30
+ const data = await page.evaluate(`
31
+ (() => {
32
+ const result = {
33
+ noteId: '${noteId}',
34
+ title: '',
35
+ author: '',
36
+ media: []
37
+ };
38
+
39
+ // Get title
40
+ const titleEl = document.querySelector('.title, #detail-title, .note-content .title');
41
+ result.title = titleEl?.textContent?.trim() || 'untitled';
42
+
43
+ // Get author
44
+ const authorEl = document.querySelector('.username, .author-name, .name');
45
+ result.author = authorEl?.textContent?.trim() || 'unknown';
46
+
47
+ // Get images - try multiple selectors
48
+ const imageSelectors = [
49
+ '.swiper-slide img',
50
+ '.carousel-image img',
51
+ '.note-slider img',
52
+ '.note-image img',
53
+ '.image-wrapper img',
54
+ '#noteContainer img[src*="xhscdn"]',
55
+ 'img[src*="ci.xiaohongshu.com"]'
56
+ ];
57
+
58
+ const imageUrls = new Set();
59
+ for (const selector of imageSelectors) {
60
+ document.querySelectorAll(selector).forEach(img => {
61
+ let src = img.src || img.getAttribute('data-src') || '';
62
+ if (src && (src.includes('xhscdn') || src.includes('xiaohongshu'))) {
63
+ // Convert to high quality URL (remove resize parameters)
64
+ src = src.split('?')[0];
65
+ // Try to get original size
66
+ src = src.replace(/\\/imageView\\d+\\/\\d+\\/w\\/\\d+/, '');
67
+ imageUrls.add(src);
68
+ }
69
+ });
70
+ }
71
+
72
+ // Get video if exists
73
+ const videoSelectors = [
74
+ 'video source',
75
+ 'video[src]',
76
+ '.player video',
77
+ '.video-player video'
78
+ ];
79
+
80
+ for (const selector of videoSelectors) {
81
+ document.querySelectorAll(selector).forEach(v => {
82
+ const src = v.src || v.getAttribute('src') || '';
83
+ if (src) {
84
+ result.media.push({
85
+ type: 'video',
86
+ url: src
87
+ });
88
+ }
89
+ });
90
+ }
91
+
92
+ // Add images to media
93
+ imageUrls.forEach(url => {
94
+ result.media.push({
95
+ type: 'image',
96
+ url: url
97
+ });
98
+ });
99
+
100
+ return result;
101
+ })()
102
+ `);
103
+ if (!data || !data.media || data.media.length === 0) {
104
+ return [{ index: 0, type: '-', status: 'failed', size: 'No media found' }];
105
+ }
106
+ // Extract cookies for authenticated downloads
107
+ const cookies = await page.evaluate(`(() => document.cookie)()`);
108
+ // Create output directory
109
+ const outputDir = path.join(output, noteId);
110
+ fs.mkdirSync(outputDir, { recursive: true });
111
+ // Download all media files
112
+ const tracker = new DownloadProgressTracker(data.media.length, true);
113
+ const results = [];
114
+ for (let i = 0; i < data.media.length; i++) {
115
+ const media = data.media[i];
116
+ const ext = media.type === 'video' ? 'mp4' : 'jpg';
117
+ const filename = `${noteId}_${i + 1}.${ext}`;
118
+ const destPath = path.join(outputDir, filename);
119
+ const progressBar = tracker.onFileStart(filename, i);
120
+ try {
121
+ const result = await httpDownload(media.url, destPath, {
122
+ cookies: typeof cookies === 'string' ? cookies : '',
123
+ timeout: 60000,
124
+ onProgress: (received, total) => {
125
+ if (progressBar)
126
+ progressBar.update(received, total);
127
+ },
128
+ });
129
+ if (progressBar) {
130
+ progressBar.complete(result.success, result.success ? formatBytes(result.size) : undefined);
131
+ }
132
+ tracker.onFileComplete(result.success);
133
+ results.push({
134
+ index: i + 1,
135
+ type: media.type,
136
+ status: result.success ? 'success' : 'failed',
137
+ size: result.success ? formatBytes(result.size) : (result.error || 'unknown error'),
138
+ });
139
+ }
140
+ catch (err) {
141
+ if (progressBar)
142
+ progressBar.fail(err.message);
143
+ tracker.onFileComplete(false);
144
+ results.push({
145
+ index: i + 1,
146
+ type: media.type,
147
+ status: 'failed',
148
+ size: err.message,
149
+ });
150
+ }
151
+ }
152
+ tracker.finish();
153
+ return results;
154
+ },
155
+ });
@@ -37,7 +37,7 @@ cli({
37
37
  const linkEl = el.querySelector('a[href*="/explore/"], a[href*="/search_result/"], a[href*="/note/"]');
38
38
 
39
39
  const href = linkEl?.getAttribute('href') || '';
40
- const noteId = href.match(/\\/(?:explore|note)\\/([a-f0-9]+)/)?.[1] || '';
40
+ const noteId = href.match(/\\/(?:explore|note)\\/([a-zA-Z0-9]+)/)?.[1] || '';
41
41
 
42
42
  results.push({
43
43
  title: (titleEl?.textContent || '').trim(),
@@ -0,0 +1,15 @@
1
+ export interface XhsUserPageSnapshot {
2
+ noteGroups?: unknown;
3
+ pageData?: unknown;
4
+ }
5
+ export interface XhsUserNoteRow {
6
+ id: string;
7
+ title: string;
8
+ type: string;
9
+ likes: string;
10
+ url: string;
11
+ }
12
+ export declare function normalizeXhsUserId(input: string): string;
13
+ export declare function flattenXhsNoteGroups(noteGroups: unknown): any[];
14
+ export declare function buildXhsNoteUrl(userId: string, noteId: string, xsecToken?: string): string;
15
+ export declare function extractXhsUserNotes(snapshot: XhsUserPageSnapshot, fallbackUserId: string): XhsUserNoteRow[];
@@ -0,0 +1,67 @@
1
+ function toCleanString(value) {
2
+ return typeof value === 'string' ? value.trim() : value == null ? '' : String(value).trim();
3
+ }
4
+ export function normalizeXhsUserId(input) {
5
+ const trimmed = toCleanString(input);
6
+ const withoutQuery = trimmed.replace(/[?#].*$/, '');
7
+ const matched = withoutQuery.match(/\/user\/profile\/([a-zA-Z0-9]+)/);
8
+ if (matched?.[1])
9
+ return matched[1];
10
+ return withoutQuery.replace(/\/+$/, '').split('/').pop() ?? withoutQuery;
11
+ }
12
+ export function flattenXhsNoteGroups(noteGroups) {
13
+ if (!Array.isArray(noteGroups))
14
+ return [];
15
+ const notes = [];
16
+ for (const group of noteGroups) {
17
+ if (!group)
18
+ continue;
19
+ if (Array.isArray(group)) {
20
+ for (const item of group) {
21
+ if (item)
22
+ notes.push(item);
23
+ }
24
+ continue;
25
+ }
26
+ notes.push(group);
27
+ }
28
+ return notes;
29
+ }
30
+ export function buildXhsNoteUrl(userId, noteId, xsecToken) {
31
+ const cleanUserId = toCleanString(userId);
32
+ const cleanNoteId = toCleanString(noteId);
33
+ if (!cleanUserId || !cleanNoteId)
34
+ return '';
35
+ const url = new URL(`https://www.xiaohongshu.com/user/profile/${cleanUserId}/${cleanNoteId}`);
36
+ const cleanToken = toCleanString(xsecToken);
37
+ if (cleanToken) {
38
+ url.searchParams.set('xsec_token', cleanToken);
39
+ url.searchParams.set('xsec_source', 'pc_user');
40
+ }
41
+ return url.toString();
42
+ }
43
+ export function extractXhsUserNotes(snapshot, fallbackUserId) {
44
+ const notes = flattenXhsNoteGroups(snapshot.noteGroups);
45
+ const rows = [];
46
+ const seen = new Set();
47
+ for (const entry of notes) {
48
+ const noteCard = entry?.noteCard ?? entry?.note_card ?? entry;
49
+ if (!noteCard || typeof noteCard !== 'object')
50
+ continue;
51
+ const noteId = toCleanString(noteCard.noteId ?? noteCard.note_id ?? entry?.noteId ?? entry?.note_id ?? entry?.id);
52
+ if (!noteId || seen.has(noteId))
53
+ continue;
54
+ seen.add(noteId);
55
+ const userId = toCleanString(noteCard.user?.userId ?? noteCard.user?.user_id ?? fallbackUserId);
56
+ const xsecToken = toCleanString(entry?.xsecToken ?? entry?.xsec_token ?? noteCard.xsecToken ?? noteCard.xsec_token);
57
+ const likes = toCleanString(noteCard.interactInfo?.likedCount ?? noteCard.interact_info?.liked_count ?? 0) || '0';
58
+ rows.push({
59
+ id: noteId,
60
+ title: toCleanString(noteCard.displayTitle ?? noteCard.display_title ?? noteCard.title),
61
+ type: toCleanString(noteCard.type),
62
+ likes,
63
+ url: buildXhsNoteUrl(userId || fallbackUserId, noteId, xsecToken),
64
+ });
65
+ }
66
+ return rows;
67
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,81 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { buildXhsNoteUrl, extractXhsUserNotes, flattenXhsNoteGroups, normalizeXhsUserId, } from './user-helpers.js';
3
+ describe('normalizeXhsUserId', () => {
4
+ it('extracts the profile id from a full Xiaohongshu URL', () => {
5
+ expect(normalizeXhsUserId('https://www.xiaohongshu.com/user/profile/615529370000000002026001?xsec_source=pc_search')).toBe('615529370000000002026001');
6
+ });
7
+ it('keeps a bare profile id unchanged', () => {
8
+ expect(normalizeXhsUserId('615529370000000002026001')).toBe('615529370000000002026001');
9
+ });
10
+ });
11
+ describe('flattenXhsNoteGroups', () => {
12
+ it('flattens grouped note arrays and ignores empty groups', () => {
13
+ expect(flattenXhsNoteGroups([[{ id: 'a' }], [], null, [{ id: 'b' }]])).toEqual([
14
+ { id: 'a' },
15
+ { id: 'b' },
16
+ ]);
17
+ });
18
+ });
19
+ describe('buildXhsNoteUrl', () => {
20
+ it('includes xsec token when available', () => {
21
+ expect(buildXhsNoteUrl('user123', 'note456', 'token789')).toBe('https://www.xiaohongshu.com/user/profile/user123/note456?xsec_token=token789&xsec_source=pc_user');
22
+ });
23
+ });
24
+ describe('extractXhsUserNotes', () => {
25
+ it('normalizes grouped note cards into CLI rows', () => {
26
+ const rows = extractXhsUserNotes({
27
+ noteGroups: [
28
+ [
29
+ {
30
+ id: 'note-1',
31
+ xsecToken: 'abc',
32
+ noteCard: {
33
+ noteId: 'note-1',
34
+ displayTitle: 'First note',
35
+ type: 'video',
36
+ interactInfo: { likedCount: '4.6万' },
37
+ user: { userId: 'user-1' },
38
+ },
39
+ },
40
+ {
41
+ noteCard: {
42
+ note_id: 'note-2',
43
+ display_title: 'Second note',
44
+ type: 'normal',
45
+ interact_info: { liked_count: 42 },
46
+ },
47
+ },
48
+ ],
49
+ [],
50
+ ],
51
+ }, 'fallback-user');
52
+ expect(rows).toEqual([
53
+ {
54
+ id: 'note-1',
55
+ title: 'First note',
56
+ type: 'video',
57
+ likes: '4.6万',
58
+ url: 'https://www.xiaohongshu.com/user/profile/user-1/note-1?xsec_token=abc&xsec_source=pc_user',
59
+ },
60
+ {
61
+ id: 'note-2',
62
+ title: 'Second note',
63
+ type: 'normal',
64
+ likes: '42',
65
+ url: 'https://www.xiaohongshu.com/user/profile/fallback-user/note-2',
66
+ },
67
+ ]);
68
+ });
69
+ it('deduplicates repeated notes by note id', () => {
70
+ const rows = extractXhsUserNotes({
71
+ noteGroups: [
72
+ [
73
+ { noteCard: { noteId: 'dup-1', displayTitle: 'keep me' } },
74
+ { noteCard: { noteId: 'dup-1', displayTitle: 'drop me' } },
75
+ ],
76
+ ],
77
+ }, 'fallback-user');
78
+ expect(rows).toHaveLength(1);
79
+ expect(rows[0]?.title).toBe('keep me');
80
+ });
81
+ });