neoctl 0.2.13 → 0.2.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/web/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import http from "node:http";
3
3
  import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
- import { existsSync, readFileSync } from "node:fs";
5
+ import { createReadStream, existsSync, readFileSync } from "node:fs";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { createRequire } from "node:module";
8
8
  import { QueryEngine } from "../core/query-engine.js";
@@ -28,9 +28,14 @@ import { createTaskTools } from "../tasks/task-tools.js";
28
28
  import { TaskStore } from "../tasks/task-store.js";
29
29
  import { parseReplCommand, helpText, replCommandDefinitions } from "../repl/commands.js";
30
30
  import { writeSessionMarkdownExport } from "../session/session-export.js";
31
+ import { DefaultContextManager } from "../context/context-manager.js";
32
+ import { buildEffectiveSystemPrompt } from "../context/prompts.js";
31
33
  import { WEB_HTML } from "./html.js";
32
34
  import { openDirectory } from "../open-directory.js";
33
- import { resolveImageBlockDataSync } from "../core/image-storage.js";
35
+ import { getNeoctlHome } from "../paths.js";
36
+ import { FileSystemSkillCatalog } from "../skills/skill-filesystem.js";
37
+ import { createSkillTool } from "../skills/skill-tool.js";
38
+ import { createSkillManagementTools } from "../skills/skill-management-tools.js";
34
39
  const require = createRequire(import.meta.url);
35
40
  const markedPackageDir = path.dirname(require.resolve("marked/package.json"));
36
41
  const highlightPackageDir = path.dirname(require.resolve("@highlightjs/cdn-assets/package.json"));
@@ -82,9 +87,9 @@ function sumUsageTokens(left, right) {
82
87
  return (left ?? 0) + (right ?? 0);
83
88
  }
84
89
  const DEFAULT_WEB_RUNTIME_KEY = "__default__";
85
- export async function runWebServer(argv = process.argv.slice(2)) {
90
+ export async function runWebServer(argv = process.argv.slice(2), runtimeOptions = {}) {
86
91
  const options = parseWebArgs(argv);
87
- const router = await createWebRuntimeRouter();
92
+ const router = await createWebRuntimeRouter(runtimeOptions);
88
93
  const server = http.createServer((req, res) => void route(req, res, router));
89
94
  await new Promise((resolve) => server.listen(options.port, options.host, resolve));
90
95
  const address = server.address();
@@ -109,13 +114,98 @@ function parseWebArgs(argv) {
109
114
  port = 3000;
110
115
  return { host, port: Math.round(port) };
111
116
  }
117
+ class SkillCatalogContextManager {
118
+ catalog;
119
+ base;
120
+ constructor(catalog, base = new DefaultContextManager()) {
121
+ this.catalog = catalog;
122
+ this.base = base;
123
+ }
124
+ async build(input) {
125
+ const runtimeContext = await this.base.build(input);
126
+ const skillSection = await buildSkillCatalogPromptSection(this.catalog);
127
+ if (!skillSection)
128
+ return runtimeContext;
129
+ const promptSections = [...runtimeContext.promptSections, skillSection];
130
+ return {
131
+ ...runtimeContext,
132
+ promptSections,
133
+ systemPrompt: buildEffectiveSystemPrompt(promptSections, input),
134
+ };
135
+ }
136
+ }
137
+ async function buildSkillCatalogPromptSection(catalog) {
138
+ const skills = await catalog.list();
139
+ if (skills.length === 0)
140
+ return undefined;
141
+ const visible = skills.slice(0, 80);
142
+ const lines = visible.map((skill) => {
143
+ const tags = skill.tags?.length ? `; tags=${skill.tags.join(",")}` : "";
144
+ const tools = skill.allowedTools?.length ? `; allowedTools=${skill.allowedTools.join(",")}` : "";
145
+ return `- ${skill.name}: ${skill.description} (execution=${skill.execution}${tags}${tools})`;
146
+ });
147
+ if (skills.length > visible.length)
148
+ lines.push(`- ... ${skills.length - visible.length} more skills available; use skill_list for the full catalog.`);
149
+ return {
150
+ name: "Available Skills",
151
+ cacheStable: false,
152
+ content: [
153
+ "Reusable skills are available through the `skill` tool and the /skill UI command.",
154
+ "When the user's task matches a skill name, description, tags, or domain capability, proactively call the `skill` tool before doing the work directly.",
155
+ "Do not wait for the user to explicitly say 'use skill'. Use skill_list/skill_read if you need to inspect details.",
156
+ "Available skill catalog:",
157
+ ...lines,
158
+ ].join("\n"),
159
+ };
160
+ }
161
+ function resolveSkillCatalogRoots(cwd, extraRoots = [], createRoot) {
162
+ const userRoot = path.resolve(process.env.NEO_SKILL_CREATE_ROOT || createRoot || path.join(getNeoctlHome(), "skills"));
163
+ const configuredRoots = splitPathList(process.env.NEO_SKILL_ROOTS);
164
+ const workspaceRoot = path.resolve(cwd, ".neo", "skills");
165
+ const roots = uniquePaths([
166
+ userRoot,
167
+ ...extraRoots,
168
+ ...configuredRoots,
169
+ workspaceRoot,
170
+ ]).map((root) => ({
171
+ root,
172
+ kind: path.resolve(root) === userRoot ? "user" : "workspace",
173
+ }));
174
+ return { roots, createRoot: userRoot };
175
+ }
176
+ function splitPathList(value) {
177
+ return String(value || "")
178
+ .split(path.delimiter)
179
+ .map((item) => item.trim())
180
+ .filter(Boolean)
181
+ .map((item) => path.resolve(item));
182
+ }
183
+ function uniquePaths(values) {
184
+ const seen = new Set();
185
+ const result = [];
186
+ for (const value of values) {
187
+ const resolved = path.resolve(value);
188
+ const key = process.platform === "win32" ? resolved.toLowerCase() : resolved;
189
+ if (seen.has(key))
190
+ continue;
191
+ seen.add(key);
192
+ result.push(resolved);
193
+ }
194
+ return result;
195
+ }
112
196
  export async function createWebRuntime(options = {}) {
113
197
  const envLoad = loadDefaultDotEnvFiles({ override: true });
198
+ const cwd = options.cwd ? path.resolve(options.cwd) : process.cwd();
114
199
  const modelConfig = readModelProviderConfig(process.env);
115
200
  const communicationLogger = new CommunicationLogger();
116
201
  const modelGateway = new LoggingModelGateway(createModelGatewayFromProcessEnv(process.env), communicationLogger);
117
202
  const taskStore = new TaskStore();
118
203
  const tools = new ToolRegistry();
204
+ const { roots: skillRoots, createRoot: skillCreateRoot } = resolveSkillCatalogRoots(cwd, options.skillRoots, options.skillCreateRoot);
205
+ const skills = new FileSystemSkillCatalog({
206
+ roots: skillRoots,
207
+ createRoot: skillCreateRoot,
208
+ });
119
209
  tools.register(editTool);
120
210
  tools.register(writeTool);
121
211
  tools.register(createExecTool({ taskStore }));
@@ -128,13 +218,18 @@ export async function createWebRuntime(options = {}) {
128
218
  if (modelConfig?.provider === "openai")
129
219
  tools.register(createOpenAIImageGenerationTool());
130
220
  tools.register(planTool);
221
+ tools.register(createSkillTool(skills));
222
+ for (const tool of createSkillManagementTools(skills, { requireApproval: true, allowDelete: false }))
223
+ tools.register(tool);
224
+ for (const tool of options.externalTools ?? [])
225
+ tools.register(tool);
131
226
  const agentRuntime = { modelGateway, tools, taskStore };
132
227
  tools.register(createAgentTool(agentRuntime));
133
228
  const resumeHandler = async (taskId, directive) => {
134
229
  const dummyContext = {
135
230
  agentId: "main",
136
231
  tools,
137
- appState: new InMemoryAppState("main"),
232
+ appState: new InMemoryAppState("main", cwd),
138
233
  emit: () => undefined,
139
234
  };
140
235
  return resumeAgentTask(taskId, directive, agentRuntime, taskStore, dummyContext);
@@ -144,6 +239,7 @@ export async function createWebRuntime(options = {}) {
144
239
  const appPromptStore = new InMemoryAppPromptStore();
145
240
  const engine = new QueryEngine({
146
241
  agentId: options.agentId ?? "main",
242
+ cwd,
147
243
  model: modelConfig?.model,
148
244
  fallbackModel: modelConfig?.fallbackModel,
149
245
  reasoning: modelConfig?.defaultReasoning,
@@ -151,6 +247,8 @@ export async function createWebRuntime(options = {}) {
151
247
  modelGateway,
152
248
  tools,
153
249
  appPromptStore,
250
+ contextManager: new SkillCatalogContextManager(skills),
251
+ skills: (await skills.list()).map((skill) => skill.name),
154
252
  taskNotificationSource: createTaskNotificationSource(taskStore),
155
253
  commands: replCommandDefinitions.map((command) => command.usage),
156
254
  session: {
@@ -504,7 +602,7 @@ export class WebRepl {
504
602
  this.broadcastSync();
505
603
  }
506
604
  setStatus(next) {
507
- this.status = next;
605
+ this.status = next.phase === "running_tools" ? next : { ...next, currentTool: undefined };
508
606
  this.broadcastSync();
509
607
  }
510
608
  finalizeForegroundView() {
@@ -674,10 +772,13 @@ export class WebRepl {
674
772
  if (event.type === "tool.started") {
675
773
  this.finalizeLiveLine(this.assistantLineId);
676
774
  this.finalizeThinkingLine();
775
+ this.broadcastSync();
677
776
  return;
678
777
  }
679
- if (event.type === "tool.finished")
778
+ if (event.type === "tool.finished") {
779
+ this.broadcastSync();
680
780
  return;
781
+ }
681
782
  if (event.type === "terminal") {
682
783
  this.finalizeLiveLine(this.assistantLineId);
683
784
  this.finalizeThinkingLine();
@@ -785,6 +886,8 @@ export class WebRepl {
785
886
  }
786
887
  const promptPayload = buildWebPromptPayload(command.text, attachments);
787
888
  this.append({ kind: "user", text: promptPayload.displayText });
889
+ for (const line of imageLinesForBlocks("user", promptPayload.blocks))
890
+ this.append(line);
788
891
  const runToken = ++this.foregroundRunToken;
789
892
  const abortController = new AbortController();
790
893
  this.activeAbortController = abortController;
@@ -876,10 +979,14 @@ export class WebRepl {
876
979
  res.write(`event: ${event}\n`);
877
980
  res.write(`data: ${JSON.stringify(data)}\n\n`);
878
981
  }
982
+ sendSerialized(res, event, data) {
983
+ res.write(`event: ${event}\n`);
984
+ res.write(`data: ${data}\n\n`);
985
+ }
879
986
  broadcastSync() {
880
- const payload = this.snapshot(false);
987
+ const payload = JSON.stringify(this.snapshot(false));
881
988
  for (const res of this.subscribers)
882
- this.send(res, "sync", payload);
989
+ this.sendSerialized(res, "sync", payload);
883
990
  }
884
991
  }
885
992
  function reqKeepAlive(res) {
@@ -898,6 +1005,8 @@ async function route(req, res, router) {
898
1005
  return sendFile(res, highlightAssetPath, "text/javascript; charset=utf-8");
899
1006
  if (req.method === "GET" && url.pathname === "/vendor/highlight-theme.css")
900
1007
  return sendFile(res, highlightThemeAssetPath, "text/css; charset=utf-8");
1008
+ if (req.method === "GET" && url.pathname === "/api/images")
1009
+ return sendImage(res, url.searchParams.get("path"), url.searchParams.get("mime"));
901
1010
  const scope = webRuntimeScopeFromUrl(url);
902
1011
  const repl = await router.get(scope);
903
1012
  if (req.method === "GET" && url.pathname === "/events")
@@ -961,6 +1070,23 @@ async function sendFile(res, filepath, contentType) {
961
1070
  res.writeHead(200, { "Content-Type": contentType, "Cache-Control": "public, max-age=3600" });
962
1071
  res.end(body);
963
1072
  }
1073
+ async function sendImage(res, encodedPath, mimeType) {
1074
+ const filepath = decodeImagePath(encodedPath);
1075
+ if (!filepath)
1076
+ return sendJson(res, { error: "missing image path" }, 400);
1077
+ const contentType = safeImageContentType(mimeType);
1078
+ const binaryPath = filepath.endsWith(".base64.txt") ? filepath.slice(0, -".base64.txt".length) : filepath;
1079
+ if (existsSync(binaryPath)) {
1080
+ res.writeHead(200, { "Content-Type": contentType, "Cache-Control": "no-store" });
1081
+ createReadStream(binaryPath).pipe(res);
1082
+ return;
1083
+ }
1084
+ if (!existsSync(filepath))
1085
+ return sendJson(res, { error: "image not found" }, 404);
1086
+ const base64 = (await fs.readFile(filepath, "utf8")).trim();
1087
+ res.writeHead(200, { "Content-Type": contentType, "Cache-Control": "no-store" });
1088
+ res.end(Buffer.from(stripDataUrlPrefix(base64), "base64"));
1089
+ }
964
1090
  function sendJson(res, value, status = 200) {
965
1091
  res.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" });
966
1092
  res.end(JSON.stringify(value));
@@ -1063,7 +1189,11 @@ function pushTextBlock(blocks, text) {
1063
1189
  }
1064
1190
  function reduceStatus(status, event) {
1065
1191
  if (event.type === "state")
1066
- return { ...status, phase: event.phase, detail: event.detail, usage: event.phase === "preparing" ? undefined : status.usage, streamedOutputTokens: event.phase === "preparing" ? 0 : status.streamedOutputTokens, inputTokenUpdatedAt: event.phase === "preparing" ? undefined : status.inputTokenUpdatedAt, outputTokenUpdatedAt: event.phase === "preparing" ? undefined : status.outputTokenUpdatedAt, retryCooldownUntil: event.phase === "preparing" ? undefined : status.retryCooldownUntil, activityTick: status.activityTick + 1 };
1192
+ return { ...status, phase: event.phase, detail: event.detail, currentTool: event.phase === "preparing" || event.phase === "calling_model" || event.phase === "ready" ? undefined : status.currentTool, usage: event.phase === "preparing" ? undefined : status.usage, streamedOutputTokens: event.phase === "preparing" ? 0 : status.streamedOutputTokens, inputTokenUpdatedAt: event.phase === "preparing" ? undefined : status.inputTokenUpdatedAt, outputTokenUpdatedAt: event.phase === "preparing" ? undefined : status.outputTokenUpdatedAt, retryCooldownUntil: event.phase === "preparing" ? undefined : status.retryCooldownUntil, activityTick: status.activityTick + 1 };
1193
+ if (event.type === "tool.started")
1194
+ return { ...status, phase: "running_tools", detail: event.toolUse.name, currentTool: { id: event.toolUse.id, name: event.toolUse.name, kind: toolKindForToolUse(event.toolUse.name, event.toolUse.input), startedAt: Date.now() }, activityTick: status.activityTick + 1 };
1195
+ if (event.type === "tool.finished")
1196
+ return { ...status, currentTool: status.currentTool?.id === event.toolUse.id ? undefined : status.currentTool, activityTick: status.activityTick + 1 };
1067
1197
  if (event.type === "context.metrics")
1068
1198
  return { ...status, metrics: event.metrics, inputTokenUpdatedAt: event.metrics.estimatedInputTokens !== status.metrics?.estimatedInputTokens ? Date.now() : status.inputTokenUpdatedAt, activityTick: status.activityTick + 1 };
1069
1199
  if (event.type === "usage")
@@ -1077,11 +1207,26 @@ function reduceStatus(status, event) {
1077
1207
  if (event.type === "retrying")
1078
1208
  return { ...status, phase: "calling_model", detail: `retrying in ${(event.delayMs / 1000).toFixed(1)}s`, retryCooldownUntil: Date.now() + event.delayMs, activityTick: status.activityTick + 1 };
1079
1209
  if (event.type === "terminal")
1080
- return { ...status, phase: "stopped", detail: event.reason, inputTokenUpdatedAt: undefined, outputTokenUpdatedAt: undefined, retryCooldownUntil: undefined, activityTick: status.activityTick + 1 };
1081
- if (event.type === "message" || event.type === "tool.started" || event.type === "tool.finished" || event.type === "error")
1210
+ return { ...status, phase: "stopped", detail: event.reason, currentTool: undefined, inputTokenUpdatedAt: undefined, outputTokenUpdatedAt: undefined, retryCooldownUntil: undefined, activityTick: status.activityTick + 1 };
1211
+ if (event.type === "message" || event.type === "error")
1082
1212
  return { ...status, activityTick: status.activityTick + 1 };
1083
1213
  return status;
1084
1214
  }
1215
+ function toolKindForToolUse(toolName, input) {
1216
+ if (toolName === "image2")
1217
+ return isRecord(input) && input.mode === "edit" ? "修图" : "作图";
1218
+ if (toolName === "edit" || toolName === "write" || toolName.includes("artifact_editor") || toolName.includes("apply_patch"))
1219
+ return "编辑";
1220
+ if (toolName === "exec" || toolName.includes("shell") || toolName.includes("command"))
1221
+ return "执行";
1222
+ if (toolName.includes("download"))
1223
+ return "下载";
1224
+ if (toolName === "read" || toolName === "list" || toolName === "grep" || toolName === "search" || toolName.includes("query") || toolName.includes("load"))
1225
+ return "查询";
1226
+ if (toolName === "plan")
1227
+ return "计划";
1228
+ return "工具";
1229
+ }
1085
1230
  async function handleExportCommand(outputPath, runtime) {
1086
1231
  const snapshot = runtime.engine.snapshot();
1087
1232
  if (!snapshot.session)
@@ -1466,6 +1611,14 @@ function renderMessageImages(message, append) {
1466
1611
  }
1467
1612
  return rendered;
1468
1613
  }
1614
+ function imageLinesForBlocks(role, blocks) {
1615
+ if (!blocks?.length)
1616
+ return [];
1617
+ return blocks
1618
+ .filter((block) => block.type === "image")
1619
+ .map((block) => imageLineForBlock(role, block))
1620
+ .filter((line) => Boolean(line));
1621
+ }
1469
1622
  function imageLineForBlock(role, block) {
1470
1623
  const kind = kindForRole(role);
1471
1624
  if (kind === "meta")
@@ -1474,20 +1627,43 @@ function imageLineForBlock(role, block) {
1474
1627
  kind,
1475
1628
  text: block.label ?? `[image ${block.mimeType}]`,
1476
1629
  image: {
1477
- src: imageBlockToDataUrl(block),
1630
+ src: imageBlockToSrc(block),
1478
1631
  label: block.label,
1479
1632
  mimeType: block.mimeType,
1480
1633
  },
1481
1634
  };
1482
1635
  }
1483
- function imageBlockToDataUrl(block) {
1484
- const data = resolveImageBlockDataSync(block);
1636
+ function imageBlockToSrc(block) {
1637
+ if (block.storage?.path) {
1638
+ return `/api/images?path=${encodeImagePath(block.storage.path)}&mime=${encodeURIComponent(block.mimeType)}`;
1639
+ }
1640
+ const data = block.data.trim();
1485
1641
  if (!data)
1486
1642
  return "";
1487
1643
  if (data.startsWith("data:"))
1488
1644
  return data;
1489
1645
  return `data:${block.mimeType};base64,${data}`;
1490
1646
  }
1647
+ function encodeImagePath(filepath) {
1648
+ return Buffer.from(filepath, "utf8").toString("base64url");
1649
+ }
1650
+ function decodeImagePath(value) {
1651
+ if (!value)
1652
+ return undefined;
1653
+ try {
1654
+ return Buffer.from(value, "base64url").toString("utf8");
1655
+ }
1656
+ catch {
1657
+ return undefined;
1658
+ }
1659
+ }
1660
+ function safeImageContentType(value) {
1661
+ return value && /^image\/[a-z0-9.+-]+$/i.test(value) ? value : "application/octet-stream";
1662
+ }
1663
+ function stripDataUrlPrefix(value) {
1664
+ const match = /^data:[^;]+;base64,(.*)$/is.exec(value);
1665
+ return match ? match[1] : value;
1666
+ }
1491
1667
  function assistantText(message) {
1492
1668
  const text = message.blocks.filter((block) => block.type === "text").map((block) => block.text).join("");
1493
1669
  return text.length > 0 ? text : undefined;
@@ -1540,14 +1716,21 @@ function thinkingLine(text, live = false) {
1540
1716
  return { kind: "thinking", title: titleForKind("thinking"), text, previewStyle: "summary", summaryMaxLines: THINKING_SUMMARY_MAX_LINES, live };
1541
1717
  }
1542
1718
  function formatToolUse(toolUse) {
1719
+ const toolKind = toolKindForToolUse(toolUse.name, toolUse.input);
1543
1720
  if (toolUse.name === "plan" && isPlanToolPayload(toolUse.input))
1544
- return { kind: "tool", title: toolTitle(toolUse.name, "running"), bodyTitle: planToolBodyTitle(toolUse.input), text: formatPlanToolPayload(toolUse.input), collapsible: true };
1721
+ return { kind: "tool", title: toolTitle(toolUse.name, "running"), bodyTitle: planToolBodyTitle(toolUse.input), toolKind, text: formatPlanToolPayload(toolUse.input), collapsible: true };
1545
1722
  const description = toolUse.name === "exec" ? execDescriptionFromInput(toolUse.input) : undefined;
1546
- return { kind: "tool", title: toolTitle(toolUse.name, "running"), bodyTitle: description, text: formatReplData(toolUse.input, 1200), previewStyle: "summary", collapsible: true };
1723
+ return { kind: "tool", title: toolTitle(toolUse.name, "running"), bodyTitle: description, toolKind, text: formatReplData(toolUse.input, 1200), previewStyle: "summary", collapsible: true };
1547
1724
  }
1548
1725
  function formatToolResultLine(toolName, output, ok) {
1549
1726
  const formatted = formatToolResult(toolName, output, ok);
1550
- return { kind: ok ? "tool" : "error", title: toolTitle(toolName, "finished"), bodyTitle: formatted.bodyTitle, titleStatus: ok ? "success" : "failure", text: formatted.text, format: formatted.format, live: false, previewStyle: formatted.full ? undefined : "summary", summaryMaxLines: formatted.summaryMaxLines, collapsible: true };
1727
+ return { kind: ok ? "tool" : "error", title: toolTitle(toolName, "finished"), bodyTitle: formatted.bodyTitle, titleStatus: ok ? "success" : "failure", toolKind: toolKindForToolUse(toolName, undefined), text: formatted.text, format: formatted.format, live: false, previewStyle: formatted.full ? undefined : "summary", summaryMaxLines: formatted.summaryMaxLines, collapsible: true, artifact: xhsArtifactFromToolOutput(toolName, output) };
1728
+ }
1729
+ function xhsArtifactFromToolOutput(toolName, output) {
1730
+ if (toolName !== "open_xhs_artifact_editor" || !isRecord(output))
1731
+ return undefined;
1732
+ const artifact = output.artifact;
1733
+ return isRecord(artifact) && typeof artifact.id === "string" ? artifact : undefined;
1551
1734
  }
1552
1735
  function toolTitle(toolName, _phase) {
1553
1736
  return toolName;
@@ -1649,17 +1832,17 @@ function isExecOutput(value) {
1649
1832
  return isRecord(value) && typeof value.command === "string" && typeof value.durationMs === "number";
1650
1833
  }
1651
1834
  function formatExecToolResult(output, ok) {
1652
- const status = output.timedOut ? "timed out" : output.exitCode === 0 ? "exit 0" : `exit ${output.exitCode ?? output.signal ?? "unknown"}`;
1835
+ const status = output.timedOut ? "已超时" : ok ? "已完成" : "执行失败";
1653
1836
  const description = typeof output.description === "string" ? output.description.trim() : "";
1654
- const lines = ["exec result", ...(description ? [`purpose: ${description}`] : []), `status: ${status}`, `duration: ${output.durationMs}ms`, `command: ${output.command}`];
1837
+ const lines = [description ? `目的:${description}` : "执行命令", `状态:${status}`, `耗时:${output.durationMs}ms`];
1655
1838
  const stdout = typeof output.stdout === "string" ? output.stdout.replace(/\s+$/u, "") : "";
1656
1839
  const stderr = typeof output.stderr === "string" ? output.stderr.replace(/\s+$/u, "") : "";
1657
1840
  if (stdout)
1658
- lines.push("stdout:", stdout);
1841
+ lines.push("输出:", stdout);
1659
1842
  if (stderr)
1660
- lines.push("stderr:", stderr);
1843
+ lines.push("错误:", stderr);
1661
1844
  if (!stdout && !stderr)
1662
- lines.push(ok ? "output: (none)" : "output: (not captured)");
1845
+ lines.push(ok ? "无输出。" : "没有捕获到输出。");
1663
1846
  return lines.join("\n");
1664
1847
  }
1665
1848
  function formatImageGenerationToolResult(output, ok) {