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/agents/agent-activity.js +11 -1
- package/dist/agents/agent-activity.js.map +1 -1
- package/dist/agents/local-agent-task.d.ts +1 -1
- package/dist/core/image-registry.js +4 -1
- package/dist/core/image-registry.js.map +1 -1
- package/dist/core/message-pipeline.js +16 -2
- package/dist/core/message-pipeline.js.map +1 -1
- package/dist/core/query-engine.d.ts +2 -0
- package/dist/core/query-engine.js +8 -4
- package/dist/core/query-engine.js.map +1 -1
- package/dist/core/query.js +6 -2
- package/dist/core/query.js.map +1 -1
- package/dist/repl/index.js +70 -91
- package/dist/repl/index.js.map +1 -1
- package/dist/tools/builtins/exec-tool.d.ts +1 -0
- package/dist/tools/builtins/exec-tool.js.map +1 -1
- package/dist/tools/builtins/image-generation-tool.d.ts +4 -0
- package/dist/tools/builtins/image-generation-tool.js +179 -52
- package/dist/tools/builtins/image-generation-tool.js.map +1 -1
- package/dist/web/index.d.ts +24 -1
- package/dist/web/index.js +206 -23
- package/dist/web/index.js.map +1 -1
- package/package.json +1 -1
- package/scripts/release-local.mjs +42 -0
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 {
|
|
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.
|
|
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 === "
|
|
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:
|
|
1630
|
+
src: imageBlockToSrc(block),
|
|
1478
1631
|
label: block.label,
|
|
1479
1632
|
mimeType: block.mimeType,
|
|
1480
1633
|
},
|
|
1481
1634
|
};
|
|
1482
1635
|
}
|
|
1483
|
-
function
|
|
1484
|
-
|
|
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 ? "
|
|
1835
|
+
const status = output.timedOut ? "已超时" : ok ? "已完成" : "执行失败";
|
|
1653
1836
|
const description = typeof output.description === "string" ? output.description.trim() : "";
|
|
1654
|
-
const lines = [
|
|
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("
|
|
1841
|
+
lines.push("输出:", stdout);
|
|
1659
1842
|
if (stderr)
|
|
1660
|
-
lines.push("
|
|
1843
|
+
lines.push("错误:", stderr);
|
|
1661
1844
|
if (!stdout && !stderr)
|
|
1662
|
-
lines.push(ok ? "
|
|
1845
|
+
lines.push(ok ? "无输出。" : "没有捕获到输出。");
|
|
1663
1846
|
return lines.join("\n");
|
|
1664
1847
|
}
|
|
1665
1848
|
function formatImageGenerationToolResult(output, ok) {
|