@co0ontty/wand 3.1.1 → 4.0.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/auth.d.ts +19 -5
- package/dist/auth.js +83 -45
- package/dist/build-info.json +3 -3
- package/dist/cert.d.ts +1 -1
- package/dist/cert.js +124 -74
- package/dist/config.js +25 -8
- package/dist/express-async.d.ts +6 -0
- package/dist/express-async.js +28 -0
- package/dist/git-quick-commit.d.ts +2 -0
- package/dist/git-quick-commit.js +215 -76
- package/dist/git-utils.d.ts +4 -0
- package/dist/git-utils.js +60 -11
- package/dist/git-worktree.d.ts +8 -1
- package/dist/git-worktree.js +406 -41
- package/dist/models.d.ts +34 -4
- package/dist/models.js +334 -48
- package/dist/process-manager.d.ts +22 -30
- package/dist/process-manager.js +374 -441
- package/dist/provider-history-scanner.d.ts +54 -0
- package/dist/provider-history-scanner.js +354 -0
- package/dist/request-limits.d.ts +1 -0
- package/dist/request-limits.js +8 -0
- package/dist/resume-policy.d.ts +2 -0
- package/dist/resume-policy.js +5 -0
- package/dist/runtime-config.d.ts +16 -0
- package/dist/runtime-config.js +49 -0
- package/dist/server-file-routes.d.ts +17 -0
- package/dist/server-file-routes.js +653 -0
- package/dist/server-session-routes.d.ts +16 -3
- package/dist/server-session-routes.js +170 -149
- package/dist/server-settings-routes.d.ts +43 -0
- package/dist/server-settings-routes.js +225 -0
- package/dist/server-update-routes.d.ts +61 -0
- package/dist/server-update-routes.js +215 -0
- package/dist/server.d.ts +6 -4
- package/dist/server.js +350 -1313
- package/dist/session-logger.d.ts +32 -2
- package/dist/session-logger.js +145 -15
- package/dist/session-registry.d.ts +27 -0
- package/dist/session-registry.js +153 -0
- package/dist/session-transport.d.ts +31 -0
- package/dist/session-transport.js +82 -0
- package/dist/storage.d.ts +24 -6
- package/dist/storage.js +291 -44
- package/dist/structured-claude-adapter.d.ts +19 -0
- package/dist/structured-claude-adapter.js +117 -0
- package/dist/structured-codex-adapter.d.ts +3 -0
- package/dist/structured-codex-adapter.js +29 -0
- package/dist/structured-opencode-adapter.d.ts +11 -0
- package/dist/structured-opencode-adapter.js +115 -0
- package/dist/structured-provider-common.d.ts +11 -0
- package/dist/structured-provider-common.js +77 -0
- package/dist/structured-session-manager.d.ts +32 -35
- package/dist/structured-session-manager.js +551 -605
- package/dist/types.d.ts +10 -0
- package/dist/update-helper.js +5 -1
- package/dist/web-ui/content/scripts.js +32 -32
- package/dist/web-ui/embedded-assets.d.ts +1 -1
- package/dist/web-ui/embedded-assets.js +2 -2
- package/dist/ws-broadcast.d.ts +16 -1
- package/dist/ws-broadcast.js +124 -58
- package/package.json +2 -1
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
import { SessionInputError } from "./process-manager.js";
|
|
2
|
-
import { getDefaultModelForProvider,
|
|
2
|
+
import { getDefaultModelForProvider, isExecutionMode } from "./config.js";
|
|
3
3
|
import { blockWindowMessagesForTransport, sliceTurnBlocksForTransport, truncateMessagesForTransport, windowMessagesForTransport } from "./message-truncator.js";
|
|
4
|
-
import {
|
|
4
|
+
import { toSessionDetailDTO, toSessionListItemDTO } from "./session-transport.js";
|
|
5
|
+
import { checkSessionWorktreeMergeabilityAsync, cleanupSessionWorktreeAsync, getWorktreeMergeErrorCode, mergeSessionWorktreeAsync, WorktreeMergeError, } from "./git-worktree.js";
|
|
5
6
|
import { resolveSessionCwd } from "./session-cwd.js";
|
|
6
7
|
import { resolveCommitAiContext } from "./session-ai-context.js";
|
|
7
|
-
import {
|
|
8
|
+
import { getGitStatusAsync, QuickCommitError, runQuickCommitWithFallback, runTagHead, runPush, generateCommitMessageOnly, } from "./git-quick-commit.js";
|
|
8
9
|
import { getErrorMessage } from "./error-utils.js";
|
|
10
|
+
import { isProviderSessionId } from "./resume-policy.js";
|
|
11
|
+
import { parseBoundedInteger } from "./request-limits.js";
|
|
12
|
+
import { asyncRoute } from "./express-async.js";
|
|
13
|
+
export function parseExecutionMode(value, fallback) {
|
|
14
|
+
if (value === undefined)
|
|
15
|
+
return fallback;
|
|
16
|
+
if (!isExecutionMode(value)) {
|
|
17
|
+
throw new Error(`无效执行模式: ${String(value)}`);
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
9
21
|
export function parseSessionCreationOrigin(body) {
|
|
10
22
|
const rawSource = body?.sessionSource;
|
|
11
23
|
if (rawSource !== undefined && rawSource !== "interactive" && rawSource !== "automation" && rawSource !== "startup") {
|
|
@@ -115,9 +127,6 @@ function removeFromHiddenClaudeSessionIds(storage, ids) {
|
|
|
115
127
|
saveHiddenClaudeSessionIds(storage, hidden);
|
|
116
128
|
}
|
|
117
129
|
}
|
|
118
|
-
function getSessionById(processes, structured, id) {
|
|
119
|
-
return structured.get(id) ?? processes.get(id);
|
|
120
|
-
}
|
|
121
130
|
/**
|
|
122
131
|
* A Wand-managed session and its provider-native history represent the same
|
|
123
132
|
* conversation. Delete both in one operation so the native history cannot
|
|
@@ -157,10 +166,26 @@ export function deleteSessionWithProviderHistory(processes, structured, storage,
|
|
|
157
166
|
// Keep a tombstone as a fallback for permission errors or process tail writes.
|
|
158
167
|
addToHiddenClaudeSessionIds(storage, [providerSessionId]);
|
|
159
168
|
}
|
|
160
|
-
/**
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
169
|
+
/**
|
|
170
|
+
* Provider history is scanned by ProcessManager, but structured sessions live
|
|
171
|
+
* in StructuredSessionManager. Annotate against the combined session list so a
|
|
172
|
+
* structured conversation is not also exposed as a recoverable native history
|
|
173
|
+
* entry. Return copies for matches because ProcessManager caches scan results.
|
|
174
|
+
*/
|
|
175
|
+
export function markManagedProviderHistory(history, sessions, provider) {
|
|
176
|
+
const managedIds = new Set();
|
|
177
|
+
for (const session of sessions) {
|
|
178
|
+
const sessionProvider = session.provider
|
|
179
|
+
?? session.structuredState?.provider
|
|
180
|
+
?? (/^codex\b/i.test(session.command.trim()) ? "codex" : "claude");
|
|
181
|
+
const providerSessionId = session.claudeSessionId?.trim();
|
|
182
|
+
if (sessionProvider === provider && providerSessionId) {
|
|
183
|
+
managedIds.add(providerSessionId);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return history.map((entry) => (entry.managedByWand || !managedIds.has(entry.claudeSessionId)
|
|
187
|
+
? entry
|
|
188
|
+
: { ...entry, managedByWand: true }));
|
|
164
189
|
}
|
|
165
190
|
function requireWorktreeSession(snapshot) {
|
|
166
191
|
if (!snapshot) {
|
|
@@ -179,15 +204,14 @@ function buildWorktreeMergeInfo(current, info) {
|
|
|
179
204
|
conflict: info?.conflict,
|
|
180
205
|
};
|
|
181
206
|
}
|
|
182
|
-
function saveWorktreeMergeState(
|
|
207
|
+
function saveWorktreeMergeState(sessions, current, status, info) {
|
|
183
208
|
const mergedInfo = buildWorktreeMergeInfo(current, info);
|
|
184
209
|
const updated = {
|
|
185
210
|
...current,
|
|
186
211
|
worktreeMergeStatus: status,
|
|
187
212
|
worktreeMergeInfo: mergedInfo,
|
|
188
213
|
};
|
|
189
|
-
|
|
190
|
-
return updated;
|
|
214
|
+
return sessions.updateWorktreeState(current.id, status, mergedInfo) ?? updated;
|
|
191
215
|
}
|
|
192
216
|
function getWorktreeMergeResponseStatus(error) {
|
|
193
217
|
return getWorktreeMergeErrorCode(error) === "WORKTREE_MERGE_CONFLICT" ? 409 : 400;
|
|
@@ -206,9 +230,6 @@ function getWorktreeMergePayload(error, fallback) {
|
|
|
206
230
|
result: null,
|
|
207
231
|
};
|
|
208
232
|
}
|
|
209
|
-
function getLatestSessionSnapshot(processes, structured, storage, id) {
|
|
210
|
-
return getSessionById(processes, structured, id) ?? storage.getSession(id);
|
|
211
|
-
}
|
|
212
233
|
function buildCodexResumeCommand(command, threadId) {
|
|
213
234
|
const trimmed = command.trim();
|
|
214
235
|
const withoutExistingResume = trimmed
|
|
@@ -244,7 +265,7 @@ function startResumedPtySession(processes, existingSession, sessionId, defaultMo
|
|
|
244
265
|
throw new Error("对应的 Codex 历史会话不存在,无法恢复。");
|
|
245
266
|
}
|
|
246
267
|
}
|
|
247
|
-
const newMode =
|
|
268
|
+
const newMode = parseExecutionMode(body.mode, parseExecutionMode(existingSession.mode, defaultMode));
|
|
248
269
|
const resumeCommand = provider === "codex"
|
|
249
270
|
? buildCodexResumeCommand(command, resumeSessionId)
|
|
250
271
|
: `${command} --resume ${resumeSessionId}`;
|
|
@@ -288,12 +309,19 @@ function canMergeSession(snapshot) {
|
|
|
288
309
|
function isMergeActionAllowed(snapshot) {
|
|
289
310
|
return snapshot.status !== "running";
|
|
290
311
|
}
|
|
291
|
-
export function registerSessionRoutes(app, processes, structured, storage, defaultMode, config, onSessionCreated) {
|
|
312
|
+
export function registerSessionRoutes(app, processes, structured, storage, defaultMode, config, sessions, onSessionCreated) {
|
|
313
|
+
const sessionResponseDTO = (snapshot) => {
|
|
314
|
+
const windowed = windowMessagesForTransport(snapshot.messages ?? [], config.cardDefaults ?? {});
|
|
315
|
+
return toSessionDetailDTO(snapshot, {
|
|
316
|
+
messages: windowed.messages,
|
|
317
|
+
messageOffset: windowed.messageOffset,
|
|
318
|
+
messageTotal: windowed.messageTotal,
|
|
319
|
+
});
|
|
320
|
+
};
|
|
292
321
|
app.get("/api/sessions", (_req, res) => {
|
|
293
|
-
|
|
294
|
-
res.json(all);
|
|
322
|
+
res.json(sessions.listSlim().map(toSessionListItemDTO));
|
|
295
323
|
});
|
|
296
|
-
app.post("/api/structured-sessions", async (req, res) => {
|
|
324
|
+
app.post("/api/structured-sessions", asyncRoute(async (req, res) => {
|
|
297
325
|
const body = req.body;
|
|
298
326
|
try {
|
|
299
327
|
if (body.provider && body.provider !== "claude" && body.provider !== "codex" && body.provider !== "opencode") {
|
|
@@ -305,9 +333,11 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
305
333
|
const origin = parseSessionCreationOrigin(body);
|
|
306
334
|
const snapshot = structured.createSession({
|
|
307
335
|
cwd: resolveSessionCwd(body.cwd, config.defaultCwd),
|
|
308
|
-
mode:
|
|
336
|
+
mode: parseExecutionMode(body.mode, defaultMode),
|
|
309
337
|
provider,
|
|
310
|
-
|
|
338
|
+
// Omit runner to let StructuredSessionManager apply the configured
|
|
339
|
+
// Claude default; explicit values are validated against the provider.
|
|
340
|
+
runner: body.runner,
|
|
311
341
|
worktreeEnabled: body.worktreeEnabled === true,
|
|
312
342
|
model: rawModel || getDefaultModelForProvider(config, provider) || undefined,
|
|
313
343
|
thinkingEffort: typeof body.thinkingEffort === "string"
|
|
@@ -322,29 +352,22 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
322
352
|
res.status(201).json(finished);
|
|
323
353
|
return;
|
|
324
354
|
}
|
|
325
|
-
res.status(201).json(snapshot);
|
|
355
|
+
res.status(201).json(sessionResponseDTO(snapshot));
|
|
326
356
|
}
|
|
327
357
|
catch (error) {
|
|
328
358
|
res.status(400).json({ error: getErrorMessage(error, "无法启动结构化会话。") });
|
|
329
359
|
}
|
|
330
|
-
});
|
|
360
|
+
}));
|
|
331
361
|
app.post("/api/sessions/:id/model", (req, res) => {
|
|
332
362
|
const body = req.body;
|
|
333
363
|
const rawModel = typeof body?.model === "string" ? body.model.trim() : null;
|
|
334
364
|
const id = req.params.id;
|
|
335
365
|
try {
|
|
336
|
-
const
|
|
337
|
-
if (
|
|
338
|
-
const updated = structured.setSessionModel(id, rawModel);
|
|
339
|
-
res.json(updated);
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
const ptySnapshot = processes.get(id);
|
|
343
|
-
if (!ptySnapshot) {
|
|
366
|
+
const updated = sessions.setSessionModel(id, rawModel);
|
|
367
|
+
if (!updated) {
|
|
344
368
|
res.status(404).json({ error: "未找到该会话。" });
|
|
345
369
|
return;
|
|
346
370
|
}
|
|
347
|
-
const updated = processes.setSessionModel(id, rawModel);
|
|
348
371
|
res.json(updated);
|
|
349
372
|
}
|
|
350
373
|
catch (error) {
|
|
@@ -358,16 +381,11 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
358
381
|
const raw = typeof body?.thinkingEffort === "string" ? body.thinkingEffort : null;
|
|
359
382
|
const id = req.params.id;
|
|
360
383
|
try {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
res.json(updated);
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
|
-
if (!processes.get(id)) {
|
|
384
|
+
const updated = sessions.setSessionThinkingEffort(id, raw);
|
|
385
|
+
if (!updated) {
|
|
367
386
|
res.status(404).json({ error: "未找到该会话。" });
|
|
368
387
|
return;
|
|
369
388
|
}
|
|
370
|
-
const updated = processes.setSessionThinkingEffort(id, raw);
|
|
371
389
|
res.json(updated);
|
|
372
390
|
}
|
|
373
391
|
catch (error) {
|
|
@@ -384,23 +402,20 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
384
402
|
res.status(400).json({ error: "缺少 mode。" });
|
|
385
403
|
return;
|
|
386
404
|
}
|
|
387
|
-
|
|
405
|
+
if (!isExecutionMode(raw)) {
|
|
406
|
+
res.status(400).json({ error: `无效执行模式: ${raw}` });
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const mode = raw;
|
|
388
410
|
const id = req.params.id;
|
|
389
411
|
try {
|
|
390
|
-
const
|
|
391
|
-
if (
|
|
392
|
-
const provider = structuredSnapshot.provider ?? "claude";
|
|
393
|
-
const effective = provider === "codex" ? "full-access" : mode;
|
|
394
|
-
res.json(structured.setSessionMode(id, effective));
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
397
|
-
const ptySnapshot = processes.get(id);
|
|
398
|
-
if (!ptySnapshot) {
|
|
412
|
+
const snapshot = sessions.get(id);
|
|
413
|
+
if (!snapshot) {
|
|
399
414
|
res.status(404).json({ error: "未找到该会话。" });
|
|
400
415
|
return;
|
|
401
416
|
}
|
|
402
|
-
const effective = (
|
|
403
|
-
res.json(
|
|
417
|
+
const effective = (snapshot.provider ?? "claude") === "codex" ? "full-access" : mode;
|
|
418
|
+
res.json(sessions.setSessionMode(id, effective));
|
|
404
419
|
}
|
|
405
420
|
catch (error) {
|
|
406
421
|
res.status(400).json({ error: getErrorMessage(error, "切换模式失败。") });
|
|
@@ -414,7 +429,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
414
429
|
}
|
|
415
430
|
res.json({ id: snapshot.id, messages: snapshot.messages ?? [] });
|
|
416
431
|
});
|
|
417
|
-
app.post("/api/structured-sessions/:id/messages", async (req, res) => {
|
|
432
|
+
app.post("/api/structured-sessions/:id/messages", asyncRoute(async (req, res) => {
|
|
418
433
|
const input = String(req.body?.input ?? "");
|
|
419
434
|
const interrupt = !!req.body?.interrupt;
|
|
420
435
|
// preserveQueue: 仅在 interrupt 路径有意义。排队条「立即」会带这个 flag,
|
|
@@ -423,7 +438,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
423
438
|
const idempotencyKey = typeof req.body?.idempotencyKey === "string" ? req.body.idempotencyKey : undefined;
|
|
424
439
|
try {
|
|
425
440
|
const snapshot = await structured.sendMessage(req.params.id, input, { interrupt, preserveQueue, idempotencyKey });
|
|
426
|
-
res.json(snapshot);
|
|
441
|
+
res.json(sessionResponseDTO(snapshot));
|
|
427
442
|
}
|
|
428
443
|
catch (error) {
|
|
429
444
|
const errorCode = error?.code;
|
|
@@ -433,7 +448,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
433
448
|
errorCode,
|
|
434
449
|
});
|
|
435
450
|
}
|
|
436
|
-
});
|
|
451
|
+
}));
|
|
437
452
|
// ── Structured queued-messages management ──
|
|
438
453
|
// 这些端点构成"排队消息条"的后端操作面:reorder、立即发送、单条删除、全部清空。
|
|
439
454
|
// 全部走乐观更新模型,失败时前端会回滚到上一次 WS 推送的 queuedMessages。
|
|
@@ -445,7 +460,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
445
460
|
}
|
|
446
461
|
try {
|
|
447
462
|
const snapshot = structured.reorderQueuedMessages(req.params.id, rawOrder.map((v) => Number(v)));
|
|
448
|
-
res.json(snapshot);
|
|
463
|
+
res.json(sessionResponseDTO(snapshot));
|
|
449
464
|
}
|
|
450
465
|
catch (error) {
|
|
451
466
|
res.status(400).json({ error: getErrorMessage(error, "无法调整排队顺序。") });
|
|
@@ -459,13 +474,13 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
459
474
|
}
|
|
460
475
|
try {
|
|
461
476
|
const snapshot = structured.deleteQueuedMessage(req.params.id, index);
|
|
462
|
-
res.json(snapshot);
|
|
477
|
+
res.json(sessionResponseDTO(snapshot));
|
|
463
478
|
}
|
|
464
479
|
catch (error) {
|
|
465
480
|
res.status(400).json({ error: getErrorMessage(error, "无法删除排队消息。") });
|
|
466
481
|
}
|
|
467
482
|
});
|
|
468
|
-
app.post("/api/structured-sessions/:id/queued/:index/promote", async (req, res) => {
|
|
483
|
+
app.post("/api/structured-sessions/:id/queued/:index/promote", asyncRoute(async (req, res) => {
|
|
469
484
|
const index = Number(req.params.index);
|
|
470
485
|
if (!Number.isInteger(index)) {
|
|
471
486
|
res.status(400).json({ error: "下标无效。" });
|
|
@@ -475,7 +490,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
475
490
|
const idempotencyKey = typeof req.body?.idempotencyKey === "string" ? req.body.idempotencyKey : undefined;
|
|
476
491
|
try {
|
|
477
492
|
const snapshot = await structured.promoteQueuedMessage(req.params.id, index, expectedText, idempotencyKey);
|
|
478
|
-
res.json(snapshot);
|
|
493
|
+
res.json(sessionResponseDTO(snapshot));
|
|
479
494
|
}
|
|
480
495
|
catch (error) {
|
|
481
496
|
const errorCode = error?.code;
|
|
@@ -485,11 +500,11 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
485
500
|
errorCode,
|
|
486
501
|
});
|
|
487
502
|
}
|
|
488
|
-
});
|
|
503
|
+
}));
|
|
489
504
|
app.delete("/api/structured-sessions/:id/queued", (req, res) => {
|
|
490
505
|
try {
|
|
491
506
|
const snapshot = structured.clearQueuedMessages(req.params.id);
|
|
492
|
-
res.json(snapshot);
|
|
507
|
+
res.json(sessionResponseDTO(snapshot));
|
|
493
508
|
}
|
|
494
509
|
catch (error) {
|
|
495
510
|
res.status(400).json({ error: getErrorMessage(error, "无法清空排队消息。") });
|
|
@@ -497,7 +512,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
497
512
|
});
|
|
498
513
|
// ── Tool content lazy-load endpoint ──
|
|
499
514
|
app.get("/api/sessions/:id/tool-content/:toolUseId", (req, res) => {
|
|
500
|
-
const snapshot =
|
|
515
|
+
const snapshot = sessions.get(req.params.id);
|
|
501
516
|
if (!snapshot) {
|
|
502
517
|
res.status(404).json({ error: "未找到该会话。" });
|
|
503
518
|
return;
|
|
@@ -518,25 +533,25 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
518
533
|
}
|
|
519
534
|
res.status(404).json({ error: "未找到该工具结果。" });
|
|
520
535
|
});
|
|
521
|
-
app.post("/api/sessions/:id/worktree/merge/check", (req, res) => {
|
|
536
|
+
app.post("/api/sessions/:id/worktree/merge/check", asyncRoute(async (req, res) => {
|
|
522
537
|
try {
|
|
523
|
-
const current = requireWorktreeSession(
|
|
538
|
+
const current = requireWorktreeSession(sessions.getLatest(req.params.id));
|
|
524
539
|
if (!isMergeActionAllowed(current)) {
|
|
525
540
|
res.status(409).json({ error: "会话仍在运行,请结束后再合并。", errorCode: "SESSION_STILL_RUNNING" });
|
|
526
541
|
return;
|
|
527
542
|
}
|
|
528
|
-
const checking = saveWorktreeMergeState(
|
|
543
|
+
const checking = saveWorktreeMergeState(sessions, current, "checking", {
|
|
529
544
|
...(current.worktreeMergeInfo ?? null),
|
|
530
545
|
targetBranch: current.worktreeMergeInfo?.targetBranch,
|
|
531
546
|
lastError: undefined,
|
|
532
547
|
conflict: false,
|
|
533
548
|
});
|
|
534
|
-
const result =
|
|
549
|
+
const result = await checkSessionWorktreeMergeabilityAsync({
|
|
535
550
|
worktree: checking.worktree,
|
|
536
551
|
targetBranch: current.worktreeMergeInfo?.targetBranch,
|
|
537
552
|
});
|
|
538
553
|
const nextStatus = result.ok ? "ready" : "failed";
|
|
539
|
-
const updated = saveWorktreeMergeState(
|
|
554
|
+
const updated = saveWorktreeMergeState(sessions, checking, nextStatus, {
|
|
540
555
|
targetBranch: result.targetBranch,
|
|
541
556
|
conflict: result.hasConflicts,
|
|
542
557
|
lastError: result.ok ? undefined : result.reason,
|
|
@@ -546,24 +561,24 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
546
561
|
catch (error) {
|
|
547
562
|
res.status(getWorktreeMergeResponseStatus(error)).json(getWorktreeMergePayload(error, "无法检查 worktree 合并状态。"));
|
|
548
563
|
}
|
|
549
|
-
});
|
|
550
|
-
app.post("/api/sessions/:id/worktree/merge", (req, res) => {
|
|
564
|
+
}));
|
|
565
|
+
app.post("/api/sessions/:id/worktree/merge", asyncRoute(async (req, res) => {
|
|
551
566
|
try {
|
|
552
|
-
const current = requireWorktreeSession(
|
|
567
|
+
const current = requireWorktreeSession(sessions.getLatest(req.params.id));
|
|
553
568
|
if (!isMergeActionAllowed(current)) {
|
|
554
569
|
res.status(409).json({ error: "会话仍在运行,请结束后再合并。", errorCode: "SESSION_STILL_RUNNING" });
|
|
555
570
|
return;
|
|
556
571
|
}
|
|
557
|
-
const merging = saveWorktreeMergeState(
|
|
572
|
+
const merging = saveWorktreeMergeState(sessions, current, "merging", {
|
|
558
573
|
...(current.worktreeMergeInfo ?? null),
|
|
559
574
|
lastError: undefined,
|
|
560
575
|
conflict: false,
|
|
561
576
|
});
|
|
562
|
-
const result =
|
|
577
|
+
const result = await mergeSessionWorktreeAsync({
|
|
563
578
|
worktree: merging.worktree,
|
|
564
579
|
targetBranch: current.worktreeMergeInfo?.targetBranch,
|
|
565
580
|
});
|
|
566
|
-
const updated = saveWorktreeMergeState(
|
|
581
|
+
const updated = saveWorktreeMergeState(sessions, merging, "merged", {
|
|
567
582
|
targetBranch: result.targetBranch,
|
|
568
583
|
mergedAt: result.mergedAt,
|
|
569
584
|
mergeCommit: result.mergeCommit,
|
|
@@ -574,10 +589,10 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
574
589
|
res.json({ session: updated, result });
|
|
575
590
|
}
|
|
576
591
|
catch (error) {
|
|
577
|
-
const current =
|
|
592
|
+
const current = sessions.getLatest(req.params.id);
|
|
578
593
|
if (current && canMergeSession(current)) {
|
|
579
594
|
const payload = getWorktreeMergePayload(error, "无法合并 worktree。");
|
|
580
|
-
saveWorktreeMergeState(
|
|
595
|
+
saveWorktreeMergeState(sessions, current, "failed", {
|
|
581
596
|
...(current.worktreeMergeInfo ?? null),
|
|
582
597
|
targetBranch: payload.result?.targetBranch ?? current.worktreeMergeInfo?.targetBranch,
|
|
583
598
|
mergedAt: payload.result?.mergedAt,
|
|
@@ -589,9 +604,9 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
589
604
|
}
|
|
590
605
|
res.status(getWorktreeMergeResponseStatus(error)).json(getWorktreeMergePayload(error, "无法合并 worktree。"));
|
|
591
606
|
}
|
|
592
|
-
});
|
|
593
|
-
app.get("/api/sessions/:id/git-status", (req, res) => {
|
|
594
|
-
const snapshot =
|
|
607
|
+
}));
|
|
608
|
+
app.get("/api/sessions/:id/git-status", asyncRoute(async (req, res) => {
|
|
609
|
+
const snapshot = sessions.getLatest(req.params.id);
|
|
595
610
|
if (!snapshot) {
|
|
596
611
|
res.status(404).json({ error: "未找到该会话。" });
|
|
597
612
|
return;
|
|
@@ -601,14 +616,14 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
601
616
|
return;
|
|
602
617
|
}
|
|
603
618
|
try {
|
|
604
|
-
res.json(
|
|
619
|
+
res.json(await getGitStatusAsync(snapshot.cwd));
|
|
605
620
|
}
|
|
606
621
|
catch (error) {
|
|
607
622
|
res.json({ isGit: false, error: getErrorMessage(error, "无法读取 git 状态。") });
|
|
608
623
|
}
|
|
609
|
-
});
|
|
610
|
-
app.post("/api/sessions/:id/quick-commit", async (req, res) => {
|
|
611
|
-
const snapshot =
|
|
624
|
+
}));
|
|
625
|
+
app.post("/api/sessions/:id/quick-commit", asyncRoute(async (req, res) => {
|
|
626
|
+
const snapshot = sessions.getLatest(req.params.id);
|
|
612
627
|
if (!snapshot) {
|
|
613
628
|
res.status(404).json({ error: "未找到该会话。" });
|
|
614
629
|
return;
|
|
@@ -641,9 +656,9 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
641
656
|
}
|
|
642
657
|
res.status(400).json({ error: getErrorMessage(error, "快捷提交失败。") });
|
|
643
658
|
}
|
|
644
|
-
});
|
|
645
|
-
app.post("/api/sessions/:id/generate-commit-message", async (req, res) => {
|
|
646
|
-
const snapshot =
|
|
659
|
+
}));
|
|
660
|
+
app.post("/api/sessions/:id/generate-commit-message", asyncRoute(async (req, res) => {
|
|
661
|
+
const snapshot = sessions.getLatest(req.params.id);
|
|
647
662
|
if (!snapshot) {
|
|
648
663
|
res.status(404).json({ error: "未找到该会话。" });
|
|
649
664
|
return;
|
|
@@ -666,9 +681,9 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
666
681
|
}
|
|
667
682
|
res.status(400).json({ error: getErrorMessage(error, "生成 commit message 失败。") });
|
|
668
683
|
}
|
|
669
|
-
});
|
|
670
|
-
app.post("/api/sessions/:id/git/tag-head", async (req, res) => {
|
|
671
|
-
const snapshot =
|
|
684
|
+
}));
|
|
685
|
+
app.post("/api/sessions/:id/git/tag-head", asyncRoute(async (req, res) => {
|
|
686
|
+
const snapshot = sessions.getLatest(req.params.id);
|
|
672
687
|
if (!snapshot) {
|
|
673
688
|
res.status(404).json({ error: "未找到该会话。" });
|
|
674
689
|
return;
|
|
@@ -698,9 +713,9 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
698
713
|
}
|
|
699
714
|
res.status(400).json({ error: getErrorMessage(error, "打 tag 失败。") });
|
|
700
715
|
}
|
|
701
|
-
});
|
|
702
|
-
app.post("/api/sessions/:id/git/push", async (req, res) => {
|
|
703
|
-
const snapshot =
|
|
716
|
+
}));
|
|
717
|
+
app.post("/api/sessions/:id/git/push", asyncRoute(async (req, res) => {
|
|
718
|
+
const snapshot = sessions.getLatest(req.params.id);
|
|
704
719
|
if (!snapshot) {
|
|
705
720
|
res.status(404).json({ error: "未找到该会话。" });
|
|
706
721
|
return;
|
|
@@ -727,16 +742,16 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
727
742
|
}
|
|
728
743
|
res.status(400).json({ error: getErrorMessage(error, "推送失败。") });
|
|
729
744
|
}
|
|
730
|
-
});
|
|
731
|
-
app.post("/api/sessions/:id/worktree/cleanup", (req, res) => {
|
|
745
|
+
}));
|
|
746
|
+
app.post("/api/sessions/:id/worktree/cleanup", asyncRoute(async (req, res) => {
|
|
732
747
|
try {
|
|
733
|
-
const current = requireWorktreeSession(
|
|
748
|
+
const current = requireWorktreeSession(sessions.getLatest(req.params.id));
|
|
734
749
|
if (current.worktreeMergeStatus !== "merged" || current.worktreeMergeInfo?.cleanupDone !== false) {
|
|
735
750
|
res.status(400).json({ error: "当前 worktree 无需补偿清理。", errorCode: "WORKTREE_CLEANUP_NOT_NEEDED" });
|
|
736
751
|
return;
|
|
737
752
|
}
|
|
738
|
-
|
|
739
|
-
const updated = saveWorktreeMergeState(
|
|
753
|
+
await cleanupSessionWorktreeAsync({ worktree: current.worktree });
|
|
754
|
+
const updated = saveWorktreeMergeState(sessions, current, "merged", {
|
|
740
755
|
...(current.worktreeMergeInfo ?? null),
|
|
741
756
|
cleanupDone: true,
|
|
742
757
|
lastError: undefined,
|
|
@@ -747,7 +762,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
747
762
|
catch (error) {
|
|
748
763
|
res.status(getWorktreeMergeResponseStatus(error)).json(getWorktreeMergePayload(error, "无法清理 worktree。"));
|
|
749
764
|
}
|
|
750
|
-
});
|
|
765
|
+
}));
|
|
751
766
|
app.post("/api/sessions/batch-delete", (req, res) => {
|
|
752
767
|
const sessionIds = Array.isArray(req.body?.sessionIds)
|
|
753
768
|
? req.body.sessionIds.filter((value) => typeof value === "string" && value.trim().length > 0)
|
|
@@ -760,7 +775,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
760
775
|
const failed = [];
|
|
761
776
|
for (const sessionId of sessionIds) {
|
|
762
777
|
try {
|
|
763
|
-
|
|
778
|
+
sessions.deleteWithProviderHistory(sessionId);
|
|
764
779
|
deleted += 1;
|
|
765
780
|
}
|
|
766
781
|
catch {
|
|
@@ -774,7 +789,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
774
789
|
res.json({ ok: true, deleted, failed });
|
|
775
790
|
});
|
|
776
791
|
app.get("/api/sessions/:id", (req, res) => {
|
|
777
|
-
const snapshot =
|
|
792
|
+
const snapshot = sessions.get(req.params.id);
|
|
778
793
|
if (!snapshot) {
|
|
779
794
|
res.status(404).json({ error: "未找到该会话,可能已被删除。" });
|
|
780
795
|
return;
|
|
@@ -785,38 +800,37 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
785
800
|
if (req.query.format === "chat") {
|
|
786
801
|
// 客户端带 blockBudget(iOS)走块级窗口:只回最近 N 个块(必要时切掉最旧 turn 的头部),
|
|
787
802
|
// 根治「单条 turn 上百块/1MB」的长任务打开慢。Web/Android 不带该参数,走原 turn 级窗口。
|
|
788
|
-
const rawBudget =
|
|
789
|
-
if (
|
|
790
|
-
const
|
|
791
|
-
|
|
792
|
-
|
|
803
|
+
const rawBudget = req.query.blockBudget;
|
|
804
|
+
if (typeof rawBudget === "string" && /^\d+$/.test(rawBudget) && Number(rawBudget) > 0) {
|
|
805
|
+
const blockBudget = parseBoundedInteger(rawBudget, 1, 1, 2_000);
|
|
806
|
+
const windowed = blockWindowMessagesForTransport(snapshot.messages ?? [], config.cardDefaults ?? {}, blockBudget);
|
|
807
|
+
res.json(toSessionDetailDTO(snapshot, {
|
|
793
808
|
output: transcriptOutput,
|
|
794
809
|
messages: windowed.messages,
|
|
795
810
|
messageOffset: windowed.messageOffset,
|
|
796
811
|
messageTotal: windowed.messageTotal,
|
|
797
812
|
leadingBlockOffset: windowed.leadingBlockOffset,
|
|
798
813
|
leadingBlockTotal: windowed.leadingBlockTotal,
|
|
799
|
-
});
|
|
814
|
+
}));
|
|
800
815
|
return;
|
|
801
816
|
}
|
|
802
817
|
// 与 WS init 对齐:只回最近一窗 turn + offset/total,更早的走 /messages 翻页。
|
|
803
818
|
const windowed = windowMessagesForTransport(snapshot.messages ?? [], config.cardDefaults ?? {});
|
|
804
|
-
res.json({
|
|
805
|
-
...snapshot,
|
|
819
|
+
res.json(toSessionDetailDTO(snapshot, {
|
|
806
820
|
output: transcriptOutput,
|
|
807
821
|
messages: windowed.messages,
|
|
808
822
|
messageOffset: windowed.messageOffset,
|
|
809
823
|
messageTotal: windowed.messageTotal,
|
|
810
|
-
});
|
|
824
|
+
}));
|
|
811
825
|
}
|
|
812
826
|
else {
|
|
813
|
-
res.json(
|
|
827
|
+
res.json(toSessionDetailDTO(snapshot, { output: transcriptOutput }));
|
|
814
828
|
}
|
|
815
829
|
});
|
|
816
830
|
// 历史消息分页拉取:客户端滚动到顶时按绝对下标往前翻。
|
|
817
831
|
// 返回 messages = 完整历史的 [offset, offset+limit)(已做 transport 截断)+ total。
|
|
818
832
|
app.get("/api/sessions/:id/messages", (req, res) => {
|
|
819
|
-
const snapshot =
|
|
833
|
+
const snapshot = sessions.get(req.params.id);
|
|
820
834
|
if (!snapshot) {
|
|
821
835
|
res.status(404).json({ error: "未找到该会话,可能已被删除。" });
|
|
822
836
|
return;
|
|
@@ -878,8 +892,8 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
878
892
|
const requestedOrigin = body.sessionSource !== undefined || body.automationId !== undefined
|
|
879
893
|
? parseSessionCreationOrigin(body)
|
|
880
894
|
: null;
|
|
881
|
-
if (!claudeSessionId) {
|
|
882
|
-
res.status(400).json({ error: "Claude 会话 ID
|
|
895
|
+
if (!isProviderSessionId(claudeSessionId)) {
|
|
896
|
+
res.status(400).json({ error: "Claude 会话 ID 必须是有效的 UUID。" });
|
|
883
897
|
return;
|
|
884
898
|
}
|
|
885
899
|
const existingSession = storage.getLatestSessionByClaudeSessionId(claudeSessionId);
|
|
@@ -901,8 +915,10 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
901
915
|
res.status(400).json({ error: "对应的 Claude 历史会话文件不存在,无法恢复。" });
|
|
902
916
|
return;
|
|
903
917
|
}
|
|
904
|
-
const newMode =
|
|
905
|
-
|
|
918
|
+
const newMode = parseExecutionMode(body.mode, parseExecutionMode(existingSession.mode, defaultMode));
|
|
919
|
+
// Do not reuse the persisted shell command here. It may contain flags or
|
|
920
|
+
// shell operators and must never become part of a resume command.
|
|
921
|
+
const resumeCommand = `claude --resume ${claudeSessionId}`;
|
|
906
922
|
const reqCols = typeof body.cols === "number" && Number.isFinite(body.cols) ? body.cols : undefined;
|
|
907
923
|
const reqRows = typeof body.rows === "number" && Number.isFinite(body.rows) ? body.rows : undefined;
|
|
908
924
|
const newSnapshot = processes.start(resumeCommand, existingSession.cwd, newMode, undefined, {
|
|
@@ -911,7 +927,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
911
927
|
rows: reqRows,
|
|
912
928
|
...(requestedOrigin ?? {}),
|
|
913
929
|
});
|
|
914
|
-
res.status(201).json({ resumedClaudeSessionId: claudeSessionId, ...newSnapshot });
|
|
930
|
+
res.status(201).json({ resumedClaudeSessionId: claudeSessionId, ...sessionResponseDTO(newSnapshot) });
|
|
915
931
|
}
|
|
916
932
|
else {
|
|
917
933
|
const cwd = body.cwd?.trim();
|
|
@@ -919,7 +935,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
919
935
|
res.status(400).json({ error: "未找到对应的会话记录,请提供工作目录 (cwd)。" });
|
|
920
936
|
return;
|
|
921
937
|
}
|
|
922
|
-
const newMode =
|
|
938
|
+
const newMode = parseExecutionMode(body.mode, defaultMode);
|
|
923
939
|
const resumeCommand = `claude --resume ${claudeSessionId}`;
|
|
924
940
|
const reqCols = typeof body.cols === "number" && Number.isFinite(body.cols) ? body.cols : undefined;
|
|
925
941
|
const reqRows = typeof body.rows === "number" && Number.isFinite(body.rows) ? body.rows : undefined;
|
|
@@ -928,14 +944,14 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
928
944
|
rows: reqRows,
|
|
929
945
|
...(requestedOrigin ?? {}),
|
|
930
946
|
});
|
|
931
|
-
res.status(201).json({ resumedClaudeSessionId: claudeSessionId, ...newSnapshot });
|
|
947
|
+
res.status(201).json({ resumedClaudeSessionId: claudeSessionId, ...sessionResponseDTO(newSnapshot) });
|
|
932
948
|
}
|
|
933
949
|
}
|
|
934
950
|
catch (error) {
|
|
935
951
|
res.status(400).json({ error: getErrorMessage(error, "无法按 Claude 会话 ID 恢复会话。") });
|
|
936
952
|
}
|
|
937
953
|
});
|
|
938
|
-
app.post("/api/sessions/:id/input", async (req, res) => {
|
|
954
|
+
app.post("/api/sessions/:id/input", asyncRoute(async (req, res) => {
|
|
939
955
|
const body = req.body;
|
|
940
956
|
const sessionId = req.params.id;
|
|
941
957
|
const input = body.input ?? "";
|
|
@@ -944,18 +960,18 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
944
960
|
try {
|
|
945
961
|
if (structured.get(sessionId)) {
|
|
946
962
|
const snapshot = await structured.sendMessage(sessionId, input);
|
|
947
|
-
res.json(snapshot);
|
|
963
|
+
res.json(sessionResponseDTO(snapshot));
|
|
948
964
|
return;
|
|
949
965
|
}
|
|
950
966
|
const existingSession = processes.get(sessionId) || storage.getSession(sessionId);
|
|
951
967
|
const autoResumeInput = getAutoResumeInitialInput(existingSession, input, view, shortcutKey);
|
|
952
968
|
if (autoResumeInput !== null && canAutoResumePtyForInput(existingSession, autoResumeInput)) {
|
|
953
969
|
const snapshot = startResumedPtySession(processes, existingSession, sessionId, defaultMode, {}, autoResumeInput);
|
|
954
|
-
res.json(snapshot);
|
|
970
|
+
res.json(sessionResponseDTO(snapshot));
|
|
955
971
|
return;
|
|
956
972
|
}
|
|
957
973
|
const snapshot = processes.sendInput(sessionId, input, view, shortcutKey);
|
|
958
|
-
res.json(snapshot);
|
|
974
|
+
res.json(sessionResponseDTO(snapshot));
|
|
959
975
|
}
|
|
960
976
|
catch (error) {
|
|
961
977
|
const response = getInputErrorResponse(error, sessionId);
|
|
@@ -966,13 +982,13 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
966
982
|
});
|
|
967
983
|
res.status(response.statusCode).json(response.payload);
|
|
968
984
|
}
|
|
969
|
-
});
|
|
970
|
-
app.post("/api/codex-sessions/:threadId/resume", async (req, res) => {
|
|
985
|
+
}));
|
|
986
|
+
app.post("/api/codex-sessions/:threadId/resume", asyncRoute(async (req, res) => {
|
|
971
987
|
const threadId = String(req.params.threadId || "").trim();
|
|
972
988
|
const body = req.body;
|
|
973
989
|
try {
|
|
974
|
-
if (!threadId) {
|
|
975
|
-
res.status(400).json({ error: "Codex 会话 ID
|
|
990
|
+
if (!isProviderSessionId(threadId)) {
|
|
991
|
+
res.status(400).json({ error: "Codex 会话 ID 必须是有效的 UUID。" });
|
|
976
992
|
return;
|
|
977
993
|
}
|
|
978
994
|
const history = processes.listCodexHistorySessions().find((s) => s.claudeSessionId === threadId);
|
|
@@ -985,7 +1001,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
985
1001
|
res.status(400).json({ error: "无法确定工作目录 (cwd),无法恢复。" });
|
|
986
1002
|
return;
|
|
987
1003
|
}
|
|
988
|
-
const newMode =
|
|
1004
|
+
const newMode = parseExecutionMode(body.mode, defaultMode);
|
|
989
1005
|
const origin = parseSessionCreationOrigin(body);
|
|
990
1006
|
const snapshot = structured.createSession({
|
|
991
1007
|
cwd,
|
|
@@ -997,21 +1013,21 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
997
1013
|
...origin,
|
|
998
1014
|
});
|
|
999
1015
|
onSessionCreated?.(cwd);
|
|
1000
|
-
res.status(201).json({ resumedClaudeSessionId: threadId, ...snapshot });
|
|
1016
|
+
res.status(201).json({ resumedClaudeSessionId: threadId, ...sessionResponseDTO(snapshot) });
|
|
1001
1017
|
}
|
|
1002
1018
|
catch (error) {
|
|
1003
1019
|
res.status(400).json({ error: getErrorMessage(error, "无法按 Codex 会话 ID 恢复会话。") });
|
|
1004
1020
|
}
|
|
1005
|
-
});
|
|
1021
|
+
}));
|
|
1006
1022
|
app.post("/api/sessions/:id/resize", (req, res) => {
|
|
1007
1023
|
const body = req.body;
|
|
1008
1024
|
try {
|
|
1009
|
-
if (
|
|
1025
|
+
if (sessions.ownerOf(req.params.id) === "structured") {
|
|
1010
1026
|
res.status(400).json({ error: "结构化会话不支持调整终端大小。" });
|
|
1011
1027
|
return;
|
|
1012
1028
|
}
|
|
1013
1029
|
const snapshot = processes.resize(req.params.id, body.cols ?? 0, body.rows ?? 0);
|
|
1014
|
-
res.json(snapshot);
|
|
1030
|
+
res.json(sessionResponseDTO(snapshot));
|
|
1015
1031
|
}
|
|
1016
1032
|
catch (error) {
|
|
1017
1033
|
res.status(400).json({ error: getErrorMessage(error, "无法调整终端大小。") });
|
|
@@ -1019,11 +1035,11 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1019
1035
|
});
|
|
1020
1036
|
app.post("/api/sessions/:id/approve-permission", (req, res) => {
|
|
1021
1037
|
try {
|
|
1022
|
-
if (
|
|
1038
|
+
if (sessions.ownerOf(req.params.id) === "structured") {
|
|
1023
1039
|
res.status(400).json({ error: "结构化会话不需要终端权限操作。" });
|
|
1024
1040
|
return;
|
|
1025
1041
|
}
|
|
1026
|
-
const snapshot =
|
|
1042
|
+
const snapshot = sessions.get(req.params.id);
|
|
1027
1043
|
if (snapshot?.provider === "codex") {
|
|
1028
1044
|
res.status(400).json({ error: "Codex provider 不支持权限批准操作。" });
|
|
1029
1045
|
return;
|
|
@@ -1036,11 +1052,11 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1036
1052
|
});
|
|
1037
1053
|
app.post("/api/sessions/:id/deny-permission", (req, res) => {
|
|
1038
1054
|
try {
|
|
1039
|
-
if (
|
|
1055
|
+
if (sessions.ownerOf(req.params.id) === "structured") {
|
|
1040
1056
|
res.status(400).json({ error: "结构化会话不需要终端权限操作。" });
|
|
1041
1057
|
return;
|
|
1042
1058
|
}
|
|
1043
|
-
const snapshot =
|
|
1059
|
+
const snapshot = sessions.get(req.params.id);
|
|
1044
1060
|
if (snapshot?.provider === "codex") {
|
|
1045
1061
|
res.status(400).json({ error: "Codex provider 不支持权限拒绝操作。" });
|
|
1046
1062
|
return;
|
|
@@ -1053,11 +1069,11 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1053
1069
|
});
|
|
1054
1070
|
app.post("/api/sessions/:id/toggle-auto-approve", (req, res) => {
|
|
1055
1071
|
try {
|
|
1056
|
-
if (
|
|
1072
|
+
if (sessions.ownerOf(req.params.id) === "structured") {
|
|
1057
1073
|
res.status(400).json({ error: "结构化会话不需要切换终端自动批准。" });
|
|
1058
1074
|
return;
|
|
1059
1075
|
}
|
|
1060
|
-
const snapshot =
|
|
1076
|
+
const snapshot = sessions.get(req.params.id);
|
|
1061
1077
|
if (snapshot?.provider === "codex") {
|
|
1062
1078
|
res.status(400).json({ error: "Codex provider 不支持自动批准切换。" });
|
|
1063
1079
|
return;
|
|
@@ -1072,11 +1088,16 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1072
1088
|
try {
|
|
1073
1089
|
const { requestId } = req.params;
|
|
1074
1090
|
const body = req.body;
|
|
1075
|
-
|
|
1076
|
-
|
|
1091
|
+
const resolution = body?.resolution;
|
|
1092
|
+
if (resolution !== "approve_once" && resolution !== "approve_turn" && resolution !== "deny") {
|
|
1093
|
+
res.status(400).json({ error: "resolution 必须是 approve_once、approve_turn 或 deny。" });
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
if (sessions.ownerOf(req.params.id) === "structured") {
|
|
1097
|
+
res.json(structured.resolveEscalation(req.params.id, requestId, resolution));
|
|
1077
1098
|
return;
|
|
1078
1099
|
}
|
|
1079
|
-
res.json(processes.resolveEscalation(req.params.id, requestId,
|
|
1100
|
+
res.json(processes.resolveEscalation(req.params.id, requestId, resolution));
|
|
1080
1101
|
}
|
|
1081
1102
|
catch (error) {
|
|
1082
1103
|
res.status(400).json({ error: getErrorMessage(error, "无法处理该授权请求。") });
|
|
@@ -1084,7 +1105,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1084
1105
|
});
|
|
1085
1106
|
app.post("/api/sessions/:id/stop", (req, res) => {
|
|
1086
1107
|
try {
|
|
1087
|
-
if (
|
|
1108
|
+
if (sessions.ownerOf(req.params.id) === "structured") {
|
|
1088
1109
|
res.json(structured.stop(req.params.id));
|
|
1089
1110
|
return;
|
|
1090
1111
|
}
|
|
@@ -1096,7 +1117,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1096
1117
|
});
|
|
1097
1118
|
app.delete("/api/sessions/:id", (req, res) => {
|
|
1098
1119
|
try {
|
|
1099
|
-
|
|
1120
|
+
sessions.deleteWithProviderHistory(req.params.id);
|
|
1100
1121
|
res.json({ ok: true });
|
|
1101
1122
|
}
|
|
1102
1123
|
catch (error) {
|
|
@@ -1104,14 +1125,14 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1104
1125
|
}
|
|
1105
1126
|
});
|
|
1106
1127
|
}
|
|
1107
|
-
export function registerClaudeHistoryRoutes(app, processes, storage) {
|
|
1128
|
+
export function registerClaudeHistoryRoutes(app, processes, structured, storage, sessionRegistry) {
|
|
1108
1129
|
app.get("/api/claude-history", (_req, res) => {
|
|
1109
1130
|
try {
|
|
1110
|
-
const
|
|
1131
|
+
const history = markManagedProviderHistory(processes.listClaudeHistorySessions(), sessionRegistry.listSlim(), "claude");
|
|
1111
1132
|
const hidden = getHiddenClaudeSessionIds(storage);
|
|
1112
1133
|
const filtered = hidden.size > 0
|
|
1113
|
-
?
|
|
1114
|
-
:
|
|
1134
|
+
? history.filter((s) => !s.claudeSessionId || !hidden.has(s.claudeSessionId))
|
|
1135
|
+
: history;
|
|
1115
1136
|
res.json(filtered);
|
|
1116
1137
|
}
|
|
1117
1138
|
catch (error) {
|
|
@@ -1212,11 +1233,11 @@ export function registerClaudeHistoryRoutes(app, processes, storage) {
|
|
|
1212
1233
|
// hidden 集合与 claude 共用(id 全局唯一,不会冲突)。
|
|
1213
1234
|
app.get("/api/codex-history", (_req, res) => {
|
|
1214
1235
|
try {
|
|
1215
|
-
const
|
|
1236
|
+
const history = markManagedProviderHistory(processes.listCodexHistorySessions(), sessionRegistry.listSlim(), "codex");
|
|
1216
1237
|
const hidden = getHiddenClaudeSessionIds(storage);
|
|
1217
1238
|
const filtered = hidden.size > 0
|
|
1218
|
-
?
|
|
1219
|
-
:
|
|
1239
|
+
? history.filter((s) => !hidden.has(s.claudeSessionId))
|
|
1240
|
+
: history;
|
|
1220
1241
|
res.json(filtered);
|
|
1221
1242
|
}
|
|
1222
1243
|
catch (error) {
|