nolo-cli 0.1.12 → 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 +54 -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 +141 -22
- package/agentRuntimeLocal.ts +7 -0
- package/ai/agent/_executeModel.ts +118 -0
- package/ai/agent/agentSlice.ts +545 -0
- 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/agent.ts +2 -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 -0
- 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/authCommands.ts +185 -21
- package/client/agentRun.test.ts +240 -0
- package/client/agentRun.ts +182 -19
- package/client/compactDialog.test.ts +238 -0
- package/client/compactDialog.ts +5 -2
- 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 +11 -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,94 @@
|
|
|
1
|
+
// 文件路径: src/database/actions/delete.ts
|
|
2
|
+
|
|
3
|
+
import type { AppThunkApi } from "app/store";
|
|
4
|
+
import { getRuntimeServerContext } from "database/runtimeServerContext";
|
|
5
|
+
|
|
6
|
+
import { fetchFromClientDb } from "./common";
|
|
7
|
+
import { deleteFileFromIndexedDb } from "../fileStorage";
|
|
8
|
+
import {
|
|
9
|
+
scheduleDeleteReplication,
|
|
10
|
+
} from "./replication";
|
|
11
|
+
import { buildTombstoneRecord } from "../tombstones";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* removeAction:
|
|
15
|
+
* 1. 先删除本地 IndexedDB 中的记录(如果存在)
|
|
16
|
+
* 2. 再异步并行通知所有远程服务器删除该记录
|
|
17
|
+
*
|
|
18
|
+
* - 服务器列表来源:
|
|
19
|
+
* - 当前服务器:settings.currentServer
|
|
20
|
+
* - 备份服务器:settings.syncServers
|
|
21
|
+
* - getAllServers 负责去重 + 离线检测(offline 时返回 [])
|
|
22
|
+
*/
|
|
23
|
+
export const removeAction = async (
|
|
24
|
+
payload: string | { dbKey: string; preferredServerOrigin?: string | null },
|
|
25
|
+
thunkApi: AppThunkApi
|
|
26
|
+
): Promise<{ dbKey: string }> => {
|
|
27
|
+
const { db: clientDb } = thunkApi.extra;
|
|
28
|
+
const dbKey = typeof payload === "string" ? payload : payload.dbKey;
|
|
29
|
+
const preferredServerOrigin =
|
|
30
|
+
typeof payload === "string" ? undefined : payload.preferredServerOrigin;
|
|
31
|
+
|
|
32
|
+
if (!clientDb) {
|
|
33
|
+
throw new Error("Client database is undefined in removeAction");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const state = thunkApi.getState();
|
|
37
|
+
const { currentServer, syncServers } = getRuntimeServerContext(state);
|
|
38
|
+
|
|
39
|
+
console.log("[removeAction] START", {
|
|
40
|
+
dbKey,
|
|
41
|
+
preferredServerOrigin,
|
|
42
|
+
currentServer,
|
|
43
|
+
syncServers,
|
|
44
|
+
hasToken: Boolean(state?.auth?.currentToken),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// 1) 先查本地是否有这条数据
|
|
48
|
+
const localData = await fetchFromClientDb(clientDb, dbKey);
|
|
49
|
+
const hadLocalData = Boolean(localData);
|
|
50
|
+
|
|
51
|
+
// 2) local mutation 只依赖本地真相;远端删除由 replication helper 负责后台收敛。
|
|
52
|
+
console.log("[removeAction] replication inputs", {
|
|
53
|
+
currentServer,
|
|
54
|
+
syncServers,
|
|
55
|
+
preferredServerOrigin,
|
|
56
|
+
hadLocalData,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// 3) 如果本地存在,则先写 tombstone 到本地。
|
|
60
|
+
// Recent / My Content 是多源 merge,直接物理删除本地会丢失“删除胜出”证据,
|
|
61
|
+
// 老版本远端返回的活记录会在下一轮 merge 时再次回流。
|
|
62
|
+
const nowIso = new Date().toISOString();
|
|
63
|
+
if (localData) {
|
|
64
|
+
if (localData.id && typeof localData.id === "string") {
|
|
65
|
+
void deleteFileFromIndexedDb(localData.id).catch((err) => {
|
|
66
|
+
console.warn("[removeAction] Failed to delete associated file:", localData.id, err);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
await clientDb.put(dbKey, buildTombstoneRecord(localData, nowIso));
|
|
70
|
+
} else {
|
|
71
|
+
// 本地无数据(仅存于远端),写最小 tombstone 防止远端记录在 merge 时回流
|
|
72
|
+
await clientDb.put(dbKey, buildTombstoneRecord({ dbKey }, nowIso));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 4) local-first 产品语义:本地 tombstone 立即成功,远端删除异步收敛。
|
|
76
|
+
// 这样离线/弱网时删除也能成立,UI 可立刻移除内容;远端复制后续自行收敛。
|
|
77
|
+
scheduleDeleteReplication({
|
|
78
|
+
currentServer,
|
|
79
|
+
syncServers,
|
|
80
|
+
preferredServerOrigin,
|
|
81
|
+
dbKey,
|
|
82
|
+
state,
|
|
83
|
+
onResult: (result) => {
|
|
84
|
+
if (result.failed.length > 0) {
|
|
85
|
+
console.warn("[removeAction] Server delete failures after local tombstone:", result.failed);
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
onError: (err) => {
|
|
89
|
+
console.warn("[removeAction] Background server delete error:", err);
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return { dbKey };
|
|
94
|
+
};
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { getAllServers } from "./common";
|
|
2
|
+
import {
|
|
3
|
+
noloDeleteRequest,
|
|
4
|
+
noloPatchRequest,
|
|
5
|
+
noloUploadRequest,
|
|
6
|
+
noloWriteRequest,
|
|
7
|
+
syncWithServers,
|
|
8
|
+
} from "../requests";
|
|
9
|
+
import { planServersForTenant } from "../tenantPlacement";
|
|
10
|
+
|
|
11
|
+
const isReadonlyPublicRecordKey = (dbKey: string): boolean =>
|
|
12
|
+
dbKey.startsWith("agent-pub-") || dbKey.startsWith("cybot-pub-");
|
|
13
|
+
|
|
14
|
+
export const resolveReplicationServers = (
|
|
15
|
+
currentServer: string | undefined,
|
|
16
|
+
syncServers: string[] | undefined,
|
|
17
|
+
preferredServerOrigin?: string | null
|
|
18
|
+
): string[] => getAllServers(currentServer, syncServers, preferredServerOrigin);
|
|
19
|
+
|
|
20
|
+
export const scheduleWriteReplication = (
|
|
21
|
+
servers: string[],
|
|
22
|
+
request: { data: any; customKey: string; userId?: string },
|
|
23
|
+
state: any
|
|
24
|
+
) => {
|
|
25
|
+
if (servers.length === 0) return;
|
|
26
|
+
Promise.resolve().then(async () => {
|
|
27
|
+
const [primaryServer, ...backupServers] = servers;
|
|
28
|
+
const primarySucceeded = await noloWriteRequest(primaryServer, request, state);
|
|
29
|
+
|
|
30
|
+
if (!primarySucceeded) {
|
|
31
|
+
console.warn(`Primary write sync failed for ${request.customKey} on ${primaryServer}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (backupServers.length === 0) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
syncWithServers(
|
|
39
|
+
backupServers,
|
|
40
|
+
(server, requestConfig, requestState, signal) =>
|
|
41
|
+
noloWriteRequest(server, requestConfig, requestState, signal, {
|
|
42
|
+
failureLogLevel: "info",
|
|
43
|
+
}),
|
|
44
|
+
`Backup write sync failed for ${request.customKey} on`,
|
|
45
|
+
request,
|
|
46
|
+
state
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const resolveTenantReplicationServers = ({
|
|
52
|
+
currentServer,
|
|
53
|
+
syncServers,
|
|
54
|
+
tenantId,
|
|
55
|
+
}: {
|
|
56
|
+
currentServer: string | undefined;
|
|
57
|
+
syncServers: string[] | undefined;
|
|
58
|
+
tenantId: string | null | undefined;
|
|
59
|
+
}): string[] => {
|
|
60
|
+
const allServers = resolveReplicationServers(currentServer, syncServers);
|
|
61
|
+
if (allServers.length === 0) {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return planServersForTenant(allServers, currentServer, tenantId);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const scheduleExistingRecordReplication = ({
|
|
69
|
+
currentServer,
|
|
70
|
+
syncServers,
|
|
71
|
+
preferredServerOrigin,
|
|
72
|
+
dbKey,
|
|
73
|
+
localData,
|
|
74
|
+
state,
|
|
75
|
+
}: {
|
|
76
|
+
currentServer: string | undefined;
|
|
77
|
+
syncServers: string[] | undefined;
|
|
78
|
+
preferredServerOrigin?: string | null;
|
|
79
|
+
dbKey: string;
|
|
80
|
+
localData: any;
|
|
81
|
+
state: any;
|
|
82
|
+
}): string[] => {
|
|
83
|
+
if (isReadonlyPublicRecordKey(dbKey)) {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const servers = resolveReplicationServers(
|
|
88
|
+
currentServer,
|
|
89
|
+
syncServers,
|
|
90
|
+
preferredServerOrigin
|
|
91
|
+
);
|
|
92
|
+
if (servers.length === 0) {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
scheduleWriteReplication(
|
|
97
|
+
servers,
|
|
98
|
+
{
|
|
99
|
+
data: localData,
|
|
100
|
+
customKey: dbKey,
|
|
101
|
+
userId:
|
|
102
|
+
typeof localData?.userId === "string"
|
|
103
|
+
? localData.userId
|
|
104
|
+
: state?.auth?.currentUser?.userId,
|
|
105
|
+
},
|
|
106
|
+
state
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
return servers;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export const schedulePatchReplication = ({
|
|
113
|
+
servers,
|
|
114
|
+
dbKey,
|
|
115
|
+
changes,
|
|
116
|
+
state,
|
|
117
|
+
preferredServerOrigin,
|
|
118
|
+
}: {
|
|
119
|
+
servers: string[];
|
|
120
|
+
dbKey: string;
|
|
121
|
+
changes: any;
|
|
122
|
+
state: any;
|
|
123
|
+
preferredServerOrigin?: string | null;
|
|
124
|
+
}) => {
|
|
125
|
+
if (servers.length === 0) return;
|
|
126
|
+
|
|
127
|
+
Promise.resolve().then(async () => {
|
|
128
|
+
const primaryServer =
|
|
129
|
+
typeof preferredServerOrigin === "string" && preferredServerOrigin.trim().length > 0
|
|
130
|
+
? preferredServerOrigin.trim().replace(/\/+$/, "")
|
|
131
|
+
: servers[0];
|
|
132
|
+
const backupServers = servers.filter(
|
|
133
|
+
(server) => server.replace(/\/+$/, "") !== primaryServer
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const primarySucceeded = await noloPatchRequest(primaryServer, dbKey, changes, state, undefined, {
|
|
137
|
+
failureLogLevel: "warn",
|
|
138
|
+
});
|
|
139
|
+
if (!primarySucceeded) {
|
|
140
|
+
console.warn(`Primary patch sync failed for ${dbKey} on ${primaryServer}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (backupServers.length > 0) {
|
|
144
|
+
syncWithServers(
|
|
145
|
+
backupServers,
|
|
146
|
+
(server, targetDbKey, nextChanges, requestState, signal) =>
|
|
147
|
+
noloPatchRequest(server, targetDbKey, nextChanges, requestState, signal, {
|
|
148
|
+
failureLogLevel: "info",
|
|
149
|
+
}),
|
|
150
|
+
`Backup patch sync failed for ${dbKey} on`,
|
|
151
|
+
dbKey,
|
|
152
|
+
changes,
|
|
153
|
+
state
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export const scheduleConfiguredPatchReplication = ({
|
|
160
|
+
currentServer,
|
|
161
|
+
syncServers,
|
|
162
|
+
preferredServerOrigin,
|
|
163
|
+
dbKey,
|
|
164
|
+
changes,
|
|
165
|
+
state,
|
|
166
|
+
}: {
|
|
167
|
+
currentServer: string | undefined;
|
|
168
|
+
syncServers: string[] | undefined;
|
|
169
|
+
preferredServerOrigin?: string | null;
|
|
170
|
+
dbKey: string;
|
|
171
|
+
changes: any;
|
|
172
|
+
state: any;
|
|
173
|
+
}): string[] => {
|
|
174
|
+
const servers = resolveReplicationServers(
|
|
175
|
+
currentServer,
|
|
176
|
+
syncServers,
|
|
177
|
+
preferredServerOrigin
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
if (servers.length === 0) {
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
schedulePatchReplication({
|
|
185
|
+
servers,
|
|
186
|
+
dbKey,
|
|
187
|
+
changes,
|
|
188
|
+
state,
|
|
189
|
+
preferredServerOrigin,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
return servers;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
export const scheduleUploadReplication = ({
|
|
196
|
+
currentServer,
|
|
197
|
+
syncServers,
|
|
198
|
+
tenantId,
|
|
199
|
+
uploadConfig,
|
|
200
|
+
state,
|
|
201
|
+
excludeServers = [],
|
|
202
|
+
}: {
|
|
203
|
+
currentServer: string | undefined;
|
|
204
|
+
syncServers: string[] | undefined;
|
|
205
|
+
tenantId: string | null | undefined;
|
|
206
|
+
uploadConfig: {
|
|
207
|
+
file: File;
|
|
208
|
+
metadata: any;
|
|
209
|
+
customKey: string;
|
|
210
|
+
userId?: string;
|
|
211
|
+
};
|
|
212
|
+
state: any;
|
|
213
|
+
excludeServers?: string[];
|
|
214
|
+
}): string[] => {
|
|
215
|
+
const servers = resolveTenantReplicationServers({
|
|
216
|
+
currentServer,
|
|
217
|
+
syncServers,
|
|
218
|
+
tenantId,
|
|
219
|
+
});
|
|
220
|
+
const excluded = new Set(
|
|
221
|
+
excludeServers
|
|
222
|
+
.filter((server): server is string => typeof server === "string")
|
|
223
|
+
.map((server) => server.trim().replace(/\/+$/, ""))
|
|
224
|
+
);
|
|
225
|
+
const remainingServers = servers.filter(
|
|
226
|
+
(server) => !excluded.has(server.trim().replace(/\/+$/, ""))
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
if (remainingServers.length === 0) {
|
|
230
|
+
return [];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
Promise.resolve().then(() => {
|
|
234
|
+
syncWithServers(
|
|
235
|
+
remainingServers,
|
|
236
|
+
noloUploadRequest,
|
|
237
|
+
`Upload sync failed for ${uploadConfig.customKey} on`,
|
|
238
|
+
uploadConfig,
|
|
239
|
+
state
|
|
240
|
+
);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
return remainingServers;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
export const uploadToCurrentServer = async ({
|
|
247
|
+
currentServer,
|
|
248
|
+
uploadConfig,
|
|
249
|
+
state,
|
|
250
|
+
}: {
|
|
251
|
+
currentServer: string | undefined;
|
|
252
|
+
uploadConfig: {
|
|
253
|
+
file: File;
|
|
254
|
+
metadata: any;
|
|
255
|
+
customKey: string;
|
|
256
|
+
userId?: string;
|
|
257
|
+
};
|
|
258
|
+
state: any;
|
|
259
|
+
}): Promise<boolean> => {
|
|
260
|
+
if (!currentServer) {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return noloUploadRequest(currentServer, uploadConfig, state);
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
export const deleteFromReplicationServers = async ({
|
|
268
|
+
servers,
|
|
269
|
+
dbKey,
|
|
270
|
+
deleteOptions = { type: "single" as const },
|
|
271
|
+
state,
|
|
272
|
+
preferredServerOrigin,
|
|
273
|
+
}: {
|
|
274
|
+
servers: string[];
|
|
275
|
+
dbKey: string;
|
|
276
|
+
deleteOptions?: { type: "single" | "table" | "messages" };
|
|
277
|
+
state: any;
|
|
278
|
+
preferredServerOrigin?: string | null;
|
|
279
|
+
}): Promise<{ succeeded: string[]; failed: string[] }> => {
|
|
280
|
+
if (!servers.length) {
|
|
281
|
+
return { succeeded: [], failed: [] };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const preferredServer =
|
|
285
|
+
typeof preferredServerOrigin === "string" && preferredServerOrigin.trim().length > 0
|
|
286
|
+
? preferredServerOrigin.trim().replace(/\/+$/, "")
|
|
287
|
+
: null;
|
|
288
|
+
const remainingServers = preferredServer
|
|
289
|
+
? servers.filter((server) => server.replace(/\/+$/, "") !== preferredServer)
|
|
290
|
+
: servers;
|
|
291
|
+
const succeeded: string[] = [];
|
|
292
|
+
const failed: string[] = [];
|
|
293
|
+
|
|
294
|
+
if (preferredServer) {
|
|
295
|
+
const ok = await noloDeleteRequest(preferredServer, dbKey, deleteOptions, state);
|
|
296
|
+
if (ok) {
|
|
297
|
+
succeeded.push(preferredServer);
|
|
298
|
+
} else {
|
|
299
|
+
failed.push(preferredServer);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (remainingServers.length > 0) {
|
|
304
|
+
const results = await Promise.all(
|
|
305
|
+
remainingServers.map(async (server) => ({
|
|
306
|
+
server,
|
|
307
|
+
ok: await noloDeleteRequest(server, dbKey, deleteOptions, state),
|
|
308
|
+
}))
|
|
309
|
+
);
|
|
310
|
+
results.forEach(({ server, ok }) => {
|
|
311
|
+
if (ok) succeeded.push(server);
|
|
312
|
+
else failed.push(server);
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return { succeeded, failed };
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
export const scheduleDeleteReplication = ({
|
|
320
|
+
currentServer,
|
|
321
|
+
syncServers,
|
|
322
|
+
preferredServerOrigin,
|
|
323
|
+
dbKey,
|
|
324
|
+
deleteOptions,
|
|
325
|
+
state,
|
|
326
|
+
onResult,
|
|
327
|
+
onError,
|
|
328
|
+
}: {
|
|
329
|
+
currentServer: string | undefined;
|
|
330
|
+
syncServers: string[] | undefined;
|
|
331
|
+
preferredServerOrigin?: string | null;
|
|
332
|
+
dbKey: string;
|
|
333
|
+
deleteOptions?: { type: "single" | "table" | "messages" };
|
|
334
|
+
state: any;
|
|
335
|
+
onResult?: (result: { succeeded: string[]; failed: string[] }) => void;
|
|
336
|
+
onError?: (error: unknown) => void;
|
|
337
|
+
}): string[] => {
|
|
338
|
+
const servers = resolveReplicationServers(
|
|
339
|
+
currentServer,
|
|
340
|
+
syncServers,
|
|
341
|
+
preferredServerOrigin
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
if (servers.length === 0) {
|
|
345
|
+
return [];
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
void Promise.resolve()
|
|
349
|
+
.then(() =>
|
|
350
|
+
deleteFromReplicationServers({
|
|
351
|
+
servers,
|
|
352
|
+
dbKey,
|
|
353
|
+
deleteOptions,
|
|
354
|
+
state,
|
|
355
|
+
preferredServerOrigin,
|
|
356
|
+
})
|
|
357
|
+
)
|
|
358
|
+
.then((result) => {
|
|
359
|
+
onResult?.(result);
|
|
360
|
+
})
|
|
361
|
+
.catch((error) => {
|
|
362
|
+
onError?.(error);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
return servers;
|
|
366
|
+
};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// 文件路径: database/actions/upload.ts
|
|
2
|
+
|
|
3
|
+
import { getRuntimeServerContext } from "database/runtimeServerContext";
|
|
4
|
+
import { ulid } from "../utils/ulid";
|
|
5
|
+
import { normalizeTimeFields, logger } from "./common";
|
|
6
|
+
import { toast } from "app/utils/toast";
|
|
7
|
+
import { saveFileToIndexedDb } from "../fileStorage";
|
|
8
|
+
import { fileKey } from "../keys";
|
|
9
|
+
import { scheduleUploadReplication, uploadToCurrentServer } from "./replication";
|
|
10
|
+
import { DataType } from "create/types";
|
|
11
|
+
import { resolveFileCategory } from "app/utils/fileUtils";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 辅助函数:保存文件元数据到客户端数据库
|
|
15
|
+
*/
|
|
16
|
+
const saveToClientDb = async (
|
|
17
|
+
clientDb: any,
|
|
18
|
+
dbKey: string,
|
|
19
|
+
metadata: any
|
|
20
|
+
): Promise<void> => {
|
|
21
|
+
if (!clientDb) {
|
|
22
|
+
logger.error({ dbKey }, "Client database is undefined in saveToClientDb");
|
|
23
|
+
throw new Error("Client database instance is required");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
await clientDb.put(dbKey, metadata);
|
|
28
|
+
logger.debug(
|
|
29
|
+
{ dbKey },
|
|
30
|
+
"File metadata saved successfully to local database."
|
|
31
|
+
);
|
|
32
|
+
} catch (err: any) {
|
|
33
|
+
logger.error(
|
|
34
|
+
{ err, dbKey },
|
|
35
|
+
"Failed to save file metadata to local database"
|
|
36
|
+
);
|
|
37
|
+
throw new Error(`Local database put failed for ${dbKey}: ${err.message}`);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Upload File Action: 上传文件并保存元数据。
|
|
43
|
+
*
|
|
44
|
+
* 设计要点:
|
|
45
|
+
* - 以 fileId 作为文件唯一 ID;
|
|
46
|
+
* - 本地 IndexedDB 完整缓存一份(离线可用);
|
|
47
|
+
* - 按 tenantId(通常为 userId)通过 hash ring 选择若干服务器写入完整副本;
|
|
48
|
+
* - 服务器只需跑单机 fileService,不感知 ring/tenant。
|
|
49
|
+
*
|
|
50
|
+
* 将来扩容到几十台服务器:
|
|
51
|
+
* - 只需要在设置里增加/调整 syncServers 列表;
|
|
52
|
+
* - getAllServers + planServersForTenant 会自动把新节点纳入分布;
|
|
53
|
+
* - 无需修改业务调用代码。
|
|
54
|
+
*/
|
|
55
|
+
export const uploadFileAction = async (
|
|
56
|
+
uploadConfig: { file: File; customKey?: string; userId?: string },
|
|
57
|
+
thunkApi: any
|
|
58
|
+
): Promise<any> => {
|
|
59
|
+
const { db: clientDb } = thunkApi.extra;
|
|
60
|
+
const state = thunkApi.getState();
|
|
61
|
+
const { currentServer, syncServers, currentUserId } =
|
|
62
|
+
getRuntimeServerContext(state);
|
|
63
|
+
|
|
64
|
+
const { file, customKey } = uploadConfig;
|
|
65
|
+
const userId = uploadConfig.userId || currentUserId;
|
|
66
|
+
// 1. 验证参数
|
|
67
|
+
if (!file) {
|
|
68
|
+
const errorMsg =
|
|
69
|
+
"Invalid arguments for uploadFileAction: file is required.";
|
|
70
|
+
logger.error(errorMsg, { uploadConfig });
|
|
71
|
+
toast.error(errorMsg);
|
|
72
|
+
throw new Error(errorMsg);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
// 2. 生成文件 ID 和文件名(fileId 将作为逻辑 ID,服务端会沿用)
|
|
77
|
+
const fileId = ulid();
|
|
78
|
+
const fileExtension = file.name.split(".").pop() || "";
|
|
79
|
+
const fileName = `${fileId}${fileExtension ? "." + fileExtension : ""}`;
|
|
80
|
+
|
|
81
|
+
// 决定最终的 dbKey (强制使用 file-userId-ulid 模式)
|
|
82
|
+
let finalDbKey = customKey;
|
|
83
|
+
if (!finalDbKey || !finalDbKey.startsWith("file-")) {
|
|
84
|
+
const actualUserId = userId || "unknown";
|
|
85
|
+
if (actualUserId === "unknown") {
|
|
86
|
+
console.warn("[uploadFileAction] User ID is unknown during upload. Key will be file-unknown.");
|
|
87
|
+
}
|
|
88
|
+
finalDbKey = fileKey.single(actualUserId, fileId);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 3. 准备文件元数据(添加时间戳、dbKey、userId 等)
|
|
92
|
+
const fileMetadata = normalizeTimeFields({
|
|
93
|
+
id: fileId,
|
|
94
|
+
title: file.name,
|
|
95
|
+
originalName: file.name,
|
|
96
|
+
fileName,
|
|
97
|
+
filePath: "",
|
|
98
|
+
size: file.size,
|
|
99
|
+
mimeType: file.type || "application/octet-stream",
|
|
100
|
+
type: DataType.FILE,
|
|
101
|
+
fileCategory: resolveFileCategory({
|
|
102
|
+
mimeType: file.type,
|
|
103
|
+
fileName: file.name,
|
|
104
|
+
}),
|
|
105
|
+
dbKey: finalDbKey,
|
|
106
|
+
userId,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// 本地结构化元数据:key = finalDbKey(供 dbSlice 等使用)
|
|
110
|
+
await saveToClientDb(clientDb, finalDbKey, fileMetadata);
|
|
111
|
+
|
|
112
|
+
// 4. 将原始文件存入 IndexedDB / Native Storage
|
|
113
|
+
// 本地以 fileId 为 key 缓存内容(离线使用)
|
|
114
|
+
// 在 RN 环境下,saveFileToIndexedDb 实际上是存储文件路径引用
|
|
115
|
+
try {
|
|
116
|
+
await saveFileToIndexedDb(fileId, file);
|
|
117
|
+
} catch (err) {
|
|
118
|
+
logger.warn(
|
|
119
|
+
{ err, fileId },
|
|
120
|
+
"[uploadFileAction] Failed to cache file locally."
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 5. 基于 tenantId(userId)为当前用户选择服务器集合,并在后台同步上传。
|
|
125
|
+
// - 同一个用户的数据总是落在同一组服务器上
|
|
126
|
+
// - 每个用户至少有固定副本数的服务器持有完整数据
|
|
127
|
+
const tenantId = userId || "default";
|
|
128
|
+
const uploadReplicationConfig = {
|
|
129
|
+
file,
|
|
130
|
+
metadata: fileMetadata,
|
|
131
|
+
customKey: finalDbKey,
|
|
132
|
+
userId,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const currentServerUploadSucceeded = await uploadToCurrentServer({
|
|
136
|
+
currentServer,
|
|
137
|
+
uploadConfig: uploadReplicationConfig,
|
|
138
|
+
state,
|
|
139
|
+
});
|
|
140
|
+
if (currentServer && !currentServerUploadSucceeded) {
|
|
141
|
+
throw new Error(`Primary upload failed on current server ${currentServer}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const serversToUse = scheduleUploadReplication({
|
|
145
|
+
currentServer,
|
|
146
|
+
syncServers,
|
|
147
|
+
tenantId,
|
|
148
|
+
uploadConfig: uploadReplicationConfig,
|
|
149
|
+
state,
|
|
150
|
+
excludeServers: currentServer ? [currentServer] : [],
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (!currentServer && !serversToUse.length) {
|
|
154
|
+
logger.warn(
|
|
155
|
+
"[uploadFileAction] No replication servers available, file metadata only saved locally.",
|
|
156
|
+
{ finalDbKey, fileName, tenantId }
|
|
157
|
+
);
|
|
158
|
+
return fileMetadata;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
logger.debug(
|
|
162
|
+
`[uploadFileAction] Uploaded primary copy for ${fileName} and scheduled background sync to ${serversToUse.length} additional servers.`,
|
|
163
|
+
{ currentServer, serversToUse, tenantId }
|
|
164
|
+
);
|
|
165
|
+
// 8. 返回本地保存的元数据
|
|
166
|
+
return fileMetadata;
|
|
167
|
+
} catch (error: any) {
|
|
168
|
+
const errorMessage = `Upload action failed for ${customKey}: ${error?.message || "Unknown error"
|
|
169
|
+
}`;
|
|
170
|
+
logger.error({ error }, "[uploadFileAction] Error");
|
|
171
|
+
toast.error(`Failed to upload file for ${customKey}.`);
|
|
172
|
+
throw new Error(errorMessage);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// src/database/actions/upsert.ts
|
|
2
|
+
import { toast } from "app/utils/toast";
|
|
3
|
+
import type { AppThunkApi } from "app/store";
|
|
4
|
+
|
|
5
|
+
import { patchAction } from "./patch";
|
|
6
|
+
import { readAction } from "./read";
|
|
7
|
+
import { writeAction } from "./write";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Upsert 数据协调器:根据数据是否存在,调度 patch 或 write 操作。
|
|
11
|
+
* 此操作本身不执行数据库读写或网络请求,而是委托给其他 async thunks。
|
|
12
|
+
*
|
|
13
|
+
* @param upsertConfig 包含 dbKey(必需)和 data(必需)的对象。
|
|
14
|
+
* @param thunkApi Redux Thunk API,包含 dispatch 和 getState。
|
|
15
|
+
* @returns Promise<any> 来自 patch或write操作成功后的数据对象。
|
|
16
|
+
* @throws 如果参数无效或调度的操作失败,则抛出错误。
|
|
17
|
+
*/
|
|
18
|
+
export const upsertAction = async (
|
|
19
|
+
upsertConfig: { dbKey: string; data: any },
|
|
20
|
+
thunkApi: AppThunkApi
|
|
21
|
+
): Promise<any> => {
|
|
22
|
+
const { dbKey, data } = upsertConfig;
|
|
23
|
+
|
|
24
|
+
// 1. 参数验证
|
|
25
|
+
if (!dbKey || !data || typeof data !== "object") {
|
|
26
|
+
const errorMsg = "upsertAction 参数无效:dbKey 和 data 对象是必需的。";
|
|
27
|
+
toast.error(errorMsg);
|
|
28
|
+
throw new Error(errorMsg);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
// 2. 调用 readAction 检查数据是否存在于本地(通过 Redux Store 或 DB)
|
|
33
|
+
// 我们不关心 readAction 是否真的从网络读取,只关心它最终返回的结果。
|
|
34
|
+
const existingData = await readAction({ dbKey }, thunkApi);
|
|
35
|
+
let finalResult;
|
|
36
|
+
|
|
37
|
+
// 3. 根据是否存在数据,决定调度 patch 还是 write
|
|
38
|
+
if (existingData && Object.keys(existingData).length > 0) {
|
|
39
|
+
// **更新路径**:数据已存在,调度 patch action
|
|
40
|
+
// patch action 内部会处理 updatedAt、userId、本地写入和服务器同步
|
|
41
|
+
finalResult = await patchAction({ dbKey, changes: data }, thunkApi);
|
|
42
|
+
} else {
|
|
43
|
+
// **插入路径**:数据不存在,调度 write action
|
|
44
|
+
// write action 内部会处理 createdAt/updatedAt、userId、本地写入和服务器同步
|
|
45
|
+
finalResult = await writeAction({ data, customKey: dbKey }, thunkApi);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 4. 返回最终执行成功的数据
|
|
49
|
+
return finalResult;
|
|
50
|
+
} catch (error: any) {
|
|
51
|
+
const errorMessage = `Upsert 协调操作失败 (dbKey: ${dbKey}): ${error.message || "未知错误"}`;
|
|
52
|
+
toast.error("数据保存失败,请稍后重试。");
|
|
53
|
+
// 重新抛出错误,以便 create.asyncThunk 能捕获到 rejected 状态
|
|
54
|
+
throw new Error(errorMessage, { cause: error });
|
|
55
|
+
}
|
|
56
|
+
};
|