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.
Files changed (323) hide show
  1. package/README.md +54 -2
  2. package/agent-runtime/hostAdapter.ts +53 -0
  3. package/agent-runtime/index.ts +28 -0
  4. package/agent-runtime/localLoop.ts +62 -0
  5. package/agent-runtime/runtimeDecision.ts +70 -0
  6. package/agent-runtime/types.ts +87 -0
  7. package/agentRuntimeCommands.ts +141 -22
  8. package/agentRuntimeLocal.ts +7 -0
  9. package/ai/agent/_executeModel.ts +118 -0
  10. package/ai/agent/agentSlice.ts +545 -0
  11. package/ai/agent/appWorkingMemory.ts +126 -0
  12. package/ai/agent/avatarUtils.ts +24 -0
  13. package/ai/agent/buildEditingContext.ts +373 -0
  14. package/ai/agent/buildSystemPrompt.ts +532 -0
  15. package/ai/agent/cleanAgentMessages.ts +140 -0
  16. package/ai/agent/cliChatClient.ts +119 -0
  17. package/ai/agent/contextCompiler.ts +107 -0
  18. package/ai/agent/contextLayerContract.ts +44 -0
  19. package/ai/agent/createAgentSchema.ts +234 -0
  20. package/ai/agent/executeToolCall.ts +58 -0
  21. package/ai/agent/fetchAgentContexts.ts +42 -0
  22. package/ai/agent/generatePrompt.ts +3 -0
  23. package/ai/agent/getFullChatContextKeys.ts +168 -0
  24. package/ai/agent/hooks/fetchPublicAgents.ts +133 -0
  25. package/ai/agent/hooks/useAgentConfig.ts +61 -0
  26. package/ai/agent/hooks/useAgentDialog.ts +35 -0
  27. package/ai/agent/hooks/useAgentFormValidation.ts +202 -0
  28. package/ai/agent/hooks/usePublicAgents.ts +473 -0
  29. package/ai/agent/persistMessageWithFixedId.ts +37 -0
  30. package/ai/agent/planSlice.ts +259 -0
  31. package/ai/agent/referenceUtils.ts +229 -0
  32. package/ai/agent/runAgentBackground.ts +238 -0
  33. package/ai/agent/runAgentClientLoop.ts +138 -0
  34. package/ai/agent/runtimeGuidance.ts +97 -0
  35. package/ai/agent/runtimeServerBase.ts +37 -0
  36. package/ai/agent/server/fetchPublicAgents.ts +128 -0
  37. package/ai/agent/startParallelAgentStreams.ts +424 -0
  38. package/ai/agent/startupProtocol.ts +53 -0
  39. package/ai/agent/streamAgentChatTurn.ts +1299 -0
  40. package/ai/agent/streamAgentChatTurnUtils.ts +738 -0
  41. package/ai/agent/types.ts +71 -0
  42. package/ai/agent/utils/imageOutput.ts +39 -0
  43. package/ai/agent/utils/publicImageAgentMode.ts +26 -0
  44. package/ai/agent/utils/sortUtils.ts +250 -0
  45. package/ai/agent/web/referencePickerUtils.ts +146 -0
  46. package/ai/agent.ts +2 -0
  47. package/ai/ai.locale.ts +1083 -0
  48. package/ai/chat/accumulateToolCallChunks.ts +95 -0
  49. package/ai/chat/fetchUtils.native.ts +276 -0
  50. package/ai/chat/fetchUtils.ts +153 -0
  51. package/ai/chat/inlineImageUrlsForCustomProvider.ts +117 -0
  52. package/ai/chat/parseApiError.ts +64 -0
  53. package/ai/chat/parseMultilineSSE.ts +95 -0
  54. package/ai/chat/sendOpenAICompletionsRequest.native.ts +682 -0
  55. package/ai/chat/sendOpenAICompletionsRequest.ts +712 -0
  56. package/ai/chat/sendOpenAIResponseRequest.ts +512 -0
  57. package/ai/chat/shouldUseServerProxy.ts +18 -0
  58. package/ai/chat/sseClient.native.ts +91 -0
  59. package/ai/chat/sseClient.ts +67 -0
  60. package/ai/chat/streamReader.native.ts +31 -0
  61. package/ai/chat/streamReader.ts +62 -0
  62. package/ai/chat/updateTotalUsage.ts +72 -0
  63. package/ai/context/buildReferenceContext.ts +437 -0
  64. package/ai/context/calculateContextUsage.ts +133 -0
  65. package/ai/context/retention.ts +165 -0
  66. package/ai/context/tokenUtils.ts +78 -0
  67. package/ai/index.ts +1 -0
  68. package/ai/llm/agentCapabilities.ts +74 -0
  69. package/ai/llm/calculateGeminiImageTokens.ts +57 -0
  70. package/ai/llm/deepinfra.ts +28 -0
  71. package/ai/llm/fireworks.ts +68 -0
  72. package/ai/llm/generateRequestBody.ts +165 -0
  73. package/ai/llm/getModelContextWindow.ts +84 -0
  74. package/ai/llm/getNoloKey.ts +37 -0
  75. package/ai/llm/getPricing.ts +232 -0
  76. package/ai/llm/hooks/useModelPricing.ts +75 -0
  77. package/ai/llm/imagePricing.ts +66 -0
  78. package/ai/llm/isResponseAPIModel.ts +13 -0
  79. package/ai/llm/kimi.ts +18 -0
  80. package/ai/llm/mimo.ts +71 -0
  81. package/ai/llm/mistral.ts +22 -0
  82. package/ai/llm/modelAvatar.ts +427 -0
  83. package/ai/llm/models.ts +45 -0
  84. package/ai/llm/openrouterModels.ts +141 -0
  85. package/ai/llm/providers.ts +307 -0
  86. package/ai/llm/reasoningModels.ts +28 -0
  87. package/ai/llm/types.ts +59 -0
  88. package/ai/llm/usageRequestOptions.ts +59 -0
  89. package/ai/memory/capture.ts +148 -0
  90. package/ai/memory/consolidate.ts +104 -0
  91. package/ai/memory/delete.ts +147 -0
  92. package/ai/memory/overlay.ts +84 -0
  93. package/ai/memory/query.ts +38 -0
  94. package/ai/memory/queryShared.ts +160 -0
  95. package/ai/memory/rank.ts +105 -0
  96. package/ai/memory/recentRelationshipRecap.ts +247 -0
  97. package/ai/memory/remember.ts +167 -0
  98. package/ai/memory/runtime.ts +76 -0
  99. package/ai/memory/store.ts +20 -0
  100. package/ai/memory/storeShared.ts +76 -0
  101. package/ai/memory/types.ts +46 -0
  102. package/ai/memory/understanding.ts +349 -0
  103. package/ai/memory/understandingGreeting.ts +264 -0
  104. package/ai/messages/type.ts +20 -0
  105. package/ai/policy/personalizationDialog.ts +333 -0
  106. package/ai/policy/runtimePolicy.ts +440 -0
  107. package/ai/policy/selfUpdateFields.ts +48 -0
  108. package/ai/policy/types.ts +64 -0
  109. package/ai/skills/referenceRuntime.ts +274 -0
  110. package/ai/skills/skillDiagnostics.ts +251 -0
  111. package/ai/skills/skillDocBuilder.ts +139 -0
  112. package/ai/skills/skillDocProtocol.ts +434 -0
  113. package/ai/skills/skillReferenceSummary.ts +63 -0
  114. package/ai/skills/skillSummaryMarker.ts +26 -0
  115. package/ai/token/calculatePrice.ts +546 -0
  116. package/ai/token/db.ts +98 -0
  117. package/ai/token/externalToolCost.ts +321 -0
  118. package/ai/token/hooks/useRecords.ts +65 -0
  119. package/ai/token/missingUsageEstimate.ts +42 -0
  120. package/ai/token/modelUsageQuery.ts +252 -0
  121. package/ai/token/normalizeUsage.ts +84 -0
  122. package/ai/token/openaiImageGenerationUsage.ts +56 -0
  123. package/ai/token/prepareTokenUsageData.ts +88 -0
  124. package/ai/token/query.ts +88 -0
  125. package/ai/token/queryUserTokens.ts +59 -0
  126. package/ai/token/resolveBillingTarget.ts +52 -0
  127. package/ai/token/saveTokenRecord.ts +53 -0
  128. package/ai/token/serverDialogProjection.ts +78 -0
  129. package/ai/token/serverTokenWriter.ts +143 -0
  130. package/ai/token/stats.ts +21 -0
  131. package/ai/token/tokenThunks.ts +24 -0
  132. package/ai/token/types.ts +93 -0
  133. package/ai/tools/agent/agentTools.ts +176 -0
  134. package/ai/tools/agent/agentUpdateShared.ts +311 -0
  135. package/ai/tools/agent/callAgentTool.ts +139 -0
  136. package/ai/tools/agent/createAgentTool.ts +512 -0
  137. package/ai/tools/agent/createDialogTool.ts +69 -0
  138. package/ai/tools/agent/createSkillAgentTool.ts +62 -0
  139. package/ai/tools/agent/parallelBudget.ts +221 -0
  140. package/ai/tools/agent/presets/appBuilderPreset.ts +147 -0
  141. package/ai/tools/agent/runLlmTool.ts +96 -0
  142. package/ai/tools/agent/runStreamingAgentTool.ts +73 -0
  143. package/ai/tools/agent/skillAgentArgs.ts +106 -0
  144. package/ai/tools/agent/skillAgentPreset.ts +89 -0
  145. package/ai/tools/agent/streamParallelAgentsTool.ts +122 -0
  146. package/ai/tools/agent/updateAgentTool.ts +96 -0
  147. package/ai/tools/agent/updateSelfTool.ts +113 -0
  148. package/ai/tools/amazonProductScraperTool.ts +86 -0
  149. package/ai/tools/apifyActorClient.ts +45 -0
  150. package/ai/tools/appEditGuard.ts +372 -0
  151. package/ai/tools/appReadSnapshot.ts +153 -0
  152. package/ai/tools/appTools.ts +1549 -0
  153. package/ai/tools/applyEditTool.ts +256 -0
  154. package/ai/tools/applyLineEditsTool.ts +312 -0
  155. package/ai/tools/browserTools/click.ts +33 -0
  156. package/ai/tools/browserTools/closeSession.ts +29 -0
  157. package/ai/tools/browserTools/common.ts +27 -0
  158. package/ai/tools/browserTools/openSession.ts +48 -0
  159. package/ai/tools/browserTools/readContent.ts +38 -0
  160. package/ai/tools/browserTools/selectOption.ts +46 -0
  161. package/ai/tools/browserTools/typeText.ts +42 -0
  162. package/ai/tools/category/createCategoryTool.ts +66 -0
  163. package/ai/tools/category/queryContentsByCategoryTool.ts +69 -0
  164. package/ai/tools/category/updateContentCategoryTool.ts +75 -0
  165. package/ai/tools/cfBrowserTools.ts +319 -0
  166. package/ai/tools/cfSpeechToTextTool.ts +49 -0
  167. package/ai/tools/checkEnvTool.ts +65 -0
  168. package/ai/tools/cloudflareCrawlTool.ts +289 -0
  169. package/ai/tools/codeSearchTool.ts +111 -0
  170. package/ai/tools/codeTools.ts +101 -0
  171. package/ai/tools/createDocTool.ts +132 -0
  172. package/ai/tools/createPlanTool.ts +999 -0
  173. package/ai/tools/createSkillDocTool.ts +155 -0
  174. package/ai/tools/createWorkflowTool.ts +154 -0
  175. package/ai/tools/deepseekOcrTool.ts +34 -0
  176. package/ai/tools/delayTool.ts +31 -0
  177. package/ai/tools/deleteSpacesTool.ts +325 -0
  178. package/ai/tools/deleteSpacesToolModel.ts +159 -0
  179. package/ai/tools/devReloadUtils.ts +29 -0
  180. package/ai/tools/dialogMessageSearch.ts +137 -0
  181. package/ai/tools/doctorSkillTool.ts +72 -0
  182. package/ai/tools/ecommerceScraperTool.ts +86 -0
  183. package/ai/tools/emailTools.ts +549 -0
  184. package/ai/tools/evalSkillTool.ts +92 -0
  185. package/ai/tools/exaSearchTool.ts +64 -0
  186. package/ai/tools/execBashTool.ts +379 -0
  187. package/ai/tools/executeSqlTool.ts +192 -0
  188. package/ai/tools/fetchWebpageSupport.ts +309 -0
  189. package/ai/tools/fetchWebpageTool.ts +84 -0
  190. package/ai/tools/geminiImagePreviewTool.ts +361 -0
  191. package/ai/tools/generateDocxTool.ts +215 -0
  192. package/ai/tools/googleSearchScraperTool.ts +106 -0
  193. package/ai/tools/importDataTool.ts +133 -0
  194. package/ai/tools/importSkillTool.ts +162 -0
  195. package/ai/tools/index.ts +1927 -0
  196. package/ai/tools/listFilesTool.ts +82 -0
  197. package/ai/tools/listUserSpacesTool.ts +113 -0
  198. package/ai/tools/modelUsageTools.ts +199 -0
  199. package/ai/tools/olmOcrTool.ts +34 -0
  200. package/ai/tools/openaiImageTool.ts +267 -0
  201. package/ai/tools/prepareTools.ts +23 -0
  202. package/ai/tools/readDocTool.ts +84 -0
  203. package/ai/tools/readFileTool.ts +211 -0
  204. package/ai/tools/readTool.ts +163 -0
  205. package/ai/tools/readXPostTool.ts +233 -0
  206. package/ai/tools/rememberMemoryTool.ts +84 -0
  207. package/ai/tools/remotionVideoTool.ts +151 -0
  208. package/ai/tools/searchDialogMessagesTool.ts +222 -0
  209. package/ai/tools/searchRepoTool.ts +115 -0
  210. package/ai/tools/searchWorkspaceTool.ts +259 -0
  211. package/ai/tools/skillFollowup.ts +86 -0
  212. package/ai/tools/surfWeatherTool.ts +169 -0
  213. package/ai/tools/table/addTableRowTool.ts +217 -0
  214. package/ai/tools/table/createTableTool.ts +315 -0
  215. package/ai/tools/table/rowTools.ts +366 -0
  216. package/ai/tools/table/schemaTools.ts +244 -0
  217. package/ai/tools/table/shareTableTool.ts +148 -0
  218. package/ai/tools/table/toolShared.ts +129 -0
  219. package/ai/tools/toolApiClient.ts +198 -0
  220. package/ai/tools/toolNameAliases.ts +57 -0
  221. package/ai/tools/toolResultError.ts +42 -0
  222. package/ai/tools/toolRunSlice.ts +303 -0
  223. package/ai/tools/toolSchemaCompatibility.ts +53 -0
  224. package/ai/tools/toolVisibility.ts +4 -0
  225. package/ai/tools/types.ts +20 -0
  226. package/ai/tools/uiAskChoiceTool.ts +104 -0
  227. package/ai/tools/updateContentTitleTool.ts +84 -0
  228. package/ai/tools/updateDocTool.ts +105 -0
  229. package/ai/tools/updateUserPreferenceProfileTool.ts +145 -0
  230. package/ai/tools/whisperTool.ts +77 -0
  231. package/ai/tools/writeFileTool.ts +210 -0
  232. package/ai/tools/youtubeScraperTool.ts +116 -0
  233. package/ai/tools/ziweiChartTool.ts +678 -0
  234. package/ai/types.ts +55 -0
  235. package/ai/workflow/workflowExecutor.ts +323 -0
  236. package/ai/workflow/workflowSlice.ts +73 -0
  237. package/ai/workflow/workflowTypes.ts +106 -0
  238. package/authCommands.ts +185 -21
  239. package/client/agentRun.test.ts +240 -0
  240. package/client/agentRun.ts +182 -19
  241. package/client/compactDialog.test.ts +238 -0
  242. package/client/compactDialog.ts +5 -2
  243. package/client/localRuntimeAdapter.test.ts +135 -0
  244. package/client/localRuntimeAdapter.ts +244 -0
  245. package/client/profileConfig.test.ts +40 -0
  246. package/client/streamingOutput.test.ts +22 -0
  247. package/client/streamingOutput.ts +38 -0
  248. package/commandRegistry.ts +11 -2
  249. package/connector-experimental/index.ts +5 -0
  250. package/database/actions/cacheMergedUserData.ts +64 -0
  251. package/database/actions/common.ts +242 -0
  252. package/database/actions/deleteFile.ts +40 -0
  253. package/database/actions/fetchUserData.ts +16 -0
  254. package/database/actions/fileContent.ts +125 -0
  255. package/database/actions/patch.ts +155 -0
  256. package/database/actions/read.ts +337 -0
  257. package/database/actions/readAndWait.ts +224 -0
  258. package/database/actions/readRequestManager.ts +120 -0
  259. package/database/actions/remove.ts +94 -0
  260. package/database/actions/replication.ts +366 -0
  261. package/database/actions/upload.ts +174 -0
  262. package/database/actions/upsert.ts +56 -0
  263. package/database/actions/write.ts +126 -0
  264. package/database/client/db.native.ts +73 -0
  265. package/database/client/db.ts +51 -0
  266. package/database/client/fetchUserData.ts +61 -0
  267. package/database/client/handleError.ts +19 -0
  268. package/database/client/queryRequest.ts +21 -0
  269. package/database/config.ts +21 -0
  270. package/database/dbActionThunks.ts +1 -0
  271. package/database/dbSlice.ts +149 -0
  272. package/database/email.ts +42 -0
  273. package/database/fileRing.ts +51 -0
  274. package/database/fileSharding.ts +70 -0
  275. package/database/fileStorage.native.ts +92 -0
  276. package/database/fileStorage.ts +232 -0
  277. package/database/fileUrl.ts +34 -0
  278. package/database/hooks/useUserData.ts +489 -0
  279. package/database/index.ts +1 -0
  280. package/database/keys.ts +765 -0
  281. package/database/queryPrefixes.ts +14 -0
  282. package/database/requests.ts +443 -0
  283. package/database/runtimeServerContext.ts +35 -0
  284. package/database/server/MemoryDB.ts +76 -0
  285. package/database/server/actorAccess.ts +76 -0
  286. package/database/server/agentDelegation.ts +124 -0
  287. package/database/server/coreDataOwnership.ts +13 -0
  288. package/database/server/coreDataProxy.ts +76 -0
  289. package/database/server/cybotReadonly.ts +18 -0
  290. package/database/server/dataHandlers.ts +111 -0
  291. package/database/server/db.ts +118 -0
  292. package/database/server/dbPath.ts +20 -0
  293. package/database/server/delete.ts +499 -0
  294. package/database/server/emailRepository.ts +1480 -0
  295. package/database/server/ensureDbOpen.ts +12 -0
  296. package/database/server/fileRead.ts +337 -0
  297. package/database/server/fileService.ts +436 -0
  298. package/database/server/handleTransaction.ts +86 -0
  299. package/database/server/patch.ts +282 -0
  300. package/database/server/query.ts +138 -0
  301. package/database/server/read.ts +325 -0
  302. package/database/server/resourceAccess.ts +211 -0
  303. package/database/server/routes.ts +110 -0
  304. package/database/server/spaceMemberAuthority.ts +67 -0
  305. package/database/server/upload.ts +159 -0
  306. package/database/server/write.ts +494 -0
  307. package/database/server/writeAuthority.ts +133 -0
  308. package/database/sqliteDb.ts +46 -0
  309. package/database/table/deleteTable.ts +120 -0
  310. package/database/tenantPlacement.ts +57 -0
  311. package/database/tombstones.ts +52 -0
  312. package/database/userDataLoadDecision.ts +17 -0
  313. package/database/userDataMerge.ts +95 -0
  314. package/database/userPreferenceRegister.ts +108 -0
  315. package/database/utils/dbPath.ts +47 -0
  316. package/database/utils/ulid.native.ts +6 -0
  317. package/database/utils/ulid.ts +1 -0
  318. package/index.ts +25 -15
  319. package/localRuntimeDb.ts +28 -0
  320. package/package.json +16 -4
  321. package/runtimeModeArgs.ts +33 -0
  322. package/tui/readlineWorkspace.ts +1 -0
  323. 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
+ };