nolo-cli 0.1.41 → 0.1.43

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 (136) hide show
  1. package/README.md +27 -12
  2. package/agent-runtime/agentThread.ts +222 -0
  3. package/agent-runtime/agentThreadAdmission.ts +74 -0
  4. package/agent-runtime/dialogWritePlan.ts +42 -0
  5. package/agent-runtime/hostAdapter.ts +1 -0
  6. package/agent-runtime/index.ts +43 -0
  7. package/agent-runtime/localLoop.ts +31 -2
  8. package/agent-runtime/localToolPolicy.ts +1 -0
  9. package/agent-runtime/localWorkspaceTools.ts +1258 -78
  10. package/agent-runtime/noloWorkspaceTools.ts +20 -1
  11. package/agent-runtime/runtimeToolPolicy.ts +1 -5
  12. package/agent-runtime/runtimeToolSurface.ts +1 -0
  13. package/agentAliases.ts +29 -9
  14. package/agentInternalCommandEntries.ts +11 -3
  15. package/agentMachineCommands.ts +1 -0
  16. package/agentRecordCommands.ts +2 -2
  17. package/agentRecordHelpers.ts +59 -6
  18. package/agentRunCommand.ts +84 -19
  19. package/ai/agent/buildSystemPrompt.ts +14 -0
  20. package/ai/agent/cliExecutor.ts +202 -43
  21. package/ai/agent/createAgentSchema.ts +2 -2
  22. package/ai/agent/liveAudioModel.ts +1 -1
  23. package/ai/agent/streamAgentChatTurn.ts +154 -20
  24. package/ai/agent/streamAgentChatTurnUtils.ts +63 -0
  25. package/ai/ai.locale.ts +70 -0
  26. package/ai/chat/inlineImageUrlsForCustomProvider.ts +7 -2
  27. package/ai/chat/sendOpenAICompletionsRequest.ts +25 -1
  28. package/ai/llm/kimi.ts +0 -1
  29. package/ai/llm/openrouterModels.ts +27 -2
  30. package/ai/memory/policy.ts +49 -0
  31. package/ai/memory/queryShared.ts +10 -4
  32. package/ai/memory/recentRelationshipRecap.ts +4 -0
  33. package/ai/memory/remember.ts +48 -104
  34. package/ai/memory/scope.ts +74 -0
  35. package/ai/memory/types.ts +6 -0
  36. package/ai/memory/understanding.ts +10 -18
  37. package/ai/policy/runtimePolicy.ts +95 -23
  38. package/ai/tools/agent/agentTools.ts +0 -21
  39. package/ai/tools/agent/startAgentDialogTool.ts +2 -1
  40. package/ai/tools/index.ts +2 -20
  41. package/ai/tools/prepareTools.ts +171 -1
  42. package/ai/tools/readXhsProfileTool.ts +80 -69
  43. package/ai/tools/table/createTableTool.ts +11 -0
  44. package/ai/tools/toolPacks.ts +19 -0
  45. package/ai/types.ts +3 -0
  46. package/app/favorite/deletedFavoriteProjection.ts +31 -0
  47. package/app/hooks/deleteDbKey.ts +24 -3
  48. package/app/i18n/translations/interface.locale.ts +48 -16
  49. package/app/types/appSummary.ts +4 -0
  50. package/app/utils/desktopAgentRuntimeTurnClient.ts +2 -0
  51. package/auth/adminPermissions.ts +64 -4
  52. package/auth/routes.ts +5 -0
  53. package/chat/chat.locale.ts +4 -0
  54. package/chat/dialog/actions/handleSendMessageAction.ts +7 -2
  55. package/chat/dialog/dialogAttachmentCleanup.ts +135 -0
  56. package/chat/dialog/dialogSlice.ts +117 -4
  57. package/chat/messages/activityCompletion.ts +97 -0
  58. package/chat/messages/imagePayloadPersistence.ts +22 -0
  59. package/chat/messages/messageContent.ts +20 -16
  60. package/chat/messages/messageSlice.ts +20 -1
  61. package/chat/messages/toolThunks.ts +32 -11
  62. package/chat/messages/types.ts +8 -1
  63. package/chat/messages/web/toolDisplayName.ts +686 -0
  64. package/cli/agentAliases.ts +29 -9
  65. package/cli/agentInternalCommandEntries.ts +11 -3
  66. package/cli/agentMachineCommands.ts +1 -0
  67. package/cli/agentRecordCommands.ts +2 -2
  68. package/cli/agentRecordHelpers.ts +59 -6
  69. package/cli/agentRunCommand.ts +84 -19
  70. package/cli/client/agentRun.ts +65 -253
  71. package/cli/client/localRuntimeAdapter.ts +179 -53
  72. package/cli/commandRegistry.ts +7 -4
  73. package/cli/dialogAttachmentCleanup.ts +151 -0
  74. package/cli/dialogCommands.ts +163 -61
  75. package/cli/dialogInternalCommandEntries.ts +1 -1
  76. package/cli/docCreateCommands.ts +345 -0
  77. package/cli/docInternalCommandEntries.ts +11 -0
  78. package/cli/docPageHelpers.ts +80 -0
  79. package/cli/docSkillHelpers.ts +67 -0
  80. package/cli/docSpaceHelpers.ts +46 -0
  81. package/cli/internalCommandEntries.ts +4 -0
  82. package/cli/machineCommands.ts +18 -34
  83. package/cli/memoryCommands.ts +253 -0
  84. package/cli/memoryInternalCommandEntries.ts +9 -0
  85. package/cli/qoderUsageProbe.ts +252 -0
  86. package/cli/scriptCommandEntries.ts +0 -3
  87. package/cli/secretScan.ts +31 -0
  88. package/cli/tableCommands.ts +248 -13
  89. package/cli/workflowInternalCommandEntries.ts +2 -3
  90. package/client/agentRun.test.ts +68 -84
  91. package/client/agentRun.ts +65 -253
  92. package/client/localRuntimeAdapter.test.ts +505 -74
  93. package/client/localRuntimeAdapter.ts +179 -53
  94. package/commandRegistry.ts +7 -4
  95. package/connector-experimental/capabilities.ts +10 -9
  96. package/create/space/sidebarVisibleTypes.ts +19 -0
  97. package/dialogAttachmentCleanup.ts +151 -0
  98. package/dialogCommands.ts +163 -61
  99. package/dialogInternalCommandEntries.ts +1 -1
  100. package/docCreateCommands.ts +345 -0
  101. package/docInternalCommandEntries.ts +11 -0
  102. package/docPageHelpers.ts +80 -0
  103. package/docSkillHelpers.ts +67 -0
  104. package/docSpaceHelpers.ts +46 -0
  105. package/integrations/google/models.ts +4 -4
  106. package/integrations/openai/generateOpenAIRequestBody.ts +0 -3
  107. package/integrations/xhs-reader/types.ts +36 -3
  108. package/integrations/xhs-reader/url.ts +24 -1
  109. package/internalCommandEntries.ts +4 -0
  110. package/machineCommands.ts +18 -34
  111. package/memoryCommands.ts +253 -0
  112. package/memoryInternalCommandEntries.ts +9 -0
  113. package/package.json +141 -114
  114. package/qoderUsageProbe.ts +252 -0
  115. package/render/contentIcon/types.ts +167 -0
  116. package/render/page/docSlice.ts +30 -3
  117. package/render/page/types.ts +2 -0
  118. package/render/table/createTableAction.ts +4 -0
  119. package/render/table/tableSlice.ts +61 -0
  120. package/render/table/types.ts +10 -0
  121. package/scriptCommandEntries.ts +0 -3
  122. package/secretScan.ts +31 -0
  123. package/server/handlers/agentRun/types.ts +24 -23
  124. package/tableCommands.ts +248 -13
  125. package/workflowInternalCommandEntries.ts +2 -3
  126. package/ai/tools/agent/taskRunTool.ts +0 -112
  127. package/cli/client/taskRunPrompt.ts +0 -27
  128. package/cli/taskRunCommand.ts +0 -265
  129. package/client/taskRunPrompt.ts +0 -27
  130. package/integrations/xhs-reader/analyze/profileAnalyzer.ts +0 -205
  131. package/integrations/xhs-reader/backends/playwrightProfileCollector.ts +0 -624
  132. package/integrations/xhs-reader/bridge/readXhsProfileWithBridge.ts +0 -227
  133. package/integrations/xhs-reader/client/xhsWebApiClient.ts +0 -229
  134. package/integrations/xhs-reader/normalize.ts +0 -390
  135. package/integrations/xhs-reader/orchestrator.ts +0 -178
  136. package/taskRunCommand.ts +0 -265
@@ -1,390 +0,0 @@
1
- /**
2
- * Normalize XHS data shapes from various sources:
3
- * - __INITIAL_STATE__ (camelCase)
4
- * - API responses (snake_case)
5
- *
6
- * xsecToken values are preserved ONLY in an internal `_xsecToken` field
7
- * that must not be exported in public-facing results.
8
- */
9
-
10
- import type {
11
- XhsProfile,
12
- XhsNoteSummary,
13
- XhsNoteDetail,
14
- XhsComment,
15
- XhsCommentPage,
16
- } from "./types";
17
-
18
- // --- Profile normalization ---
19
-
20
- interface RawProfileState {
21
- userPageData?: {
22
- basicInfo?: {
23
- nickname?: string;
24
- userid?: string;
25
- redId?: string;
26
- red_id?: string;
27
- imageb?: string;
28
- images?: string;
29
- desc?: string;
30
- ipLocation?: string;
31
- ip_location?: string;
32
- gender?: string;
33
- };
34
- interactions?: Array<{
35
- type?: string; // "follows" | "fans" | "interaction"
36
- name?: string;
37
- count?: number | string;
38
- countText?: string;
39
- i18nCount?: string;
40
- }>;
41
- };
42
- }
43
-
44
- interface RawUserPostedNote {
45
- id?: string;
46
- noteId?: string;
47
- note_id?: string;
48
- noteCard?: RawUserPostedNote;
49
- note_card?: RawUserPostedNote;
50
- displayTitle?: string;
51
- display_title?: string;
52
- title?: string;
53
- cover?: {
54
- url?: string;
55
- urlDefault?: string;
56
- url_default?: string;
57
- url_pre?: string;
58
- };
59
- type?: string;
60
- likedCount?: string | number;
61
- liked_count?: string | number;
62
- // Nested interact info shapes (camelCase and snake_case)
63
- interactInfo?: { likedCount?: string | number; liked_count?: string | number };
64
- interact_info?: { liked_count?: string | number; likedCount?: string | number };
65
- xsecToken?: string;
66
- xsec_token?: string;
67
- }
68
-
69
- /**
70
- * Extract profile from __INITIAL_STATE__ userPageData (camelCase) or API response shape.
71
- */
72
- export function normalizeProfileState(
73
- raw: RawProfileState,
74
- userId: string,
75
- ): XhsProfile {
76
- const basic = raw.userPageData?.basicInfo ?? {};
77
- const interactions = raw.userPageData?.interactions ?? [];
78
-
79
- const getInteraction = (nameOrType: string): number | undefined => {
80
- // Match by type field first (real XHS API shape), then by name substring
81
- const entry = interactions.find(
82
- (i) =>
83
- i.type === nameOrType ||
84
- i.name?.toLowerCase().includes(nameOrType.toLowerCase()),
85
- );
86
- if (!entry) return undefined;
87
-
88
- // count may be a number or a string like "777"
89
- if (typeof entry.count === "number") return entry.count;
90
- if (typeof entry.count === "string") {
91
- const n = Number(entry.count);
92
- if (!isNaN(n)) return n;
93
- }
94
- if (entry.countText) {
95
- return parseCountText(entry.countText);
96
- }
97
- // i18nCount is also a string representation
98
- if (entry.i18nCount) {
99
- const n = Number(entry.i18nCount);
100
- if (!isNaN(n)) return n;
101
- }
102
- return undefined;
103
- };
104
-
105
- return {
106
- userId: basic.userid ?? userId,
107
- nickname: basic.nickname ?? "",
108
- redId: basic.redId ?? basic.red_id,
109
- avatarUrl: basic.imageb ?? basic.images,
110
- desc: basic.desc,
111
- ipLocation: basic.ipLocation ?? basic.ip_location,
112
- gender: basic.gender,
113
- interactionCounts: {
114
- follows: getInteraction("follows") ?? getInteraction("关注"),
115
- fans: getInteraction("fans") ?? getInteraction("粉丝"),
116
- likesAndCollects:
117
- getInteraction("interaction") ??
118
- getInteraction("获赞与收藏") ??
119
- getInteraction("赞藏"),
120
- },
121
- };
122
- }
123
-
124
- /**
125
- * Normalize a note from the user_posted list (initial state or paginated API).
126
- * xsecToken is preserved only in internal `_xsecToken`.
127
- */
128
- export function normalizeProfileNote(raw: RawUserPostedNote): XhsNoteSummary {
129
- const noteCard = raw.noteCard ?? raw.note_card;
130
- const noteId =
131
- raw.noteId ??
132
- raw.note_id ??
133
- raw.id ??
134
- noteCard?.noteId ??
135
- noteCard?.note_id ??
136
- noteCard?.id ??
137
- "";
138
- const cover =
139
- raw.cover?.url ??
140
- raw.cover?.urlDefault ??
141
- raw.cover?.url_default ??
142
- raw.cover?.url_pre ??
143
- noteCard?.cover?.url ??
144
- noteCard?.cover?.urlDefault ??
145
- noteCard?.cover?.url_default ??
146
- noteCard?.cover?.url_pre;
147
-
148
- // Resolve likedCount from multiple possible locations
149
- const resolvedLikedCount =
150
- resolveLikedCount(raw) ?? (noteCard ? resolveLikedCount(noteCard) : undefined);
151
-
152
- return {
153
- noteId,
154
- title:
155
- raw.displayTitle ??
156
- raw.display_title ??
157
- raw.title ??
158
- noteCard?.displayTitle ??
159
- noteCard?.display_title ??
160
- noteCard?.title,
161
- coverUrl: cover,
162
- type: raw.type === "video" || noteCard?.type === "video" ? "video" : "normal",
163
- likedCount: resolvedLikedCount,
164
- xsecToken: raw.xsecToken ?? raw.xsec_token ?? noteCard?.xsecToken ?? noteCard?.xsec_token,
165
- };
166
- }
167
-
168
- /**
169
- * Resolve liked count from top-level fields or nested interactInfo/interact_info.
170
- */
171
- function resolveLikedCount(raw: RawUserPostedNote): number | undefined {
172
- // Direct top-level fields
173
- if (typeof raw.likedCount === "number") return raw.likedCount;
174
- if (typeof raw.liked_count === "number") return raw.liked_count;
175
- if (raw.likedCount != null) return Number(raw.likedCount) || undefined;
176
- if (raw.liked_count != null) return Number(raw.liked_count) || undefined;
177
-
178
- // Nested interactInfo (camelCase)
179
- const ii = raw.interactInfo;
180
- if (ii) {
181
- if (typeof ii.likedCount === "number") return ii.likedCount;
182
- if (typeof ii.liked_count === "number") return ii.liked_count;
183
- if (ii.likedCount != null) return Number(ii.likedCount) || undefined;
184
- }
185
-
186
- // Nested interact_info (snake_case)
187
- const iiSnake = raw.interact_info;
188
- if (iiSnake) {
189
- if (typeof iiSnake.liked_count === "number") return iiSnake.liked_count;
190
- if (typeof iiSnake.likedCount === "number") return iiSnake.likedCount;
191
- if (iiSnake.liked_count != null) return Number(iiSnake.liked_count) || undefined;
192
- }
193
-
194
- return undefined;
195
- }
196
-
197
- /**
198
- * Normalize a note detail from the /api/sns/web/v1/feed response.
199
- *
200
- * Supports both camelCase (INITIAL_STATE) and snake_case (API) shapes:
201
- * - wrapper: { note: { noteCard: ... } } or { id, note_card, model_type }
202
- * - noteCard / note_card
203
- * - interactInfo / interact_info
204
- * - imageList / image_list
205
- * - tagList / tag_list
206
- */
207
- export function normalizeNoteDetail(
208
- raw: Record<string, unknown>,
209
- ): XhsNoteDetail {
210
- // Resolve the note-level object: may be wrapped in `note` or be top-level
211
- const note = (raw.note ?? raw) as Record<string, unknown>;
212
-
213
- // Resolve the note card: camelCase `noteCard` or snake_case `note_card`
214
- const noteCard = (note.noteCard ?? note.note_card ?? note) as Record<string, unknown>;
215
-
216
- // --- noteId ---
217
- const id = (
218
- note.noteId ??
219
- note.note_id ??
220
- note.id ??
221
- noteCard.noteId ??
222
- noteCard.note_id ??
223
- raw.id ??
224
- raw.note_id ??
225
- raw.noteId ??
226
- ""
227
- ) as string;
228
-
229
- // --- basic fields ---
230
- const title = (noteCard.title ?? note.title ?? "") as string;
231
- const desc = (noteCard.desc ?? note.desc ?? "") as string;
232
- const type = (
233
- noteCard.type === "video" || note.type === "video" ? "video" : "normal"
234
- ) as "normal" | "video";
235
-
236
- // --- user ---
237
- const user = (noteCard.user ?? note.user ?? {}) as Record<string, unknown>;
238
-
239
- // --- interact info: camelCase or snake_case ---
240
- const interactInfo = (
241
- noteCard.interactInfo ??
242
- noteCard.interact_info ??
243
- note.interactInfo ??
244
- note.interact_info ??
245
- {}
246
- ) as Record<string, unknown>;
247
-
248
- // --- images: camelCase or snake_case ---
249
- const imageList = (
250
- noteCard.imageList ??
251
- noteCard.image_list ??
252
- note.imageList ??
253
- note.image_list ??
254
- []
255
- ) as Array<Record<string, unknown>>;
256
- const imageUrls = imageList
257
- .map((img) => (
258
- img.urlDefault ?? img.url_default ?? img.url ?? firstInfoUrl(img.infoList) ?? firstInfoUrl(img.info_list)
259
- ) as string)
260
- .filter(Boolean);
261
-
262
- // --- video ---
263
- const video = (noteCard.video ?? note.video ?? {}) as Record<string, unknown>;
264
- const videoMedia = (video.media ?? {}) as Record<string, unknown>;
265
- const videoStream = (videoMedia.stream ?? {}) as Record<string, unknown>;
266
- const h264 = (videoStream.h264 ?? videoStream.H264 ?? []) as Array<Record<string, unknown>>;
267
- const videoUrl = h264.length > 0 ? (h264[0].masterUrl as string) : undefined;
268
-
269
- // --- tags: camelCase or snake_case ---
270
- const rawTagList = (
271
- noteCard.tagList ??
272
- noteCard.tag_list ??
273
- note.tagList ??
274
- note.tag_list ??
275
- []
276
- ) as Array<Record<string, unknown>>;
277
- const tagList = rawTagList
278
- .map((t) => (t.name ?? t.tagType ?? t.tag_type ?? "") as string)
279
- .filter(Boolean);
280
-
281
- // --- ipLocation ---
282
- const ipLocation = (
283
- noteCard.ipLocation ??
284
- noteCard.ip_location ??
285
- note.ipLocation ??
286
- note.ip_location ??
287
- undefined
288
- ) as string | undefined;
289
-
290
- // --- metrics ---
291
- const likedCount = Number(interactInfo.likedCount ?? interactInfo.liked_count ?? 0);
292
- const collectedCount = Number(
293
- interactInfo.collectedCount ?? interactInfo.collected_count ?? 0,
294
- );
295
- const commentCount = Number(
296
- interactInfo.commentCount ?? interactInfo.comment_count ?? 0,
297
- );
298
- const shareCount = Number(
299
- interactInfo.shareCount ?? interactInfo.share_count ?? interactInfo.sharedCount ?? interactInfo.shared_count ?? 0,
300
- );
301
-
302
- return {
303
- noteId: id,
304
- title,
305
- desc,
306
- type,
307
- userId: (user.userId ?? user.userid ?? user.user_id ?? "") as string,
308
- nickname: (user.nickname ?? "") as string,
309
- avatarUrl: (user.avatar ?? user.imageb ?? undefined) as string | undefined,
310
- imageUrls: imageUrls.length > 0 ? imageUrls : undefined,
311
- videoUrl,
312
- tagList: tagList.length > 0 ? tagList : undefined,
313
- ipLocation,
314
- metrics: { likedCount, collectedCount, commentCount, shareCount },
315
- createdAt: (note.time ?? noteCard.time ?? undefined) as string | undefined,
316
- lastUpdateTime: (
317
- noteCard.lastUpdateTime ?? noteCard.last_update_time ?? note.lastUpdateTime ?? undefined
318
- ) as string | undefined,
319
- };
320
- }
321
-
322
- /**
323
- * Normalize a single comment from the comment API.
324
- */
325
- export function normalizeComment(raw: Record<string, unknown>): XhsComment {
326
- const user = (raw.user ?? {}) as Record<string, unknown>;
327
- const subComments = (raw.sub_comments ?? raw.subComments ?? []) as Array<
328
- Record<string, unknown>
329
- >;
330
-
331
- return {
332
- commentId: (raw.id ?? raw.commentId ?? "") as string,
333
- userId: (user.user_id ?? user.userId ?? "") as string,
334
- nickname: (user.nickname ?? "") as string,
335
- avatarUrl: (user.image ?? user.avatar ?? undefined) as string | undefined,
336
- content: (raw.content ?? "") as string,
337
- likeCount: Number(raw.likeCount ?? raw.like_count ?? 0),
338
- subCommentCount: Number(
339
- raw.sub_comment_count ?? raw.subCommentCount ?? subComments.length,
340
- ),
341
- subComments: subComments.map(normalizeComment),
342
- ipLocation: (raw.ipLocation ?? raw.ip_location ?? undefined) as string | undefined,
343
- createdAt: (raw.createTime ?? raw.create_time ?? undefined) as string | undefined,
344
- };
345
- }
346
-
347
- /**
348
- * Normalize a comment page response.
349
- */
350
- export function normalizeCommentPage(
351
- raw: Record<string, unknown>,
352
- ): XhsCommentPage {
353
- const data = (raw.data ?? raw) as Record<string, unknown>;
354
- const comments = (data.comments ?? []) as Array<Record<string, unknown>>;
355
- return {
356
- comments: comments.map(normalizeComment),
357
- hasMore: Boolean(data.has_more ?? data.hasMore ?? false),
358
- cursor: (data.cursor ?? undefined) as string | undefined,
359
- };
360
- }
361
-
362
- // --- Helpers ---
363
-
364
- /**
365
- * Safely extract the URL from the first entry of an infoList/info_list array.
366
- * Handles the `unknown` element type without using `any`.
367
- */
368
- function firstInfoUrl(list: unknown): string | undefined {
369
- if (!Array.isArray(list) || list.length === 0) return undefined;
370
- const first = list[0] as Record<string, unknown> | undefined;
371
- if (first && typeof first.url === "string") return first.url;
372
- return undefined;
373
- }
374
-
375
- /**
376
- * Parse Chinese count text like "1.2万" -> 12000, "999" -> 999
377
- */
378
- function parseCountText(text: string): number | undefined {
379
- const cleaned = text.trim();
380
- if (!cleaned) return undefined;
381
-
382
- const wanMatch = cleaned.match(/([\d.]+)\s*万/);
383
- if (wanMatch) return Math.round(parseFloat(wanMatch[1]) * 10000);
384
-
385
- const yiMatch = cleaned.match(/([\d.]+)\s*亿/);
386
- if (yiMatch) return Math.round(parseFloat(yiMatch[1]) * 100000000);
387
-
388
- const num = Number(cleaned.replace(/,/g, ""));
389
- return isNaN(num) ? undefined : num;
390
- }
@@ -1,178 +0,0 @@
1
- /**
2
- * XHS Profile Collection Orchestrator
3
- *
4
- * Coordinates the Playwright profile collector, API client, and analyzer
5
- * to produce a complete XhsProfileCollection.
6
- */
7
-
8
- import { collectProfilePage } from "./backends/playwrightProfileCollector";
9
- import type { XhsPageLike } from "./backends/playwrightProfileCollector";
10
- import { createXhsApiClient } from "./client/xhsWebApiClient";
11
- import { analyzeProfile } from "./analyze/profileAnalyzer";
12
- import { redactXhsSensitiveValue } from "./redaction";
13
- import type {
14
- XhsProfileCollection,
15
- XhsReadResult,
16
- XhsNoteDetail,
17
- XhsComment,
18
- XhsConnectorOptions,
19
- } from "./types";
20
- import { createXhsFailure, createXhsSuccess } from "./types";
21
-
22
- export interface CollectXhsProfileOptions {
23
- profileUrl: string;
24
- maxScrollPages?: number; // default 0
25
- enrichDetails?: boolean; // default false
26
- includeComments?: boolean; // default false
27
- maxCommentPagesPerNote?: number; // default 1
28
- headless?: boolean; // default true
29
- /** Cookie string for API requests. Not stored in results. */
30
- cookie?: string;
31
- /** Injected page for testing. */
32
- page?: XhsPageLike;
33
- /** Injected fetch for testing API calls. */
34
- fetchImpl?: typeof fetch;
35
- /** Optional: only enrich details for notes with more than N likes. */
36
- minLikesForDetail?: number;
37
- /** Optional: only collect comments for notes with more than N comments. */
38
- minCommentsForCollect?: number;
39
- }
40
-
41
- /**
42
- * Main orchestrator to collect a full XHS profile.
43
- *
44
- * Returns a redacted XhsProfileCollection or a failure.
45
- * When profile and notes are both empty, returns a structured diagnostic
46
- * instead of silent success.
47
- */
48
- export async function collectXhsProfile(
49
- options: CollectXhsProfileOptions,
50
- ): Promise<XhsReadResult<XhsProfileCollection>> {
51
- const {
52
- profileUrl,
53
- maxScrollPages = 0,
54
- enrichDetails = false,
55
- includeComments = false,
56
- maxCommentPagesPerNote = 1,
57
- cookie,
58
- page,
59
- fetchImpl,
60
- minLikesForDetail,
61
- minCommentsForCollect,
62
- } = options;
63
-
64
- // Step 1: Collect profile and note list via Playwright
65
- if (!page) {
66
- return createXhsFailure({
67
- code: "unknown",
68
- message:
69
- "A Playwright page must be provided (via options.page). " +
70
- "The orchestrator does not manage browser lifecycle.",
71
- });
72
- }
73
-
74
- let profileResult;
75
- try {
76
- profileResult = await collectProfilePage(page, profileUrl, maxScrollPages);
77
- } catch (err) {
78
- return createXhsFailure({
79
- code: "network_error",
80
- message: `Profile page collection failed: ${String(err)}`,
81
- });
82
- }
83
-
84
- const { profile, notes, diagnostic } = profileResult;
85
-
86
- // Step 2: If collector detected a blocker (login/blocked/empty), return failure
87
- // with diagnostic instead of silently succeeding with empty data.
88
- if (diagnostic && notes.length === 0) {
89
- // Profile is empty too? Return as failure with diagnostic.
90
- const profileEmpty = !profile.nickname && !profile.redId;
91
- if (profileEmpty) {
92
- return createXhsFailure({
93
- code: diagnostic.code,
94
- message: diagnostic.message,
95
- });
96
- }
97
- // Profile exists but notes are empty — still return the data but attach diagnostic.
98
- // This allows agents to see the profile but know notes collection failed.
99
- }
100
-
101
- // Step 3: Enrich with note details via API (optional)
102
- let noteDetails: XhsNoteDetail[] = [];
103
- if (enrichDetails && cookie && notes.length > 0) {
104
- const client = createXhsApiClient({ cookie, fetchImpl });
105
-
106
- const notesToEnrich = minLikesForDetail
107
- ? notes.filter((n) => (n.likedCount ?? 0) >= minLikesForDetail)
108
- : notes;
109
-
110
- for (const note of notesToEnrich) {
111
- const result = await client.getNoteDetail(
112
- note.noteId,
113
- note.xsecToken,
114
- "pc_user",
115
- );
116
- if (result.ok) {
117
- noteDetails.push(result.data);
118
- }
119
- // Skip failures silently - note details are best-effort
120
- }
121
- }
122
-
123
- // Step 4: Collect comments (optional)
124
- const commentsByNote: Record<string, XhsComment[]> = {};
125
- if (includeComments && cookie && notes.length > 0) {
126
- const client = createXhsApiClient({ cookie, fetchImpl });
127
-
128
- // Build a lookup from noteId -> xsecToken from original notes
129
- const xsecTokenByNoteId = new Map<string, string | undefined>();
130
- for (const n of notes) {
131
- xsecTokenByNoteId.set(n.noteId, n.xsecToken);
132
- }
133
-
134
- // Determine which notes to collect comments for
135
- const notesForComments: Array<{ noteId: string }> = minCommentsForCollect
136
- ? noteDetails.filter(
137
- (d) => d.metrics.commentCount >= minCommentsForCollect,
138
- )
139
- : noteDetails.length > 0
140
- ? noteDetails
141
- : notes;
142
-
143
- for (const detail of notesForComments) {
144
- const noteXsec = xsecTokenByNoteId.get(detail.noteId);
145
- const result = await client.getAllCommentPages(
146
- detail.noteId,
147
- noteXsec,
148
- maxCommentPagesPerNote,
149
- );
150
- if (result.ok) {
151
- commentsByNote[detail.noteId] = result.data.comments;
152
- }
153
- // Skip failures silently
154
- }
155
- }
156
-
157
- // Step 5: Run analyzer
158
- const analysis = analyzeProfile(notes, noteDetails, commentsByNote);
159
-
160
- // Step 6: Build result and redact sensitive fields
161
- const collection: XhsProfileCollection = {
162
- profile,
163
- notes: notes.map((n) => {
164
- // Remove xsecToken from public result
165
- const { xsecToken, ...publicNote } = n;
166
- return publicNote;
167
- }),
168
- noteDetails,
169
- commentsByNote,
170
- analysis,
171
- diagnostic,
172
- };
173
-
174
- // Final redaction pass
175
- const redacted = redactXhsSensitiveValue(collection) as XhsProfileCollection;
176
-
177
- return createXhsSuccess(redacted);
178
- }