@zenalexa/unicli 0.225.0 → 0.225.2

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 (224) hide show
  1. package/AGENTS.md +5 -5
  2. package/README.md +3 -3
  3. package/README.zh-CN.md +3 -3
  4. package/dist/adapters/_electron/desktop-shared.d.ts.map +1 -1
  5. package/dist/adapters/_electron/desktop-shared.js +2 -1
  6. package/dist/adapters/_electron/desktop-shared.js.map +1 -1
  7. package/dist/adapters/_electron/shared.d.ts +6 -0
  8. package/dist/adapters/_electron/shared.d.ts.map +1 -1
  9. package/dist/adapters/_electron/shared.js +9 -0
  10. package/dist/adapters/_electron/shared.js.map +1 -1
  11. package/dist/adapters/antigravity/extra.js +4 -1
  12. package/dist/adapters/antigravity/extra.js.map +1 -1
  13. package/dist/adapters/chatwise/extra.js +4 -1
  14. package/dist/adapters/chatwise/extra.js.map +1 -1
  15. package/dist/adapters/codex/codex.js +3 -1
  16. package/dist/adapters/codex/codex.js.map +1 -1
  17. package/dist/adapters/codex/extra.js +4 -1
  18. package/dist/adapters/codex/extra.js.map +1 -1
  19. package/dist/adapters/codex/projects.d.ts.map +1 -1
  20. package/dist/adapters/codex/projects.js +3 -1
  21. package/dist/adapters/codex/projects.js.map +1 -1
  22. package/dist/adapters/cursor/cursor.js +6 -1
  23. package/dist/adapters/cursor/cursor.js.map +1 -1
  24. package/dist/adapters/discord-app/discord-app.js +10 -1
  25. package/dist/adapters/discord-app/discord-app.js.map +1 -1
  26. package/dist/adapters/notion-app/notion-app.js +11 -1
  27. package/dist/adapters/notion-app/notion-app.js.map +1 -1
  28. package/dist/adapters/spotify/api.js +36 -6
  29. package/dist/adapters/spotify/api.js.map +1 -1
  30. package/dist/adapters/xiaohongshu/browser-state.d.ts +2 -1
  31. package/dist/adapters/xiaohongshu/browser-state.d.ts.map +1 -1
  32. package/dist/adapters/xiaohongshu/browser-state.js +56 -2
  33. package/dist/adapters/xiaohongshu/browser-state.js.map +1 -1
  34. package/dist/adapters/xiaohongshu/feed.d.ts +24 -0
  35. package/dist/adapters/xiaohongshu/feed.d.ts.map +1 -0
  36. package/dist/adapters/xiaohongshu/feed.js +82 -0
  37. package/dist/adapters/xiaohongshu/feed.js.map +1 -0
  38. package/dist/browser/cdp-client.d.ts +5 -1
  39. package/dist/browser/cdp-client.d.ts.map +1 -1
  40. package/dist/browser/cdp-client.js +24 -16
  41. package/dist/browser/cdp-client.js.map +1 -1
  42. package/dist/browser/daemon.js +29 -7
  43. package/dist/browser/daemon.js.map +1 -1
  44. package/dist/browser/launcher.d.ts.map +1 -1
  45. package/dist/browser/launcher.js +22 -8
  46. package/dist/browser/launcher.js.map +1 -1
  47. package/dist/browser/local-profiles.d.ts +2 -0
  48. package/dist/browser/local-profiles.d.ts.map +1 -1
  49. package/dist/browser/local-profiles.js +42 -2
  50. package/dist/browser/local-profiles.js.map +1 -1
  51. package/dist/browser/page.d.ts +2 -2
  52. package/dist/browser/page.d.ts.map +1 -1
  53. package/dist/browser/page.js +2 -2
  54. package/dist/browser/page.js.map +1 -1
  55. package/dist/browser/protocol.d.ts +13 -1
  56. package/dist/browser/protocol.d.ts.map +1 -1
  57. package/dist/browser/protocol.js +5 -0
  58. package/dist/browser/protocol.js.map +1 -1
  59. package/dist/commands/browser/actions.d.ts.map +1 -1
  60. package/dist/commands/browser/actions.js +428 -5
  61. package/dist/commands/browser/actions.js.map +1 -1
  62. package/dist/commands/browser/index.d.ts.map +1 -1
  63. package/dist/commands/browser/index.js +26 -4
  64. package/dist/commands/browser/index.js.map +1 -1
  65. package/dist/commands/compute.js +12 -1
  66. package/dist/commands/compute.js.map +1 -1
  67. package/dist/commands/do.d.ts +15 -13
  68. package/dist/commands/do.d.ts.map +1 -1
  69. package/dist/commands/do.js +36 -21
  70. package/dist/commands/do.js.map +1 -1
  71. package/dist/commands/schema.d.ts.map +1 -1
  72. package/dist/commands/schema.js +22 -0
  73. package/dist/commands/schema.js.map +1 -1
  74. package/dist/commands/search.d.ts.map +1 -1
  75. package/dist/commands/search.js +14 -3
  76. package/dist/commands/search.js.map +1 -1
  77. package/dist/compute/contracts.d.ts +55 -0
  78. package/dist/compute/contracts.d.ts.map +1 -0
  79. package/dist/compute/contracts.js +487 -0
  80. package/dist/compute/contracts.js.map +1 -0
  81. package/dist/discovery/aliases.d.ts +8 -1
  82. package/dist/discovery/aliases.d.ts.map +1 -1
  83. package/dist/discovery/aliases.js +151 -20
  84. package/dist/discovery/aliases.js.map +1 -1
  85. package/dist/discovery/core-catalog.d.ts.map +1 -1
  86. package/dist/discovery/core-catalog.js +39 -67
  87. package/dist/discovery/core-catalog.js.map +1 -1
  88. package/dist/discovery/intents.d.ts +32 -4
  89. package/dist/discovery/intents.d.ts.map +1 -1
  90. package/dist/discovery/intents.js +192 -3
  91. package/dist/discovery/intents.js.map +1 -1
  92. package/dist/discovery/loader.d.ts.map +1 -1
  93. package/dist/discovery/loader.js +3 -0
  94. package/dist/discovery/loader.js.map +1 -1
  95. package/dist/discovery/macos-dynamic.d.ts +1 -0
  96. package/dist/discovery/macos-dynamic.d.ts.map +1 -1
  97. package/dist/discovery/macos-dynamic.js +20 -1
  98. package/dist/discovery/macos-dynamic.js.map +1 -1
  99. package/dist/discovery/search.d.ts.map +1 -1
  100. package/dist/discovery/search.js +21 -5
  101. package/dist/discovery/search.js.map +1 -1
  102. package/dist/engine/browser/evidence.d.ts +34 -1
  103. package/dist/engine/browser/evidence.d.ts.map +1 -1
  104. package/dist/engine/browser/evidence.js +141 -6
  105. package/dist/engine/browser/evidence.js.map +1 -1
  106. package/dist/engine/kernel/stages.d.ts.map +1 -1
  107. package/dist/engine/kernel/stages.js +14 -4
  108. package/dist/engine/kernel/stages.js.map +1 -1
  109. package/dist/engine/objective/catalog.d.ts +23 -0
  110. package/dist/engine/objective/catalog.d.ts.map +1 -0
  111. package/dist/engine/objective/catalog.js +42 -0
  112. package/dist/engine/objective/catalog.js.map +1 -0
  113. package/dist/engine/objective/delivery.d.ts +18 -0
  114. package/dist/engine/objective/delivery.d.ts.map +1 -0
  115. package/dist/engine/objective/delivery.js +64 -0
  116. package/dist/engine/objective/delivery.js.map +1 -0
  117. package/dist/engine/objective/index.d.ts +20 -0
  118. package/dist/engine/objective/index.d.ts.map +1 -0
  119. package/dist/engine/objective/index.js +20 -0
  120. package/dist/engine/objective/index.js.map +1 -0
  121. package/dist/engine/objective/media-playback.d.ts +17 -0
  122. package/dist/engine/objective/media-playback.d.ts.map +1 -0
  123. package/dist/engine/objective/media-playback.js +186 -0
  124. package/dist/engine/objective/media-playback.js.map +1 -0
  125. package/dist/engine/objective/output.d.ts +20 -0
  126. package/dist/engine/objective/output.d.ts.map +1 -0
  127. package/dist/engine/objective/output.js +88 -0
  128. package/dist/engine/objective/output.js.map +1 -0
  129. package/dist/engine/objective/planner.d.ts +17 -0
  130. package/dist/engine/objective/planner.d.ts.map +1 -0
  131. package/dist/engine/objective/planner.js +60 -0
  132. package/dist/engine/objective/planner.js.map +1 -0
  133. package/dist/engine/objective/types.d.ts +66 -0
  134. package/dist/engine/objective/types.d.ts.map +1 -0
  135. package/dist/engine/objective/types.js +16 -0
  136. package/dist/engine/objective/types.js.map +1 -0
  137. package/dist/engine/steps/browser-helpers.d.ts.map +1 -1
  138. package/dist/engine/steps/browser-helpers.js +34 -0
  139. package/dist/engine/steps/browser-helpers.js.map +1 -1
  140. package/dist/engine/steps/fetch-text.d.ts.map +1 -1
  141. package/dist/engine/steps/fetch-text.js +2 -2
  142. package/dist/engine/steps/fetch-text.js.map +1 -1
  143. package/dist/engine/steps/fetch.d.ts +1 -0
  144. package/dist/engine/steps/fetch.d.ts.map +1 -1
  145. package/dist/engine/steps/fetch.js +24 -4
  146. package/dist/engine/steps/fetch.js.map +1 -1
  147. package/dist/fast-path/handlers/discovery.d.ts +5 -5
  148. package/dist/fast-path/handlers/discovery.d.ts.map +1 -1
  149. package/dist/fast-path/handlers/discovery.js +61 -8
  150. package/dist/fast-path/handlers/discovery.js.map +1 -1
  151. package/dist/fast-path/render.d.ts +2 -0
  152. package/dist/fast-path/render.d.ts.map +1 -1
  153. package/dist/fast-path/render.js +9 -0
  154. package/dist/fast-path/render.js.map +1 -1
  155. package/dist/manifest-compact.txt +3 -2
  156. package/dist/manifest.json +215 -17
  157. package/dist/mcp/handler.d.ts +2 -16
  158. package/dist/mcp/handler.d.ts.map +1 -1
  159. package/dist/mcp/handler.js.map +1 -1
  160. package/dist/mcp/http-transport.d.ts +7 -1
  161. package/dist/mcp/http-transport.d.ts.map +1 -1
  162. package/dist/mcp/http-transport.js +20 -1
  163. package/dist/mcp/http-transport.js.map +1 -1
  164. package/dist/mcp/jsonrpc.d.ts +27 -0
  165. package/dist/mcp/jsonrpc.d.ts.map +1 -0
  166. package/dist/mcp/jsonrpc.js +12 -0
  167. package/dist/mcp/jsonrpc.js.map +1 -0
  168. package/dist/mcp/origin-guard.d.ts +26 -0
  169. package/dist/mcp/origin-guard.d.ts.map +1 -0
  170. package/dist/mcp/origin-guard.js +42 -0
  171. package/dist/mcp/origin-guard.js.map +1 -0
  172. package/dist/mcp/profiles/computer-use.d.ts.map +1 -1
  173. package/dist/mcp/profiles/computer-use.js +30 -270
  174. package/dist/mcp/profiles/computer-use.js.map +1 -1
  175. package/dist/mcp/streamable-http/session.d.ts +4 -22
  176. package/dist/mcp/streamable-http/session.d.ts.map +1 -1
  177. package/dist/mcp/streamable-http/session.js +4 -24
  178. package/dist/mcp/streamable-http/session.js.map +1 -1
  179. package/dist/mcp/tools.d.ts.map +1 -1
  180. package/dist/mcp/tools.js +74 -54
  181. package/dist/mcp/tools.js.map +1 -1
  182. package/dist/output/envelope.d.ts +2 -0
  183. package/dist/output/envelope.d.ts.map +1 -1
  184. package/dist/output/envelope.js.map +1 -1
  185. package/dist/output/error-map.d.ts +14 -0
  186. package/dist/output/error-map.d.ts.map +1 -1
  187. package/dist/output/error-map.js +20 -0
  188. package/dist/output/error-map.js.map +1 -1
  189. package/dist/transport/cascade.d.ts.map +1 -1
  190. package/dist/transport/cascade.js +77 -5
  191. package/dist/transport/cascade.js.map +1 -1
  192. package/dist/transport/refs.d.ts +33 -1
  193. package/dist/transport/refs.d.ts.map +1 -1
  194. package/dist/transport/refs.js +40 -1
  195. package/dist/transport/refs.js.map +1 -1
  196. package/package.json +3 -1
  197. package/server.json +2 -2
  198. package/skills/unicli/SKILL.md +1 -1
  199. package/skills/unicli-claude-code/SKILL.md +1 -1
  200. package/skills/unicli-hermes/SKILL.md +1 -1
  201. package/src/adapters/_electron/desktop-shared.ts +5 -1
  202. package/src/adapters/_electron/shared.ts +15 -0
  203. package/src/adapters/antigravity/extra.ts +10 -1
  204. package/src/adapters/chatwise/extra.ts +10 -1
  205. package/src/adapters/codex/codex.ts +6 -0
  206. package/src/adapters/codex/extra.ts +10 -1
  207. package/src/adapters/codex/projects.ts +9 -1
  208. package/src/adapters/cursor/cursor.ts +9 -0
  209. package/src/adapters/defuddle/read.yaml +30 -0
  210. package/src/adapters/discord-app/discord-app.ts +16 -1
  211. package/src/adapters/jina/read.yaml +30 -0
  212. package/src/adapters/macos/brightness.yaml +6 -3
  213. package/src/adapters/macos/calendar-list.yaml +9 -11
  214. package/src/adapters/macos/calendar-today.yaml +1 -1
  215. package/src/adapters/macos/safari-url.yaml +8 -4
  216. package/src/adapters/maoyan/hot.yaml +1 -1
  217. package/src/adapters/markdown-new/read.yaml +50 -0
  218. package/src/adapters/notion-app/notion-app.ts +17 -1
  219. package/src/adapters/ollama-cloud/fetch.yaml +39 -0
  220. package/src/adapters/ollama-cloud/search.yaml +43 -0
  221. package/src/adapters/spotify/api.ts +54 -8
  222. package/src/adapters/weibo/trending.yaml +2 -0
  223. package/src/adapters/xiaohongshu/browser-state.ts +59 -2
  224. package/src/adapters/xiaohongshu/feed.ts +103 -0
@@ -0,0 +1,43 @@
1
+ site: ollama-cloud
2
+ name: search
3
+ description: Search the web through Ollama Cloud web_search
4
+ type: web-api
5
+ domain: ollama.com
6
+ strategy: public # auth is via OLLAMA_API_KEY Authorization header, no cookies
7
+
8
+ args:
9
+ query:
10
+ type: str
11
+ required: true
12
+ positional: true
13
+ description: Web search query
14
+ limit:
15
+ type: int
16
+ default: 5
17
+ description: Maximum number of results to return
18
+
19
+ pipeline:
20
+ - fetch:
21
+ url: https://ollama.com/api/web_search
22
+ method: POST
23
+ headers:
24
+ Accept: "application/json"
25
+ Authorization: "Bearer ${{ env.OLLAMA_API_KEY || '' }}"
26
+ body:
27
+ query: "${{ args.query }}"
28
+ - select: results
29
+ - map:
30
+ title: "${{ item.title || '' }}"
31
+ url: "${{ item.url || '' }}"
32
+ content: "${{ item.content || '' }}"
33
+ - limit: ${{ args.limit }}
34
+
35
+ columns: [title, url, content]
36
+
37
+ # schema-v2 metadata — injected by `unicli migrate schema-v2`
38
+ capabilities: ["http.fetch", "select", "map", "limit"]
39
+ minimum_capability: http.fetch
40
+ trust: public
41
+ confidentiality: public
42
+ quarantine: false
43
+ schema_version: v2
@@ -16,6 +16,12 @@ interface SpotifyConfig {
16
16
  SPOTIFY_REDIRECT_URI?: string;
17
17
  }
18
18
 
19
+ interface SpotifyTrack {
20
+ uri: string;
21
+ name: string;
22
+ artist: string;
23
+ }
24
+
19
25
  const TOKEN_PATH = join(homedir(), ".unicli", "spotify-tokens.json");
20
26
  const ENV_PATH = join(homedir(), ".unicli", "spotify.env");
21
27
 
@@ -113,13 +119,26 @@ async function spotifyApi(
113
119
  return response.json();
114
120
  }
115
121
 
116
- async function searchTrack(query: string): Promise<string> {
122
+ async function searchTrack(query: string): Promise<SpotifyTrack> {
117
123
  const data = (await spotifyApi(
118
124
  `/search?type=track&limit=1&q=${encodeURIComponent(query)}`,
119
- )) as { tracks?: { items?: Array<{ uri?: string }> } };
120
- const uri = data.tracks?.items?.[0]?.uri;
121
- if (!uri) throw new Error(`No Spotify track found for query: ${query}`);
122
- return uri;
125
+ )) as {
126
+ tracks?: {
127
+ items?: Array<{
128
+ uri?: string;
129
+ name?: string;
130
+ artists?: Array<{ name?: string }>;
131
+ }>;
132
+ };
133
+ };
134
+ const track = data.tracks?.items?.[0];
135
+ if (!track?.uri)
136
+ throw new Error(`No Spotify track found for query: ${query}`);
137
+ return {
138
+ uri: track.uri,
139
+ name: track.name ?? "",
140
+ artist: track.artists?.map((artist) => artist.name).join(", ") ?? "",
141
+ };
123
142
  }
124
143
 
125
144
  cli({
@@ -235,11 +254,38 @@ cli({
235
254
  args: [{ name: "query", type: "str", required: true, positional: true }],
236
255
  columns: ["ok", "uri"],
237
256
  func: async (_page, kwargs) => {
238
- const uri = await searchTrack(str(kwargs.query));
239
- await spotifyApi(`/me/player/queue?uri=${encodeURIComponent(uri)}`, {
257
+ const track = await searchTrack(str(kwargs.query));
258
+ await spotifyApi(`/me/player/queue?uri=${encodeURIComponent(track.uri)}`, {
240
259
  method: "POST",
241
260
  });
242
- return [{ ok: true, uri }];
261
+ return [{ ok: true, uri: track.uri }];
262
+ },
263
+ });
264
+
265
+ cli({
266
+ site: "spotify",
267
+ name: "play-track",
268
+ description: "Search Spotify for a track query and start playback",
269
+ domain: "api.spotify.com",
270
+ strategy: Strategy.COOKIE,
271
+ args: [{ name: "query", type: "str", required: true, positional: true }],
272
+ columns: ["ok", "query", "track", "artist", "uri"],
273
+ func: async (_page, kwargs) => {
274
+ const query = str(kwargs.query);
275
+ const track = await searchTrack(query);
276
+ await spotifyApi("/me/player/play", {
277
+ method: "PUT",
278
+ body: JSON.stringify({ uris: [track.uri] }),
279
+ });
280
+ return [
281
+ {
282
+ ok: true,
283
+ query,
284
+ track: track.name,
285
+ artist: track.artist,
286
+ uri: track.uri,
287
+ },
288
+ ];
243
289
  },
244
290
  });
245
291
 
@@ -14,6 +14,8 @@ args:
14
14
  pipeline:
15
15
  - fetch:
16
16
  url: https://weibo.com/ajax/side/hotSearch
17
+ headers:
18
+ Referer: https://weibo.com/
17
19
 
18
20
  - select: data.realtime
19
21
 
@@ -2,7 +2,7 @@
2
2
  * @owner Xiaohongshu browser adapters.
3
3
  * @does Detects login, risk-control, and rendered-feed state in XHS web pages.
4
4
  * @needs Browser-backed IPage from Uni-CLI runtime.
5
- * @feeds xiaohongshu.search and xiaohongshu.trending.
5
+ * @feeds xiaohongshu.feed, xiaohongshu.search, and xiaohongshu.trending.
6
6
  * @breaks XHS copy or route changes can require updating page-state detection.
7
7
  */
8
8
 
@@ -62,7 +62,7 @@ export async function assertXhsReadable(
62
62
  assertXhsReadableState(command, await readXhsPageState(page));
63
63
  }
64
64
 
65
- export async function fetchXhsFeedItems(page: IPage): Promise<unknown[]> {
65
+ async function fetchXhsStoreFeedItems(page: IPage): Promise<unknown[]> {
66
66
  const raw = await page.evaluate(`
67
67
  (async () => {
68
68
  const app = document.querySelector('#app')?.__vue_app__;
@@ -93,3 +93,60 @@ export async function fetchXhsFeedItems(page: IPage): Promise<unknown[]> {
93
93
  `);
94
94
  return Array.isArray(raw) ? raw : [];
95
95
  }
96
+
97
+ export async function fetchXhsVisibleFeedItems(
98
+ page: IPage,
99
+ ): Promise<unknown[]> {
100
+ const raw = await page.evaluate(`
101
+ (() => {
102
+ const cleanText = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
103
+ const normalizeUrl = (href) => {
104
+ if (!href) return '';
105
+ if (href.startsWith('http://') || href.startsWith('https://')) return href;
106
+ if (href.startsWith('/')) return 'https://www.xiaohongshu.com' + href;
107
+ return '';
108
+ };
109
+ const noteIdFromUrl = (url) => {
110
+ const match = url.match(/\\/(?:explore|search_result|note)\\/([^?#/]+)/i);
111
+ return match ? match[1] : '';
112
+ };
113
+ const rows = [];
114
+ const seen = new Set();
115
+ document.querySelectorAll('section.note-item, .note-item').forEach((el) => {
116
+ const link =
117
+ el.querySelector('a[href*="/explore/"]') ||
118
+ el.querySelector('a[href*="/search_result/"]') ||
119
+ el.querySelector('a[href*="/note/"]');
120
+ const url = normalizeUrl(link?.getAttribute('href') || '');
121
+ const id = noteIdFromUrl(url);
122
+ if (!id || seen.has(id)) return;
123
+ seen.add(id);
124
+
125
+ const titleEl = el.querySelector('.title, .note-title, a.title, .footer .title span');
126
+ const authorEl = el.querySelector('a.author .name, .name, .author-name, .nick-name, a.author');
127
+ const likesEl = el.querySelector('.count, .like-count, .like-wrapper .count');
128
+ const isVideo =
129
+ !!el.querySelector('video, .play-icon, .video-icon') ||
130
+ /视频/.test(cleanText(el.textContent));
131
+
132
+ rows.push({
133
+ id,
134
+ note_card: {
135
+ display_title: cleanText(titleEl?.textContent || link?.textContent || ''),
136
+ type: isVideo ? 'video' : 'normal',
137
+ user: { nickname: cleanText(authorEl?.textContent || '') },
138
+ interact_info: { liked_count: cleanText(likesEl?.textContent || '0') },
139
+ },
140
+ });
141
+ });
142
+ return rows;
143
+ })()
144
+ `);
145
+ return Array.isArray(raw) ? raw : [];
146
+ }
147
+
148
+ export async function fetchXhsFeedItems(page: IPage): Promise<unknown[]> {
149
+ const storeItems = await fetchXhsStoreFeedItems(page).catch(() => []);
150
+ if (storeItems.length > 0) return storeItems;
151
+ return fetchXhsVisibleFeedItems(page);
152
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * @owner src/adapters/xiaohongshu/feed.ts
3
+ * @does Register Xiaohongshu home-feed extraction over a logged-in browser page.
4
+ * @needs Browser-backed IPage, XHS readable-state checks, visible/store feed extraction.
5
+ * @feeds xiaohongshu.feed command.
6
+ * @breaks XHS feed route or note-card DOM drift returns structured empty_result.
7
+ * @invariants Rows expose stable note title, author, likes, type, and canonical note URL.
8
+ * @side-effects Performs authenticated read navigation to Xiaohongshu explore.
9
+ * @perf One page navigation plus at most one visible DOM extraction per invocation.
10
+ * @concurrency Stateless per invocation.
11
+ * @test tests/unit/xiaohongshu-feed.test.ts
12
+ * @stability stable
13
+ * @since 2026-06-02
14
+ */
15
+
16
+ import { cli, Strategy } from "../../registry.js";
17
+ import type { IPage } from "../../types.js";
18
+ import { socialEmptyError } from "../../social/browser-errors.js";
19
+ import { assertXhsReadable, fetchXhsFeedItems } from "./browser-state.js";
20
+
21
+ export interface XhsFeedRow {
22
+ id: string;
23
+ title: string;
24
+ author: string;
25
+ likes: string;
26
+ type: string;
27
+ url: string;
28
+ }
29
+
30
+ function asRecord(value: unknown): Record<string, unknown> {
31
+ return value && typeof value === "object"
32
+ ? (value as Record<string, unknown>)
33
+ : {};
34
+ }
35
+
36
+ function cleanText(value: unknown): string {
37
+ return String(value ?? "")
38
+ .replace(/\s+/g, " ")
39
+ .trim();
40
+ }
41
+
42
+ export function normalizeXhsFeedRows(
43
+ items: unknown[],
44
+ limit: number,
45
+ ): XhsFeedRow[] {
46
+ return items
47
+ .map((item) => {
48
+ const root = asRecord(item);
49
+ const note = asRecord(root.note_card);
50
+ const user = asRecord(note.user);
51
+ const interact = asRecord(note.interact_info);
52
+ const id = cleanText(root.id);
53
+ const title = cleanText(note.display_title);
54
+ if (!id || !title) return null;
55
+ return {
56
+ id,
57
+ title,
58
+ type: cleanText(note.type) || "normal",
59
+ author: cleanText(user.nickname),
60
+ likes: cleanText(interact.liked_count),
61
+ url: `https://www.xiaohongshu.com/explore/${id}`,
62
+ };
63
+ })
64
+ .filter((row): row is XhsFeedRow => row !== null)
65
+ .slice(0, limit);
66
+ }
67
+
68
+ cli({
69
+ site: "xiaohongshu",
70
+ name: "feed",
71
+ description: "Xiaohongshu home feed",
72
+ domain: "www.xiaohongshu.com",
73
+ strategy: Strategy.COOKIE,
74
+ browser: true,
75
+ browserSession: "user",
76
+ args: [
77
+ {
78
+ name: "limit",
79
+ type: "int",
80
+ default: 20,
81
+ description: "Number of items to return",
82
+ },
83
+ ],
84
+ columns: ["title", "author", "likes", "type", "url"],
85
+ capabilities: ["cdp-browser.navigate", "cdp-browser.evaluate"],
86
+ minimum_capability: "cdp-browser.evaluate",
87
+ async func(page, kwargs) {
88
+ const p = page as IPage;
89
+ const limit = Number(kwargs.limit) || 20;
90
+ await p.goto("https://www.xiaohongshu.com/explore", { settleMs: 2500 });
91
+ await p.wait(2);
92
+ await assertXhsReadable(p, "feed");
93
+
94
+ const rows = normalizeXhsFeedRows(await fetchXhsFeedItems(p), limit);
95
+ if (rows.length > 0) return rows;
96
+
97
+ throw socialEmptyError(
98
+ "xiaohongshu",
99
+ "feed",
100
+ "Xiaohongshu explore loaded no parseable feed rows.",
101
+ );
102
+ },
103
+ });