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.
- package/README.md +27 -12
- package/agent-runtime/agentThread.ts +222 -0
- package/agent-runtime/agentThreadAdmission.ts +74 -0
- package/agent-runtime/dialogWritePlan.ts +42 -0
- package/agent-runtime/hostAdapter.ts +1 -0
- package/agent-runtime/index.ts +43 -0
- package/agent-runtime/localLoop.ts +31 -2
- package/agent-runtime/localToolPolicy.ts +1 -0
- package/agent-runtime/localWorkspaceTools.ts +1258 -78
- package/agent-runtime/noloWorkspaceTools.ts +20 -1
- package/agent-runtime/runtimeToolPolicy.ts +1 -5
- package/agent-runtime/runtimeToolSurface.ts +1 -0
- package/agentAliases.ts +29 -9
- package/agentInternalCommandEntries.ts +11 -3
- package/agentMachineCommands.ts +1 -0
- package/agentRecordCommands.ts +2 -2
- package/agentRecordHelpers.ts +59 -6
- package/agentRunCommand.ts +84 -19
- package/ai/agent/buildSystemPrompt.ts +14 -0
- package/ai/agent/cliExecutor.ts +202 -43
- package/ai/agent/createAgentSchema.ts +2 -2
- package/ai/agent/liveAudioModel.ts +1 -1
- package/ai/agent/streamAgentChatTurn.ts +154 -20
- package/ai/agent/streamAgentChatTurnUtils.ts +63 -0
- package/ai/ai.locale.ts +70 -0
- package/ai/chat/inlineImageUrlsForCustomProvider.ts +7 -2
- package/ai/chat/sendOpenAICompletionsRequest.ts +25 -1
- package/ai/llm/kimi.ts +0 -1
- package/ai/llm/openrouterModels.ts +27 -2
- package/ai/memory/policy.ts +49 -0
- package/ai/memory/queryShared.ts +10 -4
- package/ai/memory/recentRelationshipRecap.ts +4 -0
- package/ai/memory/remember.ts +48 -104
- package/ai/memory/scope.ts +74 -0
- package/ai/memory/types.ts +6 -0
- package/ai/memory/understanding.ts +10 -18
- package/ai/policy/runtimePolicy.ts +95 -23
- package/ai/tools/agent/agentTools.ts +0 -21
- package/ai/tools/agent/startAgentDialogTool.ts +2 -1
- package/ai/tools/index.ts +2 -20
- package/ai/tools/prepareTools.ts +171 -1
- package/ai/tools/readXhsProfileTool.ts +80 -69
- package/ai/tools/table/createTableTool.ts +11 -0
- package/ai/tools/toolPacks.ts +19 -0
- package/ai/types.ts +3 -0
- package/app/favorite/deletedFavoriteProjection.ts +31 -0
- package/app/hooks/deleteDbKey.ts +24 -3
- package/app/i18n/translations/interface.locale.ts +48 -16
- package/app/types/appSummary.ts +4 -0
- package/app/utils/desktopAgentRuntimeTurnClient.ts +2 -0
- package/auth/adminPermissions.ts +64 -4
- package/auth/routes.ts +5 -0
- package/chat/chat.locale.ts +4 -0
- package/chat/dialog/actions/handleSendMessageAction.ts +7 -2
- package/chat/dialog/dialogAttachmentCleanup.ts +135 -0
- package/chat/dialog/dialogSlice.ts +117 -4
- package/chat/messages/activityCompletion.ts +97 -0
- package/chat/messages/imagePayloadPersistence.ts +22 -0
- package/chat/messages/messageContent.ts +20 -16
- package/chat/messages/messageSlice.ts +20 -1
- package/chat/messages/toolThunks.ts +32 -11
- package/chat/messages/types.ts +8 -1
- package/chat/messages/web/toolDisplayName.ts +686 -0
- package/cli/agentAliases.ts +29 -9
- package/cli/agentInternalCommandEntries.ts +11 -3
- package/cli/agentMachineCommands.ts +1 -0
- package/cli/agentRecordCommands.ts +2 -2
- package/cli/agentRecordHelpers.ts +59 -6
- package/cli/agentRunCommand.ts +84 -19
- package/cli/client/agentRun.ts +65 -253
- package/cli/client/localRuntimeAdapter.ts +179 -53
- package/cli/commandRegistry.ts +7 -4
- package/cli/dialogAttachmentCleanup.ts +151 -0
- package/cli/dialogCommands.ts +163 -61
- package/cli/dialogInternalCommandEntries.ts +1 -1
- package/cli/docCreateCommands.ts +345 -0
- package/cli/docInternalCommandEntries.ts +11 -0
- package/cli/docPageHelpers.ts +80 -0
- package/cli/docSkillHelpers.ts +67 -0
- package/cli/docSpaceHelpers.ts +46 -0
- package/cli/internalCommandEntries.ts +4 -0
- package/cli/machineCommands.ts +18 -34
- package/cli/memoryCommands.ts +253 -0
- package/cli/memoryInternalCommandEntries.ts +9 -0
- package/cli/qoderUsageProbe.ts +252 -0
- package/cli/scriptCommandEntries.ts +0 -3
- package/cli/secretScan.ts +31 -0
- package/cli/tableCommands.ts +248 -13
- package/cli/workflowInternalCommandEntries.ts +2 -3
- package/client/agentRun.test.ts +68 -84
- package/client/agentRun.ts +65 -253
- package/client/localRuntimeAdapter.test.ts +505 -74
- package/client/localRuntimeAdapter.ts +179 -53
- package/commandRegistry.ts +7 -4
- package/connector-experimental/capabilities.ts +10 -9
- package/create/space/sidebarVisibleTypes.ts +19 -0
- package/dialogAttachmentCleanup.ts +151 -0
- package/dialogCommands.ts +163 -61
- package/dialogInternalCommandEntries.ts +1 -1
- package/docCreateCommands.ts +345 -0
- package/docInternalCommandEntries.ts +11 -0
- package/docPageHelpers.ts +80 -0
- package/docSkillHelpers.ts +67 -0
- package/docSpaceHelpers.ts +46 -0
- package/integrations/google/models.ts +4 -4
- package/integrations/openai/generateOpenAIRequestBody.ts +0 -3
- package/integrations/xhs-reader/types.ts +36 -3
- package/integrations/xhs-reader/url.ts +24 -1
- package/internalCommandEntries.ts +4 -0
- package/machineCommands.ts +18 -34
- package/memoryCommands.ts +253 -0
- package/memoryInternalCommandEntries.ts +9 -0
- package/package.json +141 -114
- package/qoderUsageProbe.ts +252 -0
- package/render/contentIcon/types.ts +167 -0
- package/render/page/docSlice.ts +30 -3
- package/render/page/types.ts +2 -0
- package/render/table/createTableAction.ts +4 -0
- package/render/table/tableSlice.ts +61 -0
- package/render/table/types.ts +10 -0
- package/scriptCommandEntries.ts +0 -3
- package/secretScan.ts +31 -0
- package/server/handlers/agentRun/types.ts +24 -23
- package/tableCommands.ts +248 -13
- package/workflowInternalCommandEntries.ts +2 -3
- package/ai/tools/agent/taskRunTool.ts +0 -112
- package/cli/client/taskRunPrompt.ts +0 -27
- package/cli/taskRunCommand.ts +0 -265
- package/client/taskRunPrompt.ts +0 -27
- package/integrations/xhs-reader/analyze/profileAnalyzer.ts +0 -205
- package/integrations/xhs-reader/backends/playwrightProfileCollector.ts +0 -624
- package/integrations/xhs-reader/bridge/readXhsProfileWithBridge.ts +0 -227
- package/integrations/xhs-reader/client/xhsWebApiClient.ts +0 -229
- package/integrations/xhs-reader/normalize.ts +0 -390
- package/integrations/xhs-reader/orchestrator.ts +0 -178
- 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
|
-
}
|