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,126 @@
|
|
|
1
|
+
// 文件路径: database/actions/write.ts
|
|
2
|
+
|
|
3
|
+
import type { AppThunkApi } from "app/store";
|
|
4
|
+
import { getRuntimeServerContext } from "database/runtimeServerContext";
|
|
5
|
+
import { DataType } from "create/types";
|
|
6
|
+
import { normalizeTimeFields, logger } from "./common";
|
|
7
|
+
import {
|
|
8
|
+
resolveReplicationServers,
|
|
9
|
+
scheduleWriteReplication,
|
|
10
|
+
} from "./replication";
|
|
11
|
+
import { toast } from "app/utils/toast";
|
|
12
|
+
|
|
13
|
+
// 辅助函数:保存到本地 DB
|
|
14
|
+
const saveToClientDb = async (
|
|
15
|
+
clientDb: any,
|
|
16
|
+
dbKey: string,
|
|
17
|
+
data: any
|
|
18
|
+
): Promise<void> => {
|
|
19
|
+
if (!clientDb) {
|
|
20
|
+
logger.error({ dbKey }, "Client database is undefined in saveToClientDb");
|
|
21
|
+
throw new Error("Client database instance is required");
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
await clientDb.put(dbKey, data);
|
|
25
|
+
logger.debug({ dbKey }, "Data saved successfully to local database.");
|
|
26
|
+
} catch (err: any) {
|
|
27
|
+
logger.error({ err, dbKey }, "Failed to save data to local database");
|
|
28
|
+
throw new Error(`Local database put failed for ${dbKey}: ${err.message}`);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Write Action: 写入新数据项。
|
|
34
|
+
* 1. 验证数据类型。
|
|
35
|
+
* 2. 规范化数据(添加时间戳、dbKey、userId)。
|
|
36
|
+
* 3. 保存数据到本地数据库。
|
|
37
|
+
* 4. 若在线,异步将完整数据写入所有服务器。
|
|
38
|
+
*/
|
|
39
|
+
export const writeAction = async (
|
|
40
|
+
writeConfig: { data: any; customKey: string; userId?: string },
|
|
41
|
+
thunkApi: AppThunkApi
|
|
42
|
+
): Promise<any> => {
|
|
43
|
+
const { db: clientDb } = thunkApi.extra;
|
|
44
|
+
if (!clientDb) {
|
|
45
|
+
throw new Error("Client database instance is required in writeAction");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const state = thunkApi.getState();
|
|
49
|
+
const { currentServer, syncServers, currentUserId } =
|
|
50
|
+
getRuntimeServerContext(state);
|
|
51
|
+
|
|
52
|
+
const { data, customKey } = writeConfig;
|
|
53
|
+
const userId = writeConfig.userId || currentUserId;
|
|
54
|
+
|
|
55
|
+
// 1. 基础参数校验
|
|
56
|
+
if (!data || !customKey) {
|
|
57
|
+
const errorMsg =
|
|
58
|
+
"Invalid arguments for writeAction: data and customKey are required.";
|
|
59
|
+
logger.error(errorMsg, { writeConfig });
|
|
60
|
+
toast.error(errorMsg);
|
|
61
|
+
throw new Error(errorMsg);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 2. 类型校验(保持原有行为:非法类型只告警,不阻塞)
|
|
65
|
+
const VALID_TYPES = [
|
|
66
|
+
DataType.MSG,
|
|
67
|
+
DataType.CYBOT,
|
|
68
|
+
DataType.DOC,
|
|
69
|
+
DataType.DIALOG,
|
|
70
|
+
DataType.NOTIFICATION,
|
|
71
|
+
DataType.TOKEN,
|
|
72
|
+
DataType.TRANSACTION,
|
|
73
|
+
DataType.SPACE,
|
|
74
|
+
DataType.SETTING,
|
|
75
|
+
DataType.TABLE,
|
|
76
|
+
DataType.TABLE_ROW,
|
|
77
|
+
DataType.EMAIL,
|
|
78
|
+
];
|
|
79
|
+
if (!data.type || !VALID_TYPES.includes(data.type)) {
|
|
80
|
+
logger.warn(
|
|
81
|
+
`Invalid data type "${data.type}" for writeAction with key ${customKey}. Proceeding anyway.`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
// 3. 规范化数据(时间字段 / dbKey / userId)
|
|
87
|
+
const willSaveData = normalizeTimeFields({
|
|
88
|
+
...data,
|
|
89
|
+
dbKey: customKey,
|
|
90
|
+
userId,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// 4. 本地保存
|
|
94
|
+
await saveToClientDb(clientDb, customKey, willSaveData);
|
|
95
|
+
|
|
96
|
+
// 5. 计算远程服务器列表(currentServer + syncServers,带去重 + 离线判断)
|
|
97
|
+
const servers = resolveReplicationServers(currentServer, syncServers);
|
|
98
|
+
|
|
99
|
+
const serverWriteConfig = {
|
|
100
|
+
data: willSaveData,
|
|
101
|
+
customKey,
|
|
102
|
+
userId,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// 6. 后台异步同步到远程(若在线且有可用服务器)
|
|
106
|
+
if (servers.length > 0) {
|
|
107
|
+
logger.debug(
|
|
108
|
+
`[writeAction] Initiating background sync for key: ${customKey} to ${servers.length} servers.`
|
|
109
|
+
);
|
|
110
|
+
scheduleWriteReplication(servers, serverWriteConfig, state);
|
|
111
|
+
} else {
|
|
112
|
+
logger.warn(
|
|
113
|
+
"[writeAction] No available servers, data only saved locally.",
|
|
114
|
+
{ customKey }
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return willSaveData;
|
|
119
|
+
} catch (error: any) {
|
|
120
|
+
const errorMessage = `Write action failed for ${customKey}: ${error?.message || "Unknown error"
|
|
121
|
+
}`;
|
|
122
|
+
logger.error("[writeAction] Error:", error);
|
|
123
|
+
toast.error(`Failed to save data for ${customKey}.`);
|
|
124
|
+
throw new Error(errorMessage);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// database/client/db.native.ts
|
|
2
|
+
// React Native 版数据库 - 使用 @nolo/leveldb (monorepo 内部包)
|
|
3
|
+
|
|
4
|
+
import Level from "@nolo/leveldb";
|
|
5
|
+
|
|
6
|
+
// 扩展全局对象类型
|
|
7
|
+
declare global {
|
|
8
|
+
var noloDbInstance: Level | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 获取或创建数据库实例
|
|
13
|
+
* 使用全局变量存储实例,防止 Fast Refresh 导致模块重载时丢失引用而重复打开数据库
|
|
14
|
+
*/
|
|
15
|
+
export function getDb(): Level {
|
|
16
|
+
if (!global.noloDbInstance) {
|
|
17
|
+
console.log('[DB Native] Creating new LevelDB instance');
|
|
18
|
+
try {
|
|
19
|
+
global.noloDbInstance = new Level("nolo", { valueEncoding: "json" });
|
|
20
|
+
} catch (e: any) {
|
|
21
|
+
console.error('[DB Native] Failed to open database:', e?.message ?? e);
|
|
22
|
+
throw new Error(
|
|
23
|
+
`[DB Native] 数据库启动失败,请尝试重启应用。原因: ${e?.message ?? '未知错误'}`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return global.noloDbInstance;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 创建数据库实例(用于 store 初始化)
|
|
32
|
+
*/
|
|
33
|
+
export function createDb(): Level {
|
|
34
|
+
return getDb();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 关闭当前数据库实例
|
|
39
|
+
*/
|
|
40
|
+
export async function closeDb(): Promise<void> {
|
|
41
|
+
if (global.noloDbInstance) {
|
|
42
|
+
try {
|
|
43
|
+
await global.noloDbInstance.close();
|
|
44
|
+
console.log('[DB Native] Database closed');
|
|
45
|
+
} catch (e) {
|
|
46
|
+
console.warn('[DB Native] Error closing database:', e);
|
|
47
|
+
}
|
|
48
|
+
global.noloDbInstance = null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 关闭所有数据库(用于热重载清理)
|
|
54
|
+
*/
|
|
55
|
+
export async function closeAllDatabases(): Promise<void> {
|
|
56
|
+
try {
|
|
57
|
+
global.noloDbInstance = null;
|
|
58
|
+
await Level.closeAll();
|
|
59
|
+
console.log('[DB Native] All databases closed');
|
|
60
|
+
} catch (e) {
|
|
61
|
+
console.warn('[DB Native] Failed to close all databases:', e);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 检查是否为 React Native 环境
|
|
67
|
+
*/
|
|
68
|
+
export const isNative = true;
|
|
69
|
+
|
|
70
|
+
// 导出数据库实例(兼容旧代码)
|
|
71
|
+
export const browserDb = getDb();
|
|
72
|
+
|
|
73
|
+
export default Level;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// database/client/db.ts
|
|
2
|
+
// Web 版数据库 - 使用 level 包 (基于 IndexedDB)
|
|
3
|
+
|
|
4
|
+
import { Level } from "level";
|
|
5
|
+
|
|
6
|
+
// 数据库单例
|
|
7
|
+
let dbInstance: Level<string, any> | null = null;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 获取或创建数据库实例
|
|
11
|
+
*/
|
|
12
|
+
export function getDb(): Level<string, any> {
|
|
13
|
+
if (!dbInstance) {
|
|
14
|
+
dbInstance = new Level("nolo", { valueEncoding: "json" });
|
|
15
|
+
}
|
|
16
|
+
return dbInstance;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 创建数据库实例(用于 store 初始化)
|
|
21
|
+
*/
|
|
22
|
+
export function createDb(): Level<string, any> {
|
|
23
|
+
return getDb();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 关闭数据库
|
|
28
|
+
*/
|
|
29
|
+
export async function closeDb(): Promise<void> {
|
|
30
|
+
if (dbInstance) {
|
|
31
|
+
await dbInstance.close();
|
|
32
|
+
dbInstance = null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 关闭所有数据库(Web 版只有一个实例)
|
|
38
|
+
*/
|
|
39
|
+
export async function closeAllDatabases(): Promise<void> {
|
|
40
|
+
await closeDb();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 检查是否为 React Native 环境
|
|
45
|
+
*/
|
|
46
|
+
export const isNative = false;
|
|
47
|
+
|
|
48
|
+
// 导出数据库实例(兼容旧代码)
|
|
49
|
+
export const browserDb = getDb();
|
|
50
|
+
|
|
51
|
+
export default Level;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { getUserDataPrefixes } from "../queryPrefixes";
|
|
2
|
+
|
|
3
|
+
interface FetchUserDataOptions {
|
|
4
|
+
includeDeleted?: boolean;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const attachQueriedKey = (key: string, value: any) => {
|
|
8
|
+
if (!value || typeof value !== "object") return value;
|
|
9
|
+
if (
|
|
10
|
+
typeof value.dbKey === "string" && value.dbKey.trim().length > 0
|
|
11
|
+
) {
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
...value,
|
|
16
|
+
dbKey: key,
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// 支持单类型或多类型查询
|
|
21
|
+
// db should be passed from caller (e.g. thunk extra)
|
|
22
|
+
export async function fetchUserData(
|
|
23
|
+
db: any,
|
|
24
|
+
types: string | string[],
|
|
25
|
+
userId: string,
|
|
26
|
+
options: FetchUserDataOptions = {}
|
|
27
|
+
) {
|
|
28
|
+
const results: Record<string, any[]> = {};
|
|
29
|
+
const typeArray = Array.isArray(types) ? types : [types];
|
|
30
|
+
const includeDeleted = options.includeDeleted === true;
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
for (const type of typeArray) {
|
|
34
|
+
const prefixes = getUserDataPrefixes(type, userId);
|
|
35
|
+
results[type] = [];
|
|
36
|
+
for (const prefix of prefixes) {
|
|
37
|
+
let iterator = db.iterator({
|
|
38
|
+
gte: prefix,
|
|
39
|
+
lte: `${prefix}\uffff`,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (iterator && typeof iterator.then === "function") {
|
|
43
|
+
iterator = await iterator;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// @ts-ignore - iterator compatibility
|
|
47
|
+
for await (const [key, value] of iterator) {
|
|
48
|
+
const hydrated = attachQueriedKey(String(key), value);
|
|
49
|
+
if (!hydrated) continue;
|
|
50
|
+
if (!includeDeleted && hydrated.deletedAt) continue;
|
|
51
|
+
results[type].push(hydrated);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return Array.isArray(types) ? results : results[types] ?? [];
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error("Query error:", error);
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import i18n from "app/i18n";
|
|
2
|
+
export const handleError = (error, handleUnauthorized?) => {
|
|
3
|
+
let message;
|
|
4
|
+
switch (error.message) {
|
|
5
|
+
case "400":
|
|
6
|
+
message = i18n.t("errors.validationError");
|
|
7
|
+
break;
|
|
8
|
+
case "401":
|
|
9
|
+
message = i18n.t("errors.unauthorized");
|
|
10
|
+
handleUnauthorized && handleUnauthorized();
|
|
11
|
+
break;
|
|
12
|
+
case "500":
|
|
13
|
+
default:
|
|
14
|
+
message = i18n.t("errors.serverError");
|
|
15
|
+
break;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return message;
|
|
19
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { API_ENDPOINTS } from "database/config";
|
|
2
|
+
|
|
3
|
+
export const noloQueryRequest = async (queryConfig: any) => {
|
|
4
|
+
const { server } = queryConfig;
|
|
5
|
+
const { queryUserId, options } = queryConfig;
|
|
6
|
+
|
|
7
|
+
const queryParams = new URLSearchParams({
|
|
8
|
+
limit: options.limit?.toString() ?? "",
|
|
9
|
+
});
|
|
10
|
+
const url = `${API_ENDPOINTS.DATABASE}/query/${queryUserId}?${queryParams}`;
|
|
11
|
+
const fullUrl = server + url;
|
|
12
|
+
let headers = {
|
|
13
|
+
"Content-Type": "application/json",
|
|
14
|
+
};
|
|
15
|
+
const body = JSON.stringify(options.condition);
|
|
16
|
+
return fetch(fullUrl, {
|
|
17
|
+
method: "POST",
|
|
18
|
+
headers,
|
|
19
|
+
body,
|
|
20
|
+
});
|
|
21
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// packages/database/config.ts
|
|
2
|
+
|
|
3
|
+
// 这个文件现在是前后端通用的,不包含任何后端模块如 'fs' 或 'path'。
|
|
4
|
+
export const API_VERSION = "/api/v1";
|
|
5
|
+
export const SERVERS = {
|
|
6
|
+
MAIN: "https://nolo.chat",
|
|
7
|
+
US: "https://us.nolo.chat",
|
|
8
|
+
} as const;
|
|
9
|
+
export const NOLO_CLUSTER_SERVERS = Object.values(SERVERS);
|
|
10
|
+
|
|
11
|
+
export const API_ENDPOINTS = {
|
|
12
|
+
DATABASE: `${API_VERSION}/db`,
|
|
13
|
+
SHARE: `${API_VERSION}/share`,
|
|
14
|
+
USERS: `${API_VERSION}/users`,
|
|
15
|
+
WEATHER: `${API_VERSION}/weather`,
|
|
16
|
+
HI: `${API_VERSION}/hi`,
|
|
17
|
+
CHAT: `${API_VERSION}/chat`,
|
|
18
|
+
EXECUTE_SQL: `${API_VERSION}/sqlite/execute_sql`,
|
|
19
|
+
// --- 新增端点 ---
|
|
20
|
+
TRANSACTIONS: `${API_VERSION}/transactions`,
|
|
21
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { readAndWait, patch, upsert, write } from "./dbSlice";
|
|
@@ -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
|
+
// }
|