nolo-cli 0.1.13 → 0.1.15
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/agentRunCommand.ts +104 -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 +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 +37 -19
- package/localRuntimeDb.ts +28 -0
- package/package.json +17 -4
- package/runtimeModeArgs.ts +33 -0
- package/tui/readlineWorkspace.ts +1 -0
- package/tui/session.ts +22 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// 文件路径: database/dbSlice.ts
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
asyncThunkCreator,
|
|
5
|
+
buildCreateSlice,
|
|
6
|
+
createEntityAdapter,
|
|
7
|
+
type PayloadAction,
|
|
8
|
+
} from "@reduxjs/toolkit";
|
|
9
|
+
import type { RootState } from "app/store";
|
|
10
|
+
|
|
11
|
+
// Import actions
|
|
12
|
+
import { removeAction } from "./actions/remove";
|
|
13
|
+
import { readAction } from "./actions/read";
|
|
14
|
+
import { readAndWaitAction } from "./actions/readAndWait";
|
|
15
|
+
import { writeAction } from "./actions/write";
|
|
16
|
+
import { patchAction } from "./actions/patch";
|
|
17
|
+
import { upsertAction } from "./actions/upsert";
|
|
18
|
+
import { uploadFileAction } from "./actions/upload";
|
|
19
|
+
import { readFileContentAction } from "./actions/fileContent";
|
|
20
|
+
import { shareResourceAction } from "share/action";
|
|
21
|
+
|
|
22
|
+
// Use dbKey as the entity's unique identifier
|
|
23
|
+
export const dbAdapter = createEntityAdapter<any>({
|
|
24
|
+
selectId: (entity: any) => entity.dbKey,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Selectors
|
|
28
|
+
export const {
|
|
29
|
+
selectById,
|
|
30
|
+
selectEntities,
|
|
31
|
+
selectAll,
|
|
32
|
+
selectIds,
|
|
33
|
+
selectTotal,
|
|
34
|
+
} = dbAdapter.getSelectors((state: RootState) => state.db);
|
|
35
|
+
|
|
36
|
+
// Initial state
|
|
37
|
+
const initialState = dbAdapter.getInitialState({});
|
|
38
|
+
|
|
39
|
+
// Create slice with async thunks
|
|
40
|
+
const createSliceWithThunks = buildCreateSlice({
|
|
41
|
+
creators: { asyncThunk: asyncThunkCreator },
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Slice definition
|
|
45
|
+
const dbSlice = createSliceWithThunks({
|
|
46
|
+
name: "db",
|
|
47
|
+
initialState,
|
|
48
|
+
reducers: (create) => ({
|
|
49
|
+
// Async Thunks
|
|
50
|
+
read: create.asyncThunk(readAction, {
|
|
51
|
+
fulfilled: (state, action) => {
|
|
52
|
+
if (action.payload && Object.keys(action.payload).length > 0) {
|
|
53
|
+
dbAdapter.upsertOne(state, action.payload);
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
}),
|
|
57
|
+
readAndWait: create.asyncThunk(readAndWaitAction, {
|
|
58
|
+
fulfilled: (state, action) => {
|
|
59
|
+
if (action.payload && Object.keys(action.payload).length > 0) {
|
|
60
|
+
dbAdapter.upsertOne(state, action.payload);
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
}),
|
|
64
|
+
remove: create.asyncThunk(removeAction, {
|
|
65
|
+
fulfilled: (state, action) => {
|
|
66
|
+
const { dbKey } = action.payload;
|
|
67
|
+
if (dbKey) dbAdapter.removeOne(state, dbKey);
|
|
68
|
+
},
|
|
69
|
+
}),
|
|
70
|
+
write: create.asyncThunk(writeAction, {
|
|
71
|
+
fulfilled: (state, action) => {
|
|
72
|
+
if (
|
|
73
|
+
action.payload &&
|
|
74
|
+
action.payload.dbKey &&
|
|
75
|
+
Object.keys(action.payload).length > 0
|
|
76
|
+
) {
|
|
77
|
+
dbAdapter.upsertOne(state, action.payload);
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
}),
|
|
81
|
+
patch: create.asyncThunk(patchAction, {
|
|
82
|
+
fulfilled: (state, action) => {
|
|
83
|
+
const { payload } = action;
|
|
84
|
+
if (payload && payload.dbKey && Object.keys(payload).length > 0) {
|
|
85
|
+
dbAdapter.upsertOne(state, payload);
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
}),
|
|
89
|
+
upsert: create.asyncThunk(upsertAction, {
|
|
90
|
+
fulfilled: (state, action) => {
|
|
91
|
+
if (
|
|
92
|
+
action.payload &&
|
|
93
|
+
action.payload.dbKey &&
|
|
94
|
+
Object.keys(action.payload).length > 0
|
|
95
|
+
) {
|
|
96
|
+
dbAdapter.upsertOne(state, action.payload);
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
}),
|
|
100
|
+
// 文件上传(avatar / Slate / Space 等统一走这里)
|
|
101
|
+
upload: create.asyncThunk(uploadFileAction, {
|
|
102
|
+
fulfilled: (state, action) => {
|
|
103
|
+
const payload = action.payload;
|
|
104
|
+
if (payload && payload.dbKey && Object.keys(payload).length > 0) {
|
|
105
|
+
dbAdapter.upsertOne(state, payload);
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
}),
|
|
109
|
+
// 读取文件内容(优先本地 IndexedDB,无状态副作用)
|
|
110
|
+
readFileContent: create.asyncThunk(readFileContentAction, {
|
|
111
|
+
// fulfilled 时不修改 db state;由调用方通过 unwrap() 拿返回值使用
|
|
112
|
+
}),
|
|
113
|
+
share: create.asyncThunk(shareResourceAction, {
|
|
114
|
+
fulfilled: (state, action) => {
|
|
115
|
+
if (action.payload && action.payload.key) {
|
|
116
|
+
// We might want to store the shared object in local DB state too?
|
|
117
|
+
// writeAction already does it. This is just for return value.
|
|
118
|
+
// Actually, writeAction is dispatched inside shareResourceAction.
|
|
119
|
+
// Does writeAction update state? Yes.
|
|
120
|
+
// So here we might not need to do anything extra to state,
|
|
121
|
+
// just define the thunk.
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}),
|
|
125
|
+
// SSR 预取:服务端直接注入实体到 db slice,供首屏 hydrate 使用
|
|
126
|
+
upsertSSREntity: create.reducer((state, action: PayloadAction<any>) => {
|
|
127
|
+
if (action.payload && action.payload.dbKey) {
|
|
128
|
+
dbAdapter.upsertOne(state, action.payload);
|
|
129
|
+
}
|
|
130
|
+
}),
|
|
131
|
+
}),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Export actions
|
|
135
|
+
export const {
|
|
136
|
+
remove,
|
|
137
|
+
read,
|
|
138
|
+
readAndWait,
|
|
139
|
+
write,
|
|
140
|
+
patch,
|
|
141
|
+
upsert,
|
|
142
|
+
upload,
|
|
143
|
+
readFileContent,
|
|
144
|
+
share,
|
|
145
|
+
upsertSSREntity,
|
|
146
|
+
} = dbSlice.actions;
|
|
147
|
+
|
|
148
|
+
// Export the reducer
|
|
149
|
+
export default dbSlice.reducer;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { DataType } from "create/types";
|
|
2
|
+
|
|
3
|
+
export type EmailOwnerType = "user" | "agent";
|
|
4
|
+
export type EmailMailbox = "inbox" | "sent" | "archive" | "trash" | "drafts";
|
|
5
|
+
export type EmailStatus =
|
|
6
|
+
| "received"
|
|
7
|
+
| "draft"
|
|
8
|
+
| "queued"
|
|
9
|
+
| "sent"
|
|
10
|
+
| "failed";
|
|
11
|
+
|
|
12
|
+
export type EmailParticipant = {
|
|
13
|
+
email: string;
|
|
14
|
+
name?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type EmailRecord = {
|
|
18
|
+
dbKey: string;
|
|
19
|
+
type: DataType.EMAIL;
|
|
20
|
+
ownerType: EmailOwnerType;
|
|
21
|
+
ownerId: string;
|
|
22
|
+
tenantId: string;
|
|
23
|
+
spaceId?: string | null;
|
|
24
|
+
mailbox: EmailMailbox;
|
|
25
|
+
status: EmailStatus;
|
|
26
|
+
from: EmailParticipant;
|
|
27
|
+
to: EmailParticipant[];
|
|
28
|
+
cc?: EmailParticipant[];
|
|
29
|
+
bcc?: EmailParticipant[];
|
|
30
|
+
replyTo?: EmailParticipant[];
|
|
31
|
+
subject: string;
|
|
32
|
+
text?: string;
|
|
33
|
+
html?: string;
|
|
34
|
+
messageId?: string;
|
|
35
|
+
threadId?: string;
|
|
36
|
+
inReplyTo?: string;
|
|
37
|
+
references?: string[];
|
|
38
|
+
tags?: string[];
|
|
39
|
+
meta?: Record<string, unknown>;
|
|
40
|
+
createdAt: string | number;
|
|
41
|
+
updatedAt: string | number;
|
|
42
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// 文件路径: database/fileRing.ts
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 简单的 FNV-1a 32bit 字符串哈希
|
|
5
|
+
* - 稳定、实现简单,适合作为 hash ring 的排序依据
|
|
6
|
+
*/
|
|
7
|
+
const fnv1a32 = (str: string): number => {
|
|
8
|
+
let hash = 0x811c9dc5;
|
|
9
|
+
for (let i = 0; i < str.length; i++) {
|
|
10
|
+
hash ^= str.charCodeAt(i);
|
|
11
|
+
hash = (hash * 0x01000193) >>> 0; // 32bit
|
|
12
|
+
}
|
|
13
|
+
return hash >>> 0;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 通用服务器选择函数:
|
|
18
|
+
*
|
|
19
|
+
* 给定一组服务器和一个“key”(可以是 tenantId / userId / orgId / fileId …),
|
|
20
|
+
* 选择 replicaCount 个服务器作为该 key 的“归属服务器”。
|
|
21
|
+
*
|
|
22
|
+
* 规则:
|
|
23
|
+
* - 对每个 server 计算 hash(server + "::" + key)
|
|
24
|
+
* - 按 hash 升序排序
|
|
25
|
+
* - 取前 replicaCount 个
|
|
26
|
+
*
|
|
27
|
+
* 说明:
|
|
28
|
+
* - 这不是严格意义上的一致性哈希环,但分布效果 + 实现复杂度比较均衡;
|
|
29
|
+
* - 之后要支持更多服务器,只需要在配置中把新 server 加入列表即可,
|
|
30
|
+
* 这里不需要改代码。
|
|
31
|
+
*/
|
|
32
|
+
export const chooseServersByKey = (
|
|
33
|
+
allServers: string[],
|
|
34
|
+
key: string,
|
|
35
|
+
replicaCount: number
|
|
36
|
+
): string[] => {
|
|
37
|
+
if (!allServers.length || replicaCount <= 0) return [];
|
|
38
|
+
|
|
39
|
+
const uniqueServers = Array.from(new Set(allServers)).filter((s) => !!s);
|
|
40
|
+
if (uniqueServers.length === 0) return [];
|
|
41
|
+
|
|
42
|
+
const scored = uniqueServers.map((server) => ({
|
|
43
|
+
server,
|
|
44
|
+
score: fnv1a32(`${server}::${key}`),
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
scored.sort((a, b) => a.score - b.score);
|
|
48
|
+
|
|
49
|
+
const limit = Math.min(replicaCount, scored.length);
|
|
50
|
+
return scored.slice(0, limit).map((item) => item.server);
|
|
51
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// 文件路径: database/fileSharding.ts
|
|
2
|
+
|
|
3
|
+
import { chooseServersForFile } from "./fileRing";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 当前“切片”策略参数:
|
|
7
|
+
*
|
|
8
|
+
* - REPLICA_COUNT = 3:
|
|
9
|
+
* 在 ring 中尽量挑 3 台不同的服务器存副本。
|
|
10
|
+
*
|
|
11
|
+
* - REQUIRED_SHARDS = 2:
|
|
12
|
+
* 语义上表示:只要有 2 份副本可用就能工作(当前是完整副本,所以任意一份都能用)。
|
|
13
|
+
*
|
|
14
|
+
* 将来如果要上真正的纠删码(2-of-3),可以保留这两个名字,
|
|
15
|
+
* 只是把 encode/decode 的实现替换掉即可。
|
|
16
|
+
*/
|
|
17
|
+
export const REPLICA_COUNT = 3;
|
|
18
|
+
export const REQUIRED_SHARDS = 2;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 基于 hash ring 选择本次写入要用的服务器列表。
|
|
22
|
+
*
|
|
23
|
+
* 要求:
|
|
24
|
+
* - 优先根据 fileId 在 allServers 上做 ring 选择 REPLICA_COUNT 个;
|
|
25
|
+
* - 无论 ring 如何选择,currentServer 必须包含在结果中(保证当前前端指向的服务器上总有一份)。
|
|
26
|
+
*
|
|
27
|
+
* 说明:
|
|
28
|
+
* - 现在每个 server 存的是完整文件,将来可以改成“每个 server 存一个 shard blob”,
|
|
29
|
+
* 但对 upload 调用方来说只是「发给这些 servers」这一点不变。
|
|
30
|
+
*/
|
|
31
|
+
export const planReplicaServersForFile = (
|
|
32
|
+
allServers: string[],
|
|
33
|
+
currentServer: string | null | undefined,
|
|
34
|
+
fileId: string
|
|
35
|
+
): string[] => {
|
|
36
|
+
const uniqueServers = Array.from(new Set(allServers)).filter(Boolean);
|
|
37
|
+
if (!uniqueServers.length) return [];
|
|
38
|
+
|
|
39
|
+
const fromRing = chooseServersForFile(uniqueServers, fileId, REPLICA_COUNT);
|
|
40
|
+
const set = new Set(fromRing);
|
|
41
|
+
|
|
42
|
+
if (currentServer && uniqueServers.includes(currentServer)) {
|
|
43
|
+
set.add(currentServer);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return Array.from(set);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 预留:将来真正做 2-of-3 纠删码时在这里实现。
|
|
51
|
+
*
|
|
52
|
+
* encode:
|
|
53
|
+
* 接收完整 File/Blob,返回若干 shard(索引 + blob),
|
|
54
|
+
* 上层再根据 shardIndex + planReplicaServersForFile 决定各 shard 分布。
|
|
55
|
+
*
|
|
56
|
+
* decode:
|
|
57
|
+
* 接收至少 REQUIRED_SHARDS 份 shard,重建完整 Blob。
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
// export async function encodeToShards(
|
|
61
|
+
// file: File | Blob
|
|
62
|
+
// ): Promise<Array<{ shardIndex: number; blob: Blob }>> {
|
|
63
|
+
// // TODO: 将来在这里实现真正的 2-of-3 切片编码
|
|
64
|
+
// }
|
|
65
|
+
|
|
66
|
+
// export async function decodeFromShards(
|
|
67
|
+
// shards: Array<{ shardIndex: number; blob: Blob }>
|
|
68
|
+
// ): Promise<Blob> {
|
|
69
|
+
// // TODO: 将来在这里实现解码逻辑
|
|
70
|
+
// }
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* React Native 端的"文件存储"实现
|
|
4
|
+
*
|
|
5
|
+
* 由于 RN 环境没有 IndexedDB,且直接将文件存入 LevelDB 性能较差(虽然可行但不是最佳实践),
|
|
6
|
+
* 这里我们采用折衷方案:
|
|
7
|
+
* 1. 假设文件已经存在于设备文件系统中(例如相册选择的临时路径,或者下载后的路径)。
|
|
8
|
+
* 2. 我们只存储 fileId -> localUri 的映射关系到 LevelDB 中。
|
|
9
|
+
* 3. 读取时返回这个映射,让上层通过 Image.source 或 RNFS 读取。
|
|
10
|
+
*
|
|
11
|
+
* 注意:如果原始文件被系统清理(如 tmp 目录),这个引用会失效。
|
|
12
|
+
* 理想情况下应该 copy 到 DocumentDirectory,但为了避免引入 react-native-fs 依赖导致需要 rebuild,
|
|
13
|
+
* 暂时先存储路径映射。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { getDb } from "./client/db";
|
|
17
|
+
|
|
18
|
+
// 在 RN 中,File 对象通常是 { uri, name, type, size } 的结构
|
|
19
|
+
export interface RNFile {
|
|
20
|
+
uri: string;
|
|
21
|
+
name: string;
|
|
22
|
+
type: string;
|
|
23
|
+
size: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface StoredFileRecord {
|
|
27
|
+
id: string;
|
|
28
|
+
uri: string; // 替代 blob
|
|
29
|
+
size: number;
|
|
30
|
+
type: string;
|
|
31
|
+
createdAt: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const FILE_PREFIX = "local-file-ref:";
|
|
35
|
+
|
|
36
|
+
export const saveFileToIndexedDb = async (
|
|
37
|
+
fileId: string,
|
|
38
|
+
file: File | Blob | RNFile
|
|
39
|
+
): Promise<void> => {
|
|
40
|
+
try {
|
|
41
|
+
const db = getDb();
|
|
42
|
+
if (!db) {
|
|
43
|
+
console.warn("[fileStorage.native] Database not initialized");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 检查是否为 RN 文件结构
|
|
48
|
+
const uri = (file as any).uri;
|
|
49
|
+
if (!uri) {
|
|
50
|
+
console.warn("[fileStorage.native] File object missing URI, cannot store reference", file);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const record: StoredFileRecord = {
|
|
55
|
+
id: fileId,
|
|
56
|
+
uri: uri,
|
|
57
|
+
size: (file as any).size || 0,
|
|
58
|
+
type: (file as any).type || "application/octet-stream",
|
|
59
|
+
createdAt: new Date().toISOString(),
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
await db.put(`${FILE_PREFIX}${fileId}`, record);
|
|
63
|
+
console.log(`[fileStorage.native] Saved file reference: ${fileId} -> ${uri}`);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.error("[fileStorage.native] Failed to save file reference:", err);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const loadFileFromIndexedDb = async (
|
|
70
|
+
fileId: string
|
|
71
|
+
): Promise<StoredFileRecord | null> => {
|
|
72
|
+
try {
|
|
73
|
+
const db = getDb();
|
|
74
|
+
if (!db) return null;
|
|
75
|
+
|
|
76
|
+
const record = await db.get(`${FILE_PREFIX}${fileId}`);
|
|
77
|
+
return record as StoredFileRecord;
|
|
78
|
+
} catch (err) {
|
|
79
|
+
// LevelDB throws if key not found
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const deleteFileFromIndexedDb = async (fileId: string): Promise<void> => {
|
|
85
|
+
try {
|
|
86
|
+
const db = getDb();
|
|
87
|
+
if (!db) return;
|
|
88
|
+
await db.del(`${FILE_PREFIX}${fileId}`);
|
|
89
|
+
} catch (err) {
|
|
90
|
+
console.warn("[fileStorage.native] Failed to delete file reference:", err);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// 文件路径: database/fileStorage.ts
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 浏览器端文件存储(IndexedDB)
|
|
5
|
+
*
|
|
6
|
+
* 设计:
|
|
7
|
+
* - 库名: "nolo-file-storage"
|
|
8
|
+
* - 表名: "files"
|
|
9
|
+
* - 主键: "id" (即 fileId / BlobId)
|
|
10
|
+
*
|
|
11
|
+
* 存储内容:
|
|
12
|
+
* {
|
|
13
|
+
* id: string; // fileId
|
|
14
|
+
* blob: Blob; // 文件数据
|
|
15
|
+
* size: number;
|
|
16
|
+
* type: string; // MIME
|
|
17
|
+
* createdAt: string; // ISO 时间
|
|
18
|
+
* }
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const DB_NAME = "nolo-file-storage";
|
|
22
|
+
const STORE_NAME = "files";
|
|
23
|
+
const DB_VERSION = 1;
|
|
24
|
+
|
|
25
|
+
export interface StoredFileRecord {
|
|
26
|
+
id: string;
|
|
27
|
+
blob: Blob;
|
|
28
|
+
size: number;
|
|
29
|
+
type: string;
|
|
30
|
+
createdAt: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let dbPromise: Promise<IDBDatabase> | null = null;
|
|
34
|
+
|
|
35
|
+
const openFileDb = (): Promise<IDBDatabase> => {
|
|
36
|
+
if (dbPromise) return dbPromise;
|
|
37
|
+
|
|
38
|
+
if (typeof indexedDB === "undefined") {
|
|
39
|
+
console.warn(
|
|
40
|
+
"[fileStorage] indexedDB is not available in this environment. File caching is disabled."
|
|
41
|
+
);
|
|
42
|
+
dbPromise = Promise.reject(
|
|
43
|
+
new Error("indexedDB is not available in this environment")
|
|
44
|
+
);
|
|
45
|
+
return dbPromise;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
dbPromise = new Promise((resolve, reject) => {
|
|
49
|
+
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
50
|
+
|
|
51
|
+
request.onerror = () => {
|
|
52
|
+
console.error("[fileStorage] Failed to open IndexedDB:", request.error);
|
|
53
|
+
reject(request.error);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
request.onupgradeneeded = () => {
|
|
57
|
+
const db = request.result;
|
|
58
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
59
|
+
const store = db.createObjectStore(STORE_NAME, {
|
|
60
|
+
keyPath: "id",
|
|
61
|
+
});
|
|
62
|
+
store.createIndex("createdAt", "createdAt", { unique: false });
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
request.onsuccess = () => {
|
|
67
|
+
const db = request.result;
|
|
68
|
+
db.onversionchange = () => {
|
|
69
|
+
db.close();
|
|
70
|
+
console.warn(
|
|
71
|
+
"[fileStorage] IndexedDB version change detected, closing old connection."
|
|
72
|
+
);
|
|
73
|
+
};
|
|
74
|
+
resolve(db);
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return dbPromise;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 将文件 (File/Blob) 存入 IndexedDB
|
|
83
|
+
* - key = fileId
|
|
84
|
+
*/
|
|
85
|
+
export const saveFileToIndexedDb = async (
|
|
86
|
+
fileId: string,
|
|
87
|
+
file: File | Blob
|
|
88
|
+
): Promise<void> => {
|
|
89
|
+
try {
|
|
90
|
+
const db = await openFileDb();
|
|
91
|
+
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
92
|
+
const store = tx.objectStore(STORE_NAME);
|
|
93
|
+
|
|
94
|
+
const blob = file instanceof Blob ? file : new Blob([file]);
|
|
95
|
+
|
|
96
|
+
const record: StoredFileRecord = {
|
|
97
|
+
id: fileId,
|
|
98
|
+
blob,
|
|
99
|
+
size: blob.size,
|
|
100
|
+
type: blob.type || "application/octet-stream",
|
|
101
|
+
createdAt: new Date().toISOString(),
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const request = store.put(record);
|
|
105
|
+
|
|
106
|
+
await new Promise<void>((resolve, reject) => {
|
|
107
|
+
request.onsuccess = () => {
|
|
108
|
+
console.debug(
|
|
109
|
+
"[fileStorage] Saved file to IndexedDB:",
|
|
110
|
+
fileId,
|
|
111
|
+
"size=",
|
|
112
|
+
record.size,
|
|
113
|
+
"type=",
|
|
114
|
+
record.type
|
|
115
|
+
);
|
|
116
|
+
resolve();
|
|
117
|
+
};
|
|
118
|
+
request.onerror = () => {
|
|
119
|
+
console.error(
|
|
120
|
+
"[fileStorage] Failed to save file to IndexedDB:",
|
|
121
|
+
fileId,
|
|
122
|
+
request.error
|
|
123
|
+
);
|
|
124
|
+
reject(request.error);
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
await new Promise<void>((resolve, reject) => {
|
|
129
|
+
tx.oncomplete = () => resolve();
|
|
130
|
+
tx.onerror = () => reject(tx.error);
|
|
131
|
+
tx.onabort = () => reject(tx.error);
|
|
132
|
+
});
|
|
133
|
+
} catch (err) {
|
|
134
|
+
console.warn(
|
|
135
|
+
"[fileStorage] saveFileToIndexedDb error (non-fatal, caching disabled for this file):",
|
|
136
|
+
err
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 从 IndexedDB 中读取文件
|
|
143
|
+
* - key = fileId
|
|
144
|
+
*/
|
|
145
|
+
export const loadFileFromIndexedDb = async (
|
|
146
|
+
fileId: string
|
|
147
|
+
): Promise<StoredFileRecord | null> => {
|
|
148
|
+
try {
|
|
149
|
+
const db = await openFileDb();
|
|
150
|
+
const tx = db.transaction(STORE_NAME, "readonly");
|
|
151
|
+
const store = tx.objectStore(STORE_NAME);
|
|
152
|
+
|
|
153
|
+
const request = store.get(fileId);
|
|
154
|
+
|
|
155
|
+
const record = await new Promise<StoredFileRecord | null>(
|
|
156
|
+
(resolve, reject) => {
|
|
157
|
+
request.onsuccess = () => {
|
|
158
|
+
const result = request.result as StoredFileRecord | undefined;
|
|
159
|
+
if (result) {
|
|
160
|
+
console.debug(
|
|
161
|
+
"[fileStorage] Loaded file from IndexedDB:",
|
|
162
|
+
fileId,
|
|
163
|
+
"size=",
|
|
164
|
+
result.size,
|
|
165
|
+
"type=",
|
|
166
|
+
result.type
|
|
167
|
+
);
|
|
168
|
+
} else {
|
|
169
|
+
console.debug(
|
|
170
|
+
"[fileStorage] No local file found in IndexedDB for id:",
|
|
171
|
+
fileId
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
resolve(result ?? null);
|
|
175
|
+
};
|
|
176
|
+
request.onerror = () => {
|
|
177
|
+
console.error(
|
|
178
|
+
"[fileStorage] Failed to load file from IndexedDB:",
|
|
179
|
+
fileId,
|
|
180
|
+
request.error
|
|
181
|
+
);
|
|
182
|
+
reject(request.error);
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
return record ?? null;
|
|
188
|
+
} catch (err) {
|
|
189
|
+
console.warn(
|
|
190
|
+
"[fileStorage] loadFileFromIndexedDb error, treat as cache miss:",
|
|
191
|
+
err
|
|
192
|
+
);
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* 从 IndexedDB 中删除文件
|
|
199
|
+
* - key = fileId
|
|
200
|
+
*/
|
|
201
|
+
export const deleteFileFromIndexedDb = async (fileId: string): Promise<void> => {
|
|
202
|
+
try {
|
|
203
|
+
const db = await openFileDb();
|
|
204
|
+
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
205
|
+
const store = tx.objectStore(STORE_NAME);
|
|
206
|
+
|
|
207
|
+
const request = store.delete(fileId);
|
|
208
|
+
|
|
209
|
+
await new Promise<void>((resolve, reject) => {
|
|
210
|
+
request.onsuccess = () => {
|
|
211
|
+
console.debug("[fileStorage] Deleted file from IndexedDB:", fileId);
|
|
212
|
+
resolve();
|
|
213
|
+
};
|
|
214
|
+
request.onerror = () => {
|
|
215
|
+
console.error(
|
|
216
|
+
"[fileStorage] Failed to delete file from IndexedDB:",
|
|
217
|
+
fileId,
|
|
218
|
+
request.error
|
|
219
|
+
);
|
|
220
|
+
reject(request.error);
|
|
221
|
+
};
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
await new Promise<void>((resolve, reject) => {
|
|
225
|
+
tx.oncomplete = () => resolve();
|
|
226
|
+
tx.onerror = () => reject(tx.error);
|
|
227
|
+
tx.onabort = () => reject(tx.error);
|
|
228
|
+
});
|
|
229
|
+
} catch (err) {
|
|
230
|
+
console.warn("[fileStorage] deleteFileFromIndexedDb error:", err);
|
|
231
|
+
}
|
|
232
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { API_ENDPOINTS } from "./config";
|
|
2
|
+
|
|
3
|
+
const normalizeServerOrigin = (
|
|
4
|
+
serverOrigin: string | undefined | null
|
|
5
|
+
): string => {
|
|
6
|
+
if (typeof serverOrigin !== "string") return "";
|
|
7
|
+
return serverOrigin.trim().replace(/\/+$/, "");
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const normalizeFileId = (fileId: string | undefined | null): string => {
|
|
11
|
+
if (typeof fileId !== "string") return "";
|
|
12
|
+
return fileId.trim();
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const buildDatabaseFileContentUrl = (
|
|
16
|
+
serverOrigin: string | undefined | null,
|
|
17
|
+
fileId: string | undefined | null
|
|
18
|
+
): string | null => {
|
|
19
|
+
const normalizedServer = normalizeServerOrigin(serverOrigin);
|
|
20
|
+
const normalizedFileId = normalizeFileId(fileId);
|
|
21
|
+
|
|
22
|
+
if (!normalizedServer || !normalizedFileId) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return `${normalizedServer}${API_ENDPOINTS.DATABASE}/file/content/${normalizedFileId}`;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const isLocalDatabaseFileContentUrl = (
|
|
30
|
+
url: string | null | undefined
|
|
31
|
+
): boolean => {
|
|
32
|
+
if (typeof url !== "string" || !url) return false;
|
|
33
|
+
return url.includes("localhost") || url.includes("127.0.0.1");
|
|
34
|
+
};
|