nolo-cli 0.1.22 → 0.1.26

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 (128) hide show
  1. package/README.md +25 -3
  2. package/agent-runtime/agentRecordConfig.ts +4 -0
  3. package/agent-runtime/hostAdapter.ts +2 -0
  4. package/agent-runtime/hybridRecordStore.ts +30 -9
  5. package/agent-runtime/index.ts +7 -0
  6. package/agent-runtime/localLoop.ts +14 -4
  7. package/agent-runtime/localToolPolicy.ts +22 -10
  8. package/agent-runtime/localWorkspaceTools.ts +140 -374
  9. package/agent-runtime/platformChatProvider.ts +3 -0
  10. package/agent-runtime/runtimeToolPolicy.ts +133 -0
  11. package/agent-runtime/types.ts +47 -0
  12. package/agentAliases.ts +1 -1
  13. package/agentCommandSupport.ts +77 -0
  14. package/agentInternalCommandEntries.ts +34 -0
  15. package/agentListCommands.ts +123 -0
  16. package/agentListHelpers.ts +187 -0
  17. package/agentMachineCommands.ts +402 -0
  18. package/agentRecordCommands.ts +141 -0
  19. package/agentRecordHelpers.ts +395 -0
  20. package/agentRunCommand.ts +75 -3
  21. package/agentRuntimeCommands.ts +7 -773
  22. package/ai/agent/_executeModel.ts +29 -3
  23. package/ai/agent/buildSystemPrompt.ts +17 -0
  24. package/ai/agent/cliChatClient.ts +38 -4
  25. package/ai/agent/cliExecutor.ts +304 -25
  26. package/ai/agent/createAgentSchema.ts +2 -2
  27. package/ai/agent/guidedCreation/assistant.ts +82 -0
  28. package/ai/agent/guidedCreation/capabilities.ts +84 -0
  29. package/ai/agent/guidedCreation/draft.ts +62 -0
  30. package/ai/agent/guidedCreation/types.ts +38 -0
  31. package/ai/agent/machineRunPermissions.ts +21 -3
  32. package/ai/agent/runtimeGuidance.ts +1 -1
  33. package/ai/agent/server/fetchPublicAgents.ts +6 -1
  34. package/ai/agent/streamAgentChatTurn.ts +593 -36
  35. package/ai/agent/web/useGuidedAgentCreation.ts +217 -0
  36. package/ai/ai.locale.ts +14 -2
  37. package/ai/chat/fetchUtils.native.ts +2 -0
  38. package/ai/chat/fetchUtils.ts +2 -0
  39. package/ai/chat/sendOpenAICompletionsRequest.ts +82 -1
  40. package/ai/chat/sendOpenAIResponseRequest.ts +65 -0
  41. package/ai/llm/getPricing.ts +14 -0
  42. package/ai/llm/kimi.ts +4 -1
  43. package/ai/llm/modelAvatar.ts +1 -0
  44. package/ai/llm/providers.ts +4 -1
  45. package/ai/llm/reasoningModels.ts +1 -0
  46. package/ai/taskRun/taskRunProtocol.ts +25 -10
  47. package/ai/token/modelUsageQuery.ts +17 -0
  48. package/ai/tools/agent/agentTools.ts +38 -0
  49. package/ai/tools/agent/prepareAgentDraftTool.ts +159 -0
  50. package/ai/tools/agent/startAgentDialogTool.ts +53 -0
  51. package/ai/tools/checkEnvTool.ts +52 -20
  52. package/ai/tools/convertMarxistsBookTool.ts +103 -0
  53. package/ai/tools/{execBashTool.ts → execShellTool.ts} +44 -161
  54. package/ai/tools/index.ts +24 -14
  55. package/ai/tools/marxistsOfflineBook.ts +253 -0
  56. package/ai/tools/modelUsageTools.ts +9 -2
  57. package/ai/tools/readTool.ts +31 -0
  58. package/ai/tools/toolApiClient.ts +35 -1
  59. package/ai/tools/toolNameAliases.ts +13 -0
  60. package/cliCommandAdapters.ts +28 -0
  61. package/cliCommandDispatch.ts +66 -0
  62. package/cliCommandFactories.ts +34 -0
  63. package/cliCommandTypes.ts +49 -0
  64. package/cliEnvHelpers.ts +44 -0
  65. package/client/agentRun.test.ts +330 -24
  66. package/client/agentRun.ts +117 -35
  67. package/client/hybridRecordStore.test.ts +42 -0
  68. package/client/hybridRecordStore.ts +11 -0
  69. package/client/localRuntimeAdapter.test.ts +191 -21
  70. package/client/localRuntimeAdapter.ts +99 -10
  71. package/client/localRuntimeDryRun.test.ts +5 -2
  72. package/client/taskRunPrompt.ts +1 -0
  73. package/commandRegistry.ts +11 -61
  74. package/connector-experimental/capabilities.ts +1 -0
  75. package/database/actions/common.ts +4 -3
  76. package/database/config.ts +19 -0
  77. package/database/server/coreDataProxy.ts +43 -26
  78. package/index.ts +54 -191
  79. package/internalCommandEntries.ts +12 -0
  80. package/machineCommands.ts +688 -64
  81. package/offlineMarxistsAgentCommand.ts +314 -0
  82. package/package.json +22 -2
  83. package/render/canvas/canvasEditContext.ts +127 -0
  84. package/render/canvas/canvasRuntime.ts +57 -0
  85. package/render/canvas/canvasSnapshotParser.ts +76 -0
  86. package/render/canvas/canvasTree.ts +308 -0
  87. package/render/canvas/types.ts +46 -0
  88. package/render/layout/deleteBehavior.ts +52 -0
  89. package/render/layout/mainLayoutSidebar.ts +17 -0
  90. package/render/layout/mainLayoutViewMode.ts +56 -0
  91. package/render/layout/topbarUtils.ts +87 -0
  92. package/render/layout/useDevReloadPending.ts +30 -0
  93. package/render/page/createPageAction.ts +183 -0
  94. package/render/page/docSlice.ts +468 -0
  95. package/render/page/server/createPage.ts +174 -0
  96. package/render/page/server/handleCreatePage.ts +91 -0
  97. package/render/page/server/index.ts +4 -0
  98. package/render/page/types.ts +17 -0
  99. package/render/page/useKeyboardSave.ts +48 -0
  100. package/render/styles/zIndex.ts +12 -0
  101. package/render/surf/WeatherIconStyles.ts +17 -0
  102. package/render/surf/color.ts +9 -0
  103. package/render/surf/config.ts +46 -0
  104. package/render/surf/screens/style.ts +1 -0
  105. package/render/surf/styles/ToggleButtonStyles.ts +8 -0
  106. package/render/surf/utils/groupedWeatherData.ts +32 -0
  107. package/render/surf/weatherUtils.ts +50 -0
  108. package/render/table/activityColumns.ts +6 -0
  109. package/render/table/createTableAction.ts +270 -0
  110. package/render/table/deleteTableAction.ts +129 -0
  111. package/render/table/fetchAndCacheTableRows.ts +174 -0
  112. package/render/table/tableSlice.ts +1106 -0
  113. package/render/table/tableView.ts +289 -0
  114. package/render/table/toolValueUtils.ts +363 -0
  115. package/render/table/types.ts +252 -0
  116. package/render/table/useCreateTable.ts +72 -0
  117. package/render/table/useTable.ts +61 -0
  118. package/render/table/utils/tableSerialization.ts +50 -0
  119. package/render/web/elements/artifactPreviewCode.ts +43 -0
  120. package/render/web/elements/artifactRuntimePreload.ts +52 -0
  121. package/render/web/elements/codeBlockAutoPreview.ts +10 -0
  122. package/render/web/elements/mermaidPreview.ts +21 -0
  123. package/render/web/ui/useInlineEdit.ts +135 -0
  124. package/runtimeDoctorCommands.ts +118 -0
  125. package/scriptCommandEntries.ts +71 -0
  126. package/systemInternalCommandEntries.ts +71 -0
  127. package/tableCommands.ts +42 -5
  128. package/workflowInternalCommandEntries.ts +11 -0
@@ -34,6 +34,7 @@ import {
34
34
  type HybridRecordStore,
35
35
  } from "./hybridRecordStore";
36
36
  import { executeLocalToolWithPolicy } from "./localToolPolicy";
37
+ import { taskRunFunctionSchema } from "../ai/tools/agent/taskRunTool";
37
38
  import { prepareTaskWorktree } from "./taskWorktree";
38
39
  import {
39
40
  activateWorkspaceSession,
@@ -58,6 +59,7 @@ export type CliLocalRuntimeAdapterDeps = {
58
59
  createId?: () => string;
59
60
  fetchImpl?: typeof fetch;
60
61
  cwd?: string;
62
+ useCwdAsTaskWorkspaceBase?: boolean;
61
63
  output?: { write(chunk: string): unknown };
62
64
  prepareTaskWorktree?: typeof prepareTaskWorktree;
63
65
  localToolExecutors?: Record<string, (call: any) => Promise<{ content: string; metadata?: Record<string, unknown> }>>;
@@ -143,10 +145,13 @@ async function fetchWithTransientRetry(
143
145
 
144
146
  function buildOpenAiTools(args: { toolNames?: string[]; env: EnvLike }) {
145
147
  const toolset = buildLocalWorkspaceToolsetForEnv(args);
146
- return buildLocalWorkspaceOpenAiTools({
147
- toolNames: toolset.toolNames,
148
- exposeShellTools: toolset.exposeShellTools,
149
- });
148
+ return [
149
+ ...buildLocalWorkspaceOpenAiTools({
150
+ toolNames: toolset.toolNames,
151
+ exposeShellTools: toolset.exposeShellTools,
152
+ }),
153
+ ...buildServerPlatformOpenAiTools({ toolNames: args.toolNames }),
154
+ ];
150
155
  }
151
156
 
152
157
  function buildLocalWorkspaceToolsetForEnv(args: { toolNames?: string[]; env: EnvLike }) {
@@ -158,18 +163,86 @@ function buildLocalWorkspaceToolsetForEnv(args: { toolNames?: string[]; env: Env
158
163
  }
159
164
 
160
165
  function buildLocalPolicyToolNames(args: { toolNames?: string[]; env: EnvLike }) {
161
- return buildLocalWorkspacePolicyToolNames({
162
- declaredToolNames: args.toolNames,
163
- exposeShellTools: shellModeEnabled(args.env),
164
- });
166
+ return [
167
+ ...buildLocalWorkspacePolicyToolNames({
168
+ declaredToolNames: args.toolNames,
169
+ exposeShellTools: shellModeEnabled(args.env),
170
+ }),
171
+ ...((args.toolNames ?? []).includes("taskRun") ? ["taskRun"] : []),
172
+ ];
173
+ }
174
+
175
+ function buildServerPlatformOpenAiTools(args: { toolNames?: string[] }) {
176
+ const toolNames = new Set(args.toolNames ?? []);
177
+ return [
178
+ ...(toolNames.has("taskRun")
179
+ ? [{
180
+ type: "function",
181
+ function: taskRunFunctionSchema,
182
+ }]
183
+ : []),
184
+ ];
185
+ }
186
+
187
+ function parseToolArguments(raw: string) {
188
+ try {
189
+ const parsed = JSON.parse(raw || "{}");
190
+ return parsed && typeof parsed === "object" ? parsed : {};
191
+ } catch {
192
+ return {};
193
+ }
194
+ }
195
+
196
+ function resolveRuntimeServerUrl(env: EnvLike) {
197
+ return (env.NOLO_SERVER_URL || env.NOLO_SERVER || env.BASE_URL || "").replace(/\/+$/, "");
198
+ }
199
+
200
+ function resolveRuntimeAuthToken(env: EnvLike) {
201
+ return env.AUTH_TOKEN || env.AUTH || env.NOLO_MACHINE_API_KEY || "";
202
+ }
203
+
204
+ function buildServerPlatformToolExecutors(args: {
205
+ env: EnvLike;
206
+ fetchImpl: typeof fetch;
207
+ }) {
208
+ return {
209
+ taskRun: async (call: any) => {
210
+ const serverUrl = resolveRuntimeServerUrl(args.env);
211
+ const authToken = resolveRuntimeAuthToken(args.env);
212
+ if (!serverUrl) throw new Error("taskRun requires NOLO_SERVER_URL, NOLO_SERVER, or BASE_URL.");
213
+ if (!authToken) throw new Error("taskRun requires AUTH_TOKEN or NOLO_MACHINE_API_KEY.");
214
+ const response = await args.fetchImpl(`${serverUrl}/api/task-run`, {
215
+ method: "POST",
216
+ headers: {
217
+ "Content-Type": "application/json",
218
+ Authorization: `Bearer ${authToken}`,
219
+ },
220
+ body: JSON.stringify(parseToolArguments(call.arguments)),
221
+ });
222
+ const text = await response.text().catch(() => "");
223
+ if (!response.ok) {
224
+ throw new Error(`taskRun bridge failed: HTTP ${response.status} ${text.slice(0, 500)}`);
225
+ }
226
+ return {
227
+ content: text,
228
+ metadata: { serverPlatformTool: true },
229
+ };
230
+ },
231
+ };
165
232
  }
166
233
 
167
234
  function buildLocalToolExecutors(args: {
168
235
  workspaceRoot: string;
236
+ env: EnvLike;
237
+ fetchImpl: typeof fetch;
169
238
  localToolExecutors?: CliLocalRuntimeAdapterDeps["localToolExecutors"];
170
239
  }) {
171
240
  return {
172
241
  ...createLocalWorkspaceToolExecutors({ workspaceRoot: args.workspaceRoot }),
242
+ ...buildServerPlatformToolExecutors({
243
+ env: args.env,
244
+ fetchImpl: args.fetchImpl,
245
+ }),
173
246
  ...(args.localToolExecutors ?? {}),
174
247
  };
175
248
  }
@@ -254,9 +327,15 @@ export function createCliLocalRuntimeAdapter(
254
327
  const localToolBudgets = parseLocalToolBudgets(deps.env);
255
328
  const localToolUsage = new Map<string, number>();
256
329
  let activeAgentToolNames: string[] = [];
257
- let workspaceSession: WorkspaceSession = createWorkspaceSession({ cwd: deps.cwd });
330
+ let workspaceSession: WorkspaceSession = createWorkspaceSession(
331
+ deps.useCwdAsTaskWorkspaceBase
332
+ ? { defaultCwd: deps.cwd }
333
+ : { cwd: deps.cwd }
334
+ );
258
335
  let localToolExecutors = buildLocalToolExecutors({
259
336
  workspaceRoot: workspaceSession.workspaceRoot,
337
+ env: deps.env,
338
+ fetchImpl,
260
339
  localToolExecutors: deps.localToolExecutors,
261
340
  });
262
341
 
@@ -274,6 +353,8 @@ export function createCliLocalRuntimeAdapter(
274
353
  if (workspaceSession.workspaceRoot === previousWorkspaceRoot) return;
275
354
  localToolExecutors = buildLocalToolExecutors({
276
355
  workspaceRoot: workspaceSession.workspaceRoot,
356
+ env: deps.env,
357
+ fetchImpl,
277
358
  localToolExecutors: deps.localToolExecutors,
278
359
  });
279
360
  deps.output?.write(formatWorkspaceSessionActivation(workspaceSession));
@@ -378,12 +459,20 @@ export function createCliLocalRuntimeAdapter(
378
459
  budgets: localToolBudgets,
379
460
  usage: localToolUsage,
380
461
  });
381
- return executeLocalToolWithPolicy({
462
+ const result = await executeLocalToolWithPolicy({
382
463
  env: deps.env,
383
464
  agentToolNames: activeAgentToolNames,
384
465
  call,
385
466
  executors: localToolExecutors,
386
467
  });
468
+ return {
469
+ ...result,
470
+ metadata: {
471
+ ...(result.metadata ?? {}),
472
+ workspaceRoot: workspaceSession.workspaceRoot,
473
+ workspaceKind: workspaceSession.kind,
474
+ },
475
+ };
387
476
  },
388
477
  };
389
478
  }
@@ -4,7 +4,6 @@ import { join } from "node:path";
4
4
  import { tmpdir } from "node:os";
5
5
 
6
6
  import { runLocalAgentTurn } from "../agent-runtime/localLoop";
7
- import { buildLocalWorkspaceToolset } from "../agent-runtime/localWorkspaceTools";
8
7
  import { createCliLocalRuntimeAdapter } from "./localRuntimeAdapter";
9
8
 
10
9
  describe("CLI local runtime dry run", () => {
@@ -54,7 +53,11 @@ describe("CLI local runtime dry run", () => {
54
53
  completeCount += 1;
55
54
  if (completeCount === 1) {
56
55
  expect(body.tools.map((tool: any) => tool.function.name)).toEqual([
57
- ...buildLocalWorkspaceToolset({}).toolNames,
56
+ "writeWorkspaceFile",
57
+ "listFiles",
58
+ "readFile",
59
+ "editFile",
60
+ "searchFiles",
58
61
  ]);
59
62
  return Response.json({
60
63
  model: "qwen-coder",
@@ -16,6 +16,7 @@ export function prependTaskRunPrompt(message: string, context?: TaskRunPromptCon
16
16
  ...(context.taskRunId ? [`- taskRunId: ${context.taskRunId}`] : []),
17
17
  ...(context.workItemId ? [`- workItemId: ${context.workItemId}`] : []),
18
18
  ...(artifactIds.length ? [`- artifactIds: ${artifactIds.join(",")}`] : []),
19
+ "- Handoff rule: start from the durable task context, then inspect existing refs such as workItems.dialogIds, workItems.outcome, review.dialogId, activityRefs, agentRuns.dialogId/resultSummary, and artifactRefs. Use dialog read/search for exact evidence.",
19
20
  "- When you create, update, hand off, block, or review work, update the task context with `bun run task:run -- ...`.",
20
21
  "- If this turn already includes a server TaskRun preflight snapshot, use that snapshot directly for read-only reporting or planning.",
21
22
  "- Keep long reasoning and logs in the dialog; record only resumable references in task row meta.",
@@ -1,66 +1,16 @@
1
- export type CommandEntry = {
2
- path: string[];
3
- script: string;
4
- description: string;
5
- fixedArgs?: string[];
6
- };
1
+ export type { CliPackageInfo, CliRuntimeContext, CommandEntry } from "./cliCommandTypes";
2
+ export {
3
+ createCliRuntimeContext,
4
+ looksLikeDaemonShortcut,
5
+ runResolvedCommand,
6
+ } from "./cliCommandDispatch";
7
+ import type { CommandEntry } from "./cliCommandTypes";
8
+ import { getInternalCommandEntries } from "./internalCommandEntries";
9
+ import { getScriptCommandEntries } from "./scriptCommandEntries";
7
10
 
8
11
  export const COMMANDS: CommandEntry[] = [
9
- { path: ["chat"], script: "", description: "Chat with an agent" },
10
-
11
- { path: ["doc", "create"], script: "createDoc.ts", description: "Create a normal doc" },
12
- { path: ["doc", "read"], script: "readDoc.ts", description: "Read a normal doc" },
13
- { path: ["doc", "update"], script: "updateDoc.ts", description: "Update a normal doc" },
14
- { path: ["doc", "delete"], script: "deleteDoc.ts", description: "Delete a normal doc" },
15
-
16
- { path: ["skill-doc", "create"], script: "createSkillDoc.ts", description: "Create a skill doc" },
17
- { path: ["skill-doc", "read"], script: "readSkillDoc.ts", description: "Read a skill doc" },
18
- { path: ["skill-doc", "update"], script: "updateSkillDoc.ts", description: "Update a skill doc" },
19
- { path: ["skill-doc", "delete"], script: "deleteSkillDoc.ts", description: "Delete a skill doc" },
20
- { path: ["skill", "create-doc"], script: "createSkillDoc.ts", description: "Create a skill-backed doc" },
21
-
22
- { path: ["dialog", "read"], script: "readDialog.ts", description: "Read a dialog" },
23
- { path: ["dialog", "status"], script: "dialogStatus.ts", description: "Read compact dialog status" },
24
- { path: ["space", "list"], script: "listSpaces.ts", description: "List joined spaces" },
25
- { path: ["space", "read"], script: "readSpace.ts", description: "Read a space" },
26
- { path: ["space", "delete"], script: "deleteSpaces.ts", description: "Delete spaces by safe filters" },
27
- { path: ["space", "category"], script: "upsertSpaceCategory.ts", description: "Create or update a space category" },
28
- { path: ["space", "content-category"], script: "setSpaceContentCategory.ts", description: "Move content into a space category" },
29
-
30
- { path: ["table", "query"], script: "tableData.ts", fixedArgs: ["--action", "query"], description: "Query table rows" },
31
- { path: ["table", "add-column"], script: "tableData.ts", fixedArgs: ["--action", "add-column"], description: "Add one table column" },
32
- { path: ["table", "add-row"], script: "tableData.ts", fixedArgs: ["--action", "add-row"], description: "Add one table row" },
33
- { path: ["table", "add-rows"], script: "tableData.ts", fixedArgs: ["--action", "add-rows"], description: "Add table rows" },
34
- { path: ["table", "update-row"], script: "tableData.ts", fixedArgs: ["--action", "update-row"], description: "Update one table row" },
35
- { path: ["table", "update-rows"], script: "tableData.ts", fixedArgs: ["--action", "update-rows"], description: "Update table rows" },
36
- { path: ["table", "delete-row"], script: "tableData.ts", fixedArgs: ["--action", "delete-row"], description: "Delete one table row" },
37
- { path: ["table", "delete-rows"], script: "tableData.ts", fixedArgs: ["--action", "delete-rows"], description: "Delete table rows" },
38
- { path: ["table", "data"], script: "tableData.ts", description: "Low-level table row script bridge" },
39
- { path: ["table", "meta"], script: "upsertTableMeta.ts", description: "Create or update table metadata" },
40
- { path: ["task-run"], script: "", description: "Update durable task-run state through the runner bridge" },
41
-
42
- { path: ["agent", "list"], script: "listMyAgents.ts", description: "List owned agents" },
43
- { path: ["agent", "pull"], script: "", description: "Cache an agent for local runs" },
44
- { path: ["agent", "read"], script: "", description: "Read a single agent" },
45
- { path: ["agent", "run"], script: "", description: "Run an agent" },
46
- { path: ["agent", "update"], script: "updateAgent.ts", description: "Update agent fields" },
47
- { path: ["agent", "bind-current"], script: "", description: "Bind an agent to this machine" },
48
- { path: ["agent", "smoke-current"], script: "", description: "Smoke test a bound agent through this machine" },
49
- { path: ["agent", "runtime-doctor"], script: "", description: "Diagnose current machine runtime compatibility" },
50
- { path: ["agent", "unpublish"], script: "unpublishAgent.ts", description: "Remove an agent's public record" },
51
- { path: ["agent", "chat"], script: "", description: "Chat with an agent" },
52
- { path: ["agent", "dialogs"], script: "queryAgentDialogs.ts", description: "Inspect recent dialogs for an agent" },
53
- { path: ["agent", "doctor"], script: "doctorAgentWorkspace.ts", description: "Audit agent workspace health" },
54
- { path: ["agent", "create-custom"], script: "createCustomCodingAgent.ts", description: "Create a custom coding agent" },
55
- { path: ["agent", "create-space"], script: "createSpaceAgents.ts", description: "Create or attach shared-space agents" },
56
- { path: ["agent", "setup-demo"], script: "setupDemoAgent.ts", description: "Bootstrap demo publisher agents" },
57
- { path: ["agent", "supervise"], script: "runAutonomousAgent.ts", description: "Run an agent in autonomous cycles" },
58
-
59
- { path: ["doctor", "runtime"], script: "", description: "Diagnose local-first agent runtime selection" },
60
-
61
- { path: ["llama"], script: "llamaServerSupervisor.ts", description: "Manage the llama.cpp runtime" },
62
- { path: ["model-runtime"], script: "localModelRuntimeSupervisor.ts", description: "Manage local model runtime processes" },
63
- { path: ["dev"], script: "devControl.ts", description: "Manage the local dev environment" },
12
+ ...getInternalCommandEntries(renderHelpText),
13
+ ...getScriptCommandEntries(),
64
14
  ];
65
15
 
66
16
  export const GROUP_ORDER = [
@@ -29,6 +29,7 @@ const COMMAND_CAPABILITIES: Array<[probes: CommandProbe[], capability: string]>
29
29
  ],
30
30
  [[{ command: "gemini", args: ["--version"] }], "gemini-cli"],
31
31
  [[{ command: "kimi", args: ["--version"] }], "kimi-cli"],
32
+ [[{ command: "agy", args: ["--help"] }], "agy-cli"],
32
33
  ];
33
34
 
34
35
  function defaultCommandExists(command: string) {
@@ -2,7 +2,8 @@
2
2
 
3
3
  import pino from "pino";
4
4
  import { getIsDesktopApp } from "app/utils/env";
5
- import { API_ENDPOINTS, NOLO_CLUSTER_SERVERS } from "../config";
5
+ import { fetchWithTransientReadRetry } from "app/utils/retryFetch";
6
+ import { API_ENDPOINTS, NOLO_CLUSTER_SERVERS, normalizeKnownServerOrigin } from "../config";
6
7
 
7
8
  // RN 下 pino 的 browser 写法可能有兼容性问题
8
9
  // 使用简单的 console 封装作为 fallback
@@ -23,7 +24,7 @@ export const logger = isRN ? {
23
24
  // },
24
25
  });
25
26
  const normalizeServer = (server: string): string =>
26
- server.trim().replace(/\/+$/, "");
27
+ normalizeKnownServerOrigin(server) ?? server.trim().replace(/\/+$/, "");
27
28
  const isNoloClusterServer = (server: string): boolean =>
28
29
  /^https?:\/\/(?:us\.)?nolo\.chat$/i.test(normalizeServer(server));
29
30
 
@@ -202,7 +203,7 @@ export const fetchFromServer = async (
202
203
  signal?.addEventListener("abort", onExternalAbort);
203
204
 
204
205
  try {
205
- const res = await fetch(
206
+ const res = await fetchWithTransientReadRetry(
206
207
  `${server}${buildReadUrl(dbKey)}`,
207
208
  {
208
209
  signal: controller.signal as any,
@@ -8,6 +8,25 @@ export const SERVERS = {
8
8
  } as const;
9
9
  export const NOLO_CLUSTER_SERVERS = Object.values(SERVERS);
10
10
 
11
+ const LEGACY_SERVER_ORIGIN_MAP: Record<string, string> = {
12
+ "https://nolotus.com": SERVERS.MAIN,
13
+ "https://www.nolotus.com": SERVERS.MAIN,
14
+ "https://us.nolotus.com": SERVERS.US,
15
+ "https://www.us.nolotus.com": SERVERS.US,
16
+ };
17
+
18
+ export const normalizeKnownServerOrigin = (server: unknown): string | null => {
19
+ if (typeof server !== "string" || server.trim().length === 0) return null;
20
+ const trimmed = server.trim();
21
+ let origin: string;
22
+ try {
23
+ origin = new URL(trimmed).origin;
24
+ } catch {
25
+ origin = trimmed.replace(/\/+$/, "");
26
+ }
27
+ return LEGACY_SERVER_ORIGIN_MAP[origin.toLowerCase()] ?? origin;
28
+ };
29
+
11
30
  export const API_ENDPOINTS = {
12
31
  DATABASE: `${API_VERSION}/db`,
13
32
  SHARE: `${API_VERSION}/share`,
@@ -9,7 +9,7 @@ const normalizeOrigin = (value: string | URL | null | undefined): string => {
9
9
  }
10
10
  };
11
11
 
12
- const getDbKeyForOperation = (args: {
12
+ const getDbKeyForOperation = (args: {
13
13
  operation: string;
14
14
  pathname: string;
15
15
  req: { body?: Record<string, any> };
@@ -26,10 +26,32 @@ const getDbKeyForOperation = (args: {
26
26
  return typeof req.body?.customKey === "string" ? req.body.customKey : null;
27
27
  default:
28
28
  return null;
29
- }
30
- };
31
-
32
- export async function maybeProxyDatabaseRequestToCore(args: {
29
+ }
30
+ };
31
+
32
+ export function resolveCoreDataProxyTarget(args: {
33
+ dbKey: string;
34
+ requestUrl?: string | URL | null;
35
+ coreBaseUrl?: string | null;
36
+ }): string | null {
37
+ const coreBaseUrl = normalizeOrigin(args.coreBaseUrl);
38
+ if (!coreBaseUrl) {
39
+ return null;
40
+ }
41
+
42
+ const requestOrigin = normalizeOrigin(args.requestUrl);
43
+ if (requestOrigin && requestOrigin === coreBaseUrl) {
44
+ return null;
45
+ }
46
+
47
+ if (classifyCoreDataKey(args.dbKey).owner !== "core") {
48
+ return null;
49
+ }
50
+
51
+ return coreBaseUrl;
52
+ }
53
+
54
+ export async function maybeProxyDatabaseRequestToCore(args: {
33
55
  req: {
34
56
  method?: string;
35
57
  url?: string | URL;
@@ -40,27 +62,22 @@ export async function maybeProxyDatabaseRequestToCore(args: {
40
62
  search: string;
41
63
  coreBaseUrl?: string | null;
42
64
  fetchImpl?: typeof fetch;
43
- }): Promise<Response | null> {
44
- const { req, pathname, search, fetchImpl = fetch } = args;
45
- const coreBaseUrl = normalizeOrigin(args.coreBaseUrl);
46
- if (!coreBaseUrl) {
47
- return null;
48
- }
49
-
50
- const requestOrigin = normalizeOrigin(req.url);
51
- if (requestOrigin && requestOrigin === coreBaseUrl) {
52
- return null;
53
- }
54
-
55
- const operation = pathname.slice("/api/v1/db/".length).split("/")[0] ?? "";
56
- const dbKey = getDbKeyForOperation({ operation, pathname, req });
57
- if (!dbKey) {
58
- return null;
59
- }
60
-
61
- if (classifyCoreDataKey(dbKey).owner !== "core") {
62
- return null;
63
- }
65
+ }): Promise<Response | null> {
66
+ const { req, pathname, search, fetchImpl = fetch } = args;
67
+ const operation = pathname.slice("/api/v1/db/".length).split("/")[0] ?? "";
68
+ const dbKey = getDbKeyForOperation({ operation, pathname, req });
69
+ if (!dbKey) {
70
+ return null;
71
+ }
72
+
73
+ const coreBaseUrl = resolveCoreDataProxyTarget({
74
+ dbKey,
75
+ requestUrl: req.url,
76
+ coreBaseUrl: args.coreBaseUrl,
77
+ });
78
+ if (!coreBaseUrl) {
79
+ return null;
80
+ }
64
81
 
65
82
  const headers = new Headers(req.headers ?? {});
66
83
  const init: RequestInit = {