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,242 @@
1
+ // 文件路径: database/actions/common.ts
2
+
3
+ import pino from "pino";
4
+ import { getIsDesktopApp } from "app/utils/env";
5
+ import { API_ENDPOINTS, NOLO_CLUSTER_SERVERS } from "../config";
6
+
7
+ // RN 下 pino 的 browser 写法可能有兼容性问题
8
+ // 使用简单的 console 封装作为 fallback
9
+ const isRN = typeof navigator !== 'undefined' && (navigator as any).product === 'ReactNative';
10
+
11
+ export const logger = isRN ? {
12
+ info: (...args: any[]) => console.log(...args),
13
+ warn: (...args: any[]) => console.warn(...args),
14
+ error: (...args: any[]) => console.error(...args),
15
+ debug: (...args: any[]) => console.log(...args), // console.debug in RN behaves like log
16
+ trace: (...args: any[]) => console.log(...args),
17
+ fatal: (...args: any[]) => console.error(...args),
18
+ child: () => logger // 简单返回自己
19
+ } as unknown as pino.Logger : pino({
20
+ level: "info",
21
+ // transport: {
22
+ // target: "pino-pretty",
23
+ // },
24
+ });
25
+ const normalizeServer = (server: string): string =>
26
+ server.trim().replace(/\/+$/, "");
27
+ const isNoloClusterServer = (server: string): boolean =>
28
+ /^https?:\/\/(?:us\.)?nolo\.chat$/i.test(normalizeServer(server));
29
+
30
+ export const mergeConfiguredServers = (
31
+ currentServer: string | undefined,
32
+ syncServers: string[] | undefined
33
+ ): string[] => {
34
+ const runtimeOrigin =
35
+ !getIsDesktopApp() &&
36
+ typeof window !== "undefined" &&
37
+ typeof window.location?.origin === "string" &&
38
+ /^https?:\/\//.test(window.location.origin)
39
+ ? window.location.origin
40
+ : undefined;
41
+
42
+ const raw = [
43
+ currentServer,
44
+ ...(Array.isArray(syncServers) ? syncServers : []),
45
+ runtimeOrigin,
46
+ ].filter(
47
+ (s): s is string => typeof s === "string" && s.trim().length > 0
48
+ );
49
+
50
+ const normalized = raw.map(normalizeServer);
51
+ if (normalized.some(isNoloClusterServer)) {
52
+ normalized.push(...NOLO_CLUSTER_SERVERS);
53
+ }
54
+ return Array.from(new Set(normalized));
55
+ };
56
+ // 全局缓存网络状态(仅 React Native 使用)
57
+ let cachedNetworkState: boolean | null = null;
58
+ let netInfoListenerInitialized = false;
59
+
60
+ // 检测是否为 React Native 环境
61
+ const isReactNative = (): boolean => {
62
+ return typeof navigator !== 'undefined' && (navigator as any).product === 'ReactNative';
63
+ };
64
+
65
+ // 初始化 NetInfo 监听器(仅在 React Native 环境调用)
66
+ export const initNetworkListener = async () => {
67
+ if (!isReactNative() || netInfoListenerInitialized) return;
68
+
69
+ try {
70
+ // 动态导入 NetInfo,避免在 Web 环境下出错
71
+ const NetInfo = await import('@react-native-community/netinfo');
72
+ NetInfo.default.addEventListener(state => {
73
+ cachedNetworkState = state.isConnected ?? true;
74
+ });
75
+ netInfoListenerInitialized = true;
76
+ console.log('[NetInfo] Listener initialized');
77
+ } catch (error) {
78
+ console.warn('[NetInfo] Failed to initialize:', error);
79
+ }
80
+ };
81
+
82
+ // 改进的 isOnline 函数,兼容 Web 和 React Native
83
+ export const isOnline = (): boolean => {
84
+ // React Native 环境:使用 NetInfo 缓存
85
+ if (isReactNative()) {
86
+ if (cachedNetworkState !== null) {
87
+ return cachedNetworkState;
88
+ }
89
+ // NetInfo 还未初始化,默认假设在线
90
+ return true;
91
+ }
92
+
93
+ // Web 环境:使用 navigator.onLine
94
+ if (typeof navigator !== "undefined" && typeof (navigator as any).onLine !== "undefined") {
95
+ return (navigator as any).onLine;
96
+ }
97
+
98
+ // 降级:默认假设在线
99
+ return true;
100
+ };
101
+
102
+ export const getAllServers = (
103
+ currentServer: string | undefined,
104
+ syncServers: string[] | undefined,
105
+ preferredServer?: string | null
106
+ ): string[] => {
107
+ const preferredNormalized =
108
+ typeof preferredServer === "string" && preferredServer.trim().length > 0
109
+ ? normalizeServer(preferredServer)
110
+ : null;
111
+ const servers = mergeConfiguredServers(
112
+ currentServer,
113
+ preferredNormalized && isNoloClusterServer(preferredNormalized)
114
+ ? [...(Array.isArray(syncServers) ? syncServers : []), preferredNormalized]
115
+ : syncServers
116
+ );
117
+ if (!preferredServer || typeof preferredServer !== "string") {
118
+ return servers;
119
+ }
120
+
121
+ const remaining = servers.filter(
122
+ (server) => normalizeServer(server) !== preferredNormalized
123
+ );
124
+ return preferredNormalized ? [preferredNormalized, ...remaining] : remaining;
125
+ };
126
+
127
+ const isLevelNotFoundError = (err: any): boolean => {
128
+ const code = err?.code;
129
+ return (
130
+ err?.notFound === true ||
131
+ err?.name === "NotFoundError" ||
132
+ code === "LEVEL_NOT_FOUND" ||
133
+ code === "LEVEL_NOT_FOUND_ERROR"
134
+ );
135
+ };
136
+
137
+
138
+ // 从客户端数据库获取数据 (无需改动)
139
+ export const fetchFromClientDb = async (
140
+ clientDb: any,
141
+ dbKey: string
142
+ ): Promise<any> => {
143
+ if (!clientDb) {
144
+ logger.error(
145
+ { dbKey },
146
+ "Client database is undefined in fetchFromClientDb"
147
+ );
148
+ return null;
149
+ }
150
+ try {
151
+ return await clientDb.get(dbKey);
152
+ } catch (err) {
153
+ if (isLevelNotFoundError(err)) {
154
+ return null;
155
+ }
156
+ logger.error({ err, dbKey }, "Failed to get local data");
157
+ return null;
158
+ }
159
+ };
160
+
161
+ // ======================================================================
162
+ // 【核心改造】: fetchFromServer 函数
163
+ // ======================================================================
164
+ const SERVER_TIMEOUT = 5000;
165
+ export const READ_TIMEOUT_ERROR_NAME = "ReadTimeoutError";
166
+ const isPublicFileDbKey = (dbKey: string): boolean => dbKey.startsWith("file-");
167
+ // file-* 元数据接口对无 token 的 LLM/工具调用开放(仅 metadata);
168
+ // 常规 read 接口保留认证路径,避免普通业务数据被未授权盗链/枚举。
169
+ const buildReadUrl = (dbKey: string): string =>
170
+ isPublicFileDbKey(dbKey)
171
+ ? `${API_ENDPOINTS.DATABASE}/file/metadata/${encodeURIComponent(dbKey)}`
172
+ : `${API_ENDPOINTS.DATABASE}/read/${encodeURIComponent(dbKey)}`;
173
+
174
+ const createReadTimeoutError = (server: string, dbKey: string): Error => {
175
+ const error = new Error(
176
+ `Timed out reading key "${dbKey}" from ${normalizeServer(server)}.`
177
+ );
178
+ error.name = READ_TIMEOUT_ERROR_NAME;
179
+ return error;
180
+ };
181
+
182
+ export const isReadTimeoutError = (error: unknown): boolean =>
183
+ error instanceof Error && error.name === READ_TIMEOUT_ERROR_NAME;
184
+
185
+ export const fetchFromServer = async (
186
+ server: string,
187
+ dbKey: string,
188
+ token?: string,
189
+ signal?: AbortSignal
190
+ ): Promise<any> => {
191
+ if (signal?.aborted) {
192
+ throw new DOMException("Aborted", "AbortError");
193
+ }
194
+
195
+ const controller = new AbortController();
196
+ let didTimeout = false;
197
+ const timeoutId = setTimeout(() => {
198
+ didTimeout = true;
199
+ controller.abort();
200
+ }, SERVER_TIMEOUT);
201
+ const onExternalAbort = () => controller.abort();
202
+ signal?.addEventListener("abort", onExternalAbort);
203
+
204
+ try {
205
+ const res = await fetch(
206
+ `${server}${buildReadUrl(dbKey)}`,
207
+ {
208
+ signal: controller.signal as any,
209
+ headers: {
210
+ "Content-Type": "application/json",
211
+ ...(token && { Authorization: `Bearer ${token}` }),
212
+ },
213
+ }
214
+ );
215
+ clearTimeout(timeoutId);
216
+ signal?.removeEventListener("abort", onExternalAbort);
217
+
218
+ if (res.status === 200) {
219
+ return await res.json();
220
+ }
221
+ return null;
222
+ } catch (err: any) {
223
+ clearTimeout(timeoutId);
224
+ signal?.removeEventListener("abort", onExternalAbort);
225
+ if (didTimeout) {
226
+ throw createReadTimeoutError(server, dbKey);
227
+ }
228
+ if (signal?.aborted || err.name === "AbortError") {
229
+ throw err;
230
+ }
231
+ return null;
232
+ }
233
+ };
234
+
235
+ // 规范化时间字段 (无需改动)
236
+ export const normalizeTimeFields = (data: any): any => ({
237
+ ...data,
238
+ createdAt: data.createdAt || new Date().toISOString(),
239
+ updatedAt: new Date().toISOString(),
240
+ updated_at: undefined,
241
+ created_at: undefined,
242
+ });
@@ -0,0 +1,40 @@
1
+
2
+ import { removeAction } from "./remove";
3
+ import { fetchFromClientDb } from "./common";
4
+ import { deleteFileFromIndexedDb } from "../fileStorage";
5
+ import { logger } from "./common";
6
+
7
+ /**
8
+ * Delete File Action:
9
+ * 1. Retrieve metadata to get fileId (if different from dbKey, though usually dbKey maps to metadata which has the fileId)
10
+ * 2. Delete blob from IndexedDB (using fileId from metadata)
11
+ * 3. Call generic removeAction to delete metadata and notify servers
12
+ */
13
+ export const deleteFileAction = async (
14
+ dbKey: string,
15
+ thunkApi: any
16
+ ): Promise<void> => {
17
+ const { db: clientDb } = thunkApi.extra;
18
+
19
+ try {
20
+ // 1. Get metadata to find the internal Blob ID (fileId)
21
+ const metadata = await fetchFromClientDb(clientDb, dbKey);
22
+
23
+ if (metadata && metadata.id) {
24
+ // 2. Delete blob from IndexedDB
25
+ try {
26
+ await deleteFileFromIndexedDb(metadata.id);
27
+ logger.debug({ fileId: metadata.id }, "Deleted file blob from IndexedDB");
28
+ } catch (err) {
29
+ logger.warn({ err, fileId: metadata.id }, "Failed to delete file blob from IndexedDB, proceeding to delete metadata");
30
+ }
31
+ }
32
+
33
+ // 3. Delete metadata and sync delete to servers
34
+ await removeAction(dbKey, thunkApi);
35
+
36
+ } catch (error) {
37
+ logger.error({ error, dbKey }, "Failed to delete file completely");
38
+ throw error;
39
+ }
40
+ }
@@ -0,0 +1,16 @@
1
+ import { createAsyncThunk } from "@reduxjs/toolkit";
2
+ import { fetchUserData } from "../client/fetchUserData";
3
+ import { AppThunkApi } from "app/store";
4
+
5
+ export const fetchUserDataThunk = createAsyncThunk<
6
+ any,
7
+ { types: string | string[]; userId: string; includeDeleted?: boolean },
8
+ AppThunkApi
9
+ >("db/fetchUserData", async ({ types, userId, includeDeleted }, { extra }) => {
10
+ const { db } = extra;
11
+ if (!db) {
12
+ console.error("Database not available in fetchUserDataThunk");
13
+ throw new Error("Database not available");
14
+ }
15
+ return await fetchUserData(db, types, userId, { includeDeleted });
16
+ });
@@ -0,0 +1,125 @@
1
+ // 文件路径: database/actions/fileContent.ts
2
+
3
+ import type { AppThunkApi } from "app/store";
4
+ import { API_ENDPOINTS } from "database/config";
5
+ import { getRuntimeServerContext } from "database/runtimeServerContext";
6
+ import {
7
+ loadFileFromIndexedDb,
8
+ saveFileToIndexedDb,
9
+ StoredFileRecord,
10
+ } from "../fileStorage";
11
+ import { getFileIdFromKey } from "../keys";
12
+
13
+ /**
14
+ * 从 dbKey 中提取裸 ULID(用于 IndexedDB 查找)
15
+ *
16
+ * 如果传入的是完整 dbKey(如 file-{userId}-{fileId}),返回 fileId(ULID)。
17
+ * 如果传入的已经是裸 ULID,则原样返回。
18
+ */
19
+ const resolveLocalFileId = (fileId: string): string => {
20
+ if (fileId.startsWith("file-")) {
21
+ return getFileIdFromKey(fileId) || fileId;
22
+ }
23
+ return fileId;
24
+ };
25
+
26
+ /**
27
+ * 读取文件内容(优先本地 IndexedDB,缺失时从服务器拉取并写入本地缓存)
28
+ *
29
+ * fileId 支持两种格式:
30
+ * - 完整 dbKey:file-{userId}-{fileId}(推荐,服务端可直接查询)
31
+ * - 裸 ULID:{fileId}(需要服务端 file-id 索引)
32
+ *
33
+ * 返回:
34
+ * - fileId: string
35
+ * - blob: Blob
36
+ * - source: "local" | "remote"
37
+ */
38
+ export const readFileContentAction = async (
39
+ {
40
+ fileId,
41
+ useServerFallback = true,
42
+ }: { fileId: string; useServerFallback?: boolean },
43
+ thunkApi: AppThunkApi
44
+ ): Promise<{ fileId: string; blob: Blob; source: "local" | "remote" }> => {
45
+ if (!fileId || typeof fileId !== "string") {
46
+ throw new Error("readFileContentAction requires a valid fileId string.");
47
+ }
48
+
49
+ // IndexedDB 以裸 ULID 为 key 存储文件
50
+ const localId = resolveLocalFileId(fileId);
51
+
52
+ // 1. 先尝试从 IndexedDB 读取(使用裸 ULID)
53
+ const localRecord: StoredFileRecord | null =
54
+ await loadFileFromIndexedDb(localId);
55
+ if (localRecord) {
56
+ return {
57
+ fileId: localId,
58
+ blob: localRecord.blob,
59
+ source: "local",
60
+ };
61
+ }
62
+
63
+ // 2. 根据参数决定是否回退到服务器
64
+ if (!useServerFallback) {
65
+ throw new Error(
66
+ `Local file not found for id "${fileId}", and server fallback is disabled.`
67
+ );
68
+ }
69
+
70
+ // 3. 从服务器拉取文件内容(使用原始 fileId,完整 dbKey 可直接查询)
71
+ const state = thunkApi.getState();
72
+ const { currentServer, remoteServers: serversToTry } =
73
+ getRuntimeServerContext(state);
74
+
75
+ if (!currentServer) {
76
+ throw new Error(
77
+ `No current server configured. Cannot fetch remote file for id "${fileId}".`
78
+ );
79
+ }
80
+
81
+ let lastError = "";
82
+
83
+ for (const server of serversToTry) {
84
+ const url = `${server}${API_ENDPOINTS.DATABASE}/file/content/${fileId}`;
85
+ console.debug("[readFileContentAction] trying server:", url);
86
+
87
+ try {
88
+ const res = await fetch(url);
89
+ if (!res.ok) {
90
+ lastError = `HTTP ${res.status} from ${server}`;
91
+ console.debug("[readFileContentAction] server returned:", lastError);
92
+ continue;
93
+ }
94
+
95
+ const blob = await res.blob();
96
+
97
+ // 将从服务器获取的文件写入 IndexedDB,以裸 ULID 为 key 缓存
98
+ if (typeof indexedDB !== "undefined") {
99
+ void saveFileToIndexedDb(localId, blob).catch((err) => {
100
+ console.warn(
101
+ "[readFileContentAction] Failed to cache remote file into IndexedDB:",
102
+ err
103
+ );
104
+ });
105
+ }
106
+
107
+ return {
108
+ fileId: localId,
109
+ blob,
110
+ source: "remote" as const,
111
+ };
112
+ } catch (err: any) {
113
+ lastError = err?.message || "Network error";
114
+ console.debug(
115
+ "[readFileContentAction] fetch error from",
116
+ server,
117
+ lastError
118
+ );
119
+ }
120
+ }
121
+
122
+ throw new Error(
123
+ `Failed to fetch remote file content from all servers for id "${fileId}". Last error: ${lastError}`
124
+ );
125
+ };
@@ -0,0 +1,155 @@
1
+ // 文件路径: database/actions/patch.ts
2
+
3
+ import type { AppThunkApi } from "app/store";
4
+ import { getRuntimeServerContext } from "database/runtimeServerContext";
5
+ import { toast } from "app/utils/toast";
6
+ import {
7
+ scheduleConfiguredPatchReplication,
8
+ } from "./replication";
9
+
10
+ /**
11
+ * 深度合并两个对象。源对象中的 null 值会删除目标对象中对应的键。
12
+ * @param target - 目标对象。
13
+ * @param source - 源对象,包含要应用的更改。
14
+ * @returns {any} - 合并后的新对象。
15
+ */
16
+ const deepMerge = (target: any, source: any): any => {
17
+ const output = { ...target };
18
+ for (const key in source) {
19
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
20
+ if (source[key] === null && key in output) {
21
+ delete output[key]; // null 值用于删除键
22
+ } else if (
23
+ source[key] &&
24
+ typeof source[key] === "object" &&
25
+ !Array.isArray(source[key])
26
+ ) {
27
+ output[key] = deepMerge(output[key] || {}, source[key]); // 递归合并
28
+ } else {
29
+ output[key] = source[key]; // 直接赋值
30
+ }
31
+ }
32
+ }
33
+ return output;
34
+ };
35
+
36
+ const toTimestamp = (value: unknown): number => {
37
+ if (typeof value === "number" && Number.isFinite(value)) return value;
38
+ if (typeof value === "string") {
39
+ const parsed = Date.parse(value);
40
+ return Number.isFinite(parsed) ? parsed : 0;
41
+ }
42
+ return 0;
43
+ };
44
+
45
+ const inferNextUpdatedAt = (currentData: any): number | string | undefined => {
46
+ const previousUpdatedAt = currentData?.updatedAt;
47
+ const previousCreatedAt = currentData?.createdAt;
48
+ const previousMetaCreatedAt = currentData?.meta?.createdAt;
49
+ const previousTimestamp = Math.max(
50
+ toTimestamp(previousUpdatedAt),
51
+ toTimestamp(previousCreatedAt),
52
+ toTimestamp(previousMetaCreatedAt)
53
+ );
54
+ const nextTimestamp = Math.max(Date.now(), previousTimestamp + 1);
55
+
56
+ if (
57
+ typeof previousUpdatedAt === "number" ||
58
+ typeof previousCreatedAt === "number" ||
59
+ typeof previousMetaCreatedAt === "number"
60
+ ) {
61
+ return nextTimestamp;
62
+ }
63
+
64
+ if (
65
+ typeof previousUpdatedAt === "string" ||
66
+ typeof previousCreatedAt === "string"
67
+ ) {
68
+ return new Date(nextTimestamp).toISOString();
69
+ }
70
+
71
+ return undefined;
72
+ };
73
+
74
+ /**
75
+ * Patch Action: 对现有数据项应用增量更新。
76
+ * 1. 从本地数据库读取现有数据。
77
+ * 2. 将传入的 'changes' 对象与现有数据进行深度合并。
78
+ * 3. 将合并后的新数据写回本地数据库。
79
+ * 4. 异步地将 'changes' 对象同步到所有相关服务器。
80
+ * @param payload - 包含 dbKey 和 changes 的对象。
81
+ * @param {string} payload.dbKey - 要更新的数据的键。
82
+ * @param {object} payload.changes - 要应用的更改。
83
+ * @param thunkApi - Redux Thunk API,包含 state 和 extra arugments。
84
+ * @returns {Promise<any>} 更新后的完整数据对象。
85
+ * @throws 如果本地数据不存在或更新过程中发生任何错误,则抛出异常。
86
+ */
87
+ export const patchAction = async (
88
+ {
89
+ dbKey,
90
+ changes,
91
+ preferredServerOrigin,
92
+ }: { dbKey: string; changes: any; preferredServerOrigin?: string | null },
93
+ thunkApi: AppThunkApi
94
+ ): Promise<any> => {
95
+ // 1. 从 thunkApi.extra 中获取数据库实例
96
+ const { db } = thunkApi.extra;
97
+ if (!db) {
98
+ const errorMsg = "Database instance is not available.";
99
+ toast.error(errorMsg);
100
+ throw new Error(errorMsg);
101
+ }
102
+
103
+ // 2. 验证输入参数
104
+ if (!dbKey || !changes || typeof changes !== "object") {
105
+ const errorMsg = "Patch action requires a valid dbKey and changes object.";
106
+ toast.error(errorMsg);
107
+ throw new Error(errorMsg);
108
+ }
109
+
110
+ const state = thunkApi.getState();
111
+ const { currentServer, syncServers: configuredSyncServers } =
112
+ getRuntimeServerContext(state);
113
+
114
+ try {
115
+ // 3. 使用注入的 db 实例读取当前数据
116
+ const currentData = await db.get(dbKey);
117
+ if (!currentData) {
118
+ throw new Error(
119
+ `Cannot apply patch: Data not found locally for key: ${dbKey}.`
120
+ );
121
+ }
122
+
123
+ const patchChanges = Object.prototype.hasOwnProperty.call(changes, "updatedAt")
124
+ ? changes
125
+ : {
126
+ ...changes,
127
+ ...(inferNextUpdatedAt(currentData) !== undefined
128
+ ? { updatedAt: inferNextUpdatedAt(currentData) }
129
+ : {}),
130
+ };
131
+
132
+ // 4. 合并数据并写回本地数据库
133
+ const newData = deepMerge(currentData, patchChanges);
134
+ const persistedData =
135
+ newData && typeof newData === "object" ? { ...newData, dbKey } : { dbKey };
136
+ await db.put(dbKey, persistedData);
137
+
138
+ // 5. 异步触发对远程服务器的同步(即发即忘)
139
+ scheduleConfiguredPatchReplication({
140
+ currentServer,
141
+ syncServers: configuredSyncServers,
142
+ preferredServerOrigin,
143
+ dbKey,
144
+ changes: patchChanges,
145
+ state,
146
+ });
147
+
148
+ // 6. 乐观地返回更新后的数据
149
+ return persistedData;
150
+ } catch (error: any) {
151
+ const errorMessage = `Failed to update data for ${dbKey}.`;
152
+ toast.error(errorMessage);
153
+ throw new Error(error.message || errorMessage);
154
+ }
155
+ };