@co0ontty/wand 4.72.2-beta.gcc5d57e → 4.73.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-info.json +4 -4
- package/dist/config.js +20 -0
- package/dist/daemon-connection.d.ts +9 -0
- package/dist/daemon-connection.js +86 -0
- package/dist/distribution-manager.d.ts +5 -0
- package/dist/distribution-manager.js +79 -1
- package/dist/git-quick-commit.js +12 -0
- package/dist/message-truncator.js +6 -0
- package/dist/native/darwin-arm64/wand-render +0 -0
- package/dist/native/darwin-arm64/wand-render.sha256 +1 -0
- package/dist/native/darwin-arm64/wand-render.version +1 -0
- package/dist/native/darwin-x64/wand-render +0 -0
- package/dist/native/darwin-x64/wand-render.sha256 +1 -0
- package/dist/native/darwin-x64/wand-render.version +1 -0
- package/dist/native/linux-arm64/wand-render +0 -0
- package/dist/native/linux-arm64/wand-render.sha256 +1 -0
- package/dist/native/linux-arm64/wand-render.version +1 -0
- package/dist/native/linux-x64/wand-render +0 -0
- package/dist/native/linux-x64/wand-render.sha256 +1 -0
- package/dist/native/linux-x64/wand-render.version +1 -0
- package/dist/process-manager.d.ts +10 -0
- package/dist/process-manager.js +73 -4
- package/dist/render-binary.d.ts +61 -0
- package/dist/render-binary.js +204 -0
- package/dist/render-daemon-client.d.ts +171 -0
- package/dist/render-daemon-client.js +1114 -0
- package/dist/render-host.d.ts +64 -0
- package/dist/render-host.js +301 -0
- package/dist/render-protocol.d.ts +128 -0
- package/dist/render-protocol.js +99 -0
- package/dist/render-structured-client.d.ts +80 -0
- package/dist/render-structured-client.js +513 -0
- package/dist/render-structured-host.d.ts +31 -0
- package/dist/render-structured-host.js +218 -0
- package/dist/render-structured-protocol.d.ts +101 -0
- package/dist/render-structured-protocol.js +35 -0
- package/dist/server-session-routes.js +72 -9
- package/dist/server-task-routes.js +52 -3
- package/dist/server-update-routes.d.ts +2 -0
- package/dist/server-update-routes.js +44 -0
- package/dist/server.js +31 -9
- package/dist/session-transport.js +1 -1
- package/dist/storage.d.ts +2 -1
- package/dist/storage.js +14 -5
- package/dist/structured-client-protocol.d.ts +3 -1
- package/dist/structured-client-protocol.js +85 -25
- package/dist/structured-content.d.ts +20 -3
- package/dist/structured-content.js +80 -1
- package/dist/structured-exec-host.js +7 -1
- package/dist/structured-exec-pump.js +6 -2
- package/dist/structured-pi-adapter.js +18 -2
- package/dist/structured-session-manager.d.ts +2 -0
- package/dist/structured-session-manager.js +27 -3
- package/dist/task-types.d.ts +2 -0
- package/dist/terminal-daemon-client.d.ts +9 -2
- package/dist/terminal-daemon-client.js +119 -46
- package/dist/terminal-daemon-server.js +20 -4
- package/dist/terminal-host.d.ts +6 -0
- package/dist/terminal-host.js +3 -0
- package/dist/tui/ipc-server.js +12 -0
- package/dist/types.d.ts +19 -1
- package/dist/unix-socket-keepalive.d.ts +3 -0
- package/dist/unix-socket-keepalive.js +94 -0
- package/dist/web-ui/content/scripts.js +68 -68
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/content/tailwind.css +12 -0
- package/dist/web-ui/embedded-assets.js +2 -2
- package/dist/ws-broadcast.d.ts +1 -1
- package/dist/ws-broadcast.js +17 -5
- package/package.json +8 -3
- package/scripts/install-render-binary.js +499 -0
- package/scripts/stage-render-binaries.js +626 -0
|
@@ -137,6 +137,50 @@ export function registerPublicUpdateRoutes(app, deps) {
|
|
|
137
137
|
readErrorMessage: "读取 APK 文件失败。",
|
|
138
138
|
});
|
|
139
139
|
}));
|
|
140
|
+
// 原生 macOS Beta 只使用当前连接服务端的本地分发文件;旧 DMG 接口保持兼容。
|
|
141
|
+
app.get("/api/macos-app-update", asyncRoute(async (req, res) => {
|
|
142
|
+
const currentVersion = requireCurrentVersion(req, res);
|
|
143
|
+
if (currentVersion === null)
|
|
144
|
+
return;
|
|
145
|
+
const latest = await deps.resolveLatestMacosBeta();
|
|
146
|
+
if (!latest) {
|
|
147
|
+
res.json({ updateAvailable: false, currentVersion, latestVersion: null, downloadUrl: null, source: null, channel: "beta" });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const updateAvailable = compareWandInstallOrder(latest.version, currentVersion) > 0;
|
|
151
|
+
res.json({
|
|
152
|
+
updateAvailable,
|
|
153
|
+
currentVersion,
|
|
154
|
+
latestVersion: latest.version,
|
|
155
|
+
downloadUrl: updateAvailable ? latest.downloadUrl : null,
|
|
156
|
+
fileName: updateAvailable ? latest.fileName : null,
|
|
157
|
+
size: updateAvailable ? latest.size : null,
|
|
158
|
+
source: "local",
|
|
159
|
+
channel: "beta",
|
|
160
|
+
sha256: updateAvailable ? latest.sha256 : null,
|
|
161
|
+
});
|
|
162
|
+
}));
|
|
163
|
+
app.get("/macos/update-download", asyncRoute(async (req, res) => {
|
|
164
|
+
const fileName = text(req.query.fileName);
|
|
165
|
+
const asset = await deps.resolveMacosBetaDownload(fileName);
|
|
166
|
+
if (!asset) {
|
|
167
|
+
res.status(404).json({ error: "当前没有可下载的 macOS Beta 更新包。" });
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const sha256 = await deps.computeAssetSha256(asset);
|
|
171
|
+
if (!sha256) {
|
|
172
|
+
res.status(503).json({ error: "无法校验 macOS Beta 更新包。" });
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
streamFileWithRange(req, res, {
|
|
176
|
+
filePath: asset.filePath,
|
|
177
|
+
size: asset.size,
|
|
178
|
+
contentType: asset.fileName.toLowerCase().endsWith(".zip") ? "application/zip" : "application/x-apple-diskimage",
|
|
179
|
+
disposition: `attachment; filename="${encodeURIComponent(asset.fileName)}"`,
|
|
180
|
+
headers: { "X-MacOS-Sha256": sha256 },
|
|
181
|
+
readErrorMessage: "读取 macOS Beta 更新包失败。",
|
|
182
|
+
});
|
|
183
|
+
}));
|
|
140
184
|
app.get("/api/macos-dmg-update", asyncRoute(async (req, res) => {
|
|
141
185
|
const currentVersion = requireCurrentVersion(req, res);
|
|
142
186
|
if (currentVersion === null)
|
package/dist/server.js
CHANGED
|
@@ -55,7 +55,9 @@ import { isLogBusActive, wandTuiLog } from "./tui/log-bus.js";
|
|
|
55
55
|
import { EMBEDDED_WEB_ASSETS } from "./web-ui/embedded-assets.js";
|
|
56
56
|
import { renderApp } from "./web-ui/index.js";
|
|
57
57
|
import { WsBroadcastManager } from "./ws-broadcast.js";
|
|
58
|
-
import {
|
|
58
|
+
import { TerminalDaemonClient } from "./terminal-daemon-client.js";
|
|
59
|
+
import { createUpgradeAwareTerminalHost } from "./render-host.js";
|
|
60
|
+
import { createUpgradeAwareStructuredHost } from "./render-structured-host.js";
|
|
59
61
|
import { checkRateLimit, recordFailedLogin, resetRateLimit } from "./middleware/rate-limit.js";
|
|
60
62
|
import { updateProviderClis, verifyProviderCliUpdateResults, } from "./provider-cli-updater.js";
|
|
61
63
|
const SERVER_MODULE_DIR = path.dirname(new URL(import.meta.url).pathname);
|
|
@@ -317,13 +319,27 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
317
319
|
config,
|
|
318
320
|
repositoryUrl: PKG_REPO_URL,
|
|
319
321
|
});
|
|
320
|
-
const
|
|
322
|
+
const knownPtySessionIds = storage.loadSessions()
|
|
323
|
+
.filter((session) => (session.sessionKind ?? "pty") === "pty" && session.status === "running")
|
|
324
|
+
.map((session) => session.id);
|
|
325
|
+
// PTY 所有者可以是 legacy terminald 或常驻的 Rust Render:升级期由复合 host 按所有权
|
|
326
|
+
// 路由(旧会话留在 legacy,新会话进 Render),对客户端契约没有任何改变。
|
|
327
|
+
const ptyHosts = options.terminalHost
|
|
328
|
+
? { host: options.terminalHost, renderHost: null, legacyHost: null }
|
|
329
|
+
: await createUpgradeAwareTerminalHost(configPath, {
|
|
330
|
+
engine: config.render?.engine,
|
|
331
|
+
binaryPath: config.render?.binaryPath,
|
|
332
|
+
knownSessionIds: knownPtySessionIds,
|
|
333
|
+
});
|
|
334
|
+
const terminalHost = ptyHosts.host;
|
|
321
335
|
const processes = new ProcessManager(config, storage, configDir, terminalHost);
|
|
322
336
|
const structuredLogger = new SessionLogger(configDir, config.shortcutLogMaxBytes);
|
|
323
|
-
// Production startup
|
|
324
|
-
//
|
|
325
|
-
const
|
|
326
|
-
|
|
337
|
+
// Production startup provides a daemon-backed host for structured CLI runs
|
|
338
|
+
// even when Render owns every PTY. In-process hosts are for test injection.
|
|
339
|
+
const legacyStructuredHost = ptyHosts.legacyHost
|
|
340
|
+
?? (terminalHost instanceof TerminalDaemonClient ? terminalHost : null);
|
|
341
|
+
const structuredHosts = await createUpgradeAwareStructuredHost(configPath, legacyStructuredHost, config.structured?.processHost);
|
|
342
|
+
const structuredSessions = new StructuredSessionManager(storage, config, structuredLogger, undefined, {}, structuredHosts.host);
|
|
327
343
|
const sessionRegistry = new SessionRegistry(processes, structuredSessions, storage);
|
|
328
344
|
const missions = new Missions(storage, structuredSessions, sessionRegistry);
|
|
329
345
|
const updateState = new ServerUpdateState();
|
|
@@ -809,9 +825,7 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
809
825
|
wsManager.setup({
|
|
810
826
|
getSession: (id) => sessionRegistry.get(id),
|
|
811
827
|
getTerminalState: (id) => processes.getTerminalState(id),
|
|
812
|
-
sendPtyInput: (id, input, shortcutKey, userInput) => {
|
|
813
|
-
processes.sendInput(id, input, "terminal", shortcutKey, userInput);
|
|
814
|
-
},
|
|
828
|
+
sendPtyInput: (id, input, shortcutKey, userInput) => processes.sendInputConfirmed(id, input, "terminal", shortcutKey, userInput).then(() => { }),
|
|
815
829
|
resizePty: (id, cols, rows) => {
|
|
816
830
|
processes.resize(id, cols, rows);
|
|
817
831
|
},
|
|
@@ -886,6 +900,10 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
886
900
|
structuredSessions.dispose();
|
|
887
901
|
}
|
|
888
902
|
catch { /* noop */ }
|
|
903
|
+
try {
|
|
904
|
+
structuredHosts.rustClient?.disconnect();
|
|
905
|
+
}
|
|
906
|
+
catch { /* noop */ }
|
|
889
907
|
try {
|
|
890
908
|
structuredLogger.dispose();
|
|
891
909
|
}
|
|
@@ -1151,6 +1169,10 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
1151
1169
|
structuredSessions.dispose();
|
|
1152
1170
|
}
|
|
1153
1171
|
catch { /* best-effort shutdown */ }
|
|
1172
|
+
try {
|
|
1173
|
+
structuredHosts.rustClient?.disconnect();
|
|
1174
|
+
}
|
|
1175
|
+
catch { /* best-effort shutdown */ }
|
|
1154
1176
|
try {
|
|
1155
1177
|
structuredLogger.dispose();
|
|
1156
1178
|
}
|
|
@@ -87,7 +87,7 @@ export function toSessionDetailDTO(snapshot, options = {}) {
|
|
|
87
87
|
outputOffset,
|
|
88
88
|
outputTotal: rawOutput.length,
|
|
89
89
|
outputTruncated: outputOffset > 0,
|
|
90
|
-
...(options.messages !== undefined ? { messages: enrichStructuredMessages(options.messages) } : {}),
|
|
90
|
+
...(options.messages !== undefined ? { messages: enrichStructuredMessages(options.messages, snapshot.id) } : {}),
|
|
91
91
|
...(options.messageOffset !== undefined ? { messageOffset: options.messageOffset } : {}),
|
|
92
92
|
...(options.messageTotal !== undefined ? { messageTotal: options.messageTotal } : {}),
|
|
93
93
|
...(options.leadingBlockOffset !== undefined ? { leadingBlockOffset: options.leadingBlockOffset } : {}),
|
package/dist/storage.d.ts
CHANGED
|
@@ -125,6 +125,7 @@ export declare class WandStorage {
|
|
|
125
125
|
createWandTask(input: {
|
|
126
126
|
workspaceId?: string | null;
|
|
127
127
|
workspaceTaskId?: string | null;
|
|
128
|
+
parentTaskId?: string | null;
|
|
128
129
|
title: string;
|
|
129
130
|
titleSource?: import("./task-types.js").WandTaskTitleSource;
|
|
130
131
|
description?: string;
|
|
@@ -135,7 +136,7 @@ export declare class WandStorage {
|
|
|
135
136
|
milestoneId?: string | null;
|
|
136
137
|
agent?: import("./task-types.js").WandTaskAgent | null;
|
|
137
138
|
}): import("./task-types.js").WandTask;
|
|
138
|
-
updateWandTask(id: string, patch: Partial<Pick<import("./task-types.js").WandTask, "workspaceId" | "workspaceTaskId" | "title" | "titleSource" | "autoTitleSignature" | "description" | "status" | "priority" | "labels" | "dueDate" | "milestoneId" | "sortOrder" | "agent">>): import("./task-types.js").WandTask | null;
|
|
139
|
+
updateWandTask(id: string, patch: Partial<Pick<import("./task-types.js").WandTask, "workspaceId" | "workspaceTaskId" | "parentTaskId" | "title" | "titleSource" | "autoTitleSignature" | "description" | "status" | "priority" | "labels" | "dueDate" | "milestoneId" | "sortOrder" | "agent">>): import("./task-types.js").WandTask | null;
|
|
139
140
|
deleteWandTask(id: string): void;
|
|
140
141
|
/**
|
|
141
142
|
* 里程碑列表。传工作区 id 时返回「该工作区 + 全局(workspace_id IS NULL)」;
|
package/dist/storage.js
CHANGED
|
@@ -755,6 +755,7 @@ const INIT_SQL = `
|
|
|
755
755
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
756
756
|
agent_json TEXT,
|
|
757
757
|
workspace_task_id TEXT,
|
|
758
|
+
parent_task_id TEXT,
|
|
758
759
|
title_source TEXT,
|
|
759
760
|
auto_title_signature TEXT,
|
|
760
761
|
created_at TEXT NOT NULL,
|
|
@@ -784,6 +785,9 @@ function ensureWandTaskSchema(db) {
|
|
|
784
785
|
db.exec("ALTER TABLE wand_tasks ADD COLUMN agent_json TEXT");
|
|
785
786
|
if (columns.length > 0 && !names.has("workspace_task_id"))
|
|
786
787
|
db.exec("ALTER TABLE wand_tasks ADD COLUMN workspace_task_id TEXT");
|
|
788
|
+
// 父任务归属只加列;历史任务仍然是顶层任务。
|
|
789
|
+
if (columns.length > 0 && !names.has("parent_task_id"))
|
|
790
|
+
db.exec("ALTER TABLE wand_tasks ADD COLUMN parent_task_id TEXT");
|
|
787
791
|
// 标题来源:'user' = 用户手写,'auto' = 由描述/会话内容自动生成。只加列,历史行保持 NULL(读取时视为 user)。
|
|
788
792
|
if (columns.length > 0 && !names.has("title_source"))
|
|
789
793
|
db.exec("ALTER TABLE wand_tasks ADD COLUMN title_source TEXT");
|
|
@@ -798,6 +802,7 @@ function ensureWandTaskSchema(db) {
|
|
|
798
802
|
// INIT_SQL cannot create it: CREATE TABLE IF NOT EXISTS is a no-op on old
|
|
799
803
|
// wand_tasks, and CREATE INDEX would then fail with "no such column".
|
|
800
804
|
db.exec("CREATE INDEX IF NOT EXISTS idx_wand_tasks_workspace_task ON wand_tasks(workspace_task_id)");
|
|
805
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_wand_tasks_parent ON wand_tasks(parent_task_id)");
|
|
801
806
|
const rows = db.prepare("SELECT id FROM wand_tasks WHERE identifier IS NULL OR identifier = '' ORDER BY created_at, id").all();
|
|
802
807
|
const update = db.prepare("UPDATE wand_tasks SET identifier = ? WHERE id = ?");
|
|
803
808
|
rows.forEach((row, index) => update.run(`TASK-${index + 1}`, row.id));
|
|
@@ -1296,7 +1301,7 @@ export class WandStorage {
|
|
|
1296
1301
|
}
|
|
1297
1302
|
/** GET /api/wand-tasks 的顺序:新创建在前。客户端按此顺序渲染,不再本地排序。 */
|
|
1298
1303
|
listWandTasks(workspaceId) {
|
|
1299
|
-
const rows = this.db.prepare(`SELECT id, identifier, workspace_id, workspace_task_id, title, title_source, auto_title_signature, description, status, priority, labels_json, due_date, milestone_id, sort_order, agent_json, created_at, updated_at
|
|
1304
|
+
const rows = this.db.prepare(`SELECT id, identifier, workspace_id, workspace_task_id, parent_task_id, title, title_source, auto_title_signature, description, status, priority, labels_json, due_date, milestone_id, sort_order, agent_json, created_at, updated_at
|
|
1300
1305
|
FROM wand_tasks ${workspaceId === undefined ? "" : "WHERE workspace_id IS ?"}
|
|
1301
1306
|
ORDER BY created_at DESC, rowid DESC`).all(...(workspaceId === undefined ? [] : [workspaceId]));
|
|
1302
1307
|
return rows.map((row) => this.mapWandTaskRow(row));
|
|
@@ -1307,6 +1312,7 @@ export class WandStorage {
|
|
|
1307
1312
|
identifier: typeof row.identifier === "string" && row.identifier ? row.identifier : `TASK-${String(row.id).slice(0, 6)}`,
|
|
1308
1313
|
workspaceId: typeof row.workspace_id === "string" ? row.workspace_id : null,
|
|
1309
1314
|
workspaceTaskId: typeof row.workspace_task_id === "string" && row.workspace_task_id ? row.workspace_task_id : null,
|
|
1315
|
+
parentTaskId: typeof row.parent_task_id === "string" && row.parent_task_id ? row.parent_task_id : null,
|
|
1310
1316
|
title: String(row.title),
|
|
1311
1317
|
titleSource: row.title_source === "auto" ? "auto" : "user",
|
|
1312
1318
|
autoTitleSignature: typeof row.auto_title_signature === "string" && row.auto_title_signature ? row.auto_title_signature : null,
|
|
@@ -1326,7 +1332,7 @@ export class WandStorage {
|
|
|
1326
1332
|
return this.listWandTasks().find((task) => task.id === id) ?? null;
|
|
1327
1333
|
}
|
|
1328
1334
|
getWandTaskByWorkspaceTaskId(workspaceTaskId) {
|
|
1329
|
-
const row = this.db.prepare(`SELECT id, identifier, workspace_id, workspace_task_id, title, title_source, auto_title_signature, description, status, priority, labels_json, due_date, milestone_id, sort_order, agent_json, created_at, updated_at
|
|
1335
|
+
const row = this.db.prepare(`SELECT id, identifier, workspace_id, workspace_task_id, parent_task_id, title, title_source, auto_title_signature, description, status, priority, labels_json, due_date, milestone_id, sort_order, agent_json, created_at, updated_at
|
|
1330
1336
|
FROM wand_tasks WHERE workspace_task_id = ? ORDER BY updated_at DESC LIMIT 1`).get(workspaceTaskId);
|
|
1331
1337
|
return row ? this.mapWandTaskRow(row) : null;
|
|
1332
1338
|
}
|
|
@@ -1338,7 +1344,7 @@ export class WandStorage {
|
|
|
1338
1344
|
const max = this.db.prepare("SELECT COALESCE(MAX(sort_order), -1) AS value FROM wand_tasks WHERE workspace_id IS ? AND status = ?").get(input.workspaceId ?? null, status);
|
|
1339
1345
|
const numberRow = this.db.prepare("SELECT COALESCE(MAX(CAST(substr(identifier, 6) AS INTEGER)), 0) AS value FROM wand_tasks WHERE identifier GLOB 'TASK-[0-9]*'").get();
|
|
1340
1346
|
const identifier = `TASK-${(numberRow?.value ?? 0) + 1}`;
|
|
1341
|
-
this.db.prepare(`INSERT INTO wand_tasks (id, identifier, workspace_id, workspace_task_id, title, title_source, auto_title_signature, description, status, priority, labels_json, due_date, milestone_id, sort_order, agent_json, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, identifier, input.workspaceId ?? null, input.workspaceTaskId ?? null, input.title, input.titleSource ?? "user", null, input.description ?? "", status, priority, JSON.stringify(input.labels ?? []), input.dueDate ?? null, input.milestoneId ?? null, (max?.value ?? -1) + 1, input.agent ? JSON.stringify(input.agent) : null, now, now);
|
|
1347
|
+
this.db.prepare(`INSERT INTO wand_tasks (id, identifier, workspace_id, workspace_task_id, parent_task_id, title, title_source, auto_title_signature, description, status, priority, labels_json, due_date, milestone_id, sort_order, agent_json, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, identifier, input.workspaceId ?? null, input.workspaceTaskId ?? null, input.parentTaskId ?? null, input.title, input.titleSource ?? "user", null, input.description ?? "", status, priority, JSON.stringify(input.labels ?? []), input.dueDate ?? null, input.milestoneId ?? null, (max?.value ?? -1) + 1, input.agent ? JSON.stringify(input.agent) : null, now, now);
|
|
1342
1348
|
return this.getWandTask(id);
|
|
1343
1349
|
}
|
|
1344
1350
|
updateWandTask(id, patch) {
|
|
@@ -1346,7 +1352,7 @@ export class WandStorage {
|
|
|
1346
1352
|
if (!current)
|
|
1347
1353
|
return null;
|
|
1348
1354
|
const next = { ...current, ...patch, updatedAt: nowIso() };
|
|
1349
|
-
this.db.prepare(`UPDATE wand_tasks SET workspace_id = ?, workspace_task_id = ?, title = ?, title_source = ?, auto_title_signature = ?, description = ?, status = ?, priority = ?, labels_json = ?, due_date = ?, milestone_id = ?, sort_order = ?, agent_json = ?, updated_at = ? WHERE id = ?`).run(next.workspaceId, next.workspaceTaskId, next.title, next.titleSource, next.autoTitleSignature ?? null, next.description, next.status, next.priority, JSON.stringify(next.labels), next.dueDate, next.milestoneId ?? null, next.sortOrder, next.agent ? JSON.stringify(next.agent) : null, next.updatedAt, id);
|
|
1355
|
+
this.db.prepare(`UPDATE wand_tasks SET workspace_id = ?, workspace_task_id = ?, parent_task_id = ?, title = ?, title_source = ?, auto_title_signature = ?, description = ?, status = ?, priority = ?, labels_json = ?, due_date = ?, milestone_id = ?, sort_order = ?, agent_json = ?, updated_at = ? WHERE id = ?`).run(next.workspaceId, next.workspaceTaskId, next.parentTaskId, next.title, next.titleSource, next.autoTitleSignature ?? null, next.description, next.status, next.priority, JSON.stringify(next.labels), next.dueDate, next.milestoneId ?? null, next.sortOrder, next.agent ? JSON.stringify(next.agent) : null, next.updatedAt, id);
|
|
1350
1356
|
if (next.workspaceTaskId) {
|
|
1351
1357
|
const workspaceId = next.workspaceId ?? this.ensureGlobalWorkspace().id;
|
|
1352
1358
|
this.db.prepare(`UPDATE workspace_tasks SET name = ?, status = ?, milestone_id = ?, workspace_id = ? WHERE id = ?`)
|
|
@@ -1356,7 +1362,10 @@ export class WandStorage {
|
|
|
1356
1362
|
}
|
|
1357
1363
|
return next;
|
|
1358
1364
|
}
|
|
1359
|
-
deleteWandTask(id) {
|
|
1365
|
+
deleteWandTask(id) {
|
|
1366
|
+
this.db.prepare("UPDATE wand_tasks SET parent_task_id = NULL WHERE parent_task_id = ?").run(id);
|
|
1367
|
+
this.db.prepare("DELETE FROM wand_tasks WHERE id = ?").run(id);
|
|
1368
|
+
}
|
|
1360
1369
|
// ── 里程碑(迭代,归属工作区;任务只存 milestoneId)──
|
|
1361
1370
|
/**
|
|
1362
1371
|
* 里程碑列表。传工作区 id 时返回「该工作区 + 全局(workspace_id IS NULL)」;
|
|
@@ -3,5 +3,7 @@ export declare const WAND_PROTOCOL_VERSION = 2;
|
|
|
3
3
|
/**
|
|
4
4
|
* Add Wand-owned semantics without mutating persisted provider blocks.
|
|
5
5
|
* This is the external interface consumed by every client.
|
|
6
|
+
*
|
|
7
|
+
* 传 sessionId 时额外把内联图片改写成取图 URL(见 rewriteInlineToolImageUrls)。
|
|
6
8
|
*/
|
|
7
|
-
export declare function enrichStructuredMessages(messages: ConversationTurn[]): ConversationTurn[];
|
|
9
|
+
export declare function enrichStructuredMessages(messages: ConversationTurn[], sessionId?: string): ConversationTurn[];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { asRecord } from "./structured-content.js";
|
|
1
|
+
import { asRecord, isStructuredImagePart } from "./structured-content.js";
|
|
2
2
|
export const WAND_PROTOCOL_VERSION = 2;
|
|
3
3
|
function text(value) {
|
|
4
4
|
return typeof value === "string" && value ? value : undefined;
|
|
@@ -46,6 +46,8 @@ function questionsFromInput(input) {
|
|
|
46
46
|
/** 派发子 Agent 的工具名:Claude Code 的 Task/Agent,以及 Wand pi 扩展的 subagent。 */
|
|
47
47
|
const SUBAGENT_TOOL_NAMES = new Set(["Task", "Agent"]);
|
|
48
48
|
const PI_SUBAGENT_TOOL_NAME = "Pi/subagent";
|
|
49
|
+
/** pi 的待办扩展工具,经 `piToolName` 映射后带 `Pi/` 前缀。 */
|
|
50
|
+
const PI_TODO_TOOL_NAME = "Pi/todo";
|
|
49
51
|
/**
|
|
50
52
|
* 历史 turn / 非 Claude provider 的子 Agent 工具调用没有 `__subagent` 盖章,
|
|
51
53
|
* 按调用参数现场补一份,让「子 Agent」面板在所有端都拿得到同一份分组。
|
|
@@ -121,6 +123,10 @@ function toolResultText(block) {
|
|
|
121
123
|
return block.content;
|
|
122
124
|
return block.content.map((part) => text(part.text) ?? "").join("");
|
|
123
125
|
}
|
|
126
|
+
const TASK_LIST_TOOL_NAMES = ["TodoWrite", "TaskCreate", "TaskUpdate", "TaskList", PI_TODO_TOOL_NAME];
|
|
127
|
+
function piTodoAction(input) {
|
|
128
|
+
return text(input.action) ?? "";
|
|
129
|
+
}
|
|
124
130
|
function tasksFromSegment(messages, start, end) {
|
|
125
131
|
let latestTodoWrite = null;
|
|
126
132
|
let targetId = null;
|
|
@@ -129,7 +135,7 @@ function tasksFromSegment(messages, start, end) {
|
|
|
129
135
|
for (const block of messages[i]?.content ?? []) {
|
|
130
136
|
if (block.type === "tool_result")
|
|
131
137
|
resultByToolId.set(block.tool_use_id, toolResultText(block));
|
|
132
|
-
if (block.type === "tool_use" &&
|
|
138
|
+
if (block.type === "tool_use" && TASK_LIST_TOOL_NAMES.includes(block.name)) {
|
|
133
139
|
targetId = block.id;
|
|
134
140
|
if (block.name === "TodoWrite")
|
|
135
141
|
latestTodoWrite = block;
|
|
@@ -158,31 +164,42 @@ function tasksFromSegment(messages, start, end) {
|
|
|
158
164
|
for (const block of messages[i]?.content ?? []) {
|
|
159
165
|
if (block.type !== "tool_use")
|
|
160
166
|
continue;
|
|
161
|
-
|
|
167
|
+
// pi 的待办工具把增删改合成一条 call,靠 `action` 区分,且主键是 `id` 而非 `taskId`。
|
|
168
|
+
const piAction = block.name === PI_TODO_TOOL_NAME ? piTodoAction(block.input) : null;
|
|
169
|
+
const isCreate = block.name === "TaskCreate" || piAction === "create";
|
|
170
|
+
const isUpdate = block.name === "TaskUpdate"
|
|
171
|
+
|| (piAction !== null && piAction !== "list" && piAction !== "get");
|
|
172
|
+
if (!isCreate && !isUpdate)
|
|
173
|
+
continue;
|
|
174
|
+
if (piAction === "clear") {
|
|
162
175
|
sawTaskTool = true;
|
|
176
|
+
tasks.clear();
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const rawId = isCreate ? "" : String(block.input.taskId ?? block.input.id ?? "");
|
|
180
|
+
if (!isCreate && !rawId)
|
|
181
|
+
continue;
|
|
182
|
+
sawTaskTool = true;
|
|
183
|
+
let id;
|
|
184
|
+
if (isCreate) {
|
|
163
185
|
fallbackId++;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
tasks.set(id, {
|
|
167
|
-
id,
|
|
168
|
-
content: text(block.input.subject) ?? text(block.input.description) ?? `Task #${id}`,
|
|
169
|
-
status: "pending",
|
|
170
|
-
...(text(block.input.activeForm) ? { activeForm: text(block.input.activeForm) } : {}),
|
|
171
|
-
});
|
|
186
|
+
// 结果文本里才有分配到的 id:Claude「Task #7 created …」/ pi「Created #1: …」。
|
|
187
|
+
id = resultByToolId.get(block.id)?.match(/#([^\s:,]+)/)?.[1] ?? String(fallbackId);
|
|
172
188
|
}
|
|
173
|
-
else
|
|
174
|
-
|
|
175
|
-
const id = String(block.input.taskId ?? "");
|
|
176
|
-
if (!id)
|
|
177
|
-
continue;
|
|
178
|
-
const previous = tasks.get(id) ?? { id, content: `Task #${id}`, status: "pending" };
|
|
179
|
-
tasks.set(id, {
|
|
180
|
-
...previous,
|
|
181
|
-
...(text(block.input.subject) ? { content: text(block.input.subject) } : {}),
|
|
182
|
-
...(text(block.input.status) ? { status: text(block.input.status) } : {}),
|
|
183
|
-
...(text(block.input.activeForm) ? { activeForm: text(block.input.activeForm) } : {}),
|
|
184
|
-
});
|
|
189
|
+
else {
|
|
190
|
+
id = rawId;
|
|
185
191
|
}
|
|
192
|
+
const content = text(block.input.subject) ?? text(block.input.description);
|
|
193
|
+
const status = text(block.input.status);
|
|
194
|
+
const activeForm = text(block.input.activeForm);
|
|
195
|
+
const deleted = piAction === "delete" || status === "deleted";
|
|
196
|
+
tasks.set(id, {
|
|
197
|
+
...tasks.get(id) ?? { id, content: content ?? `Task #${id}`, status: "pending" },
|
|
198
|
+
...(content ? { content } : {}),
|
|
199
|
+
...(status ? { status } : {}),
|
|
200
|
+
...(deleted ? { status: "deleted" } : {}),
|
|
201
|
+
...(activeForm ? { activeForm } : {}),
|
|
202
|
+
});
|
|
186
203
|
}
|
|
187
204
|
}
|
|
188
205
|
return {
|
|
@@ -190,11 +207,54 @@ function tasksFromSegment(messages, start, end) {
|
|
|
190
207
|
targetId,
|
|
191
208
|
};
|
|
192
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* 把 tool_result 里的内联 base64 图片换成「按会话取图」的 URL。
|
|
212
|
+
*
|
|
213
|
+
* 为什么不能把 base64 直接下发:读一张截图动辄 1–3MB,base64 后更大,会撑爆移动端
|
|
214
|
+
* 单帧上限(iOS 默认 1MiB)导致反复断连;而且同一张图在每条快照里重复传。换成 URL
|
|
215
|
+
* 后各端用同一套带鉴权的取图通道按需加载(Web 走 <img>,原生走各自 ImageLoader)。
|
|
216
|
+
* source 本来就是 url 的图片不动,保持幂等。
|
|
217
|
+
*/
|
|
218
|
+
function rewriteInlineToolImageUrls(messages, sessionId) {
|
|
219
|
+
return messages.map((turn) => {
|
|
220
|
+
let turnChanged = false;
|
|
221
|
+
const content = turn.content.map((block) => {
|
|
222
|
+
if (block.type !== "tool_result" || !Array.isArray(block.content))
|
|
223
|
+
return block;
|
|
224
|
+
let imageIndex = -1;
|
|
225
|
+
let blockChanged = false;
|
|
226
|
+
const parts = block.content.map((part) => {
|
|
227
|
+
if (!isStructuredImagePart(part))
|
|
228
|
+
return part;
|
|
229
|
+
imageIndex += 1;
|
|
230
|
+
const source = asRecord(part.source);
|
|
231
|
+
if (source?.type !== "base64" || typeof source.data !== "string")
|
|
232
|
+
return part;
|
|
233
|
+
blockChanged = true;
|
|
234
|
+
return {
|
|
235
|
+
type: "image",
|
|
236
|
+
source: {
|
|
237
|
+
type: "url",
|
|
238
|
+
url: `/api/sessions/${encodeURIComponent(sessionId)}/tool-images/${encodeURIComponent(block.tool_use_id)}/${imageIndex}`,
|
|
239
|
+
...(typeof source.media_type === "string" ? { media_type: source.media_type } : {}),
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
});
|
|
243
|
+
if (!blockChanged)
|
|
244
|
+
return block;
|
|
245
|
+
turnChanged = true;
|
|
246
|
+
return { ...block, content: parts };
|
|
247
|
+
});
|
|
248
|
+
return turnChanged ? { ...turn, content } : turn;
|
|
249
|
+
});
|
|
250
|
+
}
|
|
193
251
|
/**
|
|
194
252
|
* Add Wand-owned semantics without mutating persisted provider blocks.
|
|
195
253
|
* This is the external interface consumed by every client.
|
|
254
|
+
*
|
|
255
|
+
* 传 sessionId 时额外把内联图片改写成取图 URL(见 rewriteInlineToolImageUrls)。
|
|
196
256
|
*/
|
|
197
|
-
export function enrichStructuredMessages(messages) {
|
|
257
|
+
export function enrichStructuredMessages(messages, sessionId) {
|
|
198
258
|
const enriched = stampDerivedSubagents(messages).map((turn) => ({
|
|
199
259
|
...turn,
|
|
200
260
|
content: turn.content.map((block) => {
|
|
@@ -231,5 +291,5 @@ export function enrichStructuredMessages(messages) {
|
|
|
231
291
|
}
|
|
232
292
|
segmentStart = i;
|
|
233
293
|
}
|
|
234
|
-
return enriched;
|
|
294
|
+
return sessionId ? rewriteInlineToolImageUrls(enriched, sessionId) : enriched;
|
|
235
295
|
}
|
|
@@ -1,7 +1,24 @@
|
|
|
1
1
|
/** 把 unknown 收敛成普通对象;数组 / null / 原始值都返回 null。 */
|
|
2
2
|
export declare function asRecord(value: unknown): Record<string, unknown> | null;
|
|
3
|
-
|
|
4
|
-
export declare function normalizeStructuredToolResultContent(content: unknown): string | Array<{
|
|
3
|
+
export type StructuredContentPart = {
|
|
5
4
|
type: string;
|
|
6
5
|
[key: string]: unknown;
|
|
7
|
-
}
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* 各家 provider 的图片 part 形态不同:
|
|
9
|
+
* · Anthropic:`{ type:"image", source:{ type:"base64", media_type, data } }` / `source:{ type:"url", url }`
|
|
10
|
+
* · OpenAI Responses:`{ type:"image_url", image_url:{ url } }` / `image_url:"…"`
|
|
11
|
+
* · Pi:`{ type:"image", data, mimeType }`
|
|
12
|
+
* 只要带图片语义就算,具体归一化交给 canonicalizeStructuredImagePart。
|
|
13
|
+
*/
|
|
14
|
+
export declare function isStructuredImagePart(value: unknown): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* 统一成客户端认识的规范形态(Web / Android / iOS / macOS 都按
|
|
17
|
+
* `{ type:"image", source:{ type:"base64"|"url", … } }` 解析)。识别不了返回 null,
|
|
18
|
+
* 由调用方原样保留,避免丢信息。
|
|
19
|
+
*/
|
|
20
|
+
export declare function canonicalizeStructuredImagePart(value: unknown): StructuredContentPart | null;
|
|
21
|
+
/** content(字符串 / part 数组)里是否含图片 part。 */
|
|
22
|
+
export declare function contentHasStructuredImage(content: unknown): boolean;
|
|
23
|
+
/** Preserve both Responses content parts and arbitrary structured tool output. */
|
|
24
|
+
export declare function normalizeStructuredToolResultContent(content: unknown): string | StructuredContentPart[];
|
|
@@ -4,12 +4,91 @@ export function asRecord(value) {
|
|
|
4
4
|
? value
|
|
5
5
|
: null;
|
|
6
6
|
}
|
|
7
|
+
function stringValue(value) {
|
|
8
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* 各家 provider 的图片 part 形态不同:
|
|
12
|
+
* · Anthropic:`{ type:"image", source:{ type:"base64", media_type, data } }` / `source:{ type:"url", url }`
|
|
13
|
+
* · OpenAI Responses:`{ type:"image_url", image_url:{ url } }` / `image_url:"…"`
|
|
14
|
+
* · Pi:`{ type:"image", data, mimeType }`
|
|
15
|
+
* 只要带图片语义就算,具体归一化交给 canonicalizeStructuredImagePart。
|
|
16
|
+
*/
|
|
17
|
+
export function isStructuredImagePart(value) {
|
|
18
|
+
const part = asRecord(value);
|
|
19
|
+
if (!part)
|
|
20
|
+
return false;
|
|
21
|
+
if (part.type === "image_url")
|
|
22
|
+
return !!imageUrlOf(part);
|
|
23
|
+
if (part.type !== "image")
|
|
24
|
+
return false;
|
|
25
|
+
const source = asRecord(part.source);
|
|
26
|
+
if (source && (stringValue(source.data) || stringValue(source.url)))
|
|
27
|
+
return true;
|
|
28
|
+
return !!(stringValue(part.data) || stringValue(part.url));
|
|
29
|
+
}
|
|
30
|
+
function imageUrlOf(part) {
|
|
31
|
+
const raw = part.image_url;
|
|
32
|
+
if (typeof raw === "string")
|
|
33
|
+
return stringValue(raw);
|
|
34
|
+
return stringValue(asRecord(raw)?.url);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 统一成客户端认识的规范形态(Web / Android / iOS / macOS 都按
|
|
38
|
+
* `{ type:"image", source:{ type:"base64"|"url", … } }` 解析)。识别不了返回 null,
|
|
39
|
+
* 由调用方原样保留,避免丢信息。
|
|
40
|
+
*/
|
|
41
|
+
export function canonicalizeStructuredImagePart(value) {
|
|
42
|
+
const part = asRecord(value);
|
|
43
|
+
if (!part)
|
|
44
|
+
return null;
|
|
45
|
+
if (part.type === "image_url") {
|
|
46
|
+
const url = imageUrlOf(part);
|
|
47
|
+
return url ? { type: "image", source: { type: "url", url } } : null;
|
|
48
|
+
}
|
|
49
|
+
if (part.type !== "image")
|
|
50
|
+
return null;
|
|
51
|
+
const source = asRecord(part.source);
|
|
52
|
+
if (source) {
|
|
53
|
+
const data = stringValue(source.data);
|
|
54
|
+
if (source.type === "base64" && data) {
|
|
55
|
+
return {
|
|
56
|
+
type: "image",
|
|
57
|
+
source: { type: "base64", media_type: stringValue(source.media_type) ?? "image/png", data },
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const url = stringValue(source.url);
|
|
61
|
+
if (url)
|
|
62
|
+
return { type: "image", source: { type: "url", url } };
|
|
63
|
+
}
|
|
64
|
+
const data = stringValue(part.data);
|
|
65
|
+
if (data) {
|
|
66
|
+
const mediaType = stringValue(part.mimeType)
|
|
67
|
+
?? stringValue(part.mime_type)
|
|
68
|
+
?? stringValue(source?.media_type)
|
|
69
|
+
?? "image/png";
|
|
70
|
+
return { type: "image", source: { type: "base64", media_type: mediaType, data } };
|
|
71
|
+
}
|
|
72
|
+
const url = stringValue(part.url);
|
|
73
|
+
if (url)
|
|
74
|
+
return { type: "image", source: { type: "url", url } };
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
/** content(字符串 / part 数组)里是否含图片 part。 */
|
|
78
|
+
export function contentHasStructuredImage(content) {
|
|
79
|
+
if (!Array.isArray(content))
|
|
80
|
+
return false;
|
|
81
|
+
return content.some((part) => isStructuredImagePart(part));
|
|
82
|
+
}
|
|
7
83
|
/** Preserve both Responses content parts and arbitrary structured tool output. */
|
|
8
84
|
export function normalizeStructuredToolResultContent(content) {
|
|
9
85
|
if (typeof content === "string")
|
|
10
86
|
return content;
|
|
11
87
|
if (Array.isArray(content)) {
|
|
12
|
-
const parts = content
|
|
88
|
+
const parts = content
|
|
89
|
+
.filter((item) => !!item && typeof item === "object" && typeof item.type === "string")
|
|
90
|
+
// 图片 part 归一化后客户端才能跨端渲染;非图片 part 原样保留。
|
|
91
|
+
.map((part) => canonicalizeStructuredImagePart(part) ?? part);
|
|
13
92
|
if (parts.length === content.length)
|
|
14
93
|
return parts;
|
|
15
94
|
try {
|
|
@@ -79,6 +79,12 @@ class InProcessStructuredExecProcess {
|
|
|
79
79
|
});
|
|
80
80
|
});
|
|
81
81
|
this.child.on("error", () => settleExit({ exitCode: null, signal: null }));
|
|
82
|
+
this.child.stdin?.on("error", () => {
|
|
83
|
+
// EPIPE is emitted by stdin, not by ChildProcess. Never claim success
|
|
84
|
+
// when a child exits before consuming its one-shot prompt.
|
|
85
|
+
settleExit({ exitCode: -1, signal: null });
|
|
86
|
+
this.interrupt();
|
|
87
|
+
});
|
|
82
88
|
}
|
|
83
89
|
get pid() {
|
|
84
90
|
return this.child.pid ?? -1;
|
|
@@ -118,9 +124,9 @@ export class InProcessStructuredExecHost {
|
|
|
118
124
|
env: request.env,
|
|
119
125
|
stdio: wantsStdin ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"],
|
|
120
126
|
});
|
|
127
|
+
const wrapped = new InProcessStructuredExecProcess(request.runId, child);
|
|
121
128
|
if (wantsStdin)
|
|
122
129
|
child.stdin?.end(request.stdinData);
|
|
123
|
-
const wrapped = new InProcessStructuredExecProcess(request.runId, child);
|
|
124
130
|
// Keep exited records until forgetRun so a late attach can still answer.
|
|
125
131
|
this.processes.set(request.runId, wrapped);
|
|
126
132
|
return wrapped;
|
|
@@ -80,19 +80,23 @@ export function startStructuredCli(options) {
|
|
|
80
80
|
env: options.env,
|
|
81
81
|
stdio: wantsStdin ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"],
|
|
82
82
|
});
|
|
83
|
-
if (wantsStdin)
|
|
84
|
-
child.stdin?.end(options.stdinData);
|
|
85
83
|
source = { pid: child.pid ?? null, interrupt: () => child.kill("SIGTERM") };
|
|
86
84
|
const stdoutDecoder = createUtf8TextDecoder();
|
|
87
85
|
const stderrDecoder = createUtf8TextDecoder();
|
|
88
86
|
child.stdout?.on("data", (chunk) => handleStdoutText(stdoutDecoder.write(chunk)));
|
|
89
87
|
child.stderr?.on("data", (chunk) => handleStderrText(stderrDecoder.write(chunk)));
|
|
90
88
|
child.on("error", (error) => finish(null, null, error));
|
|
89
|
+
child.stdin?.on("error", (error) => {
|
|
90
|
+
finish(null, null, error);
|
|
91
|
+
child.kill("SIGTERM");
|
|
92
|
+
});
|
|
91
93
|
child.on("close", (exitCode, signalName) => {
|
|
92
94
|
handleStdoutText(stdoutDecoder.end());
|
|
93
95
|
handleStderrText(stderrDecoder.end());
|
|
94
96
|
finish(exitCode, signalName === null || signalName === undefined ? null : signalName);
|
|
95
97
|
});
|
|
98
|
+
if (wantsStdin)
|
|
99
|
+
child.stdin?.end(options.stdinData);
|
|
96
100
|
sourceReady = Promise.resolve();
|
|
97
101
|
}
|
|
98
102
|
return {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { startStructuredCli } from "./structured-exec-pump.js";
|
|
3
|
-
import { asRecord } from "./structured-content.js";
|
|
3
|
+
import { asRecord, normalizeStructuredToolResultContent } from "./structured-content.js";
|
|
4
4
|
import { thinkingEffortToPiLevel } from "./structured-provider-common.js";
|
|
5
5
|
export function isMissingPiSession(error, sessionId) {
|
|
6
6
|
return !!sessionId && !!error?.includes(`No session found matching '${sessionId}'`);
|
|
@@ -17,6 +17,22 @@ function textContent(value) {
|
|
|
17
17
|
return item.text;
|
|
18
18
|
return textContent(item.content);
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Pi 的工具结果是 `{ content: [ text…, image… ] }`。图片 part 不能像以前那样只抽文本
|
|
22
|
+
* 丢掉——读图 / 截图全靠它,统一交给 normalizeStructuredToolResultContent 归一化。
|
|
23
|
+
* 纯文本结果仍压成字符串,保持既有形态。
|
|
24
|
+
*/
|
|
25
|
+
function piToolResultContent(value) {
|
|
26
|
+
const record = asRecord(value);
|
|
27
|
+
const raw = record && "content" in record ? record.content : value;
|
|
28
|
+
const normalized = normalizeStructuredToolResultContent(raw);
|
|
29
|
+
if (typeof normalized === "string")
|
|
30
|
+
return normalized;
|
|
31
|
+
if (normalized.length > 0 && normalized.every((part) => part.type === "text" && typeof part.text === "string")) {
|
|
32
|
+
return normalized.map((part) => part.text).filter(Boolean).join("\n");
|
|
33
|
+
}
|
|
34
|
+
return normalized;
|
|
35
|
+
}
|
|
20
36
|
export function buildPiArgs(session, prompt) {
|
|
21
37
|
const args = ["--mode", "json", "--print"];
|
|
22
38
|
const model = session.selectedModel?.trim();
|
|
@@ -141,7 +157,7 @@ export function applyPiEvent(state, event) {
|
|
|
141
157
|
}
|
|
142
158
|
if (event.type === "tool_execution_end") {
|
|
143
159
|
const id = typeof event.toolCallId === "string" ? event.toolCallId : "unknown";
|
|
144
|
-
state.blocks.push({ type: "tool_result", tool_use_id: id, content:
|
|
160
|
+
state.blocks.push({ type: "tool_result", tool_use_id: id, content: piToolResultContent(event.result), is_error: event.isError === true });
|
|
145
161
|
}
|
|
146
162
|
if (event.type === "message_end" || event.type === "turn_end") {
|
|
147
163
|
const message = asRecord(event.message);
|
|
@@ -155,6 +155,8 @@ export declare class StructuredSessionManager {
|
|
|
155
155
|
* 本身,flushNext 在另一段时序里读 sessions.get(...) 当前快照,已经天然安全。
|
|
156
156
|
*/
|
|
157
157
|
reorderQueuedMessages(sessionId: string, order: number[]): SessionSnapshot;
|
|
158
|
+
/** Edit only the pending item still identified by both index and original text. */
|
|
159
|
+
editQueuedMessage(sessionId: string, index: number, expectedText: string, text: string): SessionSnapshot;
|
|
158
160
|
/** Remove a single queued message only while index and text still identify the same item. */
|
|
159
161
|
deleteQueuedMessage(sessionId: string, index: number, expectedText?: string): SessionSnapshot;
|
|
160
162
|
/**
|