@chainingintention/pi-web-cn 1.202606.11 → 1.202606.13
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 +14 -7
- package/dist/cli.js +14 -2
- package/dist/cli.js.map +1 -1
- package/dist/client/assets/{CodeViewer-DzeGsHZ5.js → CodeViewer-D5OA_6r4.js} +1 -1
- package/dist/client/assets/{TerminalPanel-BghODb4T.js → TerminalPanel-kfPHfhUe.js} +9 -9
- package/dist/client/assets/index-MmspRbMF.js +2403 -0
- package/dist/client/index.html +1 -1
- package/dist/config.js +54 -3
- package/dist/config.js.map +1 -1
- package/dist/pi-web-plugins/updates/package.json +1 -1
- package/dist/pi-web-plugins/updates/pi-web-plugin.js +61 -71
- package/dist/pi-web-plugins/updates/updatesLogic.js +65 -0
- package/dist/pi-web-plugins/workspace-tasks/tasksPanelElement.js +1 -5
- package/dist/server/app.js +14 -6
- package/dist/server/app.js.map +1 -1
- package/dist/server/configRoutes.js +10 -0
- package/dist/server/configRoutes.js.map +1 -1
- package/dist/server/index.js +2 -2
- package/dist/server/index.js.map +1 -1
- package/dist/server/machines/machinePluginProxyRoutes.js +8 -0
- package/dist/server/machines/machinePluginProxyRoutes.js.map +1 -1
- package/dist/server/machines/machineRoutes.js +6 -0
- package/dist/server/machines/machineRoutes.js.map +1 -1
- package/dist/server/machines/machineService.js +97 -5
- package/dist/server/machines/machineService.js.map +1 -1
- package/dist/server/piWebPluginService.js +24 -4
- package/dist/server/piWebPluginService.js.map +1 -1
- package/dist/server/piWebStatus.js +149 -45
- package/dist/server/piWebStatus.js.map +1 -1
- package/dist/server/piWebStatusCache.js +32 -0
- package/dist/server/piWebStatusCache.js.map +1 -0
- package/dist/server/sessiond/sessionProxyRoutes.js +15 -1
- package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -1
- package/dist/server/sessiond.js +34 -9
- package/dist/server/sessiond.js.map +1 -1
- package/dist/server/sessions/attachmentService.js +61 -0
- package/dist/server/sessions/attachmentService.js.map +1 -0
- package/dist/server/sessions/builtinCommands.js +21 -21
- package/dist/server/sessions/builtinCommands.js.map +1 -1
- package/dist/server/sessions/oauthLoginFlowService.js +21 -14
- package/dist/server/sessions/oauthLoginFlowService.js.map +1 -1
- package/dist/server/sessions/piSessionManagerGateway.js +96 -0
- package/dist/server/sessions/piSessionManagerGateway.js.map +1 -0
- package/dist/server/sessions/piSessionService.js +270 -452
- package/dist/server/sessions/piSessionService.js.map +1 -1
- package/dist/server/sessions/sessionArchiveStore.js +16 -2
- package/dist/server/sessions/sessionArchiveStore.js.map +1 -1
- package/dist/server/sessions/sessionCommandService.js +32 -28
- package/dist/server/sessions/sessionCommandService.js.map +1 -1
- package/dist/server/sessions/sessionRoutes.js +157 -47
- package/dist/server/sessions/sessionRoutes.js.map +1 -1
- package/dist/server/sessions/spawnSessionTool.js +36 -0
- package/dist/server/sessions/spawnSessionTool.js.map +1 -0
- package/dist/server/sessions/spawnTargetResolver.js +36 -0
- package/dist/server/sessions/spawnTargetResolver.js.map +1 -0
- package/dist/server/terminals/terminalRoutes.js +10 -4
- package/dist/server/terminals/terminalRoutes.js.map +1 -1
- package/dist/server/workingDirectory.js +44 -0
- package/dist/server/workingDirectory.js.map +1 -0
- package/dist/server/workspaces/fileSuggestions.js +96 -16
- package/dist/server/workspaces/fileSuggestions.js.map +1 -1
- package/dist/shared/apiTypes.d.ts +77 -5
- package/dist/shared/apiTypes.js +5 -1
- package/dist/shared/apiTypes.js.map +1 -1
- package/dist/shared/capabilities.js +27 -0
- package/dist/shared/capabilities.js.map +1 -0
- package/dist/shared/federatedRoutes.js +3 -0
- package/dist/shared/federatedRoutes.js.map +1 -1
- package/dist/shared/piWebStatusParsing.js +28 -0
- package/dist/shared/piWebStatusParsing.js.map +1 -1
- package/dist/shared/promptAttachments.js +73 -0
- package/dist/shared/promptAttachments.js.map +1 -0
- package/dist/shared/thinkingLevels.d.ts +27 -0
- package/dist/shared/thinkingLevels.js +31 -0
- package/dist/shared/thinkingLevels.js.map +1 -0
- package/dist/shared/workspaceDeletion.js +1 -1
- package/dist/shared/workspaceDeletion.js.map +1 -1
- package/docs/plugins.md +17 -10
- package/package.json +24 -13
- package/dist/client/assets/index-DATsMV4H.js +0 -2203
- package/dist/server/sessions/managementPermissionSystem.js +0 -94
- package/dist/server/sessions/managementPermissionSystem.js.map +0 -1
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { access as fsAccess, mkdir as fsMkdir, readFile, readdir as fsReaddir, realpath as fsRealpath, stat as fsStat, writeFile } from "node:fs/promises";
|
|
4
|
-
import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
5
|
-
import { AuthStorage, createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, createEditToolDefinition, createFindToolDefinition, createGrepToolDefinition, createLsToolDefinition, createReadToolDefinition, createWriteToolDefinition, defineTool, getAgentDir, ModelRegistry, SessionManager, } from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import { Type } from "typebox";
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { AuthStorage, createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, createEditToolDefinition, defineTool, getAgentDir, ModelRegistry, SessionManager, } from "@earendil-works/pi-coding-agent";
|
|
7
3
|
import { pageMessagesAtSafeBoundary } from "./messagePaging.js";
|
|
8
4
|
import { BUILTIN_COMMANDS } from "./builtinCommands.js";
|
|
9
5
|
import { SessionCommandService } from "./sessionCommandService.js";
|
|
@@ -11,43 +7,37 @@ import { SessionArchiveStore } from "./sessionArchiveStore.js";
|
|
|
11
7
|
import { findArchiveCandidateByIdOrPrefix, planSessionArchiveTree } from "./sessionArchiveTree.js";
|
|
12
8
|
import { fallbackSessionName, generateShortSessionName } from "./sessionNameGenerator.js";
|
|
13
9
|
import { computeEditPreview } from "./editPreview.js";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
10
|
+
import { createPiSessionManagerGateway } from "./piSessionManagerGateway.js";
|
|
11
|
+
import { attachmentsToInlineImages, saveAttachmentsToWorkspace } from "./attachmentService.js";
|
|
12
|
+
import { parsePromptAttachments } from "../../shared/promptAttachments.js";
|
|
13
|
+
import { cwdPathsEqual } from "../workingDirectory.js";
|
|
14
|
+
import { createSpawnSessionToolDefinition } from "./spawnSessionTool.js";
|
|
15
|
+
const noopLogger = { info() { } };
|
|
17
16
|
function noop() {
|
|
18
17
|
// Intentionally empty default unsubscribe callback.
|
|
19
18
|
}
|
|
19
|
+
function spawnTargetError(decision) {
|
|
20
|
+
if (decision.reason === "not-registered")
|
|
21
|
+
return new Error("派生会话不在已注册项目中");
|
|
22
|
+
return new Error(`cwd 必须是此项目的工作区。允许的路径:${decision.allowedCwds.join(", ")}`);
|
|
23
|
+
}
|
|
20
24
|
function authLossWarningKey(sessionId, provider, modelId) {
|
|
21
25
|
return `${sessionId}:${provider}/${modelId}`;
|
|
22
26
|
}
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
function sessionIdFromLookup(ref) {
|
|
28
|
+
return typeof ref === "string" ? ref : ref.id;
|
|
29
|
+
}
|
|
30
|
+
function isPiSessionRef(ref) {
|
|
31
|
+
return typeof ref !== "string";
|
|
32
|
+
}
|
|
33
|
+
function lookupMatchesActiveSession(ref, active) {
|
|
34
|
+
return !isPiSessionRef(ref) || cwdPathsEqual(active.runtime.cwd, ref.cwd);
|
|
35
|
+
}
|
|
25
36
|
function requirePromptText(value) {
|
|
26
37
|
if (typeof value !== "string")
|
|
27
|
-
throw new Error("
|
|
38
|
+
throw new Error("提示文本为必填项");
|
|
28
39
|
return value;
|
|
29
40
|
}
|
|
30
|
-
function requirePromptImages(value) {
|
|
31
|
-
if (value === undefined)
|
|
32
|
-
return [];
|
|
33
|
-
if (!Array.isArray(value))
|
|
34
|
-
throw new Error("Prompt images must be an array");
|
|
35
|
-
if (value.length > MAX_PROMPT_IMAGES)
|
|
36
|
-
throw new Error(`Prompt images are limited to ${String(MAX_PROMPT_IMAGES)}`);
|
|
37
|
-
return value.map((image) => {
|
|
38
|
-
if (!isRecord(image))
|
|
39
|
-
throw new Error("Prompt image must be an object");
|
|
40
|
-
if (image["type"] !== "image")
|
|
41
|
-
throw new Error('Prompt image type must be "image"');
|
|
42
|
-
const data = image["data"];
|
|
43
|
-
const mimeType = image["mimeType"];
|
|
44
|
-
if (typeof data !== "string" || data === "")
|
|
45
|
-
throw new Error("Prompt image data is required");
|
|
46
|
-
if (typeof mimeType !== "string" || !SUPPORTED_PROMPT_IMAGE_MIME_TYPES.has(mimeType))
|
|
47
|
-
throw new Error(`Unsupported prompt image MIME type: ${String(mimeType)}`);
|
|
48
|
-
return { type: "image", data, mimeType };
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
41
|
function parsePromptStreamingBehavior(value) {
|
|
52
42
|
if (value === undefined)
|
|
53
43
|
return undefined;
|
|
@@ -60,49 +50,22 @@ function defaultCreateAgentRuntime(createRuntime, options) {
|
|
|
60
50
|
throw new Error("Default runtime creation requires an SDK SessionManager");
|
|
61
51
|
return createAgentSessionRuntime(createRuntime, { ...options, sessionManager: options.sessionManager });
|
|
62
52
|
}
|
|
63
|
-
function createDefaultRuntimeFactory(authStorage, modelRegistry) {
|
|
53
|
+
function createDefaultRuntimeFactory(authStorage, modelRegistry, spawn) {
|
|
64
54
|
return async ({ cwd, agentDir, sessionManager, sessionStartEvent }) => {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
function createManagementRuntimeFactory(authStorage, modelRegistry, managementContext) {
|
|
77
|
-
return async ({ cwd, agentDir, sessionManager, sessionStartEvent }) => {
|
|
78
|
-
const policyAgentDir = await writeManagementPermissionSystemPolicy(agentDir, cwd, managementContext);
|
|
79
|
-
return withRuntimeCreationEnvironment({ [PI_PERMISSION_SYSTEM_POLICY_AGENT_DIR]: policyAgentDir }, async () => {
|
|
80
|
-
const services = await createAgentSessionServices({ cwd, agentDir, authStorage, modelRegistry });
|
|
81
|
-
const customTools = createManagementSandboxToolDefinitions(cwd, managementContext);
|
|
82
|
-
const options = sessionStartEvent === undefined
|
|
83
|
-
? { services, sessionManager, customTools, tools: managementAgentToolNames(managementContext) }
|
|
84
|
-
: { services, sessionManager, sessionStartEvent, customTools, tools: managementAgentToolNames(managementContext) };
|
|
85
|
-
// @ts-expect-error SDK customTools accepts concrete ToolDefinition instances at runtime, but the published type is invariant in render callbacks.
|
|
86
|
-
const result = await createAgentSessionFromServices(options);
|
|
87
|
-
return { ...result, services, diagnostics: services.diagnostics };
|
|
88
|
-
});
|
|
55
|
+
const services = await createAgentSessionServices({ cwd, agentDir, authStorage, modelRegistry });
|
|
56
|
+
const customTools = [
|
|
57
|
+
createPiWebEditToolDefinition(cwd),
|
|
58
|
+
...(spawn === undefined ? [] : [createSpawnSessionToolDefinition(cwd, { spawn })]),
|
|
59
|
+
];
|
|
60
|
+
const options = sessionStartEvent === undefined
|
|
61
|
+
? { services, sessionManager, customTools }
|
|
62
|
+
: { services, sessionManager, sessionStartEvent, customTools };
|
|
63
|
+
const result = await createAgentSessionFromServices(options);
|
|
64
|
+
return { ...result, services, diagnostics: services.diagnostics };
|
|
89
65
|
};
|
|
90
66
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const operations = createManagedFileOperations(cwd);
|
|
94
|
-
return [
|
|
95
|
-
createReadToolDefinition(cwd, { operations: operations.read }),
|
|
96
|
-
createWriteToolDefinition(cwd, { operations: operations.write }),
|
|
97
|
-
createPiWebEditToolDefinition(cwd, operations.edit),
|
|
98
|
-
createLsToolDefinition(cwd, { operations: operations.ls }),
|
|
99
|
-
createGrepToolDefinition(cwd, { operations: operations.grep }),
|
|
100
|
-
createFindToolDefinition(cwd, { operations: operations.find }),
|
|
101
|
-
createManagedPythonToolDefinition(cwd, context),
|
|
102
|
-
];
|
|
103
|
-
}
|
|
104
|
-
function createPiWebEditToolDefinition(cwd, operations) {
|
|
105
|
-
const editTool = createEditToolDefinition(cwd, operations === undefined ? undefined : { operations });
|
|
67
|
+
function createPiWebEditToolDefinition(cwd) {
|
|
68
|
+
const editTool = createEditToolDefinition(cwd);
|
|
106
69
|
return defineTool({
|
|
107
70
|
name: editTool.name,
|
|
108
71
|
label: editTool.label,
|
|
@@ -122,211 +85,6 @@ function createPiWebEditToolDefinition(cwd, operations) {
|
|
|
122
85
|
},
|
|
123
86
|
});
|
|
124
87
|
}
|
|
125
|
-
const pythonSchema = Type.Object({
|
|
126
|
-
code: Type.String({ description: "Python code to run in the managed project workspace" }),
|
|
127
|
-
timeoutMs: Type.Optional(Type.Number({ description: "Execution timeout in milliseconds" })),
|
|
128
|
-
});
|
|
129
|
-
function createManagedPythonToolDefinition(cwd, context) {
|
|
130
|
-
return defineTool({
|
|
131
|
-
name: "python",
|
|
132
|
-
label: "python",
|
|
133
|
-
description: "Run Python code inside the managed project workspace. Shell commands and paths outside the project are blocked.",
|
|
134
|
-
promptSnippet: "Run Python code in the current project",
|
|
135
|
-
promptGuidelines: ["Use python for scripts and calculations. Do not use it to run shell commands."],
|
|
136
|
-
parameters: pythonSchema,
|
|
137
|
-
async execute(_toolCallId, params, signal) {
|
|
138
|
-
const configuredPython = context.sandbox?.pythonExecutable?.trim();
|
|
139
|
-
const pythonExecutable = configuredPython !== undefined && configuredPython !== "" ? configuredPython : "python3";
|
|
140
|
-
const timeoutMs = Math.max(1_000, Math.min(params.timeoutMs ?? 30_000, 120_000));
|
|
141
|
-
const configuredBubblewrap = process.env["PI_WEB_BWRAP_EXECUTABLE"]?.trim();
|
|
142
|
-
const bubblewrapExecutable = configuredBubblewrap !== undefined && configuredBubblewrap !== "" ? configuredBubblewrap : "bwrap";
|
|
143
|
-
const env = createManagedSandboxEnvironment({ hostEnv: process.env, context });
|
|
144
|
-
return runManagedPython({ pythonExecutable, bubblewrapExecutable, cwd, code: params.code, timeoutMs, env, signal });
|
|
145
|
-
},
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
async function runManagedPython(options) {
|
|
149
|
-
const root = await fsRealpath(options.cwd);
|
|
150
|
-
const invocation = createBubblewrapPythonInvocation({
|
|
151
|
-
bubblewrapExecutable: options.bubblewrapExecutable,
|
|
152
|
-
pythonExecutable: options.pythonExecutable,
|
|
153
|
-
workspaceRoot: root,
|
|
154
|
-
env: options.env,
|
|
155
|
-
readOnlyPaths: await readableBubblewrapPaths(),
|
|
156
|
-
});
|
|
157
|
-
const result = await runPythonProcess({ command: invocation.command, args: invocation.args, cwd: root, env: options.env, code: options.code, timeoutMs: options.timeoutMs, signal: options.signal });
|
|
158
|
-
const unavailable = bubblewrapUnavailableReason(result.output);
|
|
159
|
-
if (unavailable !== undefined) {
|
|
160
|
-
return runManagedPythonFallback({ pythonExecutable: options.pythonExecutable, cwd: root, code: options.code, timeoutMs: options.timeoutMs, env: options.env, signal: options.signal });
|
|
161
|
-
}
|
|
162
|
-
return pythonToolResult(result.code, result.output);
|
|
163
|
-
}
|
|
164
|
-
async function runManagedPythonFallback(options) {
|
|
165
|
-
const code = `${createManagedPythonFallbackPrelude(options.cwd)}\n${options.code}`;
|
|
166
|
-
const result = await runPythonProcess({ command: options.pythonExecutable, args: ["-I", "-"], cwd: options.cwd, env: options.env, code, timeoutMs: options.timeoutMs, signal: options.signal });
|
|
167
|
-
return pythonToolResult(result.code, result.output);
|
|
168
|
-
}
|
|
169
|
-
async function runPythonProcess(options) {
|
|
170
|
-
return new Promise((resolvePromise, reject) => {
|
|
171
|
-
if (options.signal?.aborted === true) {
|
|
172
|
-
reject(new Error("Operation aborted"));
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
const child = spawn(options.command, options.args, { cwd: options.cwd, env: options.env, stdio: ["pipe", "pipe", "pipe"], shell: false });
|
|
176
|
-
let stdout = "";
|
|
177
|
-
let stderr = "";
|
|
178
|
-
const timer = setTimeout(() => {
|
|
179
|
-
child.kill();
|
|
180
|
-
reject(new Error(`Python execution timed out after ${String(options.timeoutMs)}ms`));
|
|
181
|
-
}, options.timeoutMs);
|
|
182
|
-
const onAbort = () => {
|
|
183
|
-
child.kill();
|
|
184
|
-
reject(new Error("Operation aborted"));
|
|
185
|
-
};
|
|
186
|
-
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
187
|
-
child.stdout.on("data", (chunk) => { stdout += chunk.toString("utf8"); });
|
|
188
|
-
child.stderr.on("data", (chunk) => { stderr += chunk.toString("utf8"); });
|
|
189
|
-
child.on("error", (error) => {
|
|
190
|
-
clearTimeout(timer);
|
|
191
|
-
options.signal?.removeEventListener("abort", onAbort);
|
|
192
|
-
if (isNodeErrorWithCode(error, "ENOENT"))
|
|
193
|
-
reject(new Error("Python sandbox is unavailable"));
|
|
194
|
-
else
|
|
195
|
-
reject(error);
|
|
196
|
-
});
|
|
197
|
-
child.on("close", (codeValue) => {
|
|
198
|
-
clearTimeout(timer);
|
|
199
|
-
options.signal?.removeEventListener("abort", onAbort);
|
|
200
|
-
const output = truncateToolOutput([stdout.trimEnd(), stderr.trimEnd()].filter((part) => part !== "").join("\n"));
|
|
201
|
-
resolvePromise({ code: codeValue, output });
|
|
202
|
-
});
|
|
203
|
-
child.stdin.end(options.code);
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
function pythonToolResult(codeValue, output) {
|
|
207
|
-
const prefix = codeValue === 0 ? "" : `Python exited with code ${String(codeValue)}\n`;
|
|
208
|
-
return { content: [{ type: "text", text: `${prefix}${output}`.trimEnd() }], details: undefined };
|
|
209
|
-
}
|
|
210
|
-
async function readableBubblewrapPaths() {
|
|
211
|
-
const paths = await Promise.all(DEFAULT_BUBBLEWRAP_PATHS.map(async (path) => {
|
|
212
|
-
try {
|
|
213
|
-
await fsAccess(path, constants.R_OK);
|
|
214
|
-
return path;
|
|
215
|
-
}
|
|
216
|
-
catch {
|
|
217
|
-
return undefined;
|
|
218
|
-
}
|
|
219
|
-
}));
|
|
220
|
-
return paths.filter(isDefined);
|
|
221
|
-
}
|
|
222
|
-
function createManagedFileOperations(cwd) {
|
|
223
|
-
const read = {
|
|
224
|
-
readFile: async (absolutePath) => readFile(await assertExistingInside(cwd, absolutePath)),
|
|
225
|
-
access: async (absolutePath) => { await fsAccess(await assertExistingInside(cwd, absolutePath), constants.R_OK); },
|
|
226
|
-
};
|
|
227
|
-
const write = {
|
|
228
|
-
writeFile: async (absolutePath, content) => { await writeFile(await assertWritableInside(cwd, absolutePath), content, "utf8"); },
|
|
229
|
-
mkdir: async (dir) => { await fsMkdir(await assertPotentialInside(cwd, dir), { recursive: true }); },
|
|
230
|
-
};
|
|
231
|
-
const edit = {
|
|
232
|
-
readFile: read.readFile,
|
|
233
|
-
writeFile: write.writeFile,
|
|
234
|
-
access: async (absolutePath) => { await fsAccess(await assertExistingInside(cwd, absolutePath), constants.R_OK | constants.W_OK); },
|
|
235
|
-
};
|
|
236
|
-
const ls = {
|
|
237
|
-
exists: async (absolutePath) => pathExistsInside(cwd, absolutePath),
|
|
238
|
-
stat: async (absolutePath) => fsStat(await assertExistingInside(cwd, absolutePath)),
|
|
239
|
-
readdir: async (absolutePath) => fsReaddir(await assertExistingInside(cwd, absolutePath)),
|
|
240
|
-
};
|
|
241
|
-
const grep = {
|
|
242
|
-
isDirectory: async (absolutePath) => (await fsStat(await assertExistingInside(cwd, absolutePath))).isDirectory(),
|
|
243
|
-
readFile: async (absolutePath) => (await readFile(await assertExistingInside(cwd, absolutePath), "utf8")),
|
|
244
|
-
};
|
|
245
|
-
const find = {
|
|
246
|
-
exists: async (absolutePath) => pathExistsInside(cwd, absolutePath),
|
|
247
|
-
glob: async (pattern, searchPath, options) => managedGlob(cwd, pattern, searchPath, options.limit),
|
|
248
|
-
};
|
|
249
|
-
return { read, write, edit, ls, grep, find };
|
|
250
|
-
}
|
|
251
|
-
async function pathExistsInside(rootPath, targetPath) {
|
|
252
|
-
try {
|
|
253
|
-
await assertExistingInside(rootPath, targetPath);
|
|
254
|
-
return true;
|
|
255
|
-
}
|
|
256
|
-
catch {
|
|
257
|
-
return false;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
async function managedGlob(rootPath, pattern, searchPath, limit) {
|
|
261
|
-
const root = await fsRealpath(rootPath);
|
|
262
|
-
const start = await assertExistingInside(root, searchPath);
|
|
263
|
-
const regex = globPatternToRegExp(pattern);
|
|
264
|
-
const results = [];
|
|
265
|
-
async function walk(dir) {
|
|
266
|
-
if (results.length >= limit)
|
|
267
|
-
return;
|
|
268
|
-
const entries = await fsReaddir(dir, { withFileTypes: true });
|
|
269
|
-
for (const entry of entries) {
|
|
270
|
-
if (results.length >= limit)
|
|
271
|
-
return;
|
|
272
|
-
if (entry.name === ".git" || entry.name === "node_modules")
|
|
273
|
-
continue;
|
|
274
|
-
const fullPath = resolve(dir, entry.name);
|
|
275
|
-
const safePath = await assertExistingInside(root, fullPath);
|
|
276
|
-
const rel = relative(start, safePath).split(sep).join("/");
|
|
277
|
-
if (entry.isDirectory()) {
|
|
278
|
-
await walk(safePath);
|
|
279
|
-
}
|
|
280
|
-
else if (regex.test(rel)) {
|
|
281
|
-
results.push(safePath);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
await walk(start);
|
|
286
|
-
return results;
|
|
287
|
-
}
|
|
288
|
-
function globPatternToRegExp(pattern) {
|
|
289
|
-
const escaped = pattern
|
|
290
|
-
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
291
|
-
.replace(/\*\*/g, "\0")
|
|
292
|
-
.replace(/\*/g, "[^/]*")
|
|
293
|
-
.replace(/\?/g, "[^/]")
|
|
294
|
-
.replace(/\0/g, ".*");
|
|
295
|
-
return new RegExp(`^${escaped}$`);
|
|
296
|
-
}
|
|
297
|
-
async function assertExistingInside(rootPath, targetPath) {
|
|
298
|
-
const [root, target] = await Promise.all([fsRealpath(rootPath), fsRealpath(targetPath)]);
|
|
299
|
-
ensurePathInside(root, target);
|
|
300
|
-
return target;
|
|
301
|
-
}
|
|
302
|
-
async function assertWritableInside(rootPath, targetPath) {
|
|
303
|
-
try {
|
|
304
|
-
return await assertExistingInside(rootPath, targetPath);
|
|
305
|
-
}
|
|
306
|
-
catch {
|
|
307
|
-
const parent = await assertExistingInside(rootPath, dirname(targetPath));
|
|
308
|
-
const target = resolve(parent, basename(targetPath));
|
|
309
|
-
ensurePathInside(await fsRealpath(rootPath), target);
|
|
310
|
-
return target;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
async function assertPotentialInside(rootPath, targetPath) {
|
|
314
|
-
const root = await fsRealpath(rootPath);
|
|
315
|
-
const target = resolve(targetPath);
|
|
316
|
-
ensurePathInside(root, target);
|
|
317
|
-
return target;
|
|
318
|
-
}
|
|
319
|
-
function ensurePathInside(root, target) {
|
|
320
|
-
const rel = relative(root, target);
|
|
321
|
-
if (rel === "" || (!rel.startsWith("..") && !isAbsolute(rel) && (sep === "/" || !rel.split(sep).includes(".."))))
|
|
322
|
-
return;
|
|
323
|
-
throw new Error("Path is outside the managed project sandbox");
|
|
324
|
-
}
|
|
325
|
-
function truncateToolOutput(value, limit = 64_000) {
|
|
326
|
-
if (value.length <= limit)
|
|
327
|
-
return value;
|
|
328
|
-
return `${value.slice(0, limit)}\n[output truncated]`;
|
|
329
|
-
}
|
|
330
88
|
export class PiSessionService {
|
|
331
89
|
constructor(events, deps = {}) {
|
|
332
90
|
this.events = events;
|
|
@@ -335,16 +93,17 @@ export class PiSessionService {
|
|
|
335
93
|
this.compactionPromptQueues = new Map();
|
|
336
94
|
this.compactionDrainTimers = new Map();
|
|
337
95
|
this.authLossWarnings = new Set();
|
|
338
|
-
this.managementContexts = new Map();
|
|
339
96
|
this.archiveStore = deps.archiveStore ?? new SessionArchiveStore();
|
|
340
97
|
this.agentDir = deps.agentDir ?? getAgentDir();
|
|
341
|
-
this.sessionManager = deps.sessionManager ??
|
|
98
|
+
this.sessionManager = deps.sessionManager ?? createPiSessionManagerGateway({ agentDir: this.agentDir });
|
|
342
99
|
this.modelRegistry = deps.modelRegistry ?? ModelRegistry.create(AuthStorage.create());
|
|
343
|
-
this.
|
|
100
|
+
this.spawnTargets = deps.spawnTargets;
|
|
101
|
+
this.logger = deps.logger ?? noopLogger;
|
|
102
|
+
this.createRuntime = deps.createRuntime ?? createDefaultRuntimeFactory(this.modelRegistry.authStorage, this.modelRegistry, this.spawnTargets === undefined ? undefined : (input) => this.spawnSession(input));
|
|
344
103
|
this.createAgentRuntime = deps.createAgentRuntime ?? defaultCreateAgentRuntime;
|
|
345
104
|
this.workspaceActivity = deps.workspaceActivity;
|
|
346
105
|
this.heartbeat = setInterval(() => { this.publishHeartbeats(); }, deps.heartbeatIntervalMs ?? 2000);
|
|
347
|
-
this.commandService = new SessionCommandService((sessionId) => this.getActive(sessionId), (sessionId, text) => this.prompt(sessionId, text), events, {
|
|
106
|
+
this.commandService = new SessionCommandService((sessionId) => this.getActive(sessionId), (sessionId, text) => this.prompt(sessionId, text, undefined, undefined, { echoUserMessage: false }), events, {
|
|
348
107
|
onCompactionStart: (session) => {
|
|
349
108
|
this.publishActivity(session, "compacting", "active");
|
|
350
109
|
this.publishStatus(session);
|
|
@@ -366,7 +125,6 @@ export class PiSessionService {
|
|
|
366
125
|
this.activities.clear();
|
|
367
126
|
this.compactionPromptQueues.clear();
|
|
368
127
|
this.authLossWarnings.clear();
|
|
369
|
-
this.managementContexts.clear();
|
|
370
128
|
await Promise.all(activeSessions.map(async (active) => {
|
|
371
129
|
active.unsubscribe();
|
|
372
130
|
this.workspaceActivity?.removeSession(active.runtime.session.sessionId, active.runtime.session.sessionManager.getCwd());
|
|
@@ -389,12 +147,11 @@ export class PiSessionService {
|
|
|
389
147
|
.filter(isDefined);
|
|
390
148
|
return [...unarchivedSessions, ...archivedSessions];
|
|
391
149
|
}
|
|
392
|
-
async start(cwd,
|
|
393
|
-
|
|
150
|
+
async start(cwd, _managementContext) {
|
|
151
|
+
void _managementContext;
|
|
152
|
+
const active = await this.create(this.sessionManager.create(cwd), cwd);
|
|
394
153
|
const { session } = active.runtime;
|
|
395
|
-
|
|
396
|
-
this.managementContexts.set(session.sessionId, managementContext);
|
|
397
|
-
return {
|
|
154
|
+
const created = {
|
|
398
155
|
id: session.sessionId,
|
|
399
156
|
path: session.sessionFile ?? "",
|
|
400
157
|
cwd,
|
|
@@ -403,25 +160,45 @@ export class PiSessionService {
|
|
|
403
160
|
messageCount: session.messages.length,
|
|
404
161
|
firstMessage: "",
|
|
405
162
|
};
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
163
|
+
// Broadcast so other clients (and the spawning agent's UI) can add the new
|
|
164
|
+
// session to their list without a manual reload.
|
|
165
|
+
this.events.publishGlobal({ type: "session.created", session: created });
|
|
166
|
+
return created;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Start a new session on behalf of a LLM and deliver an initial prompt to it.
|
|
170
|
+
* The target cwd is constrained to a workspace of the same registered project
|
|
171
|
+
* as the spawning session so the new session is visible in the web UI.
|
|
172
|
+
*/
|
|
173
|
+
async spawnSession(input) {
|
|
174
|
+
if (this.spawnTargets === undefined)
|
|
175
|
+
throw new Error("派生会话已禁用");
|
|
176
|
+
const decision = await this.spawnTargets.resolveSpawnTarget(input.spawningCwd, input.cwd);
|
|
177
|
+
if (!decision.allowed)
|
|
178
|
+
throw spawnTargetError(decision);
|
|
179
|
+
const created = await this.start(decision.cwd);
|
|
180
|
+
await this.prompt(created.id, input.prompt);
|
|
181
|
+
this.logger.info({ spawningCwd: input.spawningCwd, sessionId: created.id, cwd: decision.cwd, promptLength: input.prompt.length }, "spawn_session started a new session");
|
|
182
|
+
return { sessionId: created.id, cwd: decision.cwd };
|
|
183
|
+
}
|
|
184
|
+
async messages(ref, page) {
|
|
185
|
+
const session = await this.getOrOpen(ref);
|
|
409
186
|
return pageMessagesAtSafeBoundary(historyMessages(session), page);
|
|
410
187
|
}
|
|
411
|
-
async status(
|
|
412
|
-
return this.statusFromSession(await this.getOrOpen(
|
|
188
|
+
async status(ref) {
|
|
189
|
+
return this.statusFromSession(await this.getOrOpen(ref));
|
|
413
190
|
}
|
|
414
|
-
async availableModels(
|
|
415
|
-
const session = await this.getOrOpen(
|
|
191
|
+
async availableModels(ref) {
|
|
192
|
+
const session = await this.getOrOpen(ref);
|
|
416
193
|
session.modelRegistry.refresh();
|
|
417
194
|
const models = session.scopedModels.length > 0
|
|
418
195
|
? session.scopedModels.map((scoped) => scoped.model)
|
|
419
196
|
: session.modelRegistry.getAvailable();
|
|
420
197
|
return models.map(modelToClientModel);
|
|
421
198
|
}
|
|
422
|
-
async setModel(
|
|
423
|
-
await this.assertWritable(
|
|
424
|
-
const session = await this.getOrOpen(
|
|
199
|
+
async setModel(ref, provider, modelId) {
|
|
200
|
+
await this.assertWritable(ref);
|
|
201
|
+
const session = await this.getOrOpen(ref);
|
|
425
202
|
session.modelRegistry.refresh();
|
|
426
203
|
const candidates = session.scopedModels.length > 0
|
|
427
204
|
? session.scopedModels.map((scoped) => scoped.model)
|
|
@@ -429,46 +206,52 @@ export class PiSessionService {
|
|
|
429
206
|
const model = candidates.find((candidate) => candidate.provider === provider && candidate.id === modelId)
|
|
430
207
|
?? session.modelRegistry.find(provider, modelId);
|
|
431
208
|
if (model === undefined)
|
|
432
|
-
throw new Error(
|
|
209
|
+
throw new Error(`未找到模型:${provider}/${modelId}`);
|
|
433
210
|
await session.setModel(model);
|
|
434
211
|
this.publishActivity(session, `model: ${model.id}`, "idle", model.provider);
|
|
435
212
|
this.publishStatus(session);
|
|
436
213
|
return this.statusFromSession(session);
|
|
437
214
|
}
|
|
438
|
-
async cycleModel(
|
|
439
|
-
await this.assertWritable(
|
|
440
|
-
const session = await this.getOrOpen(
|
|
215
|
+
async cycleModel(ref, direction) {
|
|
216
|
+
await this.assertWritable(ref);
|
|
217
|
+
const session = await this.getOrOpen(ref);
|
|
441
218
|
const result = await session.cycleModel(direction);
|
|
442
219
|
if (result === undefined)
|
|
443
|
-
throw new Error(session.scopedModels.length > 0 ? "
|
|
220
|
+
throw new Error(session.scopedModels.length > 0 ? "作用域内只有一个模型" : "只有一个可用模型");
|
|
444
221
|
this.publishActivity(session, `model: ${result.model.id}`, "idle", result.model.provider);
|
|
445
222
|
this.publishStatus(session);
|
|
446
223
|
return this.statusFromSession(session);
|
|
447
224
|
}
|
|
448
|
-
async availableThinkingLevels(
|
|
449
|
-
const session = await this.getOrOpen(
|
|
225
|
+
async availableThinkingLevels(ref) {
|
|
226
|
+
const session = await this.getOrOpen(ref);
|
|
450
227
|
return session.getAvailableThinkingLevels();
|
|
451
228
|
}
|
|
452
|
-
async setThinkingLevel(
|
|
453
|
-
await this.assertWritable(
|
|
454
|
-
const session = await this.getOrOpen(
|
|
455
|
-
session
|
|
229
|
+
async setThinkingLevel(ref, level) {
|
|
230
|
+
await this.assertWritable(ref);
|
|
231
|
+
const session = await this.getOrOpen(ref);
|
|
232
|
+
// pi owns the valid set; validate against the session's live levels rather
|
|
233
|
+
// than a hardcoded union so this stays correct if pi changes the set.
|
|
234
|
+
const available = session.getAvailableThinkingLevels();
|
|
235
|
+
const match = available.find((candidate) => candidate === level);
|
|
236
|
+
if (match === undefined)
|
|
237
|
+
throw new Error(`无效的思考级别:${level}`);
|
|
238
|
+
session.setThinkingLevel(match);
|
|
456
239
|
this.publishActivity(session, `thinking: ${session.thinkingLevel}`, "idle");
|
|
457
240
|
this.publishStatus(session);
|
|
458
241
|
return this.statusFromSession(session);
|
|
459
242
|
}
|
|
460
|
-
async cycleThinkingLevel(
|
|
461
|
-
await this.assertWritable(
|
|
462
|
-
const session = await this.getOrOpen(
|
|
243
|
+
async cycleThinkingLevel(ref) {
|
|
244
|
+
await this.assertWritable(ref);
|
|
245
|
+
const session = await this.getOrOpen(ref);
|
|
463
246
|
const level = session.cycleThinkingLevel();
|
|
464
247
|
if (level === undefined)
|
|
465
|
-
throw new Error("
|
|
248
|
+
throw new Error("当前模型不支持思考");
|
|
466
249
|
this.publishActivity(session, `thinking: ${level}`, "idle");
|
|
467
250
|
this.publishStatus(session);
|
|
468
251
|
return this.statusFromSession(session);
|
|
469
252
|
}
|
|
470
|
-
async commands(
|
|
471
|
-
const session = await this.getOrOpen(
|
|
253
|
+
async commands(ref) {
|
|
254
|
+
const session = await this.getOrOpen(ref);
|
|
472
255
|
const commands = [...BUILTIN_COMMANDS];
|
|
473
256
|
for (const command of session.extensionRunner.getRegisteredCommands()) {
|
|
474
257
|
commands.push({ name: command.invocationName, ...(command.description === undefined ? {} : { description: command.description }), source: "extension" });
|
|
@@ -481,33 +264,40 @@ export class PiSessionService {
|
|
|
481
264
|
}
|
|
482
265
|
return commands.sort((a, b) => a.name.localeCompare(b.name));
|
|
483
266
|
}
|
|
484
|
-
async prompt(
|
|
267
|
+
async prompt(ref, text, streamingBehavior, attachments, options) {
|
|
268
|
+
const _managementContext = options?.managementContext;
|
|
269
|
+
void _managementContext;
|
|
485
270
|
const promptText = requirePromptText(text);
|
|
271
|
+
// Command-forwarded prompts (e.g. /skill:*) are expanded by the agent, which
|
|
272
|
+
// streams the canonical message back. The client doesn't render the raw
|
|
273
|
+
// command text, so the server must not echo it either, or it would show up
|
|
274
|
+
// as a transient line that vanishes on reload.
|
|
275
|
+
const echoUserMessage = options?.echoUserMessage !== false;
|
|
486
276
|
const requestedBehavior = parsePromptStreamingBehavior(streamingBehavior);
|
|
487
|
-
const
|
|
488
|
-
await
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
throw new Error("当前模型不支持图片输入");
|
|
277
|
+
const parsedAttachments = parsePromptAttachments(attachments, { enforceInlineSizeLimit: false });
|
|
278
|
+
const images = (await attachmentsToInlineImages(parsedAttachments)).map((entry) => entry.image);
|
|
279
|
+
await this.assertWritable(ref);
|
|
280
|
+
const session = await this.getOrOpen(ref);
|
|
492
281
|
this.maybeGenerateSessionName(session, promptText);
|
|
493
282
|
const isQueued = session.isStreaming || session.isCompacting;
|
|
494
283
|
const behavior = isQueued ? requestedBehavior ?? "followUp" : undefined;
|
|
495
|
-
if (isQueued && this.hasQueuedMessageText(session, promptText)) {
|
|
284
|
+
if (isQueued && images.length === 0 && this.hasQueuedMessageText(session, promptText)) {
|
|
496
285
|
this.publishActivity(session, "duplicate queued message ignored", "active");
|
|
497
286
|
this.publishStatus(session);
|
|
498
287
|
return;
|
|
499
288
|
}
|
|
500
289
|
if (session.isCompacting) {
|
|
501
|
-
this.enqueuePromptDuringCompaction(session, promptText, behavior ?? "followUp", images);
|
|
290
|
+
this.enqueuePromptDuringCompaction(session, promptText, behavior ?? "followUp", images, echoUserMessage);
|
|
502
291
|
return;
|
|
503
292
|
}
|
|
504
|
-
void this.submitPrompt(session, promptText, behavior, images);
|
|
293
|
+
void this.submitPrompt(session, promptText, behavior, images, echoUserMessage);
|
|
505
294
|
}
|
|
506
|
-
submitPrompt(session, text, behavior, images = []) {
|
|
295
|
+
submitPrompt(session, text, behavior, images = [], echoUserMessage = true) {
|
|
507
296
|
this.publishActivity(session, behavior === "steer" ? "steering queued" : behavior === "followUp" ? "message queued" : "prompt accepted", "active");
|
|
508
|
-
if (behavior === undefined)
|
|
509
|
-
this.events.publish(session.sessionId, { type: "message.append", message:
|
|
510
|
-
const
|
|
297
|
+
if (behavior === undefined && echoUserMessage)
|
|
298
|
+
this.events.publish(session.sessionId, { type: "message.append", message: userMessage(text, images) });
|
|
299
|
+
const promptOptions = buildPromptOptions(behavior, images);
|
|
300
|
+
const promptPromise = session.prompt(text, promptOptions).catch((error) => {
|
|
511
301
|
const message = error instanceof Error ? error.message : String(error);
|
|
512
302
|
this.publishActivity(session, "error", "error", message);
|
|
513
303
|
this.events.publish(session.sessionId, { type: "session.error", message });
|
|
@@ -515,27 +305,32 @@ export class PiSessionService {
|
|
|
515
305
|
void promptPromise;
|
|
516
306
|
return promptPromise;
|
|
517
307
|
}
|
|
518
|
-
enqueuePromptDuringCompaction(session, text, kind, images = []) {
|
|
308
|
+
enqueuePromptDuringCompaction(session, text, kind, images = [], echoUserMessage = true) {
|
|
519
309
|
const queue = this.compactionPromptQueues.get(session.sessionId) ?? [];
|
|
520
|
-
queue.push({ kind, text, ...(images.length
|
|
310
|
+
queue.push({ kind, text, ...(images.length > 0 ? { images } : {}), ...(echoUserMessage ? {} : { echoUserMessage: false }) });
|
|
521
311
|
this.compactionPromptQueues.set(session.sessionId, queue);
|
|
522
312
|
this.publishActivity(session, "message queued during compaction", "active");
|
|
523
313
|
this.publishStatus(session);
|
|
524
314
|
}
|
|
525
|
-
async
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
315
|
+
async saveAttachments(ref, attachments, folder) {
|
|
316
|
+
const parsed = parsePromptAttachments(attachments, { enforceInlineSizeLimit: false });
|
|
317
|
+
if (parsed.length === 0)
|
|
318
|
+
return [];
|
|
319
|
+
await this.assertWritable(ref);
|
|
320
|
+
const active = await this.getActive(ref);
|
|
321
|
+
return saveAttachmentsToWorkspace(active.runtime.cwd, parsed, folder === undefined ? {} : { folder });
|
|
322
|
+
}
|
|
323
|
+
async shell(ref, text, _managementContext) {
|
|
324
|
+
void _managementContext;
|
|
325
|
+
await this.assertWritable(ref);
|
|
326
|
+
const active = await this.getActive(ref);
|
|
532
327
|
const { session } = active.runtime;
|
|
533
328
|
const isExcluded = text.startsWith("!!");
|
|
534
329
|
const command = (isExcluded ? text.slice(2) : text.slice(1)).trim();
|
|
535
330
|
if (!command)
|
|
536
|
-
throw new Error("
|
|
331
|
+
throw new Error("用法:!<shell command>");
|
|
537
332
|
if (session.isBashRunning)
|
|
538
|
-
throw new Error("
|
|
333
|
+
throw new Error("已有 bash 命令正在运行");
|
|
539
334
|
this.publishActivity(session, "running bash", "active", command);
|
|
540
335
|
this.events.publish(session.sessionId, { type: "shell.start", command, excludeFromContext: isExcluded });
|
|
541
336
|
void session.executeBash(command, (chunk) => {
|
|
@@ -561,46 +356,33 @@ export class PiSessionService {
|
|
|
561
356
|
this.publishStatus(session);
|
|
562
357
|
});
|
|
563
358
|
}
|
|
564
|
-
async runCommand(
|
|
565
|
-
|
|
566
|
-
this.
|
|
567
|
-
|
|
359
|
+
async runCommand(ref, text, _managementContext) {
|
|
360
|
+
void _managementContext;
|
|
361
|
+
await this.assertWritable(ref);
|
|
362
|
+
const active = await this.getActive(ref);
|
|
363
|
+
return this.commandService.run(active.runtime.session.sessionId, text);
|
|
568
364
|
}
|
|
569
|
-
async respondToCommand(
|
|
570
|
-
await this.assertWritable(
|
|
571
|
-
|
|
365
|
+
async respondToCommand(ref, requestId, value) {
|
|
366
|
+
await this.assertWritable(ref);
|
|
367
|
+
const active = await this.getActive(ref);
|
|
368
|
+
return this.commandService.respond(active.runtime.session.sessionId, requestId, value);
|
|
572
369
|
}
|
|
573
|
-
async archive(
|
|
574
|
-
const session = await this.getOrOpen(
|
|
370
|
+
async archive(ref) {
|
|
371
|
+
const session = await this.getOrOpen(ref);
|
|
575
372
|
if (this.hasActiveWork(session))
|
|
576
|
-
throw new Error("
|
|
373
|
+
throw new Error("归档前请先停止当前会话活动");
|
|
577
374
|
const archiveInput = await this.archiveInputForSession(session);
|
|
578
375
|
await this.closeActive(session.sessionId);
|
|
579
|
-
this.managementContexts.delete(session.sessionId);
|
|
580
376
|
await this.archiveStore.archive(archiveInput);
|
|
581
377
|
}
|
|
582
|
-
|
|
583
|
-
const
|
|
584
|
-
if (context === undefined || existing === undefined)
|
|
585
|
-
return;
|
|
586
|
-
if (existing.user.rootUserId !== context.user.rootUserId)
|
|
587
|
-
throw new Error("Session is outside the managed embed authorization scope");
|
|
588
|
-
}
|
|
589
|
-
rememberManagedSessionAccess(sessionId, context) {
|
|
590
|
-
if (context === undefined)
|
|
591
|
-
return;
|
|
592
|
-
this.assertManagedSessionAccess(sessionId, context);
|
|
593
|
-
if (!this.managementContexts.has(sessionId))
|
|
594
|
-
this.managementContexts.set(sessionId, context);
|
|
595
|
-
}
|
|
596
|
-
async archiveTree(sessionId) {
|
|
597
|
-
const session = await this.getOrOpen(sessionId);
|
|
378
|
+
async archiveTree(ref) {
|
|
379
|
+
const session = await this.getOrOpen(ref);
|
|
598
380
|
const catalog = await this.workspaceArchiveCandidates(session.sessionManager.getCwd());
|
|
599
381
|
const root = findArchiveCandidateByIdOrPrefix(catalog, session.sessionId) ?? archiveCandidateFromActiveSession(session, false);
|
|
600
382
|
const plan = planSessionArchiveTree(root, catalog);
|
|
601
383
|
const busy = plan.targets.map((target) => target.activeSession).find((target) => target !== undefined && this.hasActiveWork(target));
|
|
602
384
|
if (busy !== undefined)
|
|
603
|
-
throw new Error(
|
|
385
|
+
throw new Error(`归档 ${sessionDisplayName(busy)} 前请先停止当前会话活动`);
|
|
604
386
|
const archiveInputs = plan.unarchivedTargets.map((target) => archiveInputFromCandidate(target));
|
|
605
387
|
for (const input of archiveInputs)
|
|
606
388
|
await this.closeActive(input.sessionId);
|
|
@@ -613,29 +395,56 @@ export class PiSessionService {
|
|
|
613
395
|
skippedAlreadyArchivedCount: plan.skippedAlreadyArchivedCount,
|
|
614
396
|
};
|
|
615
397
|
}
|
|
616
|
-
async restore(
|
|
617
|
-
await this.
|
|
618
|
-
|
|
398
|
+
async restore(ref) {
|
|
399
|
+
const archived = await this.getArchived(ref);
|
|
400
|
+
if (archived === undefined)
|
|
401
|
+
throw new Error("未找到会话");
|
|
402
|
+
await this.closeActive(archived.sessionId);
|
|
403
|
+
await this.archiveStore.restore(archived.sessionId);
|
|
404
|
+
}
|
|
405
|
+
async deleteArchived(ref) {
|
|
406
|
+
const record = await this.getArchived(ref);
|
|
407
|
+
if (record === undefined)
|
|
408
|
+
throw new Error("未找到已归档会话");
|
|
409
|
+
if (this.archiveStore.deleteArchived === undefined)
|
|
410
|
+
throw new Error("归档存储不支持删除");
|
|
411
|
+
await this.closeActive(record.sessionId);
|
|
412
|
+
if (record.archivePath === undefined)
|
|
413
|
+
await this.ensureArchivedRecordMoved(record);
|
|
414
|
+
await this.archiveStore.deleteArchived(record.sessionId);
|
|
415
|
+
}
|
|
416
|
+
async reload(ref) {
|
|
417
|
+
await this.assertWritable(ref);
|
|
418
|
+
const session = await this.getOrOpen(ref);
|
|
419
|
+
if (this.hasActiveWork(session))
|
|
420
|
+
throw new Error("重新加载前请先停止当前会话活动");
|
|
421
|
+
await this.closeActive(session.sessionId);
|
|
422
|
+
const reopened = await this.getActive(ref);
|
|
423
|
+
this.publishStatus(reopened.runtime.session);
|
|
619
424
|
}
|
|
620
|
-
async detachParent(
|
|
621
|
-
const session = await this.getOrOpen(
|
|
425
|
+
async detachParent(ref) {
|
|
426
|
+
const session = await this.getOrOpen(ref);
|
|
622
427
|
const sessionFile = session.sessionFile;
|
|
623
428
|
if (sessionFile === undefined || sessionFile === "")
|
|
624
|
-
throw new Error("
|
|
429
|
+
throw new Error("会话尚未持久化");
|
|
625
430
|
await clearParentSession(sessionFile);
|
|
626
431
|
}
|
|
627
|
-
async abort(
|
|
628
|
-
const active = this.
|
|
629
|
-
if (
|
|
432
|
+
async abort(ref) {
|
|
433
|
+
const active = this.activeForLookup(ref);
|
|
434
|
+
if (active === undefined)
|
|
630
435
|
return;
|
|
436
|
+
const sessionId = active.runtime.session.sessionId;
|
|
631
437
|
this.clearCompactionPromptQueue(sessionId);
|
|
632
438
|
clearSessionQueue(active.runtime.session);
|
|
633
439
|
await active.runtime.session.abort();
|
|
634
440
|
this.publishActivity(active.runtime.session, "stopped", "idle");
|
|
635
441
|
this.publishStatus(active.runtime.session);
|
|
636
442
|
}
|
|
637
|
-
stop(
|
|
638
|
-
|
|
443
|
+
stop(ref) {
|
|
444
|
+
const active = this.activeForLookup(ref);
|
|
445
|
+
if (active === undefined)
|
|
446
|
+
return;
|
|
447
|
+
void this.closeActive(active.runtime.session.sessionId).catch(() => {
|
|
639
448
|
// Best-effort shutdown; callers that need errors await closeActive directly.
|
|
640
449
|
});
|
|
641
450
|
}
|
|
@@ -658,11 +467,17 @@ export class PiSessionService {
|
|
|
658
467
|
return record;
|
|
659
468
|
}
|
|
660
469
|
}
|
|
470
|
+
async ensureArchivedRecordMoved(record) {
|
|
471
|
+
const session = (await this.sessionManager.list(record.cwd)).find((candidate) => candidate.id === record.sessionId);
|
|
472
|
+
if (session === undefined)
|
|
473
|
+
return record;
|
|
474
|
+
return this.archiveStore.archive(archiveInputFromListEntry(session));
|
|
475
|
+
}
|
|
661
476
|
async archiveInputForSession(session) {
|
|
662
477
|
const cwd = session.sessionManager.getCwd();
|
|
663
478
|
const sessionFile = session.sessionFile;
|
|
664
479
|
if (sessionFile === undefined || sessionFile === "")
|
|
665
|
-
throw new Error("
|
|
480
|
+
throw new Error("会话尚未持久化");
|
|
666
481
|
const listed = (await this.sessionManager.list(cwd)).find((candidate) => candidate.id === session.sessionId);
|
|
667
482
|
if (listed !== undefined)
|
|
668
483
|
return archiveInputFromListEntry(listed);
|
|
@@ -723,7 +538,6 @@ export class PiSessionService {
|
|
|
723
538
|
if (!active)
|
|
724
539
|
return;
|
|
725
540
|
this.active.delete(sessionId);
|
|
726
|
-
this.managementContexts.delete(sessionId);
|
|
727
541
|
this.activities.delete(sessionId);
|
|
728
542
|
this.workspaceActivity?.removeSession(sessionId, active.runtime.session.sessionManager.getCwd());
|
|
729
543
|
this.clearAuthLossWarningsForSession(sessionId);
|
|
@@ -737,67 +551,74 @@ export class PiSessionService {
|
|
|
737
551
|
await active.runtime.dispose();
|
|
738
552
|
}
|
|
739
553
|
}
|
|
740
|
-
async assertWritable(
|
|
741
|
-
if (await this.
|
|
742
|
-
throw new Error("
|
|
554
|
+
async assertWritable(ref) {
|
|
555
|
+
if (await this.getArchived(ref) !== undefined)
|
|
556
|
+
throw new Error("已归档会话为只读。请恢复会话后继续。");
|
|
743
557
|
}
|
|
744
|
-
async getOrOpen(
|
|
745
|
-
return (await this.getActive(
|
|
558
|
+
async getOrOpen(ref) {
|
|
559
|
+
return (await this.getActive(ref)).runtime.session;
|
|
746
560
|
}
|
|
747
|
-
async getActive(
|
|
748
|
-
const active = this.
|
|
749
|
-
if (active)
|
|
750
|
-
const activeSessionId = active.runtime.session.sessionId;
|
|
751
|
-
const existingContext = this.managementContexts.get(activeSessionId);
|
|
752
|
-
if (managementContext !== undefined && existingContext === undefined) {
|
|
753
|
-
const sessionFile = active.runtime.session.sessionFile;
|
|
754
|
-
if (sessionFile === undefined || sessionFile === "")
|
|
755
|
-
throw new Error("Managed embed session must be persisted before it can be resumed safely");
|
|
756
|
-
const activeCwd = active.runtime.session.sessionManager.getCwd();
|
|
757
|
-
await this.closeActive(activeSessionId);
|
|
758
|
-
return this.create(this.sessionManager.open(sessionFile), activeCwd, managementContext);
|
|
759
|
-
}
|
|
760
|
-
this.rememberManagedSessionAccess(active.runtime.session.sessionId, managementContext);
|
|
561
|
+
async getActive(ref) {
|
|
562
|
+
const active = this.activeForLookup(ref);
|
|
563
|
+
if (active !== undefined)
|
|
761
564
|
return active;
|
|
762
|
-
|
|
763
|
-
const archived = await this.archiveStore.get(sessionId);
|
|
565
|
+
const archived = await this.getArchived(ref);
|
|
764
566
|
if (archived?.archivePath !== undefined)
|
|
765
|
-
return this.create(this.sessionManager.open(archived.archivePath), archived.cwd
|
|
766
|
-
const match = (
|
|
567
|
+
return this.create(this.sessionManager.open(archived.archivePath), archived.cwd);
|
|
568
|
+
const match = isPiSessionRef(ref)
|
|
569
|
+
? (await this.sessionManager.list(ref.cwd)).find((s) => s.id === ref.id || s.id.startsWith(ref.id))
|
|
570
|
+
: (await this.sessionManager.listAll?.() ?? []).find((s) => s.id === ref || s.id.startsWith(ref));
|
|
767
571
|
if (!match)
|
|
768
|
-
throw new Error("
|
|
769
|
-
return this.create(this.sessionManager.open(match.path), match.cwd
|
|
770
|
-
}
|
|
771
|
-
async
|
|
772
|
-
const
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
572
|
+
throw new Error("未找到会话");
|
|
573
|
+
return this.create(this.sessionManager.open(match.path), match.cwd);
|
|
574
|
+
}
|
|
575
|
+
async getArchived(ref) {
|
|
576
|
+
const archived = await this.archiveStore.get(sessionIdFromLookup(ref));
|
|
577
|
+
if (archived === undefined)
|
|
578
|
+
return undefined;
|
|
579
|
+
if (isPiSessionRef(ref) && archived.cwd !== ref.cwd)
|
|
580
|
+
return undefined;
|
|
581
|
+
return archived;
|
|
582
|
+
}
|
|
583
|
+
activeForLookup(ref) {
|
|
584
|
+
const sessionId = sessionIdFromLookup(ref);
|
|
585
|
+
const exact = this.active.get(sessionId);
|
|
586
|
+
if (exact !== undefined && lookupMatchesActiveSession(ref, exact))
|
|
587
|
+
return exact;
|
|
588
|
+
for (const [candidateId, active] of this.active.entries()) {
|
|
589
|
+
if (candidateId.startsWith(sessionId) && lookupMatchesActiveSession(ref, active))
|
|
590
|
+
return active;
|
|
591
|
+
}
|
|
592
|
+
return undefined;
|
|
593
|
+
}
|
|
594
|
+
async create(sessionManager, cwd) {
|
|
595
|
+
const runtime = await this.createAgentRuntime(this.createRuntime, { cwd, agentDir: this.agentDir, sessionManager });
|
|
596
|
+
await this.bindSessionExtensions(runtime.session);
|
|
779
597
|
const active = { runtime, unsubscribe: noop };
|
|
780
598
|
this.bindRuntime(active);
|
|
781
|
-
runtime.setRebindSession(() => {
|
|
599
|
+
runtime.setRebindSession(async (session) => {
|
|
600
|
+
await this.bindSessionExtensions(session);
|
|
782
601
|
this.bindRuntime(active);
|
|
783
|
-
return Promise.resolve();
|
|
784
602
|
});
|
|
785
603
|
this.active.set(runtime.session.sessionId, active);
|
|
786
|
-
this.rememberManagedSessionAccess(runtime.session.sessionId, managementContext);
|
|
787
604
|
this.publishStatus(runtime.session);
|
|
788
605
|
return active;
|
|
789
606
|
}
|
|
607
|
+
async bindSessionExtensions(session) {
|
|
608
|
+
await session.bindExtensions({
|
|
609
|
+
onError: (error) => {
|
|
610
|
+
const message = `${error.extensionPath}: ${error.error}`;
|
|
611
|
+
this.publishActivity(session, "extension error", "error", message);
|
|
612
|
+
this.events.publish(session.sessionId, { type: "session.error", message });
|
|
613
|
+
},
|
|
614
|
+
});
|
|
615
|
+
}
|
|
790
616
|
bindRuntime(active) {
|
|
791
617
|
active.unsubscribe();
|
|
792
618
|
const { session } = active.runtime;
|
|
793
619
|
for (const [sessionId, candidate] of this.active.entries()) {
|
|
794
620
|
if (candidate === active) {
|
|
795
621
|
this.active.delete(sessionId);
|
|
796
|
-
const context = this.managementContexts.get(sessionId);
|
|
797
|
-
if (context !== undefined && sessionId !== session.sessionId) {
|
|
798
|
-
this.managementContexts.delete(sessionId);
|
|
799
|
-
this.managementContexts.set(session.sessionId, context);
|
|
800
|
-
}
|
|
801
622
|
if (sessionId !== session.sessionId)
|
|
802
623
|
this.clearCompactionPromptQueue(sessionId);
|
|
803
624
|
}
|
|
@@ -838,14 +659,14 @@ export class PiSessionService {
|
|
|
838
659
|
return;
|
|
839
660
|
this.publishStatus(session);
|
|
840
661
|
for (const prompt of queued)
|
|
841
|
-
void this.submitPrompt(session, prompt.text, prompt.kind, prompt.images ??
|
|
662
|
+
void this.submitPrompt(session, prompt.text, prompt.kind, prompt.images, prompt.echoUserMessage ?? true);
|
|
842
663
|
return;
|
|
843
664
|
}
|
|
844
665
|
const prompt = this.shiftCompactionPrompt(sessionId);
|
|
845
666
|
if (prompt === undefined)
|
|
846
667
|
return;
|
|
847
668
|
this.publishStatus(session);
|
|
848
|
-
const submitted = this.submitPrompt(session, prompt.text, undefined, prompt.images ??
|
|
669
|
+
const submitted = this.submitPrompt(session, prompt.text, undefined, prompt.images, prompt.echoUserMessage ?? true);
|
|
849
670
|
void submitted.finally(() => { this.scheduleCompactionQueueDrain(sessionId); });
|
|
850
671
|
}
|
|
851
672
|
takeCompactionPromptQueue(sessionId) {
|
|
@@ -1080,25 +901,14 @@ function modelToClientModel(model) {
|
|
|
1080
901
|
return {};
|
|
1081
902
|
const name = getString(model, "name");
|
|
1082
903
|
const reasoning = getProperty(model, "reasoning");
|
|
1083
|
-
const input = modelInput(model);
|
|
1084
904
|
return {
|
|
1085
905
|
provider: model.provider,
|
|
1086
906
|
id: model.id,
|
|
1087
907
|
...(name === undefined ? {} : { name }),
|
|
1088
908
|
contextWindow: model.contextWindow,
|
|
1089
909
|
...(reasoning === undefined ? {} : { reasoning }),
|
|
1090
|
-
...(input === undefined ? {} : { input }),
|
|
1091
910
|
};
|
|
1092
911
|
}
|
|
1093
|
-
function modelInput(model) {
|
|
1094
|
-
const input = getProperty(model, "input");
|
|
1095
|
-
if (!Array.isArray(input))
|
|
1096
|
-
return undefined;
|
|
1097
|
-
return input.filter((item) => item === "text" || item === "image");
|
|
1098
|
-
}
|
|
1099
|
-
function modelSupportsImageInput(model) {
|
|
1100
|
-
return modelInput(model)?.includes("image") === true;
|
|
1101
|
-
}
|
|
1102
912
|
function clientSessionFromListEntry(session) {
|
|
1103
913
|
return {
|
|
1104
914
|
id: session.id,
|
|
@@ -1128,7 +938,7 @@ function archiveInputFromListEntry(session) {
|
|
|
1128
938
|
function archiveInputFromActiveSession(session) {
|
|
1129
939
|
const sessionFile = session.sessionFile;
|
|
1130
940
|
if (sessionFile === undefined || sessionFile === "")
|
|
1131
|
-
throw new Error("
|
|
941
|
+
throw new Error("会话尚未持久化");
|
|
1132
942
|
const parentSessionPath = session.sessionManager.getHeader?.()?.parentSession;
|
|
1133
943
|
return {
|
|
1134
944
|
sessionId: session.sessionId,
|
|
@@ -1169,7 +979,7 @@ function archiveCandidateFromArchivedRecord(record, fallback) {
|
|
|
1169
979
|
function archiveCandidateFromActiveSession(session, archived) {
|
|
1170
980
|
const sessionFile = session.sessionFile;
|
|
1171
981
|
if (sessionFile === undefined || sessionFile === "")
|
|
1172
|
-
throw new Error("
|
|
982
|
+
throw new Error("会话尚未持久化");
|
|
1173
983
|
const parentSessionPath = session.sessionManager.getHeader?.()?.parentSession;
|
|
1174
984
|
return {
|
|
1175
985
|
id: session.sessionId,
|
|
@@ -1232,9 +1042,6 @@ function archivedTimestamp(record) {
|
|
|
1232
1042
|
function isDefined(value) {
|
|
1233
1043
|
return value !== undefined;
|
|
1234
1044
|
}
|
|
1235
|
-
function isNodeErrorWithCode(error, code) {
|
|
1236
|
-
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
1237
|
-
}
|
|
1238
1045
|
async function clearParentSession(sessionFile) {
|
|
1239
1046
|
const content = await readFile(sessionFile, "utf8");
|
|
1240
1047
|
const newlineIndex = content.indexOf("\n");
|
|
@@ -1255,22 +1062,33 @@ function queuedMessagesFromSession(session, extraQueuedMessages = []) {
|
|
|
1255
1062
|
return [
|
|
1256
1063
|
...session.getSteeringMessages().map((text) => ({ kind: "steer", text })),
|
|
1257
1064
|
...session.getFollowUpMessages().map((text) => ({ kind: "followUp", text })),
|
|
1258
|
-
...extraQueuedMessages
|
|
1065
|
+
...extraQueuedMessages,
|
|
1259
1066
|
];
|
|
1260
1067
|
}
|
|
1261
|
-
function
|
|
1262
|
-
|
|
1263
|
-
return { kind: message.kind, text: message.text, ...(imageCount === 0 ? {} : { imageCount }) };
|
|
1068
|
+
function userTextMessage(text) {
|
|
1069
|
+
return { role: "user", content: text };
|
|
1264
1070
|
}
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
function
|
|
1071
|
+
/**
|
|
1072
|
+
* Build the optimistic user message echoed to clients. When images are present
|
|
1073
|
+
* we mirror pi's content-array shape (`[{type:"text"}, {type:"image"}, ...]`) so
|
|
1074
|
+
* the local echo matches what pi persists in the session branch.
|
|
1075
|
+
*/
|
|
1076
|
+
function userMessage(text, images) {
|
|
1271
1077
|
if (images.length === 0)
|
|
1272
|
-
return
|
|
1273
|
-
|
|
1078
|
+
return userTextMessage(text);
|
|
1079
|
+
const content = [];
|
|
1080
|
+
if (text !== "")
|
|
1081
|
+
content.push({ type: "text", text });
|
|
1082
|
+
content.push(...images);
|
|
1083
|
+
return { role: "user", content };
|
|
1084
|
+
}
|
|
1085
|
+
function buildPromptOptions(behavior, images) {
|
|
1086
|
+
const options = {};
|
|
1087
|
+
if (behavior !== undefined)
|
|
1088
|
+
options.streamingBehavior = behavior;
|
|
1089
|
+
if (images.length > 0)
|
|
1090
|
+
options.images = images;
|
|
1091
|
+
return Object.keys(options).length > 0 ? options : undefined;
|
|
1274
1092
|
}
|
|
1275
1093
|
function stringValue(value) {
|
|
1276
1094
|
return typeof value === "string" ? value : "";
|