neoctl 0.2.15 → 0.2.17
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/context/prompts.js +1 -5
- package/dist/context/prompts.js.map +1 -1
- package/dist/core/query-engine.d.ts +4 -2
- package/dist/core/query-engine.js +22 -11
- package/dist/core/query-engine.js.map +1 -1
- package/dist/core/query.d.ts +1 -0
- package/dist/core/query.js +3 -120
- package/dist/core/query.js.map +1 -1
- package/dist/core/smoke-core-loop.js +9 -1
- package/dist/core/smoke-core-loop.js.map +1 -1
- package/dist/model/anthropic-mapper.js +1 -3
- package/dist/model/anthropic-mapper.js.map +1 -1
- package/dist/model/model-gateway.d.ts +1 -0
- package/dist/model/model-gateway.js.map +1 -1
- package/dist/model/openai-chat-mapper.js +1 -0
- package/dist/model/openai-chat-mapper.js.map +1 -1
- package/dist/model/openai-mappers.js +1 -3
- package/dist/model/openai-mappers.js.map +1 -1
- package/dist/model/openai-responses-mapper.js +1 -0
- package/dist/model/openai-responses-mapper.js.map +1 -1
- package/dist/model/smoke-responses-mapper.js +5 -3
- package/dist/model/smoke-responses-mapper.js.map +1 -1
- package/dist/repl/index.js +5 -38
- package/dist/repl/index.js.map +1 -1
- package/dist/session/session-store.d.ts +10 -0
- package/dist/session/session-store.js +22 -1
- package/dist/session/session-store.js.map +1 -1
- package/dist/session/smoke-session.js +3 -0
- package/dist/session/smoke-session.js.map +1 -1
- package/dist/web/index.d.ts +10 -16
- package/dist/web/index.js +139 -193
- package/dist/web/index.js.map +1 -1
- package/package.json +1 -1
- package/scripts/install-ripgrep.cjs +196 -196
- package/vendor/ripgrep/darwin-arm64/COPYING +3 -3
- package/vendor/ripgrep/darwin-arm64/LICENSE-MIT +21 -21
- package/vendor/ripgrep/darwin-arm64/UNLICENSE +24 -24
- package/vendor/ripgrep/darwin-arm64/manifest.json +7 -7
- package/vendor/ripgrep/darwin-x64/COPYING +3 -3
- package/vendor/ripgrep/darwin-x64/LICENSE-MIT +21 -21
- package/vendor/ripgrep/darwin-x64/UNLICENSE +24 -24
- package/vendor/ripgrep/darwin-x64/manifest.json +7 -7
- package/vendor/ripgrep/linux-arm64/COPYING +3 -3
- package/vendor/ripgrep/linux-arm64/LICENSE-MIT +21 -21
- package/vendor/ripgrep/linux-arm64/UNLICENSE +24 -24
- package/vendor/ripgrep/linux-arm64/manifest.json +7 -7
- package/vendor/ripgrep/linux-x64/COPYING +3 -3
- package/vendor/ripgrep/linux-x64/LICENSE-MIT +21 -21
- package/vendor/ripgrep/linux-x64/UNLICENSE +24 -24
- package/vendor/ripgrep/linux-x64/manifest.json +7 -7
- package/vendor/ripgrep/win32-arm64/COPYING +3 -3
- package/vendor/ripgrep/win32-arm64/LICENSE-MIT +21 -21
- package/vendor/ripgrep/win32-arm64/UNLICENSE +24 -24
- package/vendor/ripgrep/win32-arm64/manifest.json +7 -7
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 {
|
|
5
|
+
import { 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";
|
|
@@ -22,20 +22,14 @@ import { searchTool } from "../tools/builtins/search-tool.js";
|
|
|
22
22
|
import { planTool } from "../tools/builtins/plan-tool.js";
|
|
23
23
|
import { createOpenAIImageGenerationTool } from "../tools/builtins/image-generation-tool.js";
|
|
24
24
|
import { createLoadImageTool } from "../tools/builtins/image-loader-tool.js";
|
|
25
|
-
import { createImageNoteTool } from "../tools/builtins/image-note-tool.js";
|
|
26
25
|
import { createAgentTool, resumeAgentTask } from "../agents/agent-tool.js";
|
|
27
26
|
import { createTaskTools } from "../tasks/task-tools.js";
|
|
28
27
|
import { TaskStore } from "../tasks/task-store.js";
|
|
29
28
|
import { parseReplCommand, helpText, replCommandDefinitions } from "../repl/commands.js";
|
|
30
29
|
import { writeSessionMarkdownExport } from "../session/session-export.js";
|
|
31
|
-
import { DefaultContextManager } from "../context/context-manager.js";
|
|
32
|
-
import { buildEffectiveSystemPrompt } from "../context/prompts.js";
|
|
33
30
|
import { WEB_HTML } from "./html.js";
|
|
34
31
|
import { openDirectory } from "../open-directory.js";
|
|
35
|
-
import {
|
|
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";
|
|
32
|
+
import { resolveImageBlockDataSync } from "../core/image-storage.js";
|
|
39
33
|
const require = createRequire(import.meta.url);
|
|
40
34
|
const markedPackageDir = path.dirname(require.resolve("marked/package.json"));
|
|
41
35
|
const highlightPackageDir = path.dirname(require.resolve("@highlightjs/cdn-assets/package.json"));
|
|
@@ -114,98 +108,13 @@ function parseWebArgs(argv) {
|
|
|
114
108
|
port = 3000;
|
|
115
109
|
return { host, port: Math.round(port) };
|
|
116
110
|
}
|
|
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
|
-
}
|
|
196
111
|
export async function createWebRuntime(options = {}) {
|
|
197
112
|
const envLoad = loadDefaultDotEnvFiles({ override: true });
|
|
198
|
-
const cwd = options.cwd ? path.resolve(options.cwd) : process.cwd();
|
|
199
113
|
const modelConfig = readModelProviderConfig(process.env);
|
|
200
114
|
const communicationLogger = new CommunicationLogger();
|
|
201
115
|
const modelGateway = new LoggingModelGateway(createModelGatewayFromProcessEnv(process.env), communicationLogger);
|
|
202
116
|
const taskStore = new TaskStore();
|
|
203
117
|
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
|
-
});
|
|
209
118
|
tools.register(editTool);
|
|
210
119
|
tools.register(writeTool);
|
|
211
120
|
tools.register(createExecTool({ taskStore }));
|
|
@@ -214,13 +123,9 @@ export async function createWebRuntime(options = {}) {
|
|
|
214
123
|
tools.register(grepTool);
|
|
215
124
|
tools.register(searchTool);
|
|
216
125
|
tools.register(createLoadImageTool());
|
|
217
|
-
tools.register(createImageNoteTool());
|
|
218
126
|
if (modelConfig?.provider === "openai")
|
|
219
127
|
tools.register(createOpenAIImageGenerationTool());
|
|
220
128
|
tools.register(planTool);
|
|
221
|
-
tools.register(createSkillTool(skills));
|
|
222
|
-
for (const tool of createSkillManagementTools(skills, { requireApproval: true, allowDelete: false }))
|
|
223
|
-
tools.register(tool);
|
|
224
129
|
for (const tool of options.externalTools ?? [])
|
|
225
130
|
tools.register(tool);
|
|
226
131
|
const agentRuntime = { modelGateway, tools, taskStore };
|
|
@@ -229,7 +134,7 @@ export async function createWebRuntime(options = {}) {
|
|
|
229
134
|
const dummyContext = {
|
|
230
135
|
agentId: "main",
|
|
231
136
|
tools,
|
|
232
|
-
appState: new InMemoryAppState("main"
|
|
137
|
+
appState: new InMemoryAppState("main"),
|
|
233
138
|
emit: () => undefined,
|
|
234
139
|
};
|
|
235
140
|
return resumeAgentTask(taskId, directive, agentRuntime, taskStore, dummyContext);
|
|
@@ -239,7 +144,6 @@ export async function createWebRuntime(options = {}) {
|
|
|
239
144
|
const appPromptStore = new InMemoryAppPromptStore();
|
|
240
145
|
const engine = new QueryEngine({
|
|
241
146
|
agentId: options.agentId ?? "main",
|
|
242
|
-
cwd,
|
|
243
147
|
model: modelConfig?.model,
|
|
244
148
|
fallbackModel: modelConfig?.fallbackModel,
|
|
245
149
|
reasoning: modelConfig?.defaultReasoning,
|
|
@@ -247,8 +151,6 @@ export async function createWebRuntime(options = {}) {
|
|
|
247
151
|
modelGateway,
|
|
248
152
|
tools,
|
|
249
153
|
appPromptStore,
|
|
250
|
-
contextManager: new SkillCatalogContextManager(skills),
|
|
251
|
-
skills: (await skills.list()).map((skill) => skill.name),
|
|
252
154
|
taskNotificationSource: createTaskNotificationSource(taskStore),
|
|
253
155
|
commands: replCommandDefinitions.map((command) => command.usage),
|
|
254
156
|
session: {
|
|
@@ -360,6 +262,7 @@ export class WebRepl {
|
|
|
360
262
|
queuedAttachments;
|
|
361
263
|
foregroundRun;
|
|
362
264
|
foregroundRunToken = 0;
|
|
265
|
+
pendingUserImageEchoLabels;
|
|
363
266
|
backgroundSessionRuns = new Map();
|
|
364
267
|
suppressReattachedStreaming = new Set();
|
|
365
268
|
backgroundTaskCount;
|
|
@@ -397,6 +300,7 @@ export class WebRepl {
|
|
|
397
300
|
backgroundSessionRunCount: this.backgroundSessionRuns.size,
|
|
398
301
|
runningSessionIds: [...this.backgroundSessionRuns.keys()],
|
|
399
302
|
session: this.runtime.engine.snapshot().session,
|
|
303
|
+
fastMode: this.runtime.engine.isFastMode(),
|
|
400
304
|
appPrompt: this.runtime.engine.getAppPrompt(),
|
|
401
305
|
catalog: includeCatalog ? webCatalog(this.runtime) : undefined,
|
|
402
306
|
interactive: includeCatalog ? webInteractiveCatalog(this.runtime) : undefined,
|
|
@@ -532,6 +436,16 @@ export class WebRepl {
|
|
|
532
436
|
return { ok: false, error: message };
|
|
533
437
|
}
|
|
534
438
|
}
|
|
439
|
+
async setFastMode(enabled) {
|
|
440
|
+
try {
|
|
441
|
+
const fastMode = await this.runtime.engine.setFastMode(enabled);
|
|
442
|
+
this.broadcastSync();
|
|
443
|
+
return { ok: true, fastMode };
|
|
444
|
+
}
|
|
445
|
+
catch (error) {
|
|
446
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
447
|
+
}
|
|
448
|
+
}
|
|
535
449
|
loginForm(providerValue) {
|
|
536
450
|
const provider = parseLoginProvider(providerValue);
|
|
537
451
|
return createLoginFormPayload(this.runtime.envPath, provider);
|
|
@@ -602,7 +516,7 @@ export class WebRepl {
|
|
|
602
516
|
this.broadcastSync();
|
|
603
517
|
}
|
|
604
518
|
setStatus(next) {
|
|
605
|
-
this.status = next
|
|
519
|
+
this.status = next;
|
|
606
520
|
this.broadcastSync();
|
|
607
521
|
}
|
|
608
522
|
finalizeForegroundView() {
|
|
@@ -705,6 +619,11 @@ export class WebRepl {
|
|
|
705
619
|
}
|
|
706
620
|
handleEvent(event) {
|
|
707
621
|
this.reduce(event);
|
|
622
|
+
if (event.type === "message" && this.matchesPendingUserImageEcho(event.message)) {
|
|
623
|
+
this.pendingUserImageEchoLabels = undefined;
|
|
624
|
+
this.broadcastSync();
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
708
627
|
if (event.type === "state" || event.type === "context.metrics" || event.type === "usage" || event.type === "tool_call.delta" || event.type === "retrying") {
|
|
709
628
|
this.broadcastSync();
|
|
710
629
|
return;
|
|
@@ -772,13 +691,10 @@ export class WebRepl {
|
|
|
772
691
|
if (event.type === "tool.started") {
|
|
773
692
|
this.finalizeLiveLine(this.assistantLineId);
|
|
774
693
|
this.finalizeThinkingLine();
|
|
775
|
-
this.broadcastSync();
|
|
776
694
|
return;
|
|
777
695
|
}
|
|
778
|
-
if (event.type === "tool.finished")
|
|
779
|
-
this.broadcastSync();
|
|
696
|
+
if (event.type === "tool.finished")
|
|
780
697
|
return;
|
|
781
|
-
}
|
|
782
698
|
if (event.type === "terminal") {
|
|
783
699
|
this.finalizeLiveLine(this.assistantLineId);
|
|
784
700
|
this.finalizeThinkingLine();
|
|
@@ -789,6 +705,15 @@ export class WebRepl {
|
|
|
789
705
|
if (event.type === "error")
|
|
790
706
|
this.append({ kind: "error", text: event.error.message });
|
|
791
707
|
}
|
|
708
|
+
matchesPendingUserImageEcho(message) {
|
|
709
|
+
const pending = this.pendingUserImageEchoLabels;
|
|
710
|
+
if (!pending?.length || message.role !== "user")
|
|
711
|
+
return false;
|
|
712
|
+
const labels = message.blocks
|
|
713
|
+
.filter((block) => block.type === "image" && typeof block.label === "string")
|
|
714
|
+
.map((block) => block.label);
|
|
715
|
+
return labels.length === pending.length && labels.every((label, index) => label === pending[index]);
|
|
716
|
+
}
|
|
792
717
|
async handleCommandOrPrompt(text, attachments = []) {
|
|
793
718
|
const command = parseReplCommand(text);
|
|
794
719
|
if (command.type === "exit") {
|
|
@@ -888,6 +813,10 @@ export class WebRepl {
|
|
|
888
813
|
this.append({ kind: "user", text: promptPayload.displayText });
|
|
889
814
|
for (const line of imageLinesForBlocks("user", promptPayload.blocks))
|
|
890
815
|
this.append(line);
|
|
816
|
+
const optimisticImageLabels = (promptPayload.blocks ?? [])
|
|
817
|
+
.filter((block) => block.type === "image" && typeof block.label === "string")
|
|
818
|
+
.map((block) => block.label);
|
|
819
|
+
this.pendingUserImageEchoLabels = optimisticImageLabels.length ? optimisticImageLabels : undefined;
|
|
891
820
|
const runToken = ++this.foregroundRunToken;
|
|
892
821
|
const abortController = new AbortController();
|
|
893
822
|
this.activeAbortController = abortController;
|
|
@@ -927,6 +856,7 @@ export class WebRepl {
|
|
|
927
856
|
this.activeAbortController = undefined;
|
|
928
857
|
this.interruptArmed = false;
|
|
929
858
|
this.finalizeForegroundView();
|
|
859
|
+
this.pendingUserImageEchoLabels = undefined;
|
|
930
860
|
const queuedText = this.queuedInput;
|
|
931
861
|
const queuedAttach = this.queuedAttachments;
|
|
932
862
|
this.queuedInput = undefined;
|
|
@@ -979,14 +909,10 @@ export class WebRepl {
|
|
|
979
909
|
res.write(`event: ${event}\n`);
|
|
980
910
|
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
981
911
|
}
|
|
982
|
-
sendSerialized(res, event, data) {
|
|
983
|
-
res.write(`event: ${event}\n`);
|
|
984
|
-
res.write(`data: ${data}\n\n`);
|
|
985
|
-
}
|
|
986
912
|
broadcastSync() {
|
|
987
|
-
const payload =
|
|
913
|
+
const payload = this.snapshot(false);
|
|
988
914
|
for (const res of this.subscribers)
|
|
989
|
-
this.
|
|
915
|
+
this.send(res, "sync", payload);
|
|
990
916
|
}
|
|
991
917
|
}
|
|
992
918
|
function reqKeepAlive(res) {
|
|
@@ -1005,8 +931,6 @@ async function route(req, res, router) {
|
|
|
1005
931
|
return sendFile(res, highlightAssetPath, "text/javascript; charset=utf-8");
|
|
1006
932
|
if (req.method === "GET" && url.pathname === "/vendor/highlight-theme.css")
|
|
1007
933
|
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"));
|
|
1010
934
|
const scope = webRuntimeScopeFromUrl(url);
|
|
1011
935
|
const repl = await router.get(scope);
|
|
1012
936
|
if (req.method === "GET" && url.pathname === "/events")
|
|
@@ -1019,6 +943,10 @@ async function route(req, res, router) {
|
|
|
1019
943
|
const body = await readJsonBody(req);
|
|
1020
944
|
return sendJson(res, repl.setAppPrompt(body));
|
|
1021
945
|
}
|
|
946
|
+
if (req.method === "POST" && url.pathname === "/api/fast-mode") {
|
|
947
|
+
const body = await readJsonBody(req);
|
|
948
|
+
return sendJson(res, await repl.setFastMode(body.enabled === true));
|
|
949
|
+
}
|
|
1022
950
|
if (req.method === "POST" && url.pathname === "/api/submit") {
|
|
1023
951
|
const body = await readJsonBody(req);
|
|
1024
952
|
return sendJson(res, await repl.submit(String(body.text ?? ""), sanitizeWebAttachments(body.attachments)));
|
|
@@ -1070,23 +998,6 @@ async function sendFile(res, filepath, contentType) {
|
|
|
1070
998
|
res.writeHead(200, { "Content-Type": contentType, "Cache-Control": "public, max-age=3600" });
|
|
1071
999
|
res.end(body);
|
|
1072
1000
|
}
|
|
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
|
-
}
|
|
1090
1001
|
function sendJson(res, value, status = 200) {
|
|
1091
1002
|
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" });
|
|
1092
1003
|
res.end(JSON.stringify(value));
|
|
@@ -1189,11 +1100,7 @@ function pushTextBlock(blocks, text) {
|
|
|
1189
1100
|
}
|
|
1190
1101
|
function reduceStatus(status, event) {
|
|
1191
1102
|
if (event.type === "state")
|
|
1192
|
-
return { ...status, phase: event.phase, detail: event.detail,
|
|
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 };
|
|
1103
|
+
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 };
|
|
1197
1104
|
if (event.type === "context.metrics")
|
|
1198
1105
|
return { ...status, metrics: event.metrics, inputTokenUpdatedAt: event.metrics.estimatedInputTokens !== status.metrics?.estimatedInputTokens ? Date.now() : status.inputTokenUpdatedAt, activityTick: status.activityTick + 1 };
|
|
1199
1106
|
if (event.type === "usage")
|
|
@@ -1207,26 +1114,11 @@ function reduceStatus(status, event) {
|
|
|
1207
1114
|
if (event.type === "retrying")
|
|
1208
1115
|
return { ...status, phase: "calling_model", detail: `retrying in ${(event.delayMs / 1000).toFixed(1)}s`, retryCooldownUntil: Date.now() + event.delayMs, activityTick: status.activityTick + 1 };
|
|
1209
1116
|
if (event.type === "terminal")
|
|
1210
|
-
return { ...status, phase: "stopped", detail: event.reason,
|
|
1211
|
-
if (event.type === "message" || event.type === "error")
|
|
1117
|
+
return { ...status, phase: "stopped", detail: event.reason, inputTokenUpdatedAt: undefined, outputTokenUpdatedAt: undefined, retryCooldownUntil: undefined, activityTick: status.activityTick + 1 };
|
|
1118
|
+
if (event.type === "message" || event.type === "tool.started" || event.type === "tool.finished" || event.type === "error")
|
|
1212
1119
|
return { ...status, activityTick: status.activityTick + 1 };
|
|
1213
1120
|
return status;
|
|
1214
1121
|
}
|
|
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
|
-
}
|
|
1230
1122
|
async function handleExportCommand(outputPath, runtime) {
|
|
1231
1123
|
const snapshot = runtime.engine.snapshot();
|
|
1232
1124
|
if (!snapshot.session)
|
|
@@ -1627,43 +1519,20 @@ function imageLineForBlock(role, block) {
|
|
|
1627
1519
|
kind,
|
|
1628
1520
|
text: block.label ?? `[image ${block.mimeType}]`,
|
|
1629
1521
|
image: {
|
|
1630
|
-
src:
|
|
1522
|
+
src: imageBlockToDataUrl(block),
|
|
1631
1523
|
label: block.label,
|
|
1632
1524
|
mimeType: block.mimeType,
|
|
1633
1525
|
},
|
|
1634
1526
|
};
|
|
1635
1527
|
}
|
|
1636
|
-
function
|
|
1637
|
-
|
|
1638
|
-
return `/api/images?path=${encodeImagePath(block.storage.path)}&mime=${encodeURIComponent(block.mimeType)}`;
|
|
1639
|
-
}
|
|
1640
|
-
const data = block.data.trim();
|
|
1528
|
+
function imageBlockToDataUrl(block) {
|
|
1529
|
+
const data = resolveImageBlockDataSync(block);
|
|
1641
1530
|
if (!data)
|
|
1642
1531
|
return "";
|
|
1643
1532
|
if (data.startsWith("data:"))
|
|
1644
1533
|
return data;
|
|
1645
1534
|
return `data:${block.mimeType};base64,${data}`;
|
|
1646
1535
|
}
|
|
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
|
-
}
|
|
1667
1536
|
function assistantText(message) {
|
|
1668
1537
|
const text = message.blocks.filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
1669
1538
|
return text.length > 0 ? text : undefined;
|
|
@@ -1716,30 +1585,59 @@ function thinkingLine(text, live = false) {
|
|
|
1716
1585
|
return { kind: "thinking", title: titleForKind("thinking"), text, previewStyle: "summary", summaryMaxLines: THINKING_SUMMARY_MAX_LINES, live };
|
|
1717
1586
|
}
|
|
1718
1587
|
function formatToolUse(toolUse) {
|
|
1719
|
-
const toolKind = toolKindForToolUse(toolUse.name, toolUse.input);
|
|
1720
1588
|
if (toolUse.name === "plan" && isPlanToolPayload(toolUse.input))
|
|
1721
|
-
return { kind: "tool", title: toolTitle(toolUse.name, "running"), bodyTitle: planToolBodyTitle(toolUse.input),
|
|
1722
|
-
const
|
|
1723
|
-
return { kind: "tool", title: toolTitle(toolUse.name, "running"), bodyTitle:
|
|
1589
|
+
return { kind: "tool", title: toolTitle(toolUse.name, "running"), bodyTitle: planToolBodyTitle(toolUse.input), text: formatPlanToolPayload(toolUse.input), collapsible: true };
|
|
1590
|
+
const summary = summarizeToolUse(toolUse.name, toolUse.input);
|
|
1591
|
+
return { kind: "tool", title: toolTitle(toolUse.name, "running"), bodyTitle: summary.bodyTitle, text: summary.text, previewStyle: "summary", collapsible: true };
|
|
1724
1592
|
}
|
|
1725
1593
|
function formatToolResultLine(toolName, output, ok) {
|
|
1726
1594
|
const formatted = formatToolResult(toolName, output, ok);
|
|
1727
|
-
return { kind: ok ? "tool" : "error", title: toolTitle(toolName, "finished"), bodyTitle: formatted.bodyTitle, titleStatus: ok ? "success" : "failure",
|
|
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;
|
|
1595
|
+
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 };
|
|
1734
1596
|
}
|
|
1735
1597
|
function toolTitle(toolName, _phase) {
|
|
1736
|
-
|
|
1598
|
+
const labels = {
|
|
1599
|
+
agent: "子任务",
|
|
1600
|
+
edit: "编辑文件",
|
|
1601
|
+
exec: "执行命令",
|
|
1602
|
+
expose_downloads: "文件下载",
|
|
1603
|
+
grep: "搜索文本",
|
|
1604
|
+
image2: "图片生成",
|
|
1605
|
+
image_note: "记录图片说明",
|
|
1606
|
+
list: "列出文件",
|
|
1607
|
+
load_image: "读取图片",
|
|
1608
|
+
plan: "任务计划",
|
|
1609
|
+
read: "读取文件",
|
|
1610
|
+
search: "网络搜索",
|
|
1611
|
+
write: "写入文件",
|
|
1612
|
+
};
|
|
1613
|
+
return labels[toolName] ?? toolName;
|
|
1614
|
+
}
|
|
1615
|
+
function summarizeToolUse(toolName, input) {
|
|
1616
|
+
const purpose = toolUsePurpose(toolName, input);
|
|
1617
|
+
return {
|
|
1618
|
+
bodyTitle: purpose,
|
|
1619
|
+
text: purpose ? `目的:${purpose}` : "正在执行工具…",
|
|
1620
|
+
};
|
|
1737
1621
|
}
|
|
1738
|
-
function
|
|
1622
|
+
function toolUsePurpose(toolName, input) {
|
|
1739
1623
|
if (!isRecord(input))
|
|
1740
1624
|
return undefined;
|
|
1741
1625
|
const description = typeof input.description === "string" ? input.description.trim() : "";
|
|
1742
|
-
|
|
1626
|
+
if (description)
|
|
1627
|
+
return description;
|
|
1628
|
+
if (toolName === "expose_downloads")
|
|
1629
|
+
return "准备网页下载链接";
|
|
1630
|
+
if (toolName === "read" && typeof input.path === "string")
|
|
1631
|
+
return `读取 ${path.basename(input.path)}`;
|
|
1632
|
+
if (toolName === "list" && typeof input.path === "string")
|
|
1633
|
+
return `查看 ${input.path}`;
|
|
1634
|
+
if (toolName === "grep" && typeof input.query === "string")
|
|
1635
|
+
return `搜索 ${input.query}`;
|
|
1636
|
+
if (toolName === "write" && typeof input.path === "string")
|
|
1637
|
+
return `写入 ${path.basename(input.path)}`;
|
|
1638
|
+
if (toolName === "edit" && typeof input.path === "string")
|
|
1639
|
+
return `修改 ${path.basename(input.path)}`;
|
|
1640
|
+
return undefined;
|
|
1743
1641
|
}
|
|
1744
1642
|
function isPlanToolPayload(value) {
|
|
1745
1643
|
if (!isRecord(value) || !Array.isArray(value.items))
|
|
@@ -1774,12 +1672,36 @@ function formatToolResult(toolName, output, ok) {
|
|
|
1774
1672
|
return { text: formatWebSearchToolResult(output, ok), summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
|
|
1775
1673
|
if (toolName === "image2" && isRecord(output))
|
|
1776
1674
|
return { text: formatImageGenerationToolResult(output, ok), format: "plain", summaryMaxLines: 4 };
|
|
1675
|
+
if (toolName === "expose_downloads")
|
|
1676
|
+
return { text: formatExposeDownloadsToolResult(output, ok), full: true, bodyTitle: ok ? "请点击下载" : "下载准备失败" };
|
|
1777
1677
|
if (toolName === "plan" && isPlanToolPayload(output))
|
|
1778
1678
|
return { text: formatPlanToolPayload(output), full: true, bodyTitle: planToolBodyTitle(output) };
|
|
1779
1679
|
if (typeof output === "string")
|
|
1780
1680
|
return { text: output, format: hasAnsi(output) ? "ansi" : undefined, summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
|
|
1781
1681
|
return { text: `${ok ? "ok" : "failed"}\n${formatReplData(output, 6000)}`, summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
|
|
1782
1682
|
}
|
|
1683
|
+
function formatExposeDownloadsToolResult(output, ok) {
|
|
1684
|
+
const downloads = extractDownloadEntries(output);
|
|
1685
|
+
if (!ok)
|
|
1686
|
+
return downloads.length ? `下载准备失败\n${formatDownloadEntries(downloads)}` : "下载准备失败";
|
|
1687
|
+
if (!downloads.length)
|
|
1688
|
+
return "文件已准备好,请点击下载。";
|
|
1689
|
+
return `文件已准备好,请点击下载。\n${formatDownloadEntries(downloads)}`;
|
|
1690
|
+
}
|
|
1691
|
+
function formatDownloadEntries(downloads) {
|
|
1692
|
+
return downloads.map((entry) => {
|
|
1693
|
+
const filename = entry.filename || entry.name || "下载文件";
|
|
1694
|
+
const url = entry.url || (entry.id ? `/api/downloads/${encodeURIComponent(entry.id)}` : "");
|
|
1695
|
+
return url ? `- ${filename}: ${url}` : `- ${filename}`;
|
|
1696
|
+
}).join("\n");
|
|
1697
|
+
}
|
|
1698
|
+
function extractDownloadEntries(output) {
|
|
1699
|
+
if (!isRecord(output))
|
|
1700
|
+
return [];
|
|
1701
|
+
const candidates = [output.downloads, isRecord(output.output) ? output.output.downloads : undefined, isRecord(output.result) ? output.result.downloads : undefined];
|
|
1702
|
+
const downloads = candidates.find(Array.isArray);
|
|
1703
|
+
return Array.isArray(downloads) ? downloads.filter(isRecord).map((entry) => ({ ...entry })) : [];
|
|
1704
|
+
}
|
|
1783
1705
|
function isEditToolOutput(value) {
|
|
1784
1706
|
return typeof value.path === "string" && typeof value.operation === "string" && typeof value.replacements === "number" && Array.isArray(value.patch) && value.patch.every(isEditPatchHunk);
|
|
1785
1707
|
}
|
|
@@ -1857,6 +1779,17 @@ function formatImageGenerationToolResult(output, ok) {
|
|
|
1857
1779
|
const quality = typeof output.quality === "string" ? output.quality : undefined;
|
|
1858
1780
|
const format = typeof output.outputFormat === "string" ? output.outputFormat : undefined;
|
|
1859
1781
|
const sourceImages = typeof output.sourceImages === "number" ? output.sourceImages : undefined;
|
|
1782
|
+
const prompt = typeof output.prompt === "string" ? output.prompt.trim() : "";
|
|
1783
|
+
const semanticName = typeof output.semanticName === "string" ? output.semanticName.trim() : "";
|
|
1784
|
+
const background = typeof output.background === "string" ? output.background : undefined;
|
|
1785
|
+
const startedAtIso = typeof output.startedAtIso === "string" ? output.startedAtIso : undefined;
|
|
1786
|
+
const finishedAtIso = typeof output.finishedAtIso === "string" ? output.finishedAtIso : undefined;
|
|
1787
|
+
const revisedPrompt = Array.isArray(output.images)
|
|
1788
|
+
? output.images
|
|
1789
|
+
.filter(isRecord)
|
|
1790
|
+
.map((image) => typeof image.revisedPrompt === "string" ? image.revisedPrompt.trim() : "")
|
|
1791
|
+
.find((value) => Boolean(value))
|
|
1792
|
+
: undefined;
|
|
1860
1793
|
const lines = [`${mode === "edit" ? "edited" : "generated"} ${returnedImages ?? 0} image${returnedImages === 1 ? "" : "s"}`];
|
|
1861
1794
|
const details = [provider, model, size, quality && quality !== "auto" ? quality : undefined, format].filter((value) => Boolean(value));
|
|
1862
1795
|
if (details.length > 0)
|
|
@@ -1866,6 +1799,19 @@ function formatImageGenerationToolResult(output, ok) {
|
|
|
1866
1799
|
const duration = imageGenerationDuration(output);
|
|
1867
1800
|
if (duration !== undefined)
|
|
1868
1801
|
lines.push(`duration: ${duration}ms`);
|
|
1802
|
+
if (prompt)
|
|
1803
|
+
lines.push(`prompt: ${prompt}`);
|
|
1804
|
+
if (revisedPrompt && revisedPrompt !== prompt)
|
|
1805
|
+
lines.push(`revisedPrompt: ${revisedPrompt}`);
|
|
1806
|
+
lines.push(`mode: ${mode}`);
|
|
1807
|
+
if (semanticName)
|
|
1808
|
+
lines.push(`semanticName: ${semanticName}`);
|
|
1809
|
+
if (background)
|
|
1810
|
+
lines.push(`background: ${background}`);
|
|
1811
|
+
if (startedAtIso)
|
|
1812
|
+
lines.push(`startedAt: ${startedAtIso}`);
|
|
1813
|
+
if (finishedAtIso)
|
|
1814
|
+
lines.push(`finishedAt: ${finishedAtIso}`);
|
|
1869
1815
|
return lines.join("\n");
|
|
1870
1816
|
}
|
|
1871
1817
|
function imageGenerationDuration(output) {
|