nolo-cli 0.1.40 → 0.1.41
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/agent-runtime/hybridRecordStore.ts +5 -3
- package/ai/agent/_executeModel.ts +146 -0
- package/ai/agent/agentSlice.ts +556 -1
- package/ai/agent/appWorkingMemory.ts +126 -0
- package/ai/agent/buildEditingContext.ts +378 -0
- package/ai/agent/buildSystemPrompt.ts +585 -0
- package/ai/agent/canvasEditingContext.ts +49 -0
- package/ai/agent/cleanAgentMessages.ts +140 -0
- package/ai/agent/cliChatClient.ts +153 -0
- package/ai/agent/cliExecutor.ts +65 -6
- package/ai/agent/contextCompiler.ts +107 -0
- package/ai/agent/contextLayerContract.ts +44 -0
- package/ai/agent/createAgentSchema.ts +272 -0
- package/ai/agent/currentTimeContext.ts +65 -0
- package/ai/agent/executeToolCall.ts +58 -0
- package/ai/agent/fetchAgentContexts.ts +42 -0
- package/ai/agent/generatePrompt.ts +3 -0
- package/ai/agent/getFullChatContextKeys.ts +168 -0
- package/ai/agent/guidedCreation/capabilities.ts +84 -0
- package/ai/agent/guidedCreation/types.ts +38 -0
- package/ai/agent/isLiveAudioOnlyAgent.ts +26 -0
- package/ai/agent/liveAudioModel.ts +2 -0
- package/ai/agent/pageBuilderHandoffRules.ts +75 -0
- package/ai/agent/persistMessageWithFixedId.ts +37 -0
- package/ai/agent/planSlice.ts +259 -0
- package/ai/agent/referenceUtils.ts +229 -0
- package/ai/agent/runAgentBackground.ts +310 -0
- package/ai/agent/runAgentClientLoop.ts +164 -0
- package/ai/agent/runtimeGuidance.ts +116 -0
- package/ai/agent/serverOrigin.ts +7 -0
- package/ai/agent/startParallelAgentStreams.ts +424 -0
- package/ai/agent/startupProtocol.ts +53 -0
- package/ai/agent/streamAgentChatTurn.ts +2001 -0
- package/ai/agent/streamAgentChatTurnUtils.ts +860 -0
- package/ai/agent/toolFailureGuard.ts +99 -0
- package/ai/agent/types.ts +73 -0
- package/ai/agent/utils/imageOutput.ts +37 -0
- package/ai/agent/utils/publicImageAgentMode.ts +26 -0
- package/ai/ai.locale.ts +1095 -0
- package/ai/chat/accumulateToolCallChunks.ts +126 -0
- package/ai/chat/fetchUtils.ts +143 -0
- package/ai/chat/inlineImageUrlsForCustomProvider.ts +117 -0
- package/ai/chat/parseApiError.ts +67 -0
- package/ai/chat/parseMultilineSSE.ts +95 -0
- package/ai/chat/sendOpenAICompletionsRequest.ts +897 -0
- package/ai/chat/sendOpenAIResponseRequest.ts +720 -0
- package/ai/chat/serverProxyRetry.ts +102 -0
- package/ai/chat/shouldUseServerProxy.ts +18 -0
- package/ai/chat/streamRetry.ts +46 -0
- package/ai/chat/updateTotalUsage.ts +89 -0
- package/ai/context/buildReferenceContext.ts +437 -0
- package/ai/context/retention.ts +165 -0
- package/ai/context/tokenUtils.ts +78 -0
- package/ai/llm/agentCapabilities.ts +74 -0
- package/ai/llm/deepinfra.ts +79 -0
- package/ai/llm/fireworks.ts +68 -0
- package/ai/llm/generateRequestBody.ts +165 -0
- package/ai/llm/getModelContextWindow.ts +93 -0
- package/ai/llm/getPricing.ts +252 -0
- package/ai/llm/imagePricing.ts +66 -0
- package/ai/llm/isResponseAPIModel.ts +13 -0
- package/ai/llm/kimi.ts +23 -0
- package/ai/llm/mimo.ts +99 -0
- package/ai/llm/mistral.ts +21 -0
- package/ai/llm/models.ts +45 -0
- package/ai/llm/openrouterModels.ts +6 -0
- package/ai/llm/providers.ts +313 -0
- package/ai/llm/reasoningModels.ts +27 -0
- package/ai/llm/types.ts +82 -0
- package/ai/llm/usageRequestOptions.ts +59 -0
- package/ai/memory/queryShared.ts +161 -0
- package/ai/memory/recentRelationshipRecap.ts +247 -0
- package/ai/memory/remember.ts +167 -0
- package/ai/memory/store.ts +20 -0
- package/ai/memory/storeShared.ts +76 -0
- package/ai/memory/types.ts +46 -0
- package/ai/memory/understanding.ts +349 -0
- package/ai/memory/understandingGreeting.ts +264 -0
- package/ai/policy/personalizationDialog.ts +333 -0
- package/ai/policy/runtimePolicy.ts +454 -0
- package/ai/policy/selfUpdateFields.ts +48 -0
- package/ai/policy/types.ts +64 -0
- package/ai/skills/referenceRuntime.ts +274 -0
- package/ai/skills/skillDiagnostics.ts +251 -0
- package/ai/skills/skillDocBuilder.ts +139 -0
- package/ai/skills/skillSummaryMarker.ts +26 -0
- package/ai/token/calculatePrice.ts +576 -0
- package/ai/token/missingUsageEstimate.ts +42 -0
- package/ai/token/normalizeUsage.ts +124 -0
- package/ai/token/openaiImageGenerationUsage.ts +56 -0
- package/ai/token/prepareTokenUsageData.ts +93 -0
- package/ai/token/resolveBillingTarget.ts +52 -0
- package/ai/token/saveTokenRecord.ts +53 -0
- package/ai/token/types.ts +103 -0
- package/ai/tools/agent/agentTools.ts +235 -0
- package/ai/tools/agent/agentUpdateShared.ts +311 -0
- package/ai/tools/agent/callAgentTool.ts +139 -0
- package/ai/tools/agent/createAgentTool.ts +512 -0
- package/ai/tools/agent/createDialogTool.ts +69 -0
- package/ai/tools/agent/createSkillAgentTool.ts +62 -0
- package/ai/tools/agent/parallelBudget.ts +221 -0
- package/ai/tools/agent/prepareAgentDraftTool.ts +159 -0
- package/ai/tools/agent/presets/appBuilderPreset.ts +154 -0
- package/ai/tools/agent/runLlmTool.ts +96 -0
- package/ai/tools/agent/runStreamingAgentTool.ts +73 -0
- package/ai/tools/agent/skillAgentArgs.ts +106 -0
- package/ai/tools/agent/startAgentDialogTool.ts +53 -0
- package/ai/tools/agent/streamParallelAgentsTool.ts +123 -0
- package/ai/tools/agent/updateAgentTool.ts +96 -0
- package/ai/tools/agent/updateSelfTool.ts +113 -0
- package/ai/tools/amazonProductScraperTool.ts +86 -0
- package/ai/tools/apifyActorClient.ts +45 -0
- package/ai/tools/appEditGuard.ts +372 -0
- package/ai/tools/appReadSnapshot.ts +153 -0
- package/ai/tools/appTools.ts +1549 -0
- package/ai/tools/applyEditTool.ts +259 -0
- package/ai/tools/applyLineEditsTool.ts +315 -0
- package/ai/tools/browserTools/click.ts +33 -0
- package/ai/tools/browserTools/closeSession.ts +29 -0
- package/ai/tools/browserTools/common.ts +27 -0
- package/ai/tools/browserTools/openSession.ts +48 -0
- package/ai/tools/browserTools/readContent.ts +38 -0
- package/ai/tools/browserTools/selectOption.ts +46 -0
- package/ai/tools/browserTools/typeText.ts +42 -0
- package/ai/tools/category/createCategoryTool.ts +66 -0
- package/ai/tools/category/queryContentsByCategoryTool.ts +69 -0
- package/ai/tools/category/updateContentCategoryTool.ts +75 -0
- package/ai/tools/cfBrowserTools.ts +319 -0
- package/ai/tools/cfSpeechToTextTool.ts +49 -0
- package/ai/tools/checkEnvTool.ts +102 -0
- package/ai/tools/cloudflareCrawlTool.ts +289 -0
- package/ai/tools/codeSearchTool.ts +123 -0
- package/ai/tools/codeTools.ts +101 -0
- package/ai/tools/convertMarxistsBookTool.ts +103 -0
- package/ai/tools/createDocTool.ts +132 -0
- package/ai/tools/createSkillDocTool.ts +155 -0
- package/ai/tools/createWorkflowTool.ts +154 -0
- package/ai/tools/delayTool.ts +31 -0
- package/ai/tools/deleteSpacesTool.ts +325 -0
- package/ai/tools/deleteSpacesToolModel.ts +159 -0
- package/ai/tools/devReloadUtils.ts +29 -0
- package/ai/tools/dialogMessageSearch.ts +137 -0
- package/ai/tools/doctorSkillTool.ts +72 -0
- package/ai/tools/ecommerceScraperTool.ts +86 -0
- package/ai/tools/emailTools.ts +549 -0
- package/ai/tools/evalSkillTool.ts +92 -0
- package/ai/tools/exaSearchTool.ts +63 -0
- package/ai/tools/execShellTool.ts +266 -0
- package/ai/tools/executeSqlTool.ts +192 -0
- package/ai/tools/fetchWebpageSupport.ts +333 -0
- package/ai/tools/fetchWebpageTool.ts +97 -0
- package/ai/tools/geminiImagePreviewTool.ts +386 -0
- package/ai/tools/generateDocxTool.ts +215 -0
- package/ai/tools/googleSearchScraperTool.ts +106 -0
- package/ai/tools/importDataTool.ts +133 -0
- package/ai/tools/importSkillTool.ts +162 -0
- package/ai/tools/index.ts +2308 -0
- package/ai/tools/jdProductScraperTool.ts +821 -0
- package/ai/tools/listUserSpacesTool.ts +113 -0
- package/ai/tools/localFilesTool.ts +276 -0
- package/ai/tools/marxistsOfflineBook.ts +253 -0
- package/ai/tools/modelUsageTools.ts +206 -0
- package/ai/tools/noloWorkspaceReadTools.ts +467 -0
- package/ai/tools/olmOcrTool.ts +33 -0
- package/ai/tools/openaiImageTool.ts +267 -0
- package/ai/tools/prepareTools.ts +23 -0
- package/ai/tools/readDocTool.ts +88 -0
- package/ai/tools/readFileTool.ts +214 -0
- package/ai/tools/readTool.ts +194 -0
- package/ai/tools/readXPostTool.ts +229 -0
- package/ai/tools/readXhsProfileTool.ts +627 -0
- package/ai/tools/rememberMemoryTool.ts +84 -0
- package/ai/tools/remotionVideoTool.ts +151 -0
- package/ai/tools/searchDialogMessagesTool.ts +222 -0
- package/ai/tools/searchWorkspaceTool.ts +259 -0
- package/ai/tools/skillFollowup.ts +86 -0
- package/ai/tools/surfWeatherTool.ts +169 -0
- package/ai/tools/table/addTableRowTool.ts +217 -0
- package/ai/tools/table/createTableTool.ts +315 -0
- package/ai/tools/table/rowTools.ts +386 -0
- package/ai/tools/table/schemaTools.ts +244 -0
- package/ai/tools/table/shareTableTool.ts +148 -0
- package/ai/tools/table/toolShared.ts +129 -0
- package/ai/tools/taobaoTmallProductScraperTool.ts +49 -0
- package/ai/tools/toolApiClient.ts +279 -0
- package/ai/tools/toolResultError.ts +42 -0
- package/ai/tools/toolRunSlice.ts +303 -0
- package/ai/tools/toolSchemaCompatibility.ts +53 -0
- package/ai/tools/uiAskChoiceTool.ts +104 -0
- package/ai/tools/updateContentTitleTool.ts +84 -0
- package/ai/tools/updateDocTool.ts +105 -0
- package/ai/tools/updateUserPreferenceProfileTool.ts +153 -0
- package/ai/tools/wereadGatewayTool.ts +152 -0
- package/ai/tools/whisperTool.ts +76 -0
- package/ai/tools/writeFileTool.ts +213 -0
- package/ai/tools/youtubeScraperTool.ts +116 -0
- package/ai/tools/ziweiChartTool.ts +678 -0
- package/ai/types.ts +55 -0
- package/ai/workflow/workflowExecutor.ts +323 -0
- package/ai/workflow/workflowSlice.ts +73 -0
- package/ai/workflow/workflowTypes.ts +106 -0
- package/app/actions/syncAppRecord.ts +73 -0
- package/app/constants/animationSets.ts +21 -0
- package/app/constants/appEditor.ts +99 -0
- package/app/favorite/favoriteSlice.ts +690 -0
- package/app/favorite/type.ts +43 -0
- package/app/hooks/deleteDbKey.ts +233 -0
- package/app/i18n/client.ts +13 -0
- package/app/i18n/i18n.base.ts +14 -0
- package/app/i18n/i18n.config.ts +77 -0
- package/app/i18n/index.ts +15 -0
- package/app/i18n/mapLanguage.ts +11 -0
- package/app/i18n/translations/appBuilder.locale.ts +453 -0
- package/app/i18n/translations/error.locale.ts +246 -0
- package/app/i18n/translations/interface.locale.ts +2174 -0
- package/app/i18n/translations/pricing.locale.ts +397 -0
- package/app/i18n/translations/privacy.locale.ts +168 -0
- package/app/i18n/translations/seo.locale.ts +129 -0
- package/app/i18n/translations/terms.locale.ts +124 -0
- package/app/i18n/types.ts +6 -0
- package/app/notifications/model.ts +77 -0
- package/app/notifications/notificationSlice.ts +141 -0
- package/app/reducer.ts +36 -0
- package/app/settings/dbActionThunks.ts +34 -0
- package/app/settings/defaultSpacePreference.ts +87 -0
- package/app/settings/settingSlice.tsx +1212 -0
- package/app/settings/settingsRecord.ts +8 -0
- package/app/stateViews/runtime.ts +90 -0
- package/app/store.ts +55 -0
- package/app/theme/colors.ts +280 -0
- package/app/theme/fontPreference.ts +77 -0
- package/app/theme/theme.config.ts +120 -0
- package/app/theme/themeModeBootstrap.ts +123 -0
- package/app/types/appSummary.ts +114 -0
- package/app/types.ts +358 -0
- package/app/utils/appKeys.ts +55 -0
- package/app/utils/async.ts +28 -0
- package/app/utils/desktopAgentRuntimeTurnClient.ts +73 -0
- package/app/utils/env.ts +17 -0
- package/app/utils/fileUtils.ts +229 -0
- package/app/utils/imageUtils.ts +274 -0
- package/app/utils/myContentItems.ts +289 -0
- package/app/utils/retryAfter.ts +36 -0
- package/app/utils/retryFetch.ts +61 -0
- package/app/utils/toast.ts +3 -0
- package/auth/action/signUpAction.ts +227 -0
- package/auth/adminPermissions.ts +18 -0
- package/auth/authSlice.ts +531 -0
- package/auth/client/loginRequest.ts +14 -0
- package/auth/resetAuthScopedClientState.ts +27 -0
- package/auth/routes.ts +178 -0
- package/auth/token.ts +63 -0
- package/auth/types.ts +40 -0
- package/chat/chat.locale.ts +813 -0
- package/chat/dialog/actions/addCybotAction.ts +52 -0
- package/chat/dialog/actions/addReferenceKeysAction.ts +62 -0
- package/chat/dialog/actions/builtinDialogLlm.ts +35 -0
- package/chat/dialog/actions/cleanupCliSession.ts +40 -0
- package/chat/dialog/actions/createDialogAction.ts +379 -0
- package/chat/dialog/actions/createScheduledTaskAction.ts +78 -0
- package/chat/dialog/actions/handleSendMessageAction.ts +197 -0
- package/chat/dialog/actions/handleSendMessageResolver.ts +27 -0
- package/chat/dialog/actions/removeCybotAction.ts +6 -0
- package/chat/dialog/actions/removeDialogAgentAction.ts +44 -0
- package/chat/dialog/actions/replacePrimaryCybotAction.ts +6 -0
- package/chat/dialog/actions/setPrimaryDialogAgentAction.ts +52 -0
- package/chat/dialog/actions/updateDialogSummaryAction.ts +430 -0
- package/chat/dialog/actions/updateDialogTitleAction.ts +151 -0
- package/chat/dialog/actions/updateDialogTitlePolicy.ts +30 -0
- package/chat/dialog/actions/updateTokensAction.ts +233 -0
- package/chat/dialog/dialogAgents.ts +72 -0
- package/chat/dialog/dialogSlice.ts +872 -0
- package/chat/dialog/dialogTitle.ts +81 -0
- package/chat/dialog/dialogTokenStats.ts +12 -0
- package/chat/dialog/dialogUrl.ts +6 -0
- package/chat/dialog/goal.ts +108 -0
- package/chat/dialog/objectAssistantRegistry.ts +286 -0
- package/chat/dialog/schedule.ts +17 -0
- package/chat/messages/fetchAndCacheMessages.ts +119 -0
- package/chat/messages/fetchConvMsgs.ts +51 -0
- package/chat/messages/fileUrl.ts +13 -0
- package/chat/messages/messageAgent.ts +8 -0
- package/chat/messages/messageContent.ts +199 -0
- package/chat/messages/messageContract.ts +88 -0
- package/chat/messages/messageSlice.ts +1367 -0
- package/chat/messages/toolThunks.ts +625 -0
- package/cli/client/agentRun.ts +27 -8
- package/cli/client/localRuntimeAdapter.ts +144 -28
- package/client/agentRun.test.ts +52 -0
- package/client/agentRun.ts +27 -8
- package/client/localRuntimeAdapter.test.ts +437 -6
- package/client/localRuntimeAdapter.ts +144 -28
- package/core/builtinAgents.ts +209 -0
- package/core/config.ts +15 -0
- package/core/crypto.ts +51 -0
- package/core/generateKeyPairFromSeedV1.ts +15 -0
- package/core/generateMainKey.ts +22 -0
- package/core/init.ts +41 -0
- package/core/localOrigins.ts +8 -0
- package/core/password.ts +13 -0
- package/core/prefix.ts +23 -0
- package/create/editor/transforms/fromMarkdown/index.ts +260 -0
- package/create/editor/transforms/fromMarkdown/inline.ts +285 -0
- package/create/editor/transforms/fromMarkdown/table.ts +130 -0
- package/create/editor/transforms/markdownToSlate.ts +24 -0
- package/create/editor/transforms/slateToRenderMarkdown.ts +218 -0
- package/create/editor/transforms/slateToSimplifiedMarkdown.ts +224 -0
- package/create/editor/transforms/slateToText.ts +33 -0
- package/create/editor/utils/slateUtils.ts +233 -0
- package/create/space/addSpaceAction.ts +193 -0
- package/create/space/category/categoryActions.ts +427 -0
- package/create/space/changeSpaceAction.ts +36 -0
- package/create/space/constants.ts +8 -0
- package/create/space/content/addContentAction.ts +116 -0
- package/create/space/content/contentThunks.ts +90 -0
- package/create/space/content/deleteContentFromSpaceAction.ts +187 -0
- package/create/space/content/deleteMultipleContentAction.ts +112 -0
- package/create/space/content/moveContentAction.ts +285 -0
- package/create/space/content/updateContentCategoryAction.ts +154 -0
- package/create/space/content/updateContentTitleAction.ts +151 -0
- package/create/space/content/uploadAndAddFileToSpaceAction.ts +98 -0
- package/create/space/contentKeyUtils.ts +117 -0
- package/create/space/deleteSpaceAction.ts +276 -0
- package/create/space/fetchSpaceAction.ts +70 -0
- package/create/space/fetchSpaceSidebarStateAction.ts +26 -0
- package/create/space/loadDefaultSpaceAction.ts +57 -0
- package/create/space/member/addMemberAction.ts +79 -0
- package/create/space/member/fetchUserSpaceMembershipsAction.ts +386 -0
- package/create/space/member/memberThunks.ts +47 -0
- package/create/space/member/removeMemberAction.ts +67 -0
- package/create/space/resolvePreferredSpaceId.ts +113 -0
- package/create/space/sidebarVisibleTypes.ts +241 -0
- package/create/space/space.locale.ts +1087 -0
- package/create/space/spaceAccess.ts +182 -0
- package/create/space/spaceCollapsedState.ts +54 -0
- package/create/space/spaceKeys.ts +65 -0
- package/create/space/spaceSlice.ts +328 -0
- package/create/space/spaceThunks.ts +410 -0
- package/create/space/types.ts +27 -0
- package/create/space/updateSpaceAction.ts +107 -0
- package/create/space/utils/permissions.ts +47 -0
- package/create/types.ts +21 -0
- package/database/actions/agentReadResolution.ts +33 -0
- package/database/actions/common.ts +243 -0
- package/database/actions/deleteFile.ts +40 -0
- package/database/actions/fileContent.ts +125 -0
- package/database/actions/patch.ts +155 -0
- package/database/actions/read.ts +358 -0
- package/database/actions/readAndWait.ts +260 -0
- package/database/actions/readRequestManager.ts +120 -0
- package/database/actions/readResolution.ts +125 -0
- package/database/actions/remove.ts +94 -0
- package/database/actions/replication.ts +477 -0
- package/database/actions/upload.ts +186 -0
- package/database/actions/upsert.ts +56 -0
- package/database/actions/write.ts +146 -0
- package/database/authority/ownerKey.ts +80 -0
- package/database/authority/recordAuthority.ts +67 -0
- package/database/authority/userAuthorityRegistry.ts +54 -0
- package/database/client/fetchUserData.ts +61 -0
- package/database/dbActionThunks.ts +1 -0
- package/database/dbSlice.ts +149 -0
- package/database/fileRing.ts +51 -0
- package/database/fileStorage.ts +232 -0
- package/database/fileUrl.ts +34 -0
- package/database/keys.ts +767 -0
- package/database/queryPrefixes.ts +14 -0
- package/database/requests.ts +443 -0
- package/database/runtimeServerContext.ts +54 -0
- package/database/tenantPlacement.ts +57 -0
- package/database/userPreferenceRegister.ts +131 -0
- package/database/utils/ulid.ts +1 -0
- package/integrations/anthropic/anthropicModels.ts +47 -0
- package/integrations/deepseek/models.ts +37 -0
- package/integrations/google/models.ts +341 -0
- package/integrations/openai/chatCompletionStreamMode.ts +10 -0
- package/integrations/openai/filterAndCleanMessages.ts +284 -0
- package/integrations/openai/flexTier.ts +102 -0
- package/integrations/openai/generateOpenAIRequestBody.ts +274 -0
- package/integrations/openai/generateResponseRequestBody.ts +77 -0
- package/integrations/openai/models.ts +271 -0
- package/integrations/openai/providerBodyCompatibility.ts +23 -0
- package/integrations/openai/responsesHelpers.ts +297 -0
- package/integrations/x-reader/types.ts +109 -0
- package/integrations/xai/models.ts +13 -0
- package/integrations/xhs-reader/analyze/profileAnalyzer.ts +205 -0
- package/integrations/xhs-reader/backends/playwrightProfileCollector.ts +624 -0
- package/integrations/xhs-reader/bridge/readXhsProfileWithBridge.ts +227 -0
- package/integrations/xhs-reader/client/xhsWebApiClient.ts +229 -0
- package/integrations/xhs-reader/normalize.ts +390 -0
- package/integrations/xhs-reader/orchestrator.ts +178 -0
- package/integrations/xhs-reader/redaction.ts +68 -0
- package/integrations/xhs-reader/types.ts +244 -0
- package/integrations/xhs-reader/url.ts +57 -0
- package/package.json +430 -4
- package/render/page/createPageAction.ts +183 -0
- package/render/page/docSlice.ts +468 -0
- package/render/page/types.ts +17 -0
- package/render/table/createTableAction.ts +270 -0
- package/render/table/deleteTableAction.ts +193 -0
- package/render/table/fetchAndCacheTableRows.ts +174 -0
- package/render/table/tableSlice.ts +1106 -0
- package/render/table/toolValueUtils.ts +363 -0
- package/render/table/types.ts +252 -0
- package/render/table/utils/tableSerialization.ts +50 -0
- package/server/handlers/agentRun/types.ts +104 -0
- package/share/action.ts +225 -0
- package/share/helpers.ts +302 -0
- package/share/keys.ts +243 -0
- package/share/publicSharePolicy.ts +27 -0
- package/share/shareSlice.ts +53 -0
- package/share/types.ts +100 -0
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playwright-based XHS profile collector.
|
|
3
|
+
*
|
|
4
|
+
* Opens a profile URL in a desktop browser, reads window.__INITIAL_STATE__,
|
|
5
|
+
* scrolls to trigger pagination via real user_posted responses,
|
|
6
|
+
* and captures + normalizes all note data.
|
|
7
|
+
*
|
|
8
|
+
* Supports dependency injection of a Page-like object for unit testing.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { normalizeProfileState, normalizeProfileNote } from "../normalize";
|
|
12
|
+
import { redactXhsSensitiveValue } from "../redaction";
|
|
13
|
+
import type { XhsProfile, XhsNoteSummary, XhsCollectorDiagnostic, XhsFailureCode } from "../types";
|
|
14
|
+
|
|
15
|
+
// Minimal page interface for dependency injection
|
|
16
|
+
export interface XhsPageLike {
|
|
17
|
+
goto(url: string, opts?: { waitUntil?: string }): Promise<void>;
|
|
18
|
+
evaluate<T>(fn: () => T): Promise<T>;
|
|
19
|
+
waitForResponse(
|
|
20
|
+
fn: (resp: { url: () => string; ok: () => boolean }) => boolean,
|
|
21
|
+
opts?: { timeout?: number },
|
|
22
|
+
): Promise<{ json: () => Promise<unknown> }>;
|
|
23
|
+
on(event: string, handler: (...args: unknown[]) => void): void;
|
|
24
|
+
/** Current page URL. Available on real Playwright pages. */
|
|
25
|
+
url?(): string;
|
|
26
|
+
/** Page title. Available on real Playwright pages. */
|
|
27
|
+
title?(): Promise<string>;
|
|
28
|
+
close(): Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface CollectProfilePageResult {
|
|
32
|
+
profile: XhsProfile;
|
|
33
|
+
notes: XhsNoteSummary[];
|
|
34
|
+
diagnostic?: XhsCollectorDiagnostic;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface InternalState {
|
|
38
|
+
noteMap: Map<string, XhsNoteSummary>;
|
|
39
|
+
profile: XhsProfile | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function unwrapReactiveValue(value: unknown): unknown {
|
|
43
|
+
if (!value || typeof value !== "object") return value;
|
|
44
|
+
const record = value as Record<string, unknown>;
|
|
45
|
+
if ("_value" in record || "_rawValue" in record) {
|
|
46
|
+
return record._value ?? record._rawValue;
|
|
47
|
+
}
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Detect whether the page shows a login wall or captcha.
|
|
53
|
+
* Returns a diagnostic code + evidence, or null if the page looks normal.
|
|
54
|
+
*/
|
|
55
|
+
function detectPageBlockers(
|
|
56
|
+
pageUrl: string,
|
|
57
|
+
pageTitle: string,
|
|
58
|
+
): { code: XhsFailureCode; reason: string } | null {
|
|
59
|
+
const lowerUrl = pageUrl.toLowerCase();
|
|
60
|
+
const lowerTitle = pageTitle.toLowerCase();
|
|
61
|
+
|
|
62
|
+
// Login redirect detection
|
|
63
|
+
if (
|
|
64
|
+
lowerUrl.includes("/login") ||
|
|
65
|
+
lowerUrl.includes("loginpage") ||
|
|
66
|
+
lowerUrl.includes("passport.xiaohongshu.com")
|
|
67
|
+
) {
|
|
68
|
+
return { code: "login_required", reason: "page redirected to login URL" };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Title-based login detection
|
|
72
|
+
if (
|
|
73
|
+
lowerTitle.includes("登录") ||
|
|
74
|
+
lowerTitle.includes("login") ||
|
|
75
|
+
lowerTitle.includes("sign in")
|
|
76
|
+
) {
|
|
77
|
+
return { code: "login_required", reason: "page title indicates login wall" };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Captcha / verification detection
|
|
81
|
+
if (
|
|
82
|
+
lowerTitle.includes("验证") ||
|
|
83
|
+
lowerTitle.includes("captcha") ||
|
|
84
|
+
lowerTitle.includes("verify") ||
|
|
85
|
+
lowerTitle.includes("安全验证")
|
|
86
|
+
) {
|
|
87
|
+
return { code: "blocked", reason: "page title indicates captcha/verification wall" };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Probe __INITIAL_STATE__ for note-like data to count how many notes
|
|
95
|
+
* are visible in the initial page load.
|
|
96
|
+
*/
|
|
97
|
+
function countNotesInState(state: unknown): number {
|
|
98
|
+
if (!state || typeof state !== "object") return 0;
|
|
99
|
+
const s = state as Record<string, unknown>;
|
|
100
|
+
const userContainer = s.user as Record<string, unknown> | undefined;
|
|
101
|
+
|
|
102
|
+
// Real XHS shape: state.user.notes[0]
|
|
103
|
+
const userNotes = unwrapReactiveValue(userContainer?.notes) as unknown[] | undefined;
|
|
104
|
+
if (Array.isArray(userNotes) && userNotes.length > 0) {
|
|
105
|
+
const firstPage = userNotes[0];
|
|
106
|
+
if (Array.isArray(firstPage)) return firstPage.length;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Alternate: state.user.userPageData.notes / noteList / choosedNotes
|
|
110
|
+
const userPageData = unwrapReactiveValue(userContainer?.userPageData ?? s.userPageData) as
|
|
111
|
+
| Record<string, unknown>
|
|
112
|
+
| undefined;
|
|
113
|
+
if (userPageData) {
|
|
114
|
+
const lists = ["notes", "noteList", "choosedNotes"] as const;
|
|
115
|
+
for (const key of lists) {
|
|
116
|
+
const arr = userPageData[key];
|
|
117
|
+
if (Array.isArray(arr) && arr.length > 0) return arr.length;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Collect profile and notes from a XHS profile page using Playwright.
|
|
126
|
+
*
|
|
127
|
+
* @param page - A Playwright Page or compatible object.
|
|
128
|
+
* @param profileUrl - The full XHS profile URL.
|
|
129
|
+
* @param maxScrollPages - Max number of scroll/pagination pages to collect.
|
|
130
|
+
* @returns Normalized profile + notes list + optional diagnostic.
|
|
131
|
+
*/
|
|
132
|
+
export async function collectProfilePage(
|
|
133
|
+
page: XhsPageLike,
|
|
134
|
+
profileUrl: string,
|
|
135
|
+
maxScrollPages: number = 5,
|
|
136
|
+
): Promise<CollectProfilePageResult> {
|
|
137
|
+
const collectorState: InternalState = {
|
|
138
|
+
noteMap: new Map(),
|
|
139
|
+
profile: null,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const diagnostic: XhsCollectorDiagnostic = {
|
|
143
|
+
code: "unknown",
|
|
144
|
+
message: "",
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// Set up response listener before navigation to capture user_posted responses
|
|
148
|
+
const capturedResponses: Array<{ url: string; status: number; json: () => Promise<unknown> }> = [];
|
|
149
|
+
const responseHandler = (resp: unknown) => {
|
|
150
|
+
const r = resp as {
|
|
151
|
+
url: () => string;
|
|
152
|
+
ok: () => boolean;
|
|
153
|
+
status: () => number;
|
|
154
|
+
json: () => Promise<unknown>;
|
|
155
|
+
};
|
|
156
|
+
if (
|
|
157
|
+
r.url().includes("/api/sns/web/v1/user_posted") &&
|
|
158
|
+
r.ok()
|
|
159
|
+
) {
|
|
160
|
+
capturedResponses.push({
|
|
161
|
+
url: r.url(),
|
|
162
|
+
status: r.status(),
|
|
163
|
+
json: r.json.bind(r),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// Some Page implementations use page.on, others need different setup
|
|
169
|
+
try {
|
|
170
|
+
page.on("response", responseHandler);
|
|
171
|
+
} catch {
|
|
172
|
+
// If on() is not available, we rely on __INITIAL_STATE__ only
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Navigate to the profile page
|
|
176
|
+
await page.goto(profileUrl, { waitUntil: "networkidle" });
|
|
177
|
+
|
|
178
|
+
// Capture page state for diagnostics
|
|
179
|
+
const finalUrl = page.url?.() ?? profileUrl;
|
|
180
|
+
let pageTitle = "";
|
|
181
|
+
try {
|
|
182
|
+
pageTitle = page.title ? await page.title() : "";
|
|
183
|
+
} catch {
|
|
184
|
+
// title() may not be available in all test mocks
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
diagnostic.finalUrl = redactXhsSensitiveValue(finalUrl) as string;
|
|
188
|
+
diagnostic.pageTitle = pageTitle;
|
|
189
|
+
|
|
190
|
+
// Check for login/captcha blockers
|
|
191
|
+
const blocker = detectPageBlockers(finalUrl, pageTitle);
|
|
192
|
+
if (blocker) {
|
|
193
|
+
// Try to detect login state more precisely via page content
|
|
194
|
+
const loginHint = await page.evaluate(() => {
|
|
195
|
+
try {
|
|
196
|
+
// Check for common XHS login indicators in the DOM
|
|
197
|
+
const body = document.body?.innerText ?? "";
|
|
198
|
+
const hasLoginText = /登录|login|sign.?in|手机号|验证码/i.test(body);
|
|
199
|
+
// Check for login modal / overlay
|
|
200
|
+
const loginModal = document.querySelector('[class*="login"]') ??
|
|
201
|
+
document.querySelector('[class*="Login"]');
|
|
202
|
+
return { hasLoginText, hasLoginModal: !!loginModal };
|
|
203
|
+
} catch {
|
|
204
|
+
return { hasLoginText: false, hasLoginModal: false };
|
|
205
|
+
}
|
|
206
|
+
}).catch(() => ({ hasLoginText: false, hasLoginModal: false }));
|
|
207
|
+
|
|
208
|
+
diagnostic.loginDetected = loginHint?.hasLoginText || loginHint?.hasLoginModal;
|
|
209
|
+
diagnostic.redirectedToLogin = finalUrl.toLowerCase().includes("login");
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
profile: { userId: extractUserId(profileUrl), nickname: "" },
|
|
213
|
+
notes: [],
|
|
214
|
+
diagnostic: {
|
|
215
|
+
...diagnostic,
|
|
216
|
+
code: blocker.code,
|
|
217
|
+
message: blocker.reason +
|
|
218
|
+
(diagnostic.loginDetected ? " (login UI detected on page)" : ""),
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
await waitForInitialProfileState(page, 15_000);
|
|
224
|
+
|
|
225
|
+
// Read __INITIAL_STATE__ from the page
|
|
226
|
+
const initialState = await readSerializableInitialState(page);
|
|
227
|
+
|
|
228
|
+
diagnostic.initialStatePresent = !!initialState;
|
|
229
|
+
diagnostic.initialStateNoteCount = countNotesInState(initialState);
|
|
230
|
+
|
|
231
|
+
if (initialState) {
|
|
232
|
+
const rawState = initialState as Record<string, unknown>;
|
|
233
|
+
|
|
234
|
+
// Extract profile info — try real path (state.user.userPageData) first,
|
|
235
|
+
// then flat path (state.userPageData) for backward compat with test fakes.
|
|
236
|
+
const userContainer = rawState.user as Record<string, unknown> | undefined;
|
|
237
|
+
const userPageData = unwrapReactiveValue(
|
|
238
|
+
userContainer?.userPageData ?? rawState.userPageData,
|
|
239
|
+
) as Record<string, unknown> | undefined;
|
|
240
|
+
|
|
241
|
+
if (userPageData) {
|
|
242
|
+
const userId = extractUserId(profileUrl);
|
|
243
|
+
collectorState.profile = normalizeProfileState(
|
|
244
|
+
{ userPageData: userPageData as any },
|
|
245
|
+
userId,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Extract initial notes from __INITIAL_STATE__
|
|
250
|
+
const noteList = extractNotesFromState(initialState);
|
|
251
|
+
for (const note of noteList) {
|
|
252
|
+
collectorState.noteMap.set(note.noteId, note);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// If no profile from state, create a minimal one from URL
|
|
257
|
+
if (!collectorState.profile) {
|
|
258
|
+
const userId = extractUserId(profileUrl);
|
|
259
|
+
collectorState.profile = { userId, nickname: "" };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Scroll to trigger pagination
|
|
263
|
+
let scrollCount = 0;
|
|
264
|
+
let consecutiveEmpty = 0;
|
|
265
|
+
|
|
266
|
+
while (scrollCount < maxScrollPages && consecutiveEmpty < 2) {
|
|
267
|
+
const countBefore = collectorState.noteMap.size;
|
|
268
|
+
|
|
269
|
+
// Scroll to bottom
|
|
270
|
+
await page.evaluate(() => {
|
|
271
|
+
window.scrollTo(0, document.body.scrollHeight);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// Wait for a user_posted response or timeout
|
|
275
|
+
try {
|
|
276
|
+
const resp = await page.waitForResponse(
|
|
277
|
+
(r) =>
|
|
278
|
+
r.url().includes("/api/sns/web/v1/user_posted") && r.ok(),
|
|
279
|
+
{ timeout: 8000 },
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
const body = await resp.json();
|
|
283
|
+
const pageNotes = extractNotesFromPostedResponse(body);
|
|
284
|
+
for (const note of pageNotes) {
|
|
285
|
+
collectorState.noteMap.set(note.noteId, note);
|
|
286
|
+
}
|
|
287
|
+
} catch {
|
|
288
|
+
// Timeout or error - may have no more pages
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
scrollCount++;
|
|
292
|
+
if (collectorState.noteMap.size === countBefore) {
|
|
293
|
+
consecutiveEmpty++;
|
|
294
|
+
} else {
|
|
295
|
+
consecutiveEmpty = 0;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Also process any captured responses we haven't handled
|
|
300
|
+
for (const resp of capturedResponses) {
|
|
301
|
+
try {
|
|
302
|
+
const body = await resp.json();
|
|
303
|
+
const pageNotes = extractNotesFromPostedResponse(body);
|
|
304
|
+
for (const note of pageNotes) {
|
|
305
|
+
collectorState.noteMap.set(note.noteId, note);
|
|
306
|
+
}
|
|
307
|
+
} catch {
|
|
308
|
+
// ignore
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (!collectorState.profile?.nickname || collectorState.noteMap.size === 0) {
|
|
313
|
+
await waitForInitialProfileState(page, 12_000);
|
|
314
|
+
const lateState = await readSerializableInitialState(page);
|
|
315
|
+
if (lateState) {
|
|
316
|
+
const rawState = lateState as Record<string, unknown>;
|
|
317
|
+
const userContainer = rawState.user as Record<string, unknown> | undefined;
|
|
318
|
+
const userPageData = unwrapReactiveValue(
|
|
319
|
+
userContainer?.userPageData ?? rawState.userPageData,
|
|
320
|
+
) as Record<string, unknown> | undefined;
|
|
321
|
+
if (userPageData && !collectorState.profile?.nickname) {
|
|
322
|
+
const userId = extractUserId(profileUrl);
|
|
323
|
+
collectorState.profile = normalizeProfileState(
|
|
324
|
+
{ userPageData: userPageData as any },
|
|
325
|
+
userId,
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
const lateNotes = extractNotesFromState(lateState);
|
|
329
|
+
for (const note of lateNotes) {
|
|
330
|
+
collectorState.noteMap.set(note.noteId, note);
|
|
331
|
+
}
|
|
332
|
+
diagnostic.initialStatePresent = true;
|
|
333
|
+
diagnostic.initialStateNoteCount = Math.max(
|
|
334
|
+
diagnostic.initialStateNoteCount ?? 0,
|
|
335
|
+
countNotesInState(lateState),
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
diagnostic.capturedApiResponseCount = capturedResponses.length;
|
|
341
|
+
if (capturedResponses.length > 0) {
|
|
342
|
+
diagnostic.firstApiResponseStatus = capturedResponses[0].status;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const notes = Array.from(collectorState.noteMap.values());
|
|
346
|
+
|
|
347
|
+
// Build diagnostic for empty results
|
|
348
|
+
if (notes.length === 0) {
|
|
349
|
+
// Check if the page has login indicators even without URL redirect
|
|
350
|
+
const loginCheck = await page.evaluate(() => {
|
|
351
|
+
try {
|
|
352
|
+
const body = document.body?.innerText ?? "";
|
|
353
|
+
return {
|
|
354
|
+
hasLoginText: /登录|login|sign.?in|请先登录|未登录/i.test(body),
|
|
355
|
+
hasLoginModal: !!(
|
|
356
|
+
document.querySelector('[class*="login"]') ??
|
|
357
|
+
document.querySelector('[class*="Login"]')
|
|
358
|
+
),
|
|
359
|
+
bodyLength: body.length,
|
|
360
|
+
};
|
|
361
|
+
} catch {
|
|
362
|
+
return { hasLoginText: false, hasLoginModal: false, bodyLength: 0 };
|
|
363
|
+
}
|
|
364
|
+
}).catch(() => ({ hasLoginText: false, hasLoginModal: false, bodyLength: 0 })) as { hasLoginText: boolean; hasLoginModal: boolean; bodyLength: number };
|
|
365
|
+
const safeLoginCheck = loginCheck ?? { hasLoginText: false, hasLoginModal: false, bodyLength: 0 };
|
|
366
|
+
|
|
367
|
+
diagnostic.loginDetected = safeLoginCheck.hasLoginText || safeLoginCheck.hasLoginModal;
|
|
368
|
+
|
|
369
|
+
// Determine the appropriate diagnostic code
|
|
370
|
+
let code: XhsFailureCode = "empty_profile_state";
|
|
371
|
+
let message = "";
|
|
372
|
+
|
|
373
|
+
if (diagnostic.loginDetected) {
|
|
374
|
+
code = "login_required";
|
|
375
|
+
message =
|
|
376
|
+
"页面检测到登录提示,未获取到笔记。" +
|
|
377
|
+
"请先用 headless=false + keepOpen=true 手动登录," +
|
|
378
|
+
"登录后再次运行即可自动复用登录态。";
|
|
379
|
+
} else if (!diagnostic.initialStatePresent) {
|
|
380
|
+
code = "empty_profile_state";
|
|
381
|
+
message =
|
|
382
|
+
"页面未注入 __INITIAL_STATE__,可能页面未完全加载或被反爬拦截。" +
|
|
383
|
+
"请尝试增加 timeoutMs 或使用本地持久 profile。";
|
|
384
|
+
} else if (
|
|
385
|
+
diagnostic.initialStatePresent &&
|
|
386
|
+
(diagnostic.initialStateNoteCount ?? 0) === 0 &&
|
|
387
|
+
diagnostic.capturedApiResponseCount === 0
|
|
388
|
+
) {
|
|
389
|
+
code = "empty_profile_state";
|
|
390
|
+
message =
|
|
391
|
+
"__INITIAL_STATE__ 存在但未发现笔记数据,且未捕获到 user_posted API 响应。" +
|
|
392
|
+
"可能该用户无公开笔记,或页面在 headless 环境下被限流。" +
|
|
393
|
+
"建议用本地持久 profile 验证。";
|
|
394
|
+
} else {
|
|
395
|
+
code = "empty_profile_state";
|
|
396
|
+
message = "收集完成但未获取到笔记数据。";
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
diagnostic.code = code;
|
|
400
|
+
diagnostic.message = message;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
profile: collectorState.profile,
|
|
405
|
+
notes,
|
|
406
|
+
diagnostic: notes.length === 0 ? diagnostic : undefined,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async function readSerializableInitialState(page: XhsPageLike): Promise<unknown> {
|
|
411
|
+
return page.evaluate(() => {
|
|
412
|
+
const unwrap = (value: any): any =>
|
|
413
|
+
value && typeof value === "object" && ("_value" in value || "_rawValue" in value)
|
|
414
|
+
? (value._value ?? value._rawValue)
|
|
415
|
+
: value;
|
|
416
|
+
const state = (window as any).__INITIAL_STATE__;
|
|
417
|
+
if (!state) return null;
|
|
418
|
+
|
|
419
|
+
const userPageData = unwrap(state.user?.userPageData ?? state.userPageData);
|
|
420
|
+
const notes = unwrap(state.user?.notes);
|
|
421
|
+
const firstPageNotes = Array.isArray(notes?.[0]) ? notes[0] : undefined;
|
|
422
|
+
|
|
423
|
+
return {
|
|
424
|
+
user: {
|
|
425
|
+
userPageData,
|
|
426
|
+
notes: firstPageNotes ? [firstPageNotes] : notes,
|
|
427
|
+
},
|
|
428
|
+
userPageData,
|
|
429
|
+
note: unwrap(state.note),
|
|
430
|
+
feed: {
|
|
431
|
+
feeds: unwrap(state.feed?.feeds),
|
|
432
|
+
items: unwrap(state.feed?.items),
|
|
433
|
+
},
|
|
434
|
+
};
|
|
435
|
+
}).catch(() => null);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async function waitForInitialProfileState(page: XhsPageLike, timeoutMs: number): Promise<"ready" | "mock_skip" | "timeout"> {
|
|
439
|
+
const started = Date.now();
|
|
440
|
+
while (Date.now() - started < timeoutMs) {
|
|
441
|
+
const ready = await page.evaluate(() => {
|
|
442
|
+
try {
|
|
443
|
+
const unwrap = (value: any): any =>
|
|
444
|
+
value && typeof value === "object" && ("_value" in value || "_rawValue" in value)
|
|
445
|
+
? (value._value ?? value._rawValue)
|
|
446
|
+
: value;
|
|
447
|
+
const state = (window as any).__INITIAL_STATE__;
|
|
448
|
+
const userPageData = unwrap(state?.user?.userPageData ?? state?.userPageData);
|
|
449
|
+
const notes = unwrap(state?.user?.notes);
|
|
450
|
+
const firstPage = Array.isArray(notes?.[0]) ? notes[0] : undefined;
|
|
451
|
+
const hasProfile = Boolean(userPageData?.basicInfo);
|
|
452
|
+
const hasNotes = Array.isArray(firstPage) && firstPage.length > 0;
|
|
453
|
+
return hasProfile || hasNotes;
|
|
454
|
+
} catch {
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
457
|
+
}).catch(() => false);
|
|
458
|
+
if (ready == null) return "mock_skip";
|
|
459
|
+
if (ready) return "ready";
|
|
460
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
461
|
+
}
|
|
462
|
+
return "timeout";
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Extract notes from __INITIAL_STATE__ object.
|
|
467
|
+
* Tries multiple known paths:
|
|
468
|
+
* - Real XHS shape: state.user.notes[0] (first page note array)
|
|
469
|
+
* - Real XHS shape: state.user.userPageData.notes (alternate)
|
|
470
|
+
* - Flat test shape: state.userPageData.notes / noteList / choosedNotes
|
|
471
|
+
*/
|
|
472
|
+
function extractNotesFromState(state: unknown): XhsNoteSummary[] {
|
|
473
|
+
const s = state as Record<string, unknown>;
|
|
474
|
+
const userContainer = s.user as Record<string, unknown> | undefined;
|
|
475
|
+
|
|
476
|
+
// Real XHS shape: state.user.notes[0] is the first-page note array
|
|
477
|
+
const userNotes = unwrapReactiveValue(userContainer?.notes) as unknown[] | undefined;
|
|
478
|
+
if (Array.isArray(userNotes) && userNotes.length > 0) {
|
|
479
|
+
const firstPage = userNotes[0];
|
|
480
|
+
if (Array.isArray(firstPage)) {
|
|
481
|
+
return normalizeNotesList(firstPage as Array<Record<string, unknown>>);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Real XHS shape: state.user.userPageData.notes
|
|
486
|
+
const userPageData = unwrapReactiveValue(userContainer?.userPageData ?? s.userPageData) as
|
|
487
|
+
| Record<string, unknown>
|
|
488
|
+
| undefined;
|
|
489
|
+
if (userPageData) {
|
|
490
|
+
const noteList = (userPageData.notes ??
|
|
491
|
+
userPageData.noteList ??
|
|
492
|
+
userPageData.choosedNotes ??
|
|
493
|
+
[]) as Array<Record<string, unknown>>;
|
|
494
|
+
if (noteList.length > 0) {
|
|
495
|
+
return normalizeNotesList(noteList);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Additional: try state.note.noteList (some XHS versions)
|
|
500
|
+
const noteContainer = unwrapReactiveValue(s.note) as Record<string, unknown> | undefined;
|
|
501
|
+
if (noteContainer) {
|
|
502
|
+
const noteList = (noteContainer.noteList ?? noteContainer.notes ?? []) as Array<Record<string, unknown>>;
|
|
503
|
+
if (Array.isArray(noteList) && noteList.length > 0) {
|
|
504
|
+
return normalizeNotesList(noteList);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Additional: try state.feed.feeds (some XHS SPA versions)
|
|
509
|
+
const feedContainer = unwrapReactiveValue(s.feed) as Record<string, unknown> | undefined;
|
|
510
|
+
if (feedContainer) {
|
|
511
|
+
const feeds = unwrapReactiveValue(feedContainer.feeds ?? feedContainer.items);
|
|
512
|
+
if (Array.isArray(feeds) && feeds.length > 0) {
|
|
513
|
+
return normalizeNotesList(feeds as Array<Record<string, unknown>>);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return [];
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Shared normalization for a raw notes list (works for both INITIAL_STATE
|
|
522
|
+
* and paginated user_posted responses).
|
|
523
|
+
*/
|
|
524
|
+
function normalizeNotesList(rawNotes: Array<Record<string, unknown>>): XhsNoteSummary[] {
|
|
525
|
+
return rawNotes
|
|
526
|
+
.map((raw) => normalizeProfileNote(unwrapNoteWrapper(raw) as any))
|
|
527
|
+
.filter((n) => n.noteId);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function unwrapNoteWrapper(raw: Record<string, unknown>): Record<string, unknown> {
|
|
531
|
+
const value = unwrapReactiveValue(raw) as Record<string, unknown>;
|
|
532
|
+
const noteCard = unwrapReactiveValue(
|
|
533
|
+
value.note_card ?? value.noteCard ?? value.note ?? value,
|
|
534
|
+
) as Record<string, unknown>;
|
|
535
|
+
const inner = unwrapReactiveValue(
|
|
536
|
+
noteCard.note_card ?? noteCard.noteCard ?? noteCard,
|
|
537
|
+
) as Record<string, unknown>;
|
|
538
|
+
if (inner === value) return value;
|
|
539
|
+
return {
|
|
540
|
+
...inner,
|
|
541
|
+
id: inner.id ?? value.id,
|
|
542
|
+
noteId: inner.noteId ?? value.noteId,
|
|
543
|
+
note_id: inner.note_id ?? value.note_id,
|
|
544
|
+
xsecToken: inner.xsecToken ?? value.xsecToken,
|
|
545
|
+
xsec_token: inner.xsec_token ?? value.xsec_token,
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Extract notes from a user_posted API response body.
|
|
551
|
+
* Supports both camelCase and snake_case shapes directly through normalizeProfileNote.
|
|
552
|
+
* Also handles nested wrappers like data.items[].note_card.
|
|
553
|
+
*/
|
|
554
|
+
function extractNotesFromPostedResponse(body: unknown): XhsNoteSummary[] {
|
|
555
|
+
const b = body as Record<string, unknown>;
|
|
556
|
+
const data = (b.data ?? b) as Record<string, unknown>;
|
|
557
|
+
|
|
558
|
+
// Direct data.notes path
|
|
559
|
+
const notes = data.notes as Array<Record<string, unknown>> | undefined;
|
|
560
|
+
if (notes && notes.length > 0) {
|
|
561
|
+
return normalizeNotesList(notes);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// data.items path (some API versions wrap notes in items)
|
|
565
|
+
const items = data.items as Array<Record<string, unknown>> | undefined;
|
|
566
|
+
if (items && items.length > 0) {
|
|
567
|
+
// Items may be note objects directly or wrapped in various wrappers:
|
|
568
|
+
// { note_card: { ... } } — snake_case (real API)
|
|
569
|
+
// { noteCard: { ... } } — camelCase
|
|
570
|
+
// { note: { ... } } — older shape
|
|
571
|
+
// { note_card: { note_card: … }} — doubly-nested snake_case
|
|
572
|
+
// { noteCard: { noteCard: … }} — doubly-nested camelCase
|
|
573
|
+
return normalizeNotesList(items);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return [];
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Extract userId from a profile URL.
|
|
581
|
+
*/
|
|
582
|
+
function extractUserId(url: string): string {
|
|
583
|
+
const match = url.match(/\/profile\/([a-f0-9]{24})/i);
|
|
584
|
+
return match ? match[1] : "";
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Create a Playwright browser context with XHS-compatible desktop viewport
|
|
589
|
+
* and user agent.
|
|
590
|
+
*/
|
|
591
|
+
export async function createXhsBrowserContext(
|
|
592
|
+
playwright: {
|
|
593
|
+
chromium: {
|
|
594
|
+
launch: (opts: Record<string, unknown>) => Promise<{
|
|
595
|
+
newContext: (opts: Record<string, unknown>) => Promise<{
|
|
596
|
+
newPage: () => Promise<XhsPageLike>;
|
|
597
|
+
}>;
|
|
598
|
+
}>;
|
|
599
|
+
};
|
|
600
|
+
},
|
|
601
|
+
opts: {
|
|
602
|
+
headless?: boolean;
|
|
603
|
+
userDataDir?: string;
|
|
604
|
+
cookies?: Array<{ name: string; value: string; domain: string; path?: string }>;
|
|
605
|
+
} = {},
|
|
606
|
+
) {
|
|
607
|
+
const browser = await playwright.chromium.launch({
|
|
608
|
+
headless: opts.headless ?? true,
|
|
609
|
+
args: ["--disable-blink-features=AutomationControlled"],
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
const context = await browser.newContext({
|
|
613
|
+
viewport: { width: 1440, height: 900 },
|
|
614
|
+
userAgent:
|
|
615
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
|
616
|
+
locale: "zh-CN",
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
if (opts.cookies && opts.cookies.length > 0) {
|
|
620
|
+
await (context as any).addCookies(opts.cookies);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return context;
|
|
624
|
+
}
|