nolo-cli 0.1.13 → 0.1.14
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 +9 -2
- package/agent-runtime/hostAdapter.ts +53 -0
- package/agent-runtime/index.ts +28 -0
- package/agent-runtime/localLoop.ts +62 -0
- package/agent-runtime/runtimeDecision.ts +70 -0
- package/agent-runtime/types.ts +87 -0
- package/agentRuntimeCommands.ts +139 -22
- package/agentRuntimeLocal.ts +7 -0
- package/ai/agent/_executeModel.ts +118 -0
- package/ai/agent/agentSlice.ts +544 -1
- package/ai/agent/appWorkingMemory.ts +126 -0
- package/ai/agent/avatarUtils.ts +24 -0
- package/ai/agent/buildEditingContext.ts +373 -0
- package/ai/agent/buildSystemPrompt.ts +532 -0
- package/ai/agent/cleanAgentMessages.ts +140 -0
- package/ai/agent/cliChatClient.ts +119 -0
- package/ai/agent/contextCompiler.ts +107 -0
- package/ai/agent/contextLayerContract.ts +44 -0
- package/ai/agent/createAgentSchema.ts +234 -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/hooks/fetchPublicAgents.ts +133 -0
- package/ai/agent/hooks/useAgentConfig.ts +61 -0
- package/ai/agent/hooks/useAgentDialog.ts +35 -0
- package/ai/agent/hooks/useAgentFormValidation.ts +202 -0
- package/ai/agent/hooks/usePublicAgents.ts +473 -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 +238 -0
- package/ai/agent/runAgentClientLoop.ts +138 -0
- package/ai/agent/runtimeGuidance.ts +97 -0
- package/ai/agent/runtimeServerBase.ts +37 -0
- package/ai/agent/server/fetchPublicAgents.ts +128 -0
- package/ai/agent/startParallelAgentStreams.ts +424 -0
- package/ai/agent/startupProtocol.ts +53 -0
- package/ai/agent/streamAgentChatTurn.ts +1299 -0
- package/ai/agent/streamAgentChatTurnUtils.ts +738 -0
- package/ai/agent/types.ts +71 -0
- package/ai/agent/utils/imageOutput.ts +39 -0
- package/ai/agent/utils/publicImageAgentMode.ts +26 -0
- package/ai/agent/utils/sortUtils.ts +250 -0
- package/ai/agent/web/referencePickerUtils.ts +146 -0
- package/ai/ai.locale.ts +1083 -0
- package/ai/chat/accumulateToolCallChunks.ts +95 -0
- package/ai/chat/fetchUtils.native.ts +276 -0
- package/ai/chat/fetchUtils.ts +153 -0
- package/ai/chat/inlineImageUrlsForCustomProvider.ts +117 -0
- package/ai/chat/parseApiError.ts +64 -0
- package/ai/chat/parseMultilineSSE.ts +95 -0
- package/ai/chat/sendOpenAICompletionsRequest.native.ts +682 -0
- package/ai/chat/sendOpenAICompletionsRequest.ts +712 -0
- package/ai/chat/sendOpenAIResponseRequest.ts +512 -0
- package/ai/chat/shouldUseServerProxy.ts +18 -0
- package/ai/chat/sseClient.native.ts +91 -0
- package/ai/chat/sseClient.ts +67 -0
- package/ai/chat/streamReader.native.ts +31 -0
- package/ai/chat/streamReader.ts +62 -0
- package/ai/chat/updateTotalUsage.ts +72 -0
- package/ai/context/buildReferenceContext.ts +437 -0
- package/ai/context/calculateContextUsage.ts +133 -0
- package/ai/context/retention.ts +165 -0
- package/ai/context/tokenUtils.ts +78 -0
- package/ai/index.ts +1 -1
- package/ai/llm/agentCapabilities.ts +74 -0
- package/ai/llm/calculateGeminiImageTokens.ts +57 -0
- package/ai/llm/deepinfra.ts +28 -0
- package/ai/llm/fireworks.ts +68 -0
- package/ai/llm/generateRequestBody.ts +165 -0
- package/ai/llm/getModelContextWindow.ts +84 -0
- package/ai/llm/getNoloKey.ts +37 -0
- package/ai/llm/getPricing.ts +232 -0
- package/ai/llm/hooks/useModelPricing.ts +75 -0
- package/ai/llm/imagePricing.ts +66 -0
- package/ai/llm/isResponseAPIModel.ts +13 -0
- package/ai/llm/kimi.ts +18 -0
- package/ai/llm/mimo.ts +71 -0
- package/ai/llm/mistral.ts +22 -0
- package/ai/llm/modelAvatar.ts +427 -0
- package/ai/llm/models.ts +45 -0
- package/ai/llm/openrouterModels.ts +141 -0
- package/ai/llm/providers.ts +307 -0
- package/ai/llm/reasoningModels.ts +28 -0
- package/ai/llm/types.ts +59 -0
- package/ai/llm/usageRequestOptions.ts +59 -0
- package/ai/memory/capture.ts +148 -0
- package/ai/memory/consolidate.ts +104 -0
- package/ai/memory/delete.ts +147 -0
- package/ai/memory/overlay.ts +84 -0
- package/ai/memory/query.ts +38 -0
- package/ai/memory/queryShared.ts +160 -0
- package/ai/memory/rank.ts +105 -0
- package/ai/memory/recentRelationshipRecap.ts +247 -0
- package/ai/memory/remember.ts +167 -0
- package/ai/memory/runtime.ts +76 -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/messages/type.ts +20 -0
- package/ai/policy/personalizationDialog.ts +333 -0
- package/ai/policy/runtimePolicy.ts +440 -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/skillDocProtocol.ts +434 -0
- package/ai/skills/skillReferenceSummary.ts +63 -0
- package/ai/skills/skillSummaryMarker.ts +26 -0
- package/ai/token/calculatePrice.ts +546 -0
- package/ai/token/db.ts +98 -0
- package/ai/token/externalToolCost.ts +321 -0
- package/ai/token/hooks/useRecords.ts +65 -0
- package/ai/token/missingUsageEstimate.ts +42 -0
- package/ai/token/modelUsageQuery.ts +252 -0
- package/ai/token/normalizeUsage.ts +84 -0
- package/ai/token/openaiImageGenerationUsage.ts +56 -0
- package/ai/token/prepareTokenUsageData.ts +88 -0
- package/ai/token/query.ts +88 -0
- package/ai/token/queryUserTokens.ts +59 -0
- package/ai/token/resolveBillingTarget.ts +52 -0
- package/ai/token/saveTokenRecord.ts +53 -0
- package/ai/token/serverDialogProjection.ts +78 -0
- package/ai/token/serverTokenWriter.ts +143 -0
- package/ai/token/stats.ts +21 -0
- package/ai/token/tokenThunks.ts +24 -0
- package/ai/token/types.ts +93 -0
- package/ai/tools/agent/agentTools.ts +176 -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/presets/appBuilderPreset.ts +147 -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/skillAgentPreset.ts +89 -0
- package/ai/tools/agent/streamParallelAgentsTool.ts +122 -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 +256 -0
- package/ai/tools/applyLineEditsTool.ts +312 -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 +65 -0
- package/ai/tools/cloudflareCrawlTool.ts +289 -0
- package/ai/tools/codeSearchTool.ts +111 -0
- package/ai/tools/codeTools.ts +101 -0
- package/ai/tools/createDocTool.ts +132 -0
- package/ai/tools/createPlanTool.ts +999 -0
- package/ai/tools/createSkillDocTool.ts +155 -0
- package/ai/tools/createWorkflowTool.ts +154 -0
- package/ai/tools/deepseekOcrTool.ts +34 -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 +64 -0
- package/ai/tools/execBashTool.ts +379 -0
- package/ai/tools/executeSqlTool.ts +192 -0
- package/ai/tools/fetchWebpageSupport.ts +309 -0
- package/ai/tools/fetchWebpageTool.ts +84 -0
- package/ai/tools/geminiImagePreviewTool.ts +361 -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 +1927 -0
- package/ai/tools/listFilesTool.ts +82 -0
- package/ai/tools/listUserSpacesTool.ts +113 -0
- package/ai/tools/modelUsageTools.ts +199 -0
- package/ai/tools/olmOcrTool.ts +34 -0
- package/ai/tools/openaiImageTool.ts +267 -0
- package/ai/tools/prepareTools.ts +23 -0
- package/ai/tools/readDocTool.ts +84 -0
- package/ai/tools/readFileTool.ts +211 -0
- package/ai/tools/readTool.ts +163 -0
- package/ai/tools/readXPostTool.ts +233 -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/searchRepoTool.ts +115 -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 +366 -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/toolApiClient.ts +198 -0
- package/ai/tools/toolNameAliases.ts +57 -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/toolVisibility.ts +4 -0
- package/ai/tools/types.ts +20 -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 +145 -0
- package/ai/tools/whisperTool.ts +77 -0
- package/ai/tools/writeFileTool.ts +210 -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/client/agentRun.test.ts +240 -0
- package/client/agentRun.ts +182 -19
- package/client/compactDialog.test.ts +238 -0
- package/client/localRuntimeAdapter.test.ts +135 -0
- package/client/localRuntimeAdapter.ts +244 -0
- package/client/profileConfig.test.ts +40 -0
- package/client/streamingOutput.test.ts +22 -0
- package/client/streamingOutput.ts +38 -0
- package/commandRegistry.ts +9 -2
- package/connector-experimental/index.ts +5 -0
- package/database/actions/cacheMergedUserData.ts +64 -0
- package/database/actions/common.ts +242 -0
- package/database/actions/deleteFile.ts +40 -0
- package/database/actions/fetchUserData.ts +16 -0
- package/database/actions/fileContent.ts +125 -0
- package/database/actions/patch.ts +155 -0
- package/database/actions/read.ts +337 -0
- package/database/actions/readAndWait.ts +224 -0
- package/database/actions/readRequestManager.ts +120 -0
- package/database/actions/remove.ts +94 -0
- package/database/actions/replication.ts +366 -0
- package/database/actions/upload.ts +174 -0
- package/database/actions/upsert.ts +56 -0
- package/database/actions/write.ts +126 -0
- package/database/client/db.native.ts +73 -0
- package/database/client/db.ts +51 -0
- package/database/client/fetchUserData.ts +61 -0
- package/database/client/handleError.ts +19 -0
- package/database/client/queryRequest.ts +21 -0
- package/database/config.ts +21 -0
- package/database/dbActionThunks.ts +1 -0
- package/database/dbSlice.ts +149 -0
- package/database/email.ts +42 -0
- package/database/fileRing.ts +51 -0
- package/database/fileSharding.ts +70 -0
- package/database/fileStorage.native.ts +92 -0
- package/database/fileStorage.ts +232 -0
- package/database/fileUrl.ts +34 -0
- package/database/hooks/useUserData.ts +489 -0
- package/database/index.ts +1 -0
- package/database/keys.ts +765 -0
- package/database/queryPrefixes.ts +14 -0
- package/database/requests.ts +443 -0
- package/database/runtimeServerContext.ts +35 -0
- package/database/server/MemoryDB.ts +76 -0
- package/database/server/actorAccess.ts +76 -0
- package/database/server/agentDelegation.ts +124 -0
- package/database/server/coreDataOwnership.ts +13 -0
- package/database/server/coreDataProxy.ts +76 -0
- package/database/server/cybotReadonly.ts +18 -0
- package/database/server/dataHandlers.ts +111 -0
- package/database/server/db.ts +118 -0
- package/database/server/dbPath.ts +20 -0
- package/database/server/delete.ts +499 -0
- package/database/server/emailRepository.ts +1480 -0
- package/database/server/ensureDbOpen.ts +12 -0
- package/database/server/fileRead.ts +337 -0
- package/database/server/fileService.ts +436 -0
- package/database/server/handleTransaction.ts +86 -0
- package/database/server/patch.ts +282 -0
- package/database/server/query.ts +138 -0
- package/database/server/read.ts +325 -0
- package/database/server/resourceAccess.ts +211 -0
- package/database/server/routes.ts +110 -0
- package/database/server/spaceMemberAuthority.ts +67 -0
- package/database/server/upload.ts +159 -0
- package/database/server/write.ts +494 -0
- package/database/server/writeAuthority.ts +133 -0
- package/database/sqliteDb.ts +46 -0
- package/database/table/deleteTable.ts +120 -0
- package/database/tenantPlacement.ts +57 -0
- package/database/tombstones.ts +52 -0
- package/database/userDataLoadDecision.ts +17 -0
- package/database/userDataMerge.ts +95 -0
- package/database/userPreferenceRegister.ts +108 -0
- package/database/utils/dbPath.ts +47 -0
- package/database/utils/ulid.native.ts +6 -0
- package/database/utils/ulid.ts +1 -0
- package/index.ts +25 -15
- package/localRuntimeDb.ts +28 -0
- package/package.json +16 -4
- package/runtimeModeArgs.ts +33 -0
- package/tui/readlineWorkspace.ts +1 -0
- package/tui/session.ts +22 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
type OutputLike = {
|
|
2
|
+
write(chunk: string): unknown;
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
export function splitStreamingText(content: string): string[] {
|
|
6
|
+
const Segmenter = Intl.Segmenter;
|
|
7
|
+
if (typeof Segmenter === "function") {
|
|
8
|
+
const segmenter = new Segmenter(undefined, { granularity: "grapheme" });
|
|
9
|
+
return Array.from(segmenter.segment(content), (part) => part.segment);
|
|
10
|
+
}
|
|
11
|
+
return Array.from(content);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function createStreamingTextWriter({
|
|
15
|
+
write,
|
|
16
|
+
batchSize = 3,
|
|
17
|
+
}: {
|
|
18
|
+
write: OutputLike["write"];
|
|
19
|
+
batchSize?: number;
|
|
20
|
+
}) {
|
|
21
|
+
const queue: string[] = [];
|
|
22
|
+
const safeBatchSize = Math.max(1, batchSize);
|
|
23
|
+
|
|
24
|
+
const flushNext = () => {
|
|
25
|
+
if (!queue.length) return;
|
|
26
|
+
write(queue.splice(0, safeBatchSize).join(""));
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
push(content: string) {
|
|
31
|
+
queue.push(...splitStreamingText(content));
|
|
32
|
+
flushNext();
|
|
33
|
+
},
|
|
34
|
+
flushAll() {
|
|
35
|
+
while (queue.length) flushNext();
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
package/commandRegistry.ts
CHANGED
|
@@ -16,6 +16,7 @@ export const COMMANDS: CommandEntry[] = [
|
|
|
16
16
|
{ path: ["skill-doc", "read"], script: "readSkillDoc.ts", description: "Read a skill doc" },
|
|
17
17
|
{ path: ["skill-doc", "update"], script: "updateSkillDoc.ts", description: "Update a skill doc" },
|
|
18
18
|
{ path: ["skill-doc", "delete"], script: "deleteSkillDoc.ts", description: "Delete a skill doc" },
|
|
19
|
+
{ path: ["skill", "create-doc"], script: "createSkillDoc.ts", description: "Create a skill-backed doc" },
|
|
19
20
|
|
|
20
21
|
{ path: ["dialog", "read"], script: "readDialog.ts", description: "Read a dialog" },
|
|
21
22
|
{ path: ["space", "read"], script: "readSpace.ts", description: "Read a space" },
|
|
@@ -41,6 +42,8 @@ export const COMMANDS: CommandEntry[] = [
|
|
|
41
42
|
{ path: ["agent", "setup-demo"], script: "setupDemoAgent.ts", description: "Bootstrap demo publisher agents" },
|
|
42
43
|
{ path: ["agent", "supervise"], script: "runAutonomousAgent.ts", description: "Run an agent in autonomous cycles" },
|
|
43
44
|
|
|
45
|
+
{ path: ["doctor", "runtime"], script: "", description: "Diagnose local-first agent runtime selection" },
|
|
46
|
+
|
|
44
47
|
{ path: ["llama"], script: "llamaServerSupervisor.ts", description: "Manage the llama.cpp runtime" },
|
|
45
48
|
{ path: ["model-runtime"], script: "localModelRuntimeSupervisor.ts", description: "Manage local model runtime processes" },
|
|
46
49
|
{ path: ["dev"], script: "devControl.ts", description: "Manage the local dev environment" },
|
|
@@ -50,7 +53,9 @@ export const GROUP_ORDER = [
|
|
|
50
53
|
"chat",
|
|
51
54
|
"doc",
|
|
52
55
|
"skill-doc",
|
|
56
|
+
"skill",
|
|
53
57
|
"agent",
|
|
58
|
+
"doctor",
|
|
54
59
|
"dialog",
|
|
55
60
|
"space",
|
|
56
61
|
"table",
|
|
@@ -83,15 +88,17 @@ export function renderHelpText() {
|
|
|
83
88
|
" nolo connect --daemon --server-url https://api.nolo.chat --api-key sk_machine_xxx",
|
|
84
89
|
" nolo machine status",
|
|
85
90
|
" nolo doctor",
|
|
91
|
+
" nolo doctor runtime",
|
|
86
92
|
" nolo update",
|
|
87
|
-
' nolo doc create --title "Trip Notes" --body "hello"',
|
|
88
|
-
' nolo skill-doc create --title "Agent Query Skill" --description "Inspect recent agent dialogs"',
|
|
93
|
+
' nolo doc create --title "Trip Notes" --body "hello" --sync local,us --dry-run',
|
|
94
|
+
' nolo skill-doc create --title "Agent Query Skill" --description "Inspect recent agent dialogs" --sync local,main,us',
|
|
89
95
|
" nolo agent list --json",
|
|
90
96
|
" nolo agent read agent-pub-01APPBUILDER00000001YAII3I",
|
|
91
97
|
" nolo agent bind-current agent-user-1-agent-1",
|
|
92
98
|
" nolo agent runtime-doctor agent-user-1-agent-1",
|
|
93
99
|
' nolo agent smoke-current agent-user-1-agent-1 --msg "ping"',
|
|
94
100
|
' nolo chat --agent agent-pub-01APPBUILDER00000001YAII3I --msg "你好"',
|
|
101
|
+
" nolo space read 01KKY77TT0DA9NY7TNW3R7255N --content-key page-user-id --brief",
|
|
95
102
|
" nolo space delete --name-prefix rn_owner_verify_0504 --yes",
|
|
96
103
|
" nolo table data --table 01ABCXYZ --action query",
|
|
97
104
|
" nolo llama status",
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { createAsyncThunk } from "@reduxjs/toolkit";
|
|
2
|
+
|
|
3
|
+
import type { AppThunkApi } from "app/store";
|
|
4
|
+
|
|
5
|
+
import { fetchFromClientDb } from "./common";
|
|
6
|
+
import { shouldReplaceWithNextRecord } from "../tombstones";
|
|
7
|
+
|
|
8
|
+
type CacheableUserDataRecord = {
|
|
9
|
+
dbKey?: string;
|
|
10
|
+
serverOrigin?: string;
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const shouldUpdateLocalUserDataCache = (
|
|
15
|
+
nextRecord: CacheableUserDataRecord,
|
|
16
|
+
localRecord: CacheableUserDataRecord | null
|
|
17
|
+
): boolean => {
|
|
18
|
+
if (!nextRecord || typeof nextRecord !== "object") {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!localRecord || typeof localRecord !== "object") {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (shouldReplaceWithNextRecord(nextRecord, localRecord)) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
typeof localRecord.serverOrigin !== "string" &&
|
|
32
|
+
typeof nextRecord.serverOrigin === "string" &&
|
|
33
|
+
nextRecord.serverOrigin.trim().length > 0
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const cacheMergedUserDataThunk = createAsyncThunk<
|
|
38
|
+
void,
|
|
39
|
+
{ records: CacheableUserDataRecord[] },
|
|
40
|
+
AppThunkApi
|
|
41
|
+
>("db/cacheMergedUserData", async ({ records }, { extra }) => {
|
|
42
|
+
const clientDb = extra.db;
|
|
43
|
+
if (!clientDb) {
|
|
44
|
+
throw new Error("Client database is not available.");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for (const record of records) {
|
|
48
|
+
const dbKey = typeof record?.dbKey === "string" ? record.dbKey.trim() : "";
|
|
49
|
+
if (!dbKey) continue;
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const localRecord = await fetchFromClientDb(clientDb, dbKey);
|
|
53
|
+
if (!shouldUpdateLocalUserDataCache(record, localRecord)) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
await clientDb.put(dbKey, record);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.warn("[useUserData] Failed to cache merged user data record", {
|
|
59
|
+
dbKey,
|
|
60
|
+
error,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
});
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// 文件路径: database/actions/common.ts
|
|
2
|
+
|
|
3
|
+
import pino from "pino";
|
|
4
|
+
import { getIsDesktopApp } from "app/utils/env";
|
|
5
|
+
import { API_ENDPOINTS, NOLO_CLUSTER_SERVERS } from "../config";
|
|
6
|
+
|
|
7
|
+
// RN 下 pino 的 browser 写法可能有兼容性问题
|
|
8
|
+
// 使用简单的 console 封装作为 fallback
|
|
9
|
+
const isRN = typeof navigator !== 'undefined' && (navigator as any).product === 'ReactNative';
|
|
10
|
+
|
|
11
|
+
export const logger = isRN ? {
|
|
12
|
+
info: (...args: any[]) => console.log(...args),
|
|
13
|
+
warn: (...args: any[]) => console.warn(...args),
|
|
14
|
+
error: (...args: any[]) => console.error(...args),
|
|
15
|
+
debug: (...args: any[]) => console.log(...args), // console.debug in RN behaves like log
|
|
16
|
+
trace: (...args: any[]) => console.log(...args),
|
|
17
|
+
fatal: (...args: any[]) => console.error(...args),
|
|
18
|
+
child: () => logger // 简单返回自己
|
|
19
|
+
} as unknown as pino.Logger : pino({
|
|
20
|
+
level: "info",
|
|
21
|
+
// transport: {
|
|
22
|
+
// target: "pino-pretty",
|
|
23
|
+
// },
|
|
24
|
+
});
|
|
25
|
+
const normalizeServer = (server: string): string =>
|
|
26
|
+
server.trim().replace(/\/+$/, "");
|
|
27
|
+
const isNoloClusterServer = (server: string): boolean =>
|
|
28
|
+
/^https?:\/\/(?:us\.)?nolo\.chat$/i.test(normalizeServer(server));
|
|
29
|
+
|
|
30
|
+
export const mergeConfiguredServers = (
|
|
31
|
+
currentServer: string | undefined,
|
|
32
|
+
syncServers: string[] | undefined
|
|
33
|
+
): string[] => {
|
|
34
|
+
const runtimeOrigin =
|
|
35
|
+
!getIsDesktopApp() &&
|
|
36
|
+
typeof window !== "undefined" &&
|
|
37
|
+
typeof window.location?.origin === "string" &&
|
|
38
|
+
/^https?:\/\//.test(window.location.origin)
|
|
39
|
+
? window.location.origin
|
|
40
|
+
: undefined;
|
|
41
|
+
|
|
42
|
+
const raw = [
|
|
43
|
+
currentServer,
|
|
44
|
+
...(Array.isArray(syncServers) ? syncServers : []),
|
|
45
|
+
runtimeOrigin,
|
|
46
|
+
].filter(
|
|
47
|
+
(s): s is string => typeof s === "string" && s.trim().length > 0
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const normalized = raw.map(normalizeServer);
|
|
51
|
+
if (normalized.some(isNoloClusterServer)) {
|
|
52
|
+
normalized.push(...NOLO_CLUSTER_SERVERS);
|
|
53
|
+
}
|
|
54
|
+
return Array.from(new Set(normalized));
|
|
55
|
+
};
|
|
56
|
+
// 全局缓存网络状态(仅 React Native 使用)
|
|
57
|
+
let cachedNetworkState: boolean | null = null;
|
|
58
|
+
let netInfoListenerInitialized = false;
|
|
59
|
+
|
|
60
|
+
// 检测是否为 React Native 环境
|
|
61
|
+
const isReactNative = (): boolean => {
|
|
62
|
+
return typeof navigator !== 'undefined' && (navigator as any).product === 'ReactNative';
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// 初始化 NetInfo 监听器(仅在 React Native 环境调用)
|
|
66
|
+
export const initNetworkListener = async () => {
|
|
67
|
+
if (!isReactNative() || netInfoListenerInitialized) return;
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
// 动态导入 NetInfo,避免在 Web 环境下出错
|
|
71
|
+
const NetInfo = await import('@react-native-community/netinfo');
|
|
72
|
+
NetInfo.default.addEventListener(state => {
|
|
73
|
+
cachedNetworkState = state.isConnected ?? true;
|
|
74
|
+
});
|
|
75
|
+
netInfoListenerInitialized = true;
|
|
76
|
+
console.log('[NetInfo] Listener initialized');
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.warn('[NetInfo] Failed to initialize:', error);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// 改进的 isOnline 函数,兼容 Web 和 React Native
|
|
83
|
+
export const isOnline = (): boolean => {
|
|
84
|
+
// React Native 环境:使用 NetInfo 缓存
|
|
85
|
+
if (isReactNative()) {
|
|
86
|
+
if (cachedNetworkState !== null) {
|
|
87
|
+
return cachedNetworkState;
|
|
88
|
+
}
|
|
89
|
+
// NetInfo 还未初始化,默认假设在线
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Web 环境:使用 navigator.onLine
|
|
94
|
+
if (typeof navigator !== "undefined" && typeof (navigator as any).onLine !== "undefined") {
|
|
95
|
+
return (navigator as any).onLine;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 降级:默认假设在线
|
|
99
|
+
return true;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export const getAllServers = (
|
|
103
|
+
currentServer: string | undefined,
|
|
104
|
+
syncServers: string[] | undefined,
|
|
105
|
+
preferredServer?: string | null
|
|
106
|
+
): string[] => {
|
|
107
|
+
const preferredNormalized =
|
|
108
|
+
typeof preferredServer === "string" && preferredServer.trim().length > 0
|
|
109
|
+
? normalizeServer(preferredServer)
|
|
110
|
+
: null;
|
|
111
|
+
const servers = mergeConfiguredServers(
|
|
112
|
+
currentServer,
|
|
113
|
+
preferredNormalized && isNoloClusterServer(preferredNormalized)
|
|
114
|
+
? [...(Array.isArray(syncServers) ? syncServers : []), preferredNormalized]
|
|
115
|
+
: syncServers
|
|
116
|
+
);
|
|
117
|
+
if (!preferredServer || typeof preferredServer !== "string") {
|
|
118
|
+
return servers;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const remaining = servers.filter(
|
|
122
|
+
(server) => normalizeServer(server) !== preferredNormalized
|
|
123
|
+
);
|
|
124
|
+
return preferredNormalized ? [preferredNormalized, ...remaining] : remaining;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const isLevelNotFoundError = (err: any): boolean => {
|
|
128
|
+
const code = err?.code;
|
|
129
|
+
return (
|
|
130
|
+
err?.notFound === true ||
|
|
131
|
+
err?.name === "NotFoundError" ||
|
|
132
|
+
code === "LEVEL_NOT_FOUND" ||
|
|
133
|
+
code === "LEVEL_NOT_FOUND_ERROR"
|
|
134
|
+
);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
// 从客户端数据库获取数据 (无需改动)
|
|
139
|
+
export const fetchFromClientDb = async (
|
|
140
|
+
clientDb: any,
|
|
141
|
+
dbKey: string
|
|
142
|
+
): Promise<any> => {
|
|
143
|
+
if (!clientDb) {
|
|
144
|
+
logger.error(
|
|
145
|
+
{ dbKey },
|
|
146
|
+
"Client database is undefined in fetchFromClientDb"
|
|
147
|
+
);
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
return await clientDb.get(dbKey);
|
|
152
|
+
} catch (err) {
|
|
153
|
+
if (isLevelNotFoundError(err)) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
logger.error({ err, dbKey }, "Failed to get local data");
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// ======================================================================
|
|
162
|
+
// 【核心改造】: fetchFromServer 函数
|
|
163
|
+
// ======================================================================
|
|
164
|
+
const SERVER_TIMEOUT = 5000;
|
|
165
|
+
export const READ_TIMEOUT_ERROR_NAME = "ReadTimeoutError";
|
|
166
|
+
const isPublicFileDbKey = (dbKey: string): boolean => dbKey.startsWith("file-");
|
|
167
|
+
// file-* 元数据接口对无 token 的 LLM/工具调用开放(仅 metadata);
|
|
168
|
+
// 常规 read 接口保留认证路径,避免普通业务数据被未授权盗链/枚举。
|
|
169
|
+
const buildReadUrl = (dbKey: string): string =>
|
|
170
|
+
isPublicFileDbKey(dbKey)
|
|
171
|
+
? `${API_ENDPOINTS.DATABASE}/file/metadata/${encodeURIComponent(dbKey)}`
|
|
172
|
+
: `${API_ENDPOINTS.DATABASE}/read/${encodeURIComponent(dbKey)}`;
|
|
173
|
+
|
|
174
|
+
const createReadTimeoutError = (server: string, dbKey: string): Error => {
|
|
175
|
+
const error = new Error(
|
|
176
|
+
`Timed out reading key "${dbKey}" from ${normalizeServer(server)}.`
|
|
177
|
+
);
|
|
178
|
+
error.name = READ_TIMEOUT_ERROR_NAME;
|
|
179
|
+
return error;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
export const isReadTimeoutError = (error: unknown): boolean =>
|
|
183
|
+
error instanceof Error && error.name === READ_TIMEOUT_ERROR_NAME;
|
|
184
|
+
|
|
185
|
+
export const fetchFromServer = async (
|
|
186
|
+
server: string,
|
|
187
|
+
dbKey: string,
|
|
188
|
+
token?: string,
|
|
189
|
+
signal?: AbortSignal
|
|
190
|
+
): Promise<any> => {
|
|
191
|
+
if (signal?.aborted) {
|
|
192
|
+
throw new DOMException("Aborted", "AbortError");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const controller = new AbortController();
|
|
196
|
+
let didTimeout = false;
|
|
197
|
+
const timeoutId = setTimeout(() => {
|
|
198
|
+
didTimeout = true;
|
|
199
|
+
controller.abort();
|
|
200
|
+
}, SERVER_TIMEOUT);
|
|
201
|
+
const onExternalAbort = () => controller.abort();
|
|
202
|
+
signal?.addEventListener("abort", onExternalAbort);
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const res = await fetch(
|
|
206
|
+
`${server}${buildReadUrl(dbKey)}`,
|
|
207
|
+
{
|
|
208
|
+
signal: controller.signal as any,
|
|
209
|
+
headers: {
|
|
210
|
+
"Content-Type": "application/json",
|
|
211
|
+
...(token && { Authorization: `Bearer ${token}` }),
|
|
212
|
+
},
|
|
213
|
+
}
|
|
214
|
+
);
|
|
215
|
+
clearTimeout(timeoutId);
|
|
216
|
+
signal?.removeEventListener("abort", onExternalAbort);
|
|
217
|
+
|
|
218
|
+
if (res.status === 200) {
|
|
219
|
+
return await res.json();
|
|
220
|
+
}
|
|
221
|
+
return null;
|
|
222
|
+
} catch (err: any) {
|
|
223
|
+
clearTimeout(timeoutId);
|
|
224
|
+
signal?.removeEventListener("abort", onExternalAbort);
|
|
225
|
+
if (didTimeout) {
|
|
226
|
+
throw createReadTimeoutError(server, dbKey);
|
|
227
|
+
}
|
|
228
|
+
if (signal?.aborted || err.name === "AbortError") {
|
|
229
|
+
throw err;
|
|
230
|
+
}
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
// 规范化时间字段 (无需改动)
|
|
236
|
+
export const normalizeTimeFields = (data: any): any => ({
|
|
237
|
+
...data,
|
|
238
|
+
createdAt: data.createdAt || new Date().toISOString(),
|
|
239
|
+
updatedAt: new Date().toISOString(),
|
|
240
|
+
updated_at: undefined,
|
|
241
|
+
created_at: undefined,
|
|
242
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
|
|
2
|
+
import { removeAction } from "./remove";
|
|
3
|
+
import { fetchFromClientDb } from "./common";
|
|
4
|
+
import { deleteFileFromIndexedDb } from "../fileStorage";
|
|
5
|
+
import { logger } from "./common";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Delete File Action:
|
|
9
|
+
* 1. Retrieve metadata to get fileId (if different from dbKey, though usually dbKey maps to metadata which has the fileId)
|
|
10
|
+
* 2. Delete blob from IndexedDB (using fileId from metadata)
|
|
11
|
+
* 3. Call generic removeAction to delete metadata and notify servers
|
|
12
|
+
*/
|
|
13
|
+
export const deleteFileAction = async (
|
|
14
|
+
dbKey: string,
|
|
15
|
+
thunkApi: any
|
|
16
|
+
): Promise<void> => {
|
|
17
|
+
const { db: clientDb } = thunkApi.extra;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
// 1. Get metadata to find the internal Blob ID (fileId)
|
|
21
|
+
const metadata = await fetchFromClientDb(clientDb, dbKey);
|
|
22
|
+
|
|
23
|
+
if (metadata && metadata.id) {
|
|
24
|
+
// 2. Delete blob from IndexedDB
|
|
25
|
+
try {
|
|
26
|
+
await deleteFileFromIndexedDb(metadata.id);
|
|
27
|
+
logger.debug({ fileId: metadata.id }, "Deleted file blob from IndexedDB");
|
|
28
|
+
} catch (err) {
|
|
29
|
+
logger.warn({ err, fileId: metadata.id }, "Failed to delete file blob from IndexedDB, proceeding to delete metadata");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 3. Delete metadata and sync delete to servers
|
|
34
|
+
await removeAction(dbKey, thunkApi);
|
|
35
|
+
|
|
36
|
+
} catch (error) {
|
|
37
|
+
logger.error({ error, dbKey }, "Failed to delete file completely");
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createAsyncThunk } from "@reduxjs/toolkit";
|
|
2
|
+
import { fetchUserData } from "../client/fetchUserData";
|
|
3
|
+
import { AppThunkApi } from "app/store";
|
|
4
|
+
|
|
5
|
+
export const fetchUserDataThunk = createAsyncThunk<
|
|
6
|
+
any,
|
|
7
|
+
{ types: string | string[]; userId: string; includeDeleted?: boolean },
|
|
8
|
+
AppThunkApi
|
|
9
|
+
>("db/fetchUserData", async ({ types, userId, includeDeleted }, { extra }) => {
|
|
10
|
+
const { db } = extra;
|
|
11
|
+
if (!db) {
|
|
12
|
+
console.error("Database not available in fetchUserDataThunk");
|
|
13
|
+
throw new Error("Database not available");
|
|
14
|
+
}
|
|
15
|
+
return await fetchUserData(db, types, userId, { includeDeleted });
|
|
16
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// 文件路径: database/actions/fileContent.ts
|
|
2
|
+
|
|
3
|
+
import type { AppThunkApi } from "app/store";
|
|
4
|
+
import { API_ENDPOINTS } from "database/config";
|
|
5
|
+
import { getRuntimeServerContext } from "database/runtimeServerContext";
|
|
6
|
+
import {
|
|
7
|
+
loadFileFromIndexedDb,
|
|
8
|
+
saveFileToIndexedDb,
|
|
9
|
+
StoredFileRecord,
|
|
10
|
+
} from "../fileStorage";
|
|
11
|
+
import { getFileIdFromKey } from "../keys";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 从 dbKey 中提取裸 ULID(用于 IndexedDB 查找)
|
|
15
|
+
*
|
|
16
|
+
* 如果传入的是完整 dbKey(如 file-{userId}-{fileId}),返回 fileId(ULID)。
|
|
17
|
+
* 如果传入的已经是裸 ULID,则原样返回。
|
|
18
|
+
*/
|
|
19
|
+
const resolveLocalFileId = (fileId: string): string => {
|
|
20
|
+
if (fileId.startsWith("file-")) {
|
|
21
|
+
return getFileIdFromKey(fileId) || fileId;
|
|
22
|
+
}
|
|
23
|
+
return fileId;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 读取文件内容(优先本地 IndexedDB,缺失时从服务器拉取并写入本地缓存)
|
|
28
|
+
*
|
|
29
|
+
* fileId 支持两种格式:
|
|
30
|
+
* - 完整 dbKey:file-{userId}-{fileId}(推荐,服务端可直接查询)
|
|
31
|
+
* - 裸 ULID:{fileId}(需要服务端 file-id 索引)
|
|
32
|
+
*
|
|
33
|
+
* 返回:
|
|
34
|
+
* - fileId: string
|
|
35
|
+
* - blob: Blob
|
|
36
|
+
* - source: "local" | "remote"
|
|
37
|
+
*/
|
|
38
|
+
export const readFileContentAction = async (
|
|
39
|
+
{
|
|
40
|
+
fileId,
|
|
41
|
+
useServerFallback = true,
|
|
42
|
+
}: { fileId: string; useServerFallback?: boolean },
|
|
43
|
+
thunkApi: AppThunkApi
|
|
44
|
+
): Promise<{ fileId: string; blob: Blob; source: "local" | "remote" }> => {
|
|
45
|
+
if (!fileId || typeof fileId !== "string") {
|
|
46
|
+
throw new Error("readFileContentAction requires a valid fileId string.");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// IndexedDB 以裸 ULID 为 key 存储文件
|
|
50
|
+
const localId = resolveLocalFileId(fileId);
|
|
51
|
+
|
|
52
|
+
// 1. 先尝试从 IndexedDB 读取(使用裸 ULID)
|
|
53
|
+
const localRecord: StoredFileRecord | null =
|
|
54
|
+
await loadFileFromIndexedDb(localId);
|
|
55
|
+
if (localRecord) {
|
|
56
|
+
return {
|
|
57
|
+
fileId: localId,
|
|
58
|
+
blob: localRecord.blob,
|
|
59
|
+
source: "local",
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 2. 根据参数决定是否回退到服务器
|
|
64
|
+
if (!useServerFallback) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`Local file not found for id "${fileId}", and server fallback is disabled.`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 3. 从服务器拉取文件内容(使用原始 fileId,完整 dbKey 可直接查询)
|
|
71
|
+
const state = thunkApi.getState();
|
|
72
|
+
const { currentServer, remoteServers: serversToTry } =
|
|
73
|
+
getRuntimeServerContext(state);
|
|
74
|
+
|
|
75
|
+
if (!currentServer) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`No current server configured. Cannot fetch remote file for id "${fileId}".`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let lastError = "";
|
|
82
|
+
|
|
83
|
+
for (const server of serversToTry) {
|
|
84
|
+
const url = `${server}${API_ENDPOINTS.DATABASE}/file/content/${fileId}`;
|
|
85
|
+
console.debug("[readFileContentAction] trying server:", url);
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const res = await fetch(url);
|
|
89
|
+
if (!res.ok) {
|
|
90
|
+
lastError = `HTTP ${res.status} from ${server}`;
|
|
91
|
+
console.debug("[readFileContentAction] server returned:", lastError);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const blob = await res.blob();
|
|
96
|
+
|
|
97
|
+
// 将从服务器获取的文件写入 IndexedDB,以裸 ULID 为 key 缓存
|
|
98
|
+
if (typeof indexedDB !== "undefined") {
|
|
99
|
+
void saveFileToIndexedDb(localId, blob).catch((err) => {
|
|
100
|
+
console.warn(
|
|
101
|
+
"[readFileContentAction] Failed to cache remote file into IndexedDB:",
|
|
102
|
+
err
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
fileId: localId,
|
|
109
|
+
blob,
|
|
110
|
+
source: "remote" as const,
|
|
111
|
+
};
|
|
112
|
+
} catch (err: any) {
|
|
113
|
+
lastError = err?.message || "Network error";
|
|
114
|
+
console.debug(
|
|
115
|
+
"[readFileContentAction] fetch error from",
|
|
116
|
+
server,
|
|
117
|
+
lastError
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
throw new Error(
|
|
123
|
+
`Failed to fetch remote file content from all servers for id "${fileId}". Last error: ${lastError}`
|
|
124
|
+
);
|
|
125
|
+
};
|