neoctl 0.2.16 → 0.2.22
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/README.md +11 -9
- package/dist/app/app-prompt.d.ts +2 -0
- package/dist/app/app-prompt.js +2 -0
- package/dist/app/app-prompt.js.map +1 -1
- package/dist/context/compaction.d.ts +12 -0
- package/dist/context/compaction.js +22 -0
- package/dist/context/compaction.js.map +1 -1
- package/dist/context/prompts.js +1 -5
- package/dist/context/prompts.js.map +1 -1
- package/dist/context/smoke-context.js +30 -3
- package/dist/context/smoke-context.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 +11 -124
- 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/config.js +1 -1
- package/dist/model/env.js +1 -1
- package/dist/model/model-gateway.d.ts +1 -0
- package/dist/model/model-gateway.js.map +1 -1
- package/dist/model/model-metadata.json +70 -6
- 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/commands.js +1 -1
- package/dist/repl/index.js +7 -40
- 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/tools/builtins/ripgrep-binary.js +1 -1
- package/dist/tools/builtins/ripgrep-binary.js.map +1 -1
- package/dist/web/index.d.ts +21 -16
- package/dist/web/index.js +141 -197
- package/dist/web/index.js.map +1 -1
- package/package.json +5 -3
- package/scripts/install-ripgrep.cjs +210 -196
- package/scripts/verify-ripgrep.cjs +83 -0
- 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-arm64/rg +0 -0
- 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/darwin-x64/rg +0 -0
- 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-arm64/rg +0 -0
- 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/linux-x64/rg +0 -0
- 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: {
|
|
@@ -351,6 +253,7 @@ export class WebRepl {
|
|
|
351
253
|
assistantLineId;
|
|
352
254
|
thinkingLineId;
|
|
353
255
|
finalizedThinkingLineId;
|
|
256
|
+
liveToolLineIds = new Map();
|
|
354
257
|
activeAbortController;
|
|
355
258
|
interruptArmed = false;
|
|
356
259
|
lines;
|
|
@@ -398,6 +301,7 @@ export class WebRepl {
|
|
|
398
301
|
backgroundSessionRunCount: this.backgroundSessionRuns.size,
|
|
399
302
|
runningSessionIds: [...this.backgroundSessionRuns.keys()],
|
|
400
303
|
session: this.runtime.engine.snapshot().session,
|
|
304
|
+
fastMode: this.runtime.engine.isFastMode(),
|
|
401
305
|
appPrompt: this.runtime.engine.getAppPrompt(),
|
|
402
306
|
catalog: includeCatalog ? webCatalog(this.runtime) : undefined,
|
|
403
307
|
interactive: includeCatalog ? webInteractiveCatalog(this.runtime) : undefined,
|
|
@@ -491,6 +395,7 @@ export class WebRepl {
|
|
|
491
395
|
this.assistantLineId = undefined;
|
|
492
396
|
this.thinkingLineId = undefined;
|
|
493
397
|
this.finalizedThinkingLineId = undefined;
|
|
398
|
+
this.liveToolLineIds.clear();
|
|
494
399
|
if (line)
|
|
495
400
|
this.append(line);
|
|
496
401
|
this.broadcastSync();
|
|
@@ -523,8 +428,18 @@ export class WebRepl {
|
|
|
523
428
|
content: payload.content ?? "",
|
|
524
429
|
id: payload.id,
|
|
525
430
|
title: payload.title,
|
|
431
|
+
usage: payload.usage,
|
|
526
432
|
source: payload.source,
|
|
527
433
|
});
|
|
434
|
+
const usage = payload.usage?.trim();
|
|
435
|
+
if (!shouldClear && usage) {
|
|
436
|
+
this.append({
|
|
437
|
+
kind: "meta",
|
|
438
|
+
title: "提示词用法",
|
|
439
|
+
text: usage,
|
|
440
|
+
format: "markdown",
|
|
441
|
+
});
|
|
442
|
+
}
|
|
528
443
|
this.broadcastSync();
|
|
529
444
|
return { ok: true, appPrompt };
|
|
530
445
|
}
|
|
@@ -533,6 +448,16 @@ export class WebRepl {
|
|
|
533
448
|
return { ok: false, error: message };
|
|
534
449
|
}
|
|
535
450
|
}
|
|
451
|
+
async setFastMode(enabled) {
|
|
452
|
+
try {
|
|
453
|
+
const fastMode = await this.runtime.engine.setFastMode(enabled);
|
|
454
|
+
this.broadcastSync();
|
|
455
|
+
return { ok: true, fastMode };
|
|
456
|
+
}
|
|
457
|
+
catch (error) {
|
|
458
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
459
|
+
}
|
|
460
|
+
}
|
|
536
461
|
loginForm(providerValue) {
|
|
537
462
|
const provider = parseLoginProvider(providerValue);
|
|
538
463
|
return createLoginFormPayload(this.runtime.envPath, provider);
|
|
@@ -603,7 +528,7 @@ export class WebRepl {
|
|
|
603
528
|
this.broadcastSync();
|
|
604
529
|
}
|
|
605
530
|
setStatus(next) {
|
|
606
|
-
this.status = next
|
|
531
|
+
this.status = next;
|
|
607
532
|
this.broadcastSync();
|
|
608
533
|
}
|
|
609
534
|
finalizeForegroundView() {
|
|
@@ -625,8 +550,9 @@ export class WebRepl {
|
|
|
625
550
|
this.queuedInput = undefined;
|
|
626
551
|
this.queuedAttachments = undefined;
|
|
627
552
|
this.finalizeForegroundView();
|
|
553
|
+
this.finalizeLiveToolLines();
|
|
628
554
|
this.busy = false;
|
|
629
|
-
this.status = { ...this.status, phase: "ready", detail: undefined, inputTokenUpdatedAt: undefined, outputTokenUpdatedAt: undefined, retryCooldownUntil: undefined };
|
|
555
|
+
this.status = { ...this.status, phase: "ready", detail: undefined, currentTool: undefined, inputTokenUpdatedAt: undefined, outputTokenUpdatedAt: undefined, retryCooldownUntil: undefined };
|
|
630
556
|
this.broadcastSync();
|
|
631
557
|
return runWasActive;
|
|
632
558
|
}
|
|
@@ -704,6 +630,11 @@ export class WebRepl {
|
|
|
704
630
|
this.finalizedThinkingLineId = id;
|
|
705
631
|
this.thinkingLineId = undefined;
|
|
706
632
|
}
|
|
633
|
+
finalizeLiveToolLines() {
|
|
634
|
+
for (const id of this.liveToolLineIds.values())
|
|
635
|
+
this.finalizeLiveLine(id);
|
|
636
|
+
this.liveToolLineIds.clear();
|
|
637
|
+
}
|
|
707
638
|
handleEvent(event) {
|
|
708
639
|
this.reduce(event);
|
|
709
640
|
if (event.type === "message" && this.matchesPendingUserImageEcho(event.message)) {
|
|
@@ -757,7 +688,14 @@ export class WebRepl {
|
|
|
757
688
|
return;
|
|
758
689
|
}
|
|
759
690
|
if (event.message.role === "tool_result") {
|
|
760
|
-
renderToolResultMessage(event.message, (line) =>
|
|
691
|
+
renderToolResultMessage(event.message, (line, toolUseId) => {
|
|
692
|
+
const liveLineId = this.liveToolLineIds.get(toolUseId);
|
|
693
|
+
if (liveLineId === undefined)
|
|
694
|
+
return this.append(line);
|
|
695
|
+
this.replaceLine(liveLineId, line);
|
|
696
|
+
this.liveToolLineIds.delete(toolUseId);
|
|
697
|
+
return liveLineId;
|
|
698
|
+
});
|
|
761
699
|
renderMessageImages(event.message, (line) => this.append(line));
|
|
762
700
|
return;
|
|
763
701
|
}
|
|
@@ -778,16 +716,22 @@ export class WebRepl {
|
|
|
778
716
|
if (event.type === "tool.started") {
|
|
779
717
|
this.finalizeLiveLine(this.assistantLineId);
|
|
780
718
|
this.finalizeThinkingLine();
|
|
781
|
-
this.
|
|
719
|
+
const id = this.append({ ...formatToolUse(event.toolUse), live: true });
|
|
720
|
+
this.liveToolLineIds.set(event.toolUse.id, id);
|
|
782
721
|
return;
|
|
783
722
|
}
|
|
784
723
|
if (event.type === "tool.finished") {
|
|
785
|
-
this.
|
|
724
|
+
const id = this.liveToolLineIds.get(event.toolUse.id);
|
|
725
|
+
if (id !== undefined) {
|
|
726
|
+
this.finalizeLiveLine(id);
|
|
727
|
+
this.liveToolLineIds.delete(event.toolUse.id);
|
|
728
|
+
}
|
|
786
729
|
return;
|
|
787
730
|
}
|
|
788
731
|
if (event.type === "terminal") {
|
|
789
732
|
this.finalizeLiveLine(this.assistantLineId);
|
|
790
733
|
this.finalizeThinkingLine();
|
|
734
|
+
this.finalizeLiveToolLines();
|
|
791
735
|
this.assistantLineId = undefined;
|
|
792
736
|
this.broadcastSync();
|
|
793
737
|
return;
|
|
@@ -999,14 +943,10 @@ export class WebRepl {
|
|
|
999
943
|
res.write(`event: ${event}\n`);
|
|
1000
944
|
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
1001
945
|
}
|
|
1002
|
-
sendSerialized(res, event, data) {
|
|
1003
|
-
res.write(`event: ${event}\n`);
|
|
1004
|
-
res.write(`data: ${data}\n\n`);
|
|
1005
|
-
}
|
|
1006
946
|
broadcastSync() {
|
|
1007
|
-
const payload =
|
|
947
|
+
const payload = this.snapshot(false);
|
|
1008
948
|
for (const res of this.subscribers)
|
|
1009
|
-
this.
|
|
949
|
+
this.send(res, "sync", payload);
|
|
1010
950
|
}
|
|
1011
951
|
}
|
|
1012
952
|
function reqKeepAlive(res) {
|
|
@@ -1025,8 +965,6 @@ async function route(req, res, router) {
|
|
|
1025
965
|
return sendFile(res, highlightAssetPath, "text/javascript; charset=utf-8");
|
|
1026
966
|
if (req.method === "GET" && url.pathname === "/vendor/highlight-theme.css")
|
|
1027
967
|
return sendFile(res, highlightThemeAssetPath, "text/css; charset=utf-8");
|
|
1028
|
-
if (req.method === "GET" && url.pathname === "/api/images")
|
|
1029
|
-
return sendImage(res, url.searchParams.get("path"), url.searchParams.get("mime"));
|
|
1030
968
|
const scope = webRuntimeScopeFromUrl(url);
|
|
1031
969
|
const repl = await router.get(scope);
|
|
1032
970
|
if (req.method === "GET" && url.pathname === "/events")
|
|
@@ -1039,6 +977,10 @@ async function route(req, res, router) {
|
|
|
1039
977
|
const body = await readJsonBody(req);
|
|
1040
978
|
return sendJson(res, repl.setAppPrompt(body));
|
|
1041
979
|
}
|
|
980
|
+
if (req.method === "POST" && url.pathname === "/api/fast-mode") {
|
|
981
|
+
const body = await readJsonBody(req);
|
|
982
|
+
return sendJson(res, await repl.setFastMode(body.enabled === true));
|
|
983
|
+
}
|
|
1042
984
|
if (req.method === "POST" && url.pathname === "/api/submit") {
|
|
1043
985
|
const body = await readJsonBody(req);
|
|
1044
986
|
return sendJson(res, await repl.submit(String(body.text ?? ""), sanitizeWebAttachments(body.attachments)));
|
|
@@ -1090,23 +1032,6 @@ async function sendFile(res, filepath, contentType) {
|
|
|
1090
1032
|
res.writeHead(200, { "Content-Type": contentType, "Cache-Control": "public, max-age=3600" });
|
|
1091
1033
|
res.end(body);
|
|
1092
1034
|
}
|
|
1093
|
-
async function sendImage(res, encodedPath, mimeType) {
|
|
1094
|
-
const filepath = decodeImagePath(encodedPath);
|
|
1095
|
-
if (!filepath)
|
|
1096
|
-
return sendJson(res, { error: "missing image path" }, 400);
|
|
1097
|
-
const contentType = safeImageContentType(mimeType);
|
|
1098
|
-
const binaryPath = filepath.endsWith(".base64.txt") ? filepath.slice(0, -".base64.txt".length) : filepath;
|
|
1099
|
-
if (existsSync(binaryPath)) {
|
|
1100
|
-
res.writeHead(200, { "Content-Type": contentType, "Cache-Control": "no-store" });
|
|
1101
|
-
createReadStream(binaryPath).pipe(res);
|
|
1102
|
-
return;
|
|
1103
|
-
}
|
|
1104
|
-
if (!existsSync(filepath))
|
|
1105
|
-
return sendJson(res, { error: "image not found" }, 404);
|
|
1106
|
-
const base64 = (await fs.readFile(filepath, "utf8")).trim();
|
|
1107
|
-
res.writeHead(200, { "Content-Type": contentType, "Cache-Control": "no-store" });
|
|
1108
|
-
res.end(Buffer.from(stripDataUrlPrefix(base64), "base64"));
|
|
1109
|
-
}
|
|
1110
1035
|
function sendJson(res, value, status = 200) {
|
|
1111
1036
|
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" });
|
|
1112
1037
|
res.end(JSON.stringify(value));
|
|
@@ -1209,11 +1134,7 @@ function pushTextBlock(blocks, text) {
|
|
|
1209
1134
|
}
|
|
1210
1135
|
function reduceStatus(status, event) {
|
|
1211
1136
|
if (event.type === "state")
|
|
1212
|
-
return { ...status, phase: event.phase, detail: event.detail, currentTool: event.phase === "
|
|
1213
|
-
if (event.type === "tool.started")
|
|
1214
|
-
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 };
|
|
1215
|
-
if (event.type === "tool.finished")
|
|
1216
|
-
return { ...status, currentTool: status.currentTool?.id === event.toolUse.id ? undefined : status.currentTool, activityTick: status.activityTick + 1 };
|
|
1137
|
+
return { ...status, phase: event.phase, detail: event.detail, currentTool: event.phase === "running_tools" ? status.currentTool : undefined, 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 };
|
|
1217
1138
|
if (event.type === "context.metrics")
|
|
1218
1139
|
return { ...status, metrics: event.metrics, inputTokenUpdatedAt: event.metrics.estimatedInputTokens !== status.metrics?.estimatedInputTokens ? Date.now() : status.inputTokenUpdatedAt, activityTick: status.activityTick + 1 };
|
|
1219
1140
|
if (event.type === "usage")
|
|
@@ -1226,27 +1147,16 @@ function reduceStatus(status, event) {
|
|
|
1226
1147
|
return { ...status, phase: "calling_model", streamedOutputTokens: status.streamedOutputTokens + estimateTokens(event.argumentsDelta), outputTokenUpdatedAt: Date.now(), activityTick: status.activityTick + 1 };
|
|
1227
1148
|
if (event.type === "retrying")
|
|
1228
1149
|
return { ...status, phase: "calling_model", detail: `retrying in ${(event.delayMs / 1000).toFixed(1)}s`, retryCooldownUntil: Date.now() + event.delayMs, activityTick: status.activityTick + 1 };
|
|
1150
|
+
if (event.type === "tool.started")
|
|
1151
|
+
return { ...status, phase: "running_tools", currentTool: { id: event.toolUse.id, name: event.toolUse.name, input: event.toolUse.input, startedAt: Date.now() }, activityTick: status.activityTick + 1 };
|
|
1152
|
+
if (event.type === "tool.finished")
|
|
1153
|
+
return { ...status, currentTool: status.currentTool?.id === event.toolUse.id ? undefined : status.currentTool, activityTick: status.activityTick + 1 };
|
|
1229
1154
|
if (event.type === "terminal")
|
|
1230
1155
|
return { ...status, phase: "stopped", detail: event.reason, currentTool: undefined, inputTokenUpdatedAt: undefined, outputTokenUpdatedAt: undefined, retryCooldownUntil: undefined, activityTick: status.activityTick + 1 };
|
|
1231
1156
|
if (event.type === "message" || event.type === "error")
|
|
1232
1157
|
return { ...status, activityTick: status.activityTick + 1 };
|
|
1233
1158
|
return status;
|
|
1234
1159
|
}
|
|
1235
|
-
function toolKindForToolUse(toolName, input) {
|
|
1236
|
-
if (toolName === "image2")
|
|
1237
|
-
return isRecord(input) && input.mode === "edit" ? "修图" : "作图";
|
|
1238
|
-
if (toolName === "edit" || toolName === "write" || toolName.includes("artifact_editor") || toolName.includes("apply_patch"))
|
|
1239
|
-
return "编辑";
|
|
1240
|
-
if (toolName === "exec" || toolName.includes("shell") || toolName.includes("command"))
|
|
1241
|
-
return "执行";
|
|
1242
|
-
if (toolName.includes("download"))
|
|
1243
|
-
return "下载";
|
|
1244
|
-
if (toolName === "read" || toolName === "list" || toolName === "grep" || toolName === "search" || toolName.includes("query") || toolName.includes("load"))
|
|
1245
|
-
return "查询";
|
|
1246
|
-
if (toolName === "plan")
|
|
1247
|
-
return "计划";
|
|
1248
|
-
return "工具";
|
|
1249
|
-
}
|
|
1250
1160
|
async function handleExportCommand(outputPath, runtime) {
|
|
1251
1161
|
const snapshot = runtime.engine.snapshot();
|
|
1252
1162
|
if (!snapshot.session)
|
|
@@ -1365,7 +1275,7 @@ const LOGIN_FIELD_DEFINITIONS = {
|
|
|
1365
1275
|
openai: [
|
|
1366
1276
|
{ key: "apiKey", label: "API key", envKey: "OPENAI_API_KEY", scope: "provider", required: true, secret: true, placeholder: "sk-..." },
|
|
1367
1277
|
{ key: "baseUrl", label: "Base URL", envKey: "OPENAI_BASE_URL", scope: "provider", placeholder: "https://api.openai.com" },
|
|
1368
|
-
{ key: "model", label: "Model", envKey: "OPENAI_MODEL", scope: "provider", required: true, placeholder: "gpt-5.
|
|
1278
|
+
{ key: "model", label: "Model", envKey: "OPENAI_MODEL", scope: "provider", required: true, placeholder: "gpt-5.6" },
|
|
1369
1279
|
{ key: "fallbackModel", label: "Fallback model", envKey: "OPENAI_FALLBACK_MODEL", scope: "provider" },
|
|
1370
1280
|
{ key: "endpoint", label: "Endpoint", envKey: "OPENAI_ENDPOINT", scope: "provider", placeholder: "auto", options: ["auto", "responses", "chat"] },
|
|
1371
1281
|
...SHARED_LOGIN_FIELDS,
|
|
@@ -1420,7 +1330,7 @@ function defaultBaseUrlForLoginProvider(provider) {
|
|
|
1420
1330
|
function defaultModelForLoginProvider(provider) {
|
|
1421
1331
|
if (provider === "anthropic")
|
|
1422
1332
|
return "claude-sonnet-4-6";
|
|
1423
|
-
return "gpt-5.
|
|
1333
|
+
return "gpt-5.6";
|
|
1424
1334
|
}
|
|
1425
1335
|
function validateLoginFormPayload(payload) {
|
|
1426
1336
|
for (const field of LOGIN_FIELD_DEFINITIONS[payload.provider]) {
|
|
@@ -1572,6 +1482,7 @@ function renderMessage(message, append, activeAssistantId, options = {}) {
|
|
|
1572
1482
|
return false;
|
|
1573
1483
|
if (message.role === "assistant" && activeAssistantId !== undefined && message.blocks.some((block) => block.type === "text"))
|
|
1574
1484
|
return true;
|
|
1485
|
+
const parentToolName = typeof message.metadata?.tool === "string" ? message.metadata.tool : undefined;
|
|
1575
1486
|
let rendered = false;
|
|
1576
1487
|
for (const block of message.blocks) {
|
|
1577
1488
|
if (block.type === "text") {
|
|
@@ -1588,7 +1499,7 @@ function renderMessage(message, append, activeAssistantId, options = {}) {
|
|
|
1588
1499
|
const line = imageLineForBlock(message.role, block);
|
|
1589
1500
|
if (!line)
|
|
1590
1501
|
continue;
|
|
1591
|
-
append(line);
|
|
1502
|
+
append({ ...line, parentToolName });
|
|
1592
1503
|
rendered = true;
|
|
1593
1504
|
}
|
|
1594
1505
|
else if (block.type === "thinking") {
|
|
@@ -1613,20 +1524,21 @@ function renderToolResultMessage(message, append) {
|
|
|
1613
1524
|
for (const block of message.blocks) {
|
|
1614
1525
|
if (block.type !== "tool_result")
|
|
1615
1526
|
continue;
|
|
1616
|
-
append(formatToolResultLine(block.name, block.output, block.ok));
|
|
1527
|
+
append(formatToolResultLine(block.name, block.output, block.ok), block.toolUseId);
|
|
1617
1528
|
rendered = true;
|
|
1618
1529
|
}
|
|
1619
1530
|
return rendered;
|
|
1620
1531
|
}
|
|
1621
1532
|
function renderMessageImages(message, append) {
|
|
1622
1533
|
let rendered = false;
|
|
1534
|
+
const parentToolName = typeof message.metadata?.tool === "string" ? message.metadata.tool : undefined;
|
|
1623
1535
|
for (const block of message.blocks) {
|
|
1624
1536
|
if (block.type !== "image")
|
|
1625
1537
|
continue;
|
|
1626
1538
|
const line = imageLineForBlock(message.role, block);
|
|
1627
1539
|
if (!line)
|
|
1628
1540
|
continue;
|
|
1629
|
-
append(line);
|
|
1541
|
+
append({ ...line, parentToolName });
|
|
1630
1542
|
rendered = true;
|
|
1631
1543
|
}
|
|
1632
1544
|
return rendered;
|
|
@@ -1647,43 +1559,20 @@ function imageLineForBlock(role, block) {
|
|
|
1647
1559
|
kind,
|
|
1648
1560
|
text: block.label ?? `[image ${block.mimeType}]`,
|
|
1649
1561
|
image: {
|
|
1650
|
-
src:
|
|
1562
|
+
src: imageBlockToDataUrl(block),
|
|
1651
1563
|
label: block.label,
|
|
1652
1564
|
mimeType: block.mimeType,
|
|
1653
1565
|
},
|
|
1654
1566
|
};
|
|
1655
1567
|
}
|
|
1656
|
-
function
|
|
1657
|
-
|
|
1658
|
-
return `/api/images?path=${encodeImagePath(block.storage.path)}&mime=${encodeURIComponent(block.mimeType)}`;
|
|
1659
|
-
}
|
|
1660
|
-
const data = block.data.trim();
|
|
1568
|
+
function imageBlockToDataUrl(block) {
|
|
1569
|
+
const data = resolveImageBlockDataSync(block);
|
|
1661
1570
|
if (!data)
|
|
1662
1571
|
return "";
|
|
1663
1572
|
if (data.startsWith("data:"))
|
|
1664
1573
|
return data;
|
|
1665
1574
|
return `data:${block.mimeType};base64,${data}`;
|
|
1666
1575
|
}
|
|
1667
|
-
function encodeImagePath(filepath) {
|
|
1668
|
-
return Buffer.from(filepath, "utf8").toString("base64url");
|
|
1669
|
-
}
|
|
1670
|
-
function decodeImagePath(value) {
|
|
1671
|
-
if (!value)
|
|
1672
|
-
return undefined;
|
|
1673
|
-
try {
|
|
1674
|
-
return Buffer.from(value, "base64url").toString("utf8");
|
|
1675
|
-
}
|
|
1676
|
-
catch {
|
|
1677
|
-
return undefined;
|
|
1678
|
-
}
|
|
1679
|
-
}
|
|
1680
|
-
function safeImageContentType(value) {
|
|
1681
|
-
return value && /^image\/[a-z0-9.+-]+$/i.test(value) ? value : "application/octet-stream";
|
|
1682
|
-
}
|
|
1683
|
-
function stripDataUrlPrefix(value) {
|
|
1684
|
-
const match = /^data:[^;]+;base64,(.*)$/is.exec(value);
|
|
1685
|
-
return match ? match[1] : value;
|
|
1686
|
-
}
|
|
1687
1576
|
function assistantText(message) {
|
|
1688
1577
|
const text = message.blocks.filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
1689
1578
|
return text.length > 0 ? text : undefined;
|
|
@@ -1736,30 +1625,61 @@ function thinkingLine(text, live = false) {
|
|
|
1736
1625
|
return { kind: "thinking", title: titleForKind("thinking"), text, previewStyle: "summary", summaryMaxLines: THINKING_SUMMARY_MAX_LINES, live };
|
|
1737
1626
|
}
|
|
1738
1627
|
function formatToolUse(toolUse) {
|
|
1739
|
-
const toolKind = toolKindForToolUse(toolUse.name, toolUse.input);
|
|
1740
1628
|
if (toolUse.name === "plan" && isPlanToolPayload(toolUse.input))
|
|
1741
|
-
return { kind: "tool", title: toolTitle(toolUse.name, "running"), bodyTitle: planToolBodyTitle(toolUse.input),
|
|
1742
|
-
|
|
1743
|
-
|
|
1629
|
+
return { kind: "tool", toolName: toolUse.name, title: toolTitle(toolUse.name, "running"), bodyTitle: planToolBodyTitle(toolUse.input), text: formatPlanToolPayload(toolUse.input), collapsible: true };
|
|
1630
|
+
if (toolUse.name === "image2")
|
|
1631
|
+
return { kind: "tool", toolName: toolUse.name, title: toolTitle(toolUse.name, "running"), bodyTitle: "图片模型处理中", text: JSON.stringify(toolUse.input ?? {}, null, 2), format: "plain", previewStyle: "summary", collapsible: true };
|
|
1632
|
+
const summary = summarizeToolUse(toolUse.name, toolUse.input);
|
|
1633
|
+
return { kind: "tool", toolName: toolUse.name, title: toolTitle(toolUse.name, "running"), bodyTitle: summary.bodyTitle, text: summary.text, previewStyle: "summary", collapsible: true };
|
|
1744
1634
|
}
|
|
1745
1635
|
function formatToolResultLine(toolName, output, ok) {
|
|
1746
1636
|
const formatted = formatToolResult(toolName, output, ok);
|
|
1747
|
-
return { kind: ok ? "tool" : "error", title: toolTitle(toolName, "finished"), bodyTitle: formatted.bodyTitle, titleStatus: ok ? "success" : "failure",
|
|
1748
|
-
}
|
|
1749
|
-
function xhsArtifactFromToolOutput(toolName, output) {
|
|
1750
|
-
if (toolName !== "open_xhs_artifact_editor" || !isRecord(output))
|
|
1751
|
-
return undefined;
|
|
1752
|
-
const artifact = output.artifact;
|
|
1753
|
-
return isRecord(artifact) && typeof artifact.id === "string" ? artifact : undefined;
|
|
1637
|
+
return { kind: ok ? "tool" : "error", toolName, 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 };
|
|
1754
1638
|
}
|
|
1755
1639
|
function toolTitle(toolName, _phase) {
|
|
1756
|
-
|
|
1640
|
+
const labels = {
|
|
1641
|
+
agent: "子任务",
|
|
1642
|
+
edit: "编辑文件",
|
|
1643
|
+
exec: "执行命令",
|
|
1644
|
+
expose_downloads: "文件下载",
|
|
1645
|
+
grep: "搜索文本",
|
|
1646
|
+
image2: "图片生成",
|
|
1647
|
+
image_note: "记录图片说明",
|
|
1648
|
+
list: "列出文件",
|
|
1649
|
+
load_image: "读取图片",
|
|
1650
|
+
plan: "任务计划",
|
|
1651
|
+
read: "读取文件",
|
|
1652
|
+
search: "网络搜索",
|
|
1653
|
+
write: "写入文件",
|
|
1654
|
+
};
|
|
1655
|
+
return labels[toolName] ?? toolName;
|
|
1656
|
+
}
|
|
1657
|
+
function summarizeToolUse(toolName, input) {
|
|
1658
|
+
const purpose = toolUsePurpose(toolName, input);
|
|
1659
|
+
return {
|
|
1660
|
+
bodyTitle: purpose,
|
|
1661
|
+
text: purpose ? `目的:${purpose}` : "正在执行工具…",
|
|
1662
|
+
};
|
|
1757
1663
|
}
|
|
1758
|
-
function
|
|
1664
|
+
function toolUsePurpose(toolName, input) {
|
|
1759
1665
|
if (!isRecord(input))
|
|
1760
1666
|
return undefined;
|
|
1761
1667
|
const description = typeof input.description === "string" ? input.description.trim() : "";
|
|
1762
|
-
|
|
1668
|
+
if (description)
|
|
1669
|
+
return description;
|
|
1670
|
+
if (toolName === "expose_downloads")
|
|
1671
|
+
return "准备网页下载链接";
|
|
1672
|
+
if (toolName === "read" && typeof input.path === "string")
|
|
1673
|
+
return `读取 ${path.basename(input.path)}`;
|
|
1674
|
+
if (toolName === "list" && typeof input.path === "string")
|
|
1675
|
+
return `查看 ${input.path}`;
|
|
1676
|
+
if (toolName === "grep" && typeof input.query === "string")
|
|
1677
|
+
return `搜索 ${input.query}`;
|
|
1678
|
+
if (toolName === "write" && typeof input.path === "string")
|
|
1679
|
+
return `写入 ${path.basename(input.path)}`;
|
|
1680
|
+
if (toolName === "edit" && typeof input.path === "string")
|
|
1681
|
+
return `修改 ${path.basename(input.path)}`;
|
|
1682
|
+
return undefined;
|
|
1763
1683
|
}
|
|
1764
1684
|
function isPlanToolPayload(value) {
|
|
1765
1685
|
if (!isRecord(value) || !Array.isArray(value.items))
|
|
@@ -1794,12 +1714,36 @@ function formatToolResult(toolName, output, ok) {
|
|
|
1794
1714
|
return { text: formatWebSearchToolResult(output, ok), summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
|
|
1795
1715
|
if (toolName === "image2" && isRecord(output))
|
|
1796
1716
|
return { text: formatImageGenerationToolResult(output, ok), format: "plain", summaryMaxLines: 4 };
|
|
1717
|
+
if (toolName === "expose_downloads")
|
|
1718
|
+
return { text: formatExposeDownloadsToolResult(output, ok), full: true, bodyTitle: ok ? "请点击下载" : "下载准备失败" };
|
|
1797
1719
|
if (toolName === "plan" && isPlanToolPayload(output))
|
|
1798
1720
|
return { text: formatPlanToolPayload(output), full: true, bodyTitle: planToolBodyTitle(output) };
|
|
1799
1721
|
if (typeof output === "string")
|
|
1800
1722
|
return { text: output, format: hasAnsi(output) ? "ansi" : undefined, summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
|
|
1801
1723
|
return { text: `${ok ? "ok" : "failed"}\n${formatReplData(output, 6000)}`, summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
|
|
1802
1724
|
}
|
|
1725
|
+
function formatExposeDownloadsToolResult(output, ok) {
|
|
1726
|
+
const downloads = extractDownloadEntries(output);
|
|
1727
|
+
if (!ok)
|
|
1728
|
+
return downloads.length ? `下载准备失败\n${formatDownloadEntries(downloads)}` : "下载准备失败";
|
|
1729
|
+
if (!downloads.length)
|
|
1730
|
+
return "文件已准备好,请点击下载。";
|
|
1731
|
+
return `文件已准备好,请点击下载。\n${formatDownloadEntries(downloads)}`;
|
|
1732
|
+
}
|
|
1733
|
+
function formatDownloadEntries(downloads) {
|
|
1734
|
+
return downloads.map((entry) => {
|
|
1735
|
+
const filename = entry.filename || entry.name || "下载文件";
|
|
1736
|
+
const url = entry.url || (entry.id ? `/api/downloads/${encodeURIComponent(entry.id)}` : "");
|
|
1737
|
+
return url ? `- ${filename}: ${url}` : `- ${filename}`;
|
|
1738
|
+
}).join("\n");
|
|
1739
|
+
}
|
|
1740
|
+
function extractDownloadEntries(output) {
|
|
1741
|
+
if (!isRecord(output))
|
|
1742
|
+
return [];
|
|
1743
|
+
const candidates = [output.downloads, isRecord(output.output) ? output.output.downloads : undefined, isRecord(output.result) ? output.result.downloads : undefined];
|
|
1744
|
+
const downloads = candidates.find(Array.isArray);
|
|
1745
|
+
return Array.isArray(downloads) ? downloads.filter(isRecord).map((entry) => ({ ...entry })) : [];
|
|
1746
|
+
}
|
|
1803
1747
|
function isEditToolOutput(value) {
|
|
1804
1748
|
return typeof value.path === "string" && typeof value.operation === "string" && typeof value.replacements === "number" && Array.isArray(value.patch) && value.patch.every(isEditPatchHunk);
|
|
1805
1749
|
}
|
|
@@ -2122,4 +2066,4 @@ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.me
|
|
|
2122
2066
|
process.exitCode = 1;
|
|
2123
2067
|
});
|
|
2124
2068
|
}
|
|
2125
|
-
//# sourceMappingURL=index.js.map
|
|
2069
|
+
//# sourceMappingURL=index.js.map
|