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
@@ -1,9 +1,10 @@
1
- import { DEFAULT_LOCAL_API_ORIGIN } from "core/localOrigins";
2
- import { callToolApi } from "./toolApiClient";
1
+ import { DEFAULT_LOCAL_API_ORIGIN } from "core/localOrigins";
2
+ import { callToolApi } from "./toolApiClient";
3
+ import { ToolResultError } from "./toolResultError";
3
4
 
4
5
  type ExecShellKind = "auto" | "bash" | "powershell";
5
6
 
6
- type ExecBashRequestArgs = {
7
+ type ExecShellRequestArgs = {
7
8
  command?: string;
8
9
  cwd?: string;
9
10
  interactive?: boolean;
@@ -14,58 +15,7 @@ type ExecBashRequestArgs = {
14
15
  close?: boolean;
15
16
  };
16
17
 
17
- export const execBashFunctionSchema = {
18
- name: "execBash",
19
- description:
20
- "兼容旧 agent 的 shell 执行工具。底层会调用后端 shell 执行接口;在 Windows 上可能自动回退到 PowerShell。环境不明确时,先调用 checkEnv({check:'context'}) 判断当前平台与可用 shell。支持交互式 session:先用 interactive=true 启动,再通过 sessionId + input 继续写入 stdin,或用 sessionId 单独轮询输出。仅用于本地开发环境。对 rm 等危险命令默认拦截,需显式传 unsafe: true 才会执行。不要用它执行 rg/grep 代码搜索;代码搜索请改用 codeSearch。",
21
- parameters: {
22
- type: "object",
23
- properties: {
24
- command: {
25
- type: "string",
26
- description:
27
- "要执行的 shell 命令,例如: 'git status --short', 'bun test --watch=false'。",
28
- },
29
- cwd: {
30
- type: "string",
31
- description:
32
- "可选:命令执行的工作目录(相对于 diff-server 所在机器),默认使用后端的 process.cwd()。",
33
- },
34
- unsafe: {
35
- type: "boolean",
36
- description:
37
- "可选:是否允许执行被判定为危险的命令(例如 rm -rf)。默认 false;当为 false 且命令被识别为危险时,会直接拦截而不真正执行。",
38
- },
39
- interactive: {
40
- type: "boolean",
41
- description:
42
- "可选:是否以交互式 session 启动命令。为 true 时接口会立即返回 sessionId,后续可继续写 stdin 或轮询输出。",
43
- },
44
- pty: {
45
- type: "boolean",
46
- description:
47
- "可选:是否为交互式 session 分配伪终端(PTY)。适合 python -i、bash、node 等真正依赖终端语义的 REPL。",
48
- },
49
- sessionId: {
50
- type: "string",
51
- description:
52
- "可选:已存在的交互式 sessionId。传入后可继续写入 input、查询最新输出或关闭该 session。",
53
- },
54
- input: {
55
- type: "string",
56
- description:
57
- "可选:要写入 session stdin 的文本。通常需要自己补换行,例如 'y\\n' 或 'exit\\n'。",
58
- },
59
- close: {
60
- type: "boolean",
61
- description:
62
- "可选:是否关闭指定 session。通常与 sessionId 搭配使用。",
63
- },
64
- },
65
- },
66
- };
67
-
68
- export const execShellFunctionSchema = {
18
+ export const execShellFunctionSchema = {
69
19
  name: "execShell",
70
20
  description:
71
21
  "跨平台 shell 执行工具。会在 Windows 上优先使用 PowerShell,在 Linux/macOS 上使用 bash。环境不明确时,先调用 checkEnv({check:'context'}) 判断当前平台与可用 shell。支持交互式 session:先用 interactive=true 启动,再通过 sessionId + input 继续写入 stdin,或用 sessionId 单独轮询输出。仅用于本地开发环境。对 rm 等危险命令默认拦截,需显式传 unsafe: true 才会执行。不要用它执行 rg/grep 代码搜索;代码搜索请改用 codeSearch。",
@@ -143,9 +93,9 @@ function isDangerousCommand(rawCommand: string): boolean {
143
93
  }
144
94
 
145
95
  async function requestExec(
146
- args: ExecBashRequestArgs,
96
+ args: ExecShellRequestArgs,
147
97
  thunkApi?: any,
148
- endpoint = "/api/exec-bash",
98
+ endpoint = "/api/exec-shell",
149
99
  context?: { agentKey?: string },
150
100
  ): Promise<any> {
151
101
  const payload = Object.fromEntries(
@@ -163,12 +113,16 @@ async function requestExec(
163
113
  (typeof process !== "undefined" &&
164
114
  (process as any).env?.DIFF_SERVER_ORIGIN) ||
165
115
  (typeof window !== "undefined" ? window.location.origin : DEFAULT_LOCAL_API_ORIGIN);
166
- const url = `${baseUrl}${endpoint}`;
167
- const res = await fetch(url, {
168
- method: "POST",
169
- headers: { "Content-Type": "application/json" },
170
- body: JSON.stringify(payload),
171
- });
116
+ const url = `${baseUrl}${endpoint}`;
117
+ const headers: Record<string, string> = { "Content-Type": "application/json" };
118
+ if (typeof window !== "undefined" && (window as any).__NOLO_DESKTOP__) {
119
+ headers["X-Nolo-Desktop-Tool"] = "1";
120
+ }
121
+ const res = await fetch(url, {
122
+ method: "POST",
123
+ headers,
124
+ body: JSON.stringify(payload),
125
+ });
172
126
 
173
127
  const text = await res.text();
174
128
  try {
@@ -178,7 +132,7 @@ async function requestExec(
178
132
  }
179
133
  }
180
134
 
181
- function formatExecResult(prefixLabel: string, args: any, json: any, trimmedCommand: string, sessionId?: string) {
135
+ function formatExecResult(prefixLabel: string, args: any, json: any, trimmedCommand: string, sessionId?: string) {
182
136
  const prefix = json?.ok === false || json?.error ? `[${prefixLabel} 失败]` : `[${prefixLabel} 成功]`;
183
137
  const stdout = json.stdout ?? "";
184
138
  const stderr = json.stderr ?? "";
@@ -219,10 +173,20 @@ function formatExecResult(prefixLabel: string, args: any, json: any, trimmedComm
219
173
  ...json,
220
174
  },
221
175
  displayData: displayParts.join("\n"),
222
- };
223
- }
176
+ };
177
+ }
178
+
179
+ function throwExecToolError(toolName: "execShell", message: string, rawData?: any): never {
180
+ const displayData = `${toolName} 调用失败:${message}`;
181
+ throw new ToolResultError(displayData, {
182
+ code: "EXEC_TOOL_FAILED",
183
+ rawData: rawData ?? { error: displayData },
184
+ displayData,
185
+ retryable: true,
186
+ });
187
+ }
224
188
 
225
- function buildBlockedResponse(toolName: "execBash" | "execShell", trimmedCommand: string, cwd?: string, shell?: ExecShellKind) {
189
+ function buildBlockedResponse(toolName: "execShell", trimmedCommand: string, cwd?: string, shell?: ExecShellKind) {
226
190
  const lines: string[] = [];
227
191
  lines.push(`[${toolName} 拦截] 检测到潜在危险命令,默认不执行。`);
228
192
  lines.push(`command: ${trimmedCommand}`);
@@ -247,57 +211,7 @@ function buildBlockedResponse(toolName: "execBash" | "execShell", trimmedCommand
247
211
  };
248
212
  }
249
213
 
250
- export const startExecBashSession = (
251
- thunkApi: any,
252
- args: { command: string; cwd?: string; pty?: boolean }
253
- ) =>
254
- requestExec(
255
- {
256
- command: args.command,
257
- cwd: args.cwd,
258
- interactive: true,
259
- pty: args.pty,
260
- },
261
- thunkApi,
262
- );
263
-
264
- export const writeExecBashSession = (
265
- thunkApi: any,
266
- args: { sessionId: string; input: string }
267
- ) =>
268
- requestExec(
269
- {
270
- sessionId: args.sessionId,
271
- input: args.input,
272
- },
273
- thunkApi,
274
- );
275
-
276
- export const readExecBashSession = (
277
- thunkApi: any,
278
- args: { sessionId: string }
279
- ) =>
280
- requestExec(
281
- {
282
- sessionId: args.sessionId,
283
- },
284
- thunkApi,
285
- );
286
-
287
- export const closeExecBashSession = (
288
- thunkApi: any,
289
- args: { sessionId: string; input?: string }
290
- ) =>
291
- requestExec(
292
- {
293
- sessionId: args.sessionId,
294
- input: args.input,
295
- close: true,
296
- },
297
- thunkApi,
298
- );
299
-
300
- export const startExecShellSession = (
214
+ export const startExecShellSession = (
301
215
  thunkApi: any,
302
216
  args: { command: string; cwd?: string; pty?: boolean; shell?: ExecShellKind }
303
217
  ) =>
@@ -310,43 +224,8 @@ export const startExecShellSession = (
310
224
  shell: args.shell,
311
225
  },
312
226
  thunkApi,
313
- "/api/exec-shell",
314
- );
315
-
316
- export async function execBashFunc(args: any, thunkApi?: any, context?: { agentKey?: string }): Promise<{
317
- rawData: any;
318
- displayData: string;
319
- }> {
320
- const { command, cwd, unsafe, interactive, pty, sessionId, input, close } = args || {};
321
- const isSessionFollowup = typeof sessionId === "string" && sessionId.trim().length > 0;
322
-
323
- if (!isSessionFollowup && (!command || typeof command !== "string")) {
324
- const msg = "execBash 需要提供 command,或提供有效的 sessionId 来继续已有交互式 session。";
325
- return { rawData: { error: msg }, displayData: msg };
326
- }
327
-
328
- const trimmedCommand = typeof command === "string" ? command.trim() : "";
329
- if (!isSessionFollowup && !unsafe && isDangerousCommand(trimmedCommand)) {
330
- return buildBlockedResponse("execBash", trimmedCommand, cwd);
331
- }
332
-
333
- try {
334
- const json = await requestExec({
335
- ...(trimmedCommand ? { command: trimmedCommand } : {}),
336
- ...(cwd ? { cwd } : {}),
337
- ...(interactive ? { interactive: true } : {}),
338
- ...(pty ? { pty: true } : {}),
339
- ...(isSessionFollowup ? { sessionId } : {}),
340
- ...(typeof input === "string" ? { input } : {}),
341
- ...(close ? { close: true } : {}),
342
- }, thunkApi, "/api/exec-bash", context);
343
- return formatExecResult("execBash", { cwd }, json, trimmedCommand, sessionId);
344
- } catch (error: any) {
345
- const msg = `execBash 调用失败:${error?.message || String(error)}`;
346
- return { rawData: { error: msg }, displayData: msg };
347
- }
348
- }
349
-
227
+ );
228
+
350
229
  export async function execShellFunc(args: any, thunkApi?: any, context?: { agentKey?: string }): Promise<{
351
230
  rawData: any;
352
231
  displayData: string;
@@ -375,9 +254,13 @@ export async function execShellFunc(args: any, thunkApi?: any, context?: { agent
375
254
  ...(typeof input === "string" ? { input } : {}),
376
255
  ...(close ? { close: true } : {}),
377
256
  }, thunkApi, "/api/exec-shell", context);
378
- return formatExecResult("execShell", { cwd, shell }, json, trimmedCommand, sessionId);
379
- } catch (error: any) {
380
- const msg = `execShell 调用失败:${error?.message || String(error)}`;
381
- return { rawData: { error: msg }, displayData: msg };
382
- }
383
- }
257
+ if (json?.ok === false || json?.error) {
258
+ const message = typeof json?.error === "string" ? json.error : JSON.stringify(json?.error ?? json);
259
+ throwExecToolError("execShell", message, { command: trimmedCommand || null, ...json });
260
+ }
261
+ return formatExecResult("execShell", { cwd, shell }, json, trimmedCommand, sessionId);
262
+ } catch (error: any) {
263
+ if (error instanceof ToolResultError) throw error;
264
+ throwExecToolError("execShell", error?.message || String(error));
265
+ }
266
+ }
package/ai/tools/index.ts CHANGED
@@ -22,6 +22,7 @@ import {
22
22
  createWorkflowFunc,
23
23
  } from "./createWorkflowTool";
24
24
  import { delayFunctionSchema, delayFunc } from "./delayTool";
25
+ import { canonicalizeToolName } from "./toolNameAliases";
25
26
  import {
26
27
  completeDialogGoalFunc,
27
28
  completeDialogGoalFunctionSchema,
@@ -160,6 +161,10 @@ import {
160
161
  fetchWebpageFunctionSchema,
161
162
  fetchWebpageFunc,
162
163
  } from "./fetchWebpageTool";
164
+ import {
165
+ convertMarxistsBookToOfflineHtmlFunctionSchema,
166
+ convertMarxistsBookToOfflineHtmlFunc,
167
+ } from "./convertMarxistsBookTool";
163
168
  import {
164
169
  readXPostFunctionSchema,
165
170
  readXPostFunc,
@@ -281,7 +286,7 @@ import {
281
286
  rememberMemoryFunc,
282
287
  rememberMemoryFunctionSchema,
283
288
  } from "./rememberMemoryTool";
284
- import { execBashFunctionSchema, execBashFunc, execShellFunctionSchema, execShellFunc } from "./execBashTool";
289
+ import { execShellFunctionSchema, execShellFunc } from "./execShellTool";
285
290
  import { checkEnvFunctionSchema, checkEnvFunc } from "./checkEnvTool";
286
291
 
287
292
  import { agentToolDefinitions } from "./agent/agentTools";
@@ -1464,6 +1469,23 @@ const baseToolDefinitions: ToolDefinition[] = [
1464
1469
  },
1465
1470
  behavior: "data",
1466
1471
  },
1472
+ {
1473
+ id: "convertMarxistsBookToOfflineHtml",
1474
+ schema: convertMarxistsBookToOfflineHtmlFunctionSchema,
1475
+ executor: convertMarxistsBookToOfflineHtmlFunc,
1476
+ description: {
1477
+ name: "convertMarxistsBookToOfflineHtml",
1478
+ description:
1479
+ "将 Marxists.org 中文旧式书籍页面转换成保留原 CSS、背景和排版的离线单 HTML。",
1480
+ category: "网络与智能",
1481
+ },
1482
+ behavior: "data",
1483
+ capability: "web_access",
1484
+ riskLevel: "low",
1485
+ costLevel: "medium",
1486
+ defaultConsent: "auto",
1487
+ cancelable: true,
1488
+ },
1467
1489
  {
1468
1490
  id: "readXPost",
1469
1491
  schema: readXPostFunctionSchema,
@@ -1837,18 +1859,6 @@ const baseToolDefinitions: ToolDefinition[] = [
1837
1859
  behavior: "data",
1838
1860
  uiGroup: "general",
1839
1861
  },
1840
- {
1841
- id: "execBash",
1842
- schema: execBashFunctionSchema,
1843
- executor: execBashFunc,
1844
- description: {
1845
- name: "exec_bash",
1846
- description:
1847
- "通过后端 diff-server 的 /api/exec-bash 接口执行一条 bash 命令,仅用于本地开发调试。",
1848
- category: "网络与智能",
1849
- },
1850
- behavior: "action",
1851
- },
1852
1862
  {
1853
1863
  id: "execShell",
1854
1864
  schema: execShellFunctionSchema,
@@ -2070,7 +2080,7 @@ export const findToolExecutor = (
2070
2080
  executor: ToolDefinition["executor"];
2071
2081
  canonicalName: string;
2072
2082
  } => {
2073
- const normalizedRawName = normalizeToolName(rawName);
2083
+ const normalizedRawName = normalizeToolName(canonicalizeToolName(rawName));
2074
2084
  const canonicalName = Object.keys(toolExecutors).find(
2075
2085
  (key) => normalizeToolName(key) === normalizedRawName
2076
2086
  );
@@ -0,0 +1,253 @@
1
+ export type MarxistsOfflineBookPage = {
2
+ name: string;
3
+ url: string;
4
+ title: string;
5
+ };
6
+
7
+ export type MarxistsOfflineBookResult = {
8
+ title: string;
9
+ startUrl: string;
10
+ indexUrl: string;
11
+ stylesheetUrl: string | null;
12
+ pages: MarxistsOfflineBookPage[];
13
+ html: string;
14
+ validation: {
15
+ pageCount: number;
16
+ hasEmbeddedAssets: boolean;
17
+ hasNetworkUrls: boolean;
18
+ hasUtf8Meta: boolean;
19
+ byteLength: number;
20
+ };
21
+ };
22
+
23
+ export type ConvertMarxistsBookArgs = {
24
+ startUrl: string;
25
+ encoding?: string;
26
+ fetchImpl?: typeof fetch;
27
+ maxPages?: number;
28
+ };
29
+
30
+ const DEFAULT_ENCODING = "gb2312";
31
+ const USER_AGENT =
32
+ "Mozilla/5.0 (compatible; NoloMarxistsOfflineBook/1.0; +https://nolo.chat)";
33
+
34
+ const normalizeBookBaseUrl = (startUrl: string): URL => {
35
+ const parsed = new URL(startUrl);
36
+ if (!/^https?:$/i.test(parsed.protocol)) {
37
+ throw new Error("startUrl must be an http or https URL.");
38
+ }
39
+ return new URL("./", parsed);
40
+ };
41
+
42
+ const decodeBytes = (bytes: ArrayBuffer, encoding: string): string => {
43
+ try {
44
+ return new TextDecoder(encoding).decode(bytes);
45
+ } catch {
46
+ return new TextDecoder(DEFAULT_ENCODING).decode(bytes);
47
+ }
48
+ };
49
+
50
+ const fetchBytes = async (
51
+ url: string,
52
+ fetchImpl: typeof fetch,
53
+ ): Promise<ArrayBuffer> => {
54
+ const response = await fetchImpl(url, {
55
+ headers: {
56
+ "user-agent": USER_AGENT,
57
+ accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
58
+ },
59
+ });
60
+ if (!response.ok) {
61
+ throw new Error(`Failed to fetch ${url}: HTTP ${response.status}`);
62
+ }
63
+ return response.arrayBuffer();
64
+ };
65
+
66
+ const extractTitle = (html: string): string => {
67
+ return (
68
+ html.match(/<title>([\s\S]*?)<\/title>/i)?.[1]
69
+ ?.replace(/<[^>]+>/g, " ")
70
+ .replace(/\s+/g, " ")
71
+ .trim() || ""
72
+ );
73
+ };
74
+
75
+ const extractBody = (html: string): string => {
76
+ return html.match(/<body[^>]*>([\s\S]*?)<\/body>/i)?.[1] ?? html;
77
+ };
78
+
79
+ export const discoverMarxistsBookPageNames = (
80
+ indexHtml: string,
81
+ maxPages = 80,
82
+ ): string[] => {
83
+ const pageNames = [...indexHtml.matchAll(/href=["']?([^"' >#]+\.htm)(#[^"' >]*)?["']?/gi)]
84
+ .map((match) => match[1])
85
+ .filter((href) => !href.includes("/") && !href.startsWith(".."))
86
+ .map((href) => href.trim())
87
+ .filter(Boolean)
88
+ .sort();
89
+
90
+ const uniqueNames = Array.from(new Set(["index.htm", ...pageNames]));
91
+ if (uniqueNames.length <= 1) {
92
+ throw new Error("No same-directory .htm chapter links were found in index.htm.");
93
+ }
94
+ if (uniqueNames.length > maxPages) {
95
+ throw new Error(
96
+ `Refusing to download ${uniqueNames.length} pages; maxPages is ${maxPages}.`,
97
+ );
98
+ }
99
+ return uniqueNames;
100
+ };
101
+
102
+ const extractStylesheetHref = (html: string): string | null => {
103
+ return (
104
+ html.match(/<link[^>]+href=["']([^"']+\.css)["'][^>]*>/i)?.[1] ?? null
105
+ );
106
+ };
107
+
108
+ const guessMimeType = (url: string): string => {
109
+ const pathname = new URL(url).pathname.toLowerCase();
110
+ if (pathname.endsWith(".gif")) return "image/gif";
111
+ if (pathname.endsWith(".png")) return "image/png";
112
+ if (pathname.endsWith(".jpg") || pathname.endsWith(".jpeg")) return "image/jpeg";
113
+ if (pathname.endsWith(".webp")) return "image/webp";
114
+ if (pathname.endsWith(".css")) return "text/css";
115
+ return "application/octet-stream";
116
+ };
117
+
118
+ export const inlineCssUrlAssets = async (args: {
119
+ css: string;
120
+ cssUrl: string;
121
+ fetchImpl: typeof fetch;
122
+ }): Promise<string> => {
123
+ const replacements = new Map<string, string>();
124
+ const matches = [...args.css.matchAll(/url\((['"]?)([^)'"]+)\1\)/gi)];
125
+
126
+ for (const match of matches) {
127
+ const rawRef = match[2].trim();
128
+ if (!rawRef || /^(data:|https?:)/i.test(rawRef)) continue;
129
+ const assetUrl = new URL(rawRef, args.cssUrl).toString();
130
+ if (!replacements.has(rawRef)) {
131
+ const bytes = await fetchBytes(assetUrl, args.fetchImpl);
132
+ const base64 = Buffer.from(bytes).toString("base64");
133
+ replacements.set(
134
+ rawRef,
135
+ `data:${guessMimeType(assetUrl)};base64,${base64}`,
136
+ );
137
+ }
138
+ }
139
+
140
+ return args.css.replace(/url\((['"]?)([^)'"]+)\1\)/gi, (full, quote, rawRef) => {
141
+ const replacement = replacements.get(String(rawRef).trim());
142
+ return replacement ? `url('${replacement}')` : full;
143
+ });
144
+ };
145
+
146
+ const rewriteLocalChapterLinks = (body: string): string => {
147
+ return body.replace(
148
+ /href=["']([^"']+\.htm)(#[^"']*)?["']/gi,
149
+ (_full, href: string, hash: string | undefined) => {
150
+ const anchor = href.replace(/\.htm$/i, "");
151
+ return `href="#${anchor}${hash ? `-${hash.replace(/^#/, "")}` : ""}"`;
152
+ },
153
+ );
154
+ };
155
+
156
+ const buildSingleHtml = (args: {
157
+ title: string;
158
+ css: string;
159
+ pages: Array<MarxistsOfflineBookPage & { body: string }>;
160
+ }): string => {
161
+ const sections = args.pages.map((page) => {
162
+ const anchor = page.name.replace(/\.htm$/i, "");
163
+ return `<section class="chapter" id="${anchor}"><div class="chapter-source">${page.title}</div>${rewriteLocalChapterLinks(page.body)}</section>`;
164
+ });
165
+
166
+ return `<!doctype html>
167
+ <html lang="zh-CN">
168
+ <head>
169
+ <meta charset="utf-8">
170
+ <meta name="viewport" content="width=device-width, initial-scale=1">
171
+ <title>${args.title}</title>
172
+ <style>
173
+ ${args.css}
174
+ body { margin: 0 10px; }
175
+ .table1 { max-width: 960px; width: min(960px, calc(100vw - 20px)); }
176
+ .chapter { page-break-after: always; break-after: page; }
177
+ .chapter-source { display: none; }
178
+ @media screen and (max-width: 700px) {
179
+ html { margin-left: 8px !important; margin-right: 8px !important; }
180
+ body { margin-left: 8px !important; margin-right: 8px !important; }
181
+ .table1 { width: 100% !important; max-width: 100% !important; }
182
+ table { max-width: 100% !important; }
183
+ }
184
+ @media print {
185
+ .chapter { page-break-after: always; }
186
+ }
187
+ </style>
188
+ </head>
189
+ <body id="top">
190
+ ${sections.join("\n")}
191
+ </body>
192
+ </html>`;
193
+ };
194
+
195
+ export async function convertMarxistsBookToOfflineHtml(
196
+ args: ConvertMarxistsBookArgs,
197
+ ): Promise<MarxistsOfflineBookResult> {
198
+ const startUrl = args.startUrl?.trim();
199
+ if (!startUrl) throw new Error("startUrl is required.");
200
+
201
+ const fetchImpl = args.fetchImpl ?? fetch;
202
+ const encoding = args.encoding || DEFAULT_ENCODING;
203
+ const bookBaseUrl = normalizeBookBaseUrl(startUrl);
204
+ const indexUrl = new URL("index.htm", bookBaseUrl).toString();
205
+ const indexHtml = decodeBytes(await fetchBytes(indexUrl, fetchImpl), encoding);
206
+ const pageNames = discoverMarxistsBookPageNames(indexHtml, args.maxPages);
207
+
208
+ const pages: Array<MarxistsOfflineBookPage & { body: string }> = [];
209
+ for (const name of pageNames) {
210
+ const pageUrl = new URL(name, bookBaseUrl).toString();
211
+ const html = decodeBytes(await fetchBytes(pageUrl, fetchImpl), encoding);
212
+ pages.push({
213
+ name,
214
+ url: pageUrl,
215
+ title: extractTitle(html),
216
+ body: extractBody(html),
217
+ });
218
+ }
219
+
220
+ const stylesheetHref =
221
+ extractStylesheetHref(indexHtml) ||
222
+ pages.map((page) => extractStylesheetHref(page.body)).find(Boolean) ||
223
+ "../../MIA01.css";
224
+ const stylesheetUrl = stylesheetHref
225
+ ? new URL(stylesheetHref, indexUrl).toString()
226
+ : null;
227
+ const rawCss = stylesheetUrl
228
+ ? decodeBytes(await fetchBytes(stylesheetUrl, fetchImpl), encoding)
229
+ : "";
230
+ const css = stylesheetUrl
231
+ ? await inlineCssUrlAssets({ css: rawCss, cssUrl: stylesheetUrl, fetchImpl })
232
+ : rawCss;
233
+
234
+ const title = extractTitle(indexHtml) || pages[0]?.title || "Offline Book";
235
+ const html = buildSingleHtml({ title, css, pages });
236
+ const byteLength = new TextEncoder().encode(html).byteLength;
237
+
238
+ return {
239
+ title,
240
+ startUrl,
241
+ indexUrl,
242
+ stylesheetUrl,
243
+ pages: pages.map(({ body: _body, ...page }) => page),
244
+ html,
245
+ validation: {
246
+ pageCount: pages.length,
247
+ hasEmbeddedAssets: html.includes("data:image/"),
248
+ hasNetworkUrls: /https?:\/\//i.test(html),
249
+ hasUtf8Meta: html.includes('<meta charset="utf-8">'),
250
+ byteLength,
251
+ },
252
+ };
253
+ }
@@ -52,11 +52,13 @@ export const queryModelUsageFunctionSchema = {
52
52
  },
53
53
  thresholdCredits: {
54
54
  type: "number",
55
- description: "可选阈值,单位 credits;返回 threshold.exceeded。",
55
+ description:
56
+ "可选阈值,单位 credits;返回 threshold.exceeded 与 0-100 的 threshold.usedPercent。",
56
57
  },
57
58
  thresholdUsd: {
58
59
  type: "number",
59
- description: "可选阈值,单位 USD;会按 creditsPerUsd 换算。",
60
+ description:
61
+ "可选阈值,单位 USD;会按 creditsPerUsd 换算,并返回 0-100 的 threshold.usedPercent。",
60
62
  },
61
63
  },
62
64
  },
@@ -179,6 +181,11 @@ export const notifyUserFunctionSchema = {
179
181
  type: "string",
180
182
  description: "可选跳转链接。",
181
183
  },
184
+ targetUserId: {
185
+ type: "string",
186
+ description:
187
+ "可选目标用户 ID。默认通知当前用户;只有受信任的系统 agent 或管理员可以通知其他用户。",
188
+ },
182
189
  },
183
190
  required: ["title", "message"],
184
191
  },
@@ -9,6 +9,7 @@ import { isTableMetaKey, rowKey, isAgentKey } from "database/keys";
9
9
  import { DataType } from "create/types";
10
10
  import { TableMeta } from "render/table/types";
11
11
  import { fetchAndSerializeTable } from "render/table/utils/tableSerialization";
12
+ import { readFileFunc } from "./readFileTool";
12
13
 
13
14
  // ---- Types ----
14
15
 
@@ -24,6 +25,13 @@ export type ReadArgs = {
24
25
  * - true:使用 readAndWaitAction,等待远程与本地决策后返回“权威”结果
25
26
  */
26
27
  waitRemote?: boolean;
28
+
29
+ /**
30
+ * Backward-compatible escape hatch for models that call read with a local
31
+ * file path even when the readFile tool is available.
32
+ */
33
+ filePath?: string;
34
+ path?: string;
27
35
  };
28
36
 
29
37
  // ---- 工具 Schema,供 LLM 调用 ----
@@ -68,6 +76,29 @@ export async function readFunc(
68
76
  context?: { parentMessageId?: string; signal?: AbortSignal }
69
77
  ): Promise<{ rawData: any; displayData?: string }> {
70
78
  const { dbKey, waitRemote = false } = args || {};
79
+ const localFilePath =
80
+ typeof args?.filePath === "string"
81
+ ? args.filePath
82
+ : typeof args?.path === "string"
83
+ ? args.path
84
+ : "";
85
+
86
+ if (!dbKey && localFilePath) {
87
+ const result = await readFileFunc(
88
+ {
89
+ filePath: localFilePath,
90
+ startLine: (args as any).startLine,
91
+ endLine: (args as any).endLine,
92
+ },
93
+ thunkApi,
94
+ context
95
+ );
96
+ return {
97
+ rawData: result,
98
+ displayData:
99
+ typeof result === "string" ? result : JSON.stringify(result, null, 2),
100
+ };
101
+ }
71
102
 
72
103
  if (!dbKey || typeof dbKey !== "string") {
73
104
  throw new Error("read 工具需要提供字符串类型的 id(dbKey)。");