@co0ontty/wand 4.60.0 → 4.61.0-beta.g14d0be2

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.
@@ -0,0 +1,25 @@
1
+ /**
2
+ * 按 provider 原生会话 ID 恢复 structured 会话的路由。
3
+ *
4
+ * 两条历史路径含义不同,必须分开表达:
5
+ * - `codex` / `opencode` / `qoder` 的历史由 Wand 扫描出来,缺 `cwd` 时可以从历史补齐;
6
+ * - `grok` / `pi` 的恢复 ID 由客户端自己持有,服务端无法反查记录,必须显式带 `cwd`。
7
+ *
8
+ * 以前每个 provider 各抄一份 handler(5 份几乎相同的 200 行);现在只有一个
9
+ * 恢复流程,provider 之间的差异收敛成 `StructuredResumeSpec` 里的几个字段。
10
+ */
11
+ import type { Express } from "express";
12
+ import type { ProcessManager } from "./process-manager.js";
13
+ import type { StructuredSessionManager } from "./structured-session-manager.js";
14
+ import type { WandStorage } from "./storage.js";
15
+ import type { ExecutionMode, SessionSnapshot } from "./types.js";
16
+ export interface ResumeRouteDependencies {
17
+ processes: ProcessManager;
18
+ structured: StructuredSessionManager;
19
+ storage: WandStorage;
20
+ defaultMode: ExecutionMode;
21
+ onSessionCreated?: (cwd: string | undefined | null) => void;
22
+ /** 会话快照 → 响应 DTO;由调用方注入以免这里再依赖消息截断层。 */
23
+ toDetailDTO: (snapshot: SessionSnapshot) => unknown;
24
+ }
25
+ export declare function registerStructuredResumeRoutes(app: Express, deps: ResumeRouteDependencies): void;
@@ -0,0 +1,124 @@
1
+ /**
2
+ * 按 provider 原生会话 ID 恢复 structured 会话的路由。
3
+ *
4
+ * 两条历史路径含义不同,必须分开表达:
5
+ * - `codex` / `opencode` / `qoder` 的历史由 Wand 扫描出来,缺 `cwd` 时可以从历史补齐;
6
+ * - `grok` / `pi` 的恢复 ID 由客户端自己持有,服务端无法反查记录,必须显式带 `cwd`。
7
+ *
8
+ * 以前每个 provider 各抄一份 handler(5 份几乎相同的 200 行);现在只有一个
9
+ * 恢复流程,provider 之间的差异收敛成 `StructuredResumeSpec` 里的几个字段。
10
+ */
11
+ import { asyncRoute } from "./express-async.js";
12
+ import { getErrorMessage } from "./error-utils.js";
13
+ import { isProviderSessionId, isSafeProviderSessionId } from "./resume-policy.js";
14
+ import { parseExecutionMode, parseSessionCreationOrigin } from "./server-session-routes.js";
15
+ import { resolveWorkspaceIdForNewSession } from "./workspace-binding.js";
16
+ const STRUCTURED_RESUME_SPECS = [
17
+ {
18
+ path: "/api/codex-sessions/:nativeId/resume",
19
+ param: "nativeId",
20
+ spec: {
21
+ label: "Codex",
22
+ provider: "codex",
23
+ runner: "codex-cli-exec",
24
+ nativeIdKey: "claudeSessionId",
25
+ idFormat: "uuid",
26
+ findHistory: (processes, nativeId) => processes.listCodexHistorySessions().find((session) => session.claudeSessionId === nativeId),
27
+ },
28
+ },
29
+ {
30
+ path: "/api/opencode-sessions/:nativeId/resume",
31
+ param: "nativeId",
32
+ spec: {
33
+ label: "OpenCode",
34
+ provider: "opencode",
35
+ runner: "opencode-cli-run",
36
+ nativeIdKey: "claudeSessionId",
37
+ idFormat: "safe",
38
+ findHistory: (processes, nativeId) => processes.listOpenCodeHistorySessions().find((session) => session.claudeSessionId === nativeId),
39
+ },
40
+ },
41
+ {
42
+ path: "/api/qoder-sessions/:nativeId/resume",
43
+ param: "nativeId",
44
+ spec: {
45
+ label: "Qoder",
46
+ provider: "qoder",
47
+ runner: "qoder-cli-print",
48
+ nativeIdKey: "claudeSessionId",
49
+ idFormat: "safe",
50
+ findHistory: (processes, nativeId) => processes.listQoderHistorySessions().find((session) => session.claudeSessionId === nativeId),
51
+ },
52
+ },
53
+ {
54
+ path: "/api/grok-sessions/:nativeId/resume",
55
+ param: "nativeId",
56
+ spec: {
57
+ label: "Grok",
58
+ provider: "grok",
59
+ runner: "grok-cli-headless",
60
+ nativeIdKey: "claudeSessionId",
61
+ idFormat: "safe",
62
+ },
63
+ },
64
+ {
65
+ path: "/api/pi-sessions/:nativeId/resume",
66
+ param: "nativeId",
67
+ spec: {
68
+ label: "Pi",
69
+ provider: "pi",
70
+ runner: "pi-cli-json",
71
+ nativeIdKey: "claudeSessionId",
72
+ idFormat: "safe",
73
+ },
74
+ },
75
+ ];
76
+ /** 参数是合法 UUID 时用 `isProviderSessionId`,否则用宽松校验;返回错误文案或 null。 */
77
+ function validateNativeId(nativeId, spec) {
78
+ if (spec.idFormat === "uuid") {
79
+ return isProviderSessionId(nativeId) ? null : `${spec.label} 会话 ID 必须是有效的 UUID。`;
80
+ }
81
+ return isSafeProviderSessionId(nativeId) ? null : `${spec.label} 会话 ID 格式无效。`;
82
+ }
83
+ export function registerStructuredResumeRoutes(app, deps) {
84
+ const { processes, structured, storage, defaultMode, onSessionCreated, toDetailDTO } = deps;
85
+ for (const { path, param, spec } of STRUCTURED_RESUME_SPECS) {
86
+ app.post(path, asyncRoute(async (req, res) => {
87
+ const nativeId = String(req.params[param] || "").trim();
88
+ const body = (req.body ?? {});
89
+ try {
90
+ const invalid = validateNativeId(nativeId, spec);
91
+ if (invalid) {
92
+ res.status(400).json({ error: invalid });
93
+ return;
94
+ }
95
+ const history = spec.findHistory?.(processes, nativeId);
96
+ if (spec.findHistory && !history) {
97
+ res.status(400).json({ error: `对应的 ${spec.label} 历史会话不存在,无法恢复。` });
98
+ return;
99
+ }
100
+ // 自持 ID 的 provider(Grok / Pi)无法反查历史,必须由客户端给 cwd。
101
+ const cwd = body.cwd?.trim() || history?.cwd;
102
+ if (!cwd) {
103
+ res.status(400).json({ error: "无法确定工作目录 (cwd),无法恢复。" });
104
+ return;
105
+ }
106
+ const snapshot = structured.createSession({
107
+ cwd,
108
+ mode: parseExecutionMode(body.mode, defaultMode),
109
+ provider: spec.provider,
110
+ runner: spec.runner,
111
+ worktreeEnabled: body.worktreeEnabled === true,
112
+ [spec.nativeIdKey]: nativeId,
113
+ workspaceId: resolveWorkspaceIdForNewSession(storage, cwd),
114
+ ...parseSessionCreationOrigin(body),
115
+ });
116
+ onSessionCreated?.(cwd);
117
+ res.status(201).json({ resumedClaudeSessionId: nativeId, ...toDetailDTO(snapshot) });
118
+ }
119
+ catch (error) {
120
+ res.status(400).json({ error: getErrorMessage(error, `无法按 ${spec.label} 会话 ID 恢复会话。`) });
121
+ }
122
+ }));
123
+ }
124
+ }
@@ -8,12 +8,14 @@ import { resolveSessionCwd } from "./session-cwd.js";
8
8
  import { resolveCommitAiContext } from "./session-ai-context.js";
9
9
  import { getGitStatusAsync, QuickCommitError, runQuickCommitWithFallback, runTagHead, runPush, generateCommitMessageOnly, } from "./git-quick-commit.js";
10
10
  import { getErrorMessage } from "./error-utils.js";
11
- import { buildProviderResumeCommand, isProviderSessionId, isSafeProviderSessionId } from "./resume-policy.js";
11
+ import { buildProviderResumeCommand, isProviderSessionId } from "./resume-policy.js";
12
12
  import { parseBoundedInteger } from "./request-limits.js";
13
13
  import { asyncRoute } from "./express-async.js";
14
+ import { sendRouteError } from "./server-request.js";
15
+ import { registerStructuredResumeRoutes } from "./server-resume-routes.js";
14
16
  import { enrichStructuredMessages, WAND_PROTOCOL_VERSION } from "./structured-client-protocol.js";
15
17
  import { buildDirectoryTree, normalizeSessionDirectory, } from "./session-directory-tree.js";
16
- import { projectCwdForSession, resolveWorkspaceIdForNewSession, } from "./workspace-binding.js";
18
+ import { projectCwdForSession, renameWorkspaceDirectory, resolveWorkspaceIdForNewSession, } from "./workspace-binding.js";
17
19
  export function parseExecutionMode(value, fallback) {
18
20
  if (value === undefined)
19
21
  return fallback;
@@ -426,7 +428,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
426
428
  res.json(page);
427
429
  }
428
430
  catch (error) {
429
- res.status(500).json({ error: getErrorMessage(error, "无法加载会话列表。") });
431
+ sendRouteError(res, error, "无法加载会话列表。", 500);
430
432
  }
431
433
  });
432
434
  app.get("/api/session-directories", (_req, res) => {
@@ -434,7 +436,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
434
436
  res.json(currentDirectoryTree());
435
437
  }
436
438
  catch (error) {
437
- res.status(500).json({ error: getErrorMessage(error, "无法加载会话目录。") });
439
+ sendRouteError(res, error, "无法加载会话目录。", 500);
438
440
  }
439
441
  });
440
442
  app.put("/api/session-directories/name", (req, res) => {
@@ -468,10 +470,12 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
468
470
  return;
469
471
  }
470
472
  storage.setSessionDirectoryName(directoryPath, customName || null);
473
+ // 同一步把项目名改成同一个名字,避免「工作区」改名后只有会话目录视图生效。
474
+ renameWorkspaceDirectory(storage, directoryPath, customName);
471
475
  res.json({ ok: true, path: directoryPath, name: customName || null });
472
476
  }
473
477
  catch (error) {
474
- res.status(500).json({ error: getErrorMessage(error, "无法保存工作区名称。") });
478
+ sendRouteError(res, error, "无法保存工作区名称。", 500);
475
479
  }
476
480
  });
477
481
  app.get("/api/sessions", (_req, res) => {
@@ -514,7 +518,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
514
518
  res.status(201).json(sessionResponseDTO(snapshot));
515
519
  }
516
520
  catch (error) {
517
- res.status(400).json({ error: getErrorMessage(error, "无法启动结构化会话。") });
521
+ sendRouteError(res, error, "无法启动结构化会话。");
518
522
  }
519
523
  }));
520
524
  app.post("/api/sessions/:id/model", (req, res) => {
@@ -530,7 +534,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
530
534
  res.json(updated);
531
535
  }
532
536
  catch (error) {
533
- res.status(400).json({ error: getErrorMessage(error, "切换模型失败。") });
537
+ sendRouteError(res, error, "切换模型失败。");
534
538
  }
535
539
  });
536
540
  // 思考深度切换:与 /model 路由对称。结构化会话立即影响下一条 prompt(CLI/SDK 各自接入
@@ -548,7 +552,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
548
552
  res.json(updated);
549
553
  }
550
554
  catch (error) {
551
- res.status(400).json({ error: getErrorMessage(error, "切换思考深度失败。") });
555
+ sendRouteError(res, error, "切换思考深度失败。");
552
556
  }
553
557
  });
554
558
  // 执行模式切换:与 /model、/thinking-effort 路由对称。结构化会话立即影响下一条
@@ -577,7 +581,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
577
581
  res.json(sessions.setSessionMode(id, effective));
578
582
  }
579
583
  catch (error) {
580
- res.status(400).json({ error: getErrorMessage(error, "切换模式失败。") });
584
+ sendRouteError(res, error, "切换模式失败。");
581
585
  }
582
586
  });
583
587
  app.get("/api/structured-sessions/:id/messages", (req, res) => {
@@ -638,7 +642,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
638
642
  res.json(sessionResponseDTO(snapshot));
639
643
  }
640
644
  catch (error) {
641
- res.status(400).json({ error: getErrorMessage(error, "无法调整排队顺序。") });
645
+ sendRouteError(res, error, "无法调整排队顺序。");
642
646
  }
643
647
  });
644
648
  app.delete("/api/structured-sessions/:id/queued/:index", (req, res) => {
@@ -653,7 +657,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
653
657
  res.json(sessionResponseDTO(snapshot));
654
658
  }
655
659
  catch (error) {
656
- res.status(400).json({ error: getErrorMessage(error, "无法删除排队消息。") });
660
+ sendRouteError(res, error, "无法删除排队消息。");
657
661
  }
658
662
  });
659
663
  app.post("/api/structured-sessions/:id/queued/:index/promote", asyncRoute(async (req, res) => {
@@ -683,7 +687,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
683
687
  res.json(sessionResponseDTO(snapshot));
684
688
  }
685
689
  catch (error) {
686
- res.status(400).json({ error: getErrorMessage(error, "无法清空排队消息。") });
690
+ sendRouteError(res, error, "无法清空排队消息。");
687
691
  }
688
692
  });
689
693
  // ── Tool content lazy-load endpoint ──
@@ -830,7 +834,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
830
834
  res.status(status).json({ error: error.message, errorCode: error.code });
831
835
  return;
832
836
  }
833
- res.status(400).json({ error: getErrorMessage(error, "快捷提交失败。") });
837
+ sendRouteError(res, error, "快捷提交失败。");
834
838
  }
835
839
  }));
836
840
  app.post("/api/sessions/:id/generate-commit-message", asyncRoute(async (req, res) => {
@@ -855,7 +859,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
855
859
  res.status(400).json({ error: error.message, errorCode: error.code });
856
860
  return;
857
861
  }
858
- res.status(400).json({ error: getErrorMessage(error, "生成 commit message 失败。") });
862
+ sendRouteError(res, error, "生成 commit message 失败。");
859
863
  }
860
864
  }));
861
865
  app.post("/api/sessions/:id/git/tag-head", asyncRoute(async (req, res) => {
@@ -887,7 +891,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
887
891
  res.status(status).json({ error: error.message, errorCode: error.code });
888
892
  return;
889
893
  }
890
- res.status(400).json({ error: getErrorMessage(error, "打 tag 失败。") });
894
+ sendRouteError(res, error, "打 tag 失败。");
891
895
  }
892
896
  }));
893
897
  app.post("/api/sessions/:id/git/push", asyncRoute(async (req, res) => {
@@ -916,7 +920,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
916
920
  res.status(400).json({ error: error.message, errorCode: error.code });
917
921
  return;
918
922
  }
919
- res.status(400).json({ error: getErrorMessage(error, "推送失败。") });
923
+ sendRouteError(res, error, "推送失败。");
920
924
  }
921
925
  }));
922
926
  app.post("/api/sessions/:id/worktree/cleanup", asyncRoute(async (req, res) => {
@@ -1061,7 +1065,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1061
1065
  res.status(201).json(sessionResponseDTO(newSnapshot));
1062
1066
  }
1063
1067
  catch (error) {
1064
- res.status(400).json({ error: getErrorMessage(error, "无法恢复会话。") });
1068
+ sendRouteError(res, error, "无法恢复会话。");
1065
1069
  }
1066
1070
  }));
1067
1071
  app.post("/api/claude-sessions/:claudeSessionId/resume", asyncRoute(async (req, res) => {
@@ -1131,7 +1135,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1131
1135
  }
1132
1136
  }
1133
1137
  catch (error) {
1134
- res.status(400).json({ error: getErrorMessage(error, "无法按 Claude 会话 ID 恢复会话。") });
1138
+ sendRouteError(res, error, "无法按 Claude 会话 ID 恢复会话。");
1135
1139
  }
1136
1140
  }));
1137
1141
  app.post("/api/sessions/:id/input", asyncRoute(async (req, res) => {
@@ -1194,162 +1198,13 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1194
1198
  res.status(response.statusCode).json(response.payload);
1195
1199
  }
1196
1200
  }));
1197
- app.post("/api/codex-sessions/:threadId/resume", asyncRoute(async (req, res) => {
1198
- const threadId = String(req.params.threadId || "").trim();
1199
- const body = req.body;
1200
- try {
1201
- if (!isProviderSessionId(threadId)) {
1202
- res.status(400).json({ error: "Codex 会话 ID 必须是有效的 UUID。" });
1203
- return;
1204
- }
1205
- const history = processes.listCodexHistorySessions().find((s) => s.claudeSessionId === threadId);
1206
- if (!history) {
1207
- res.status(400).json({ error: "对应的 Codex 历史会话不存在,无法恢复。" });
1208
- return;
1209
- }
1210
- const cwd = body.cwd?.trim() || history.cwd;
1211
- if (!cwd) {
1212
- res.status(400).json({ error: "无法确定工作目录 (cwd),无法恢复。" });
1213
- return;
1214
- }
1215
- const newMode = parseExecutionMode(body.mode, defaultMode);
1216
- const origin = parseSessionCreationOrigin(body);
1217
- const snapshot = structured.createSession({
1218
- cwd,
1219
- mode: newMode,
1220
- provider: "codex",
1221
- runner: "codex-cli-exec",
1222
- worktreeEnabled: body.worktreeEnabled === true,
1223
- claudeSessionId: threadId,
1224
- workspaceId: resolveWorkspaceIdForNewSession(storage, cwd),
1225
- ...origin,
1226
- });
1227
- onSessionCreated?.(cwd);
1228
- res.status(201).json({ resumedClaudeSessionId: threadId, ...sessionResponseDTO(snapshot) });
1229
- }
1230
- catch (error) {
1231
- res.status(400).json({ error: getErrorMessage(error, "无法按 Codex 会话 ID 恢复会话。") });
1232
- }
1233
- }));
1234
- /**
1235
- * OpenCode and Qoder own their native transcript storage. Restoring an
1236
- * external transcript creates a Wand structured shell carrying the native
1237
- * ID; each provider runner adds its own resume flag when the next input is
1238
- * sent, preserving the original context without importing the full history.
1239
- */
1240
- app.post("/api/opencode-sessions/:sessionId/resume", (req, res) => {
1241
- const sessionId = String(req.params.sessionId || "").trim();
1242
- const body = req.body;
1243
- try {
1244
- if (!isSafeProviderSessionId(sessionId)) {
1245
- res.status(400).json({ error: "OpenCode 会话 ID 格式无效。" });
1246
- return;
1247
- }
1248
- const history = processes.listOpenCodeHistorySessions().find((session) => session.claudeSessionId === sessionId);
1249
- if (!history) {
1250
- res.status(400).json({ error: "对应的 OpenCode 历史会话不存在,无法恢复。" });
1251
- return;
1252
- }
1253
- const cwd = body.cwd?.trim() || history.cwd;
1254
- if (!cwd) {
1255
- res.status(400).json({ error: "无法确定工作目录 (cwd),无法恢复。" });
1256
- return;
1257
- }
1258
- const snapshot = structured.createSession({
1259
- cwd,
1260
- mode: parseExecutionMode(body.mode, defaultMode),
1261
- provider: "opencode",
1262
- runner: "opencode-cli-run",
1263
- worktreeEnabled: body.worktreeEnabled === true,
1264
- claudeSessionId: sessionId,
1265
- workspaceId: resolveWorkspaceIdForNewSession(storage, cwd),
1266
- ...parseSessionCreationOrigin(body),
1267
- });
1268
- onSessionCreated?.(cwd);
1269
- res.status(201).json({ resumedClaudeSessionId: sessionId, ...sessionResponseDTO(snapshot) });
1270
- }
1271
- catch (error) {
1272
- res.status(400).json({ error: getErrorMessage(error, "无法按 OpenCode 会话 ID 恢复会话。") });
1273
- }
1274
- });
1275
- app.post("/api/qoder-sessions/:sessionId/resume", (req, res) => {
1276
- const sessionId = String(req.params.sessionId || "").trim();
1277
- const body = req.body;
1278
- try {
1279
- if (!isSafeProviderSessionId(sessionId)) {
1280
- res.status(400).json({ error: "Qoder 会话 ID 格式无效。" });
1281
- return;
1282
- }
1283
- const history = processes.listQoderHistorySessions().find((session) => session.claudeSessionId === sessionId);
1284
- if (!history) {
1285
- res.status(400).json({ error: "对应的 Qoder 历史会话不存在,无法恢复。" });
1286
- return;
1287
- }
1288
- const cwd = body.cwd?.trim() || history.cwd;
1289
- if (!cwd) {
1290
- res.status(400).json({ error: "无法确定工作目录 (cwd),无法恢复。" });
1291
- return;
1292
- }
1293
- const snapshot = structured.createSession({
1294
- cwd,
1295
- mode: parseExecutionMode(body.mode, defaultMode),
1296
- provider: "qoder",
1297
- runner: "qoder-cli-print",
1298
- worktreeEnabled: body.worktreeEnabled === true,
1299
- claudeSessionId: sessionId,
1300
- workspaceId: resolveWorkspaceIdForNewSession(storage, cwd),
1301
- ...parseSessionCreationOrigin(body),
1302
- });
1303
- onSessionCreated?.(cwd);
1304
- res.status(201).json({ resumedClaudeSessionId: sessionId, ...sessionResponseDTO(snapshot) });
1305
- }
1306
- catch (error) {
1307
- res.status(400).json({ error: getErrorMessage(error, "无法按 Qoder 会话 ID 恢复会话。") });
1308
- }
1309
- });
1310
- const resumeNamedStructuredSession = (req, res, spec) => {
1311
- const sessionId = String(req.params.sessionId || "").trim();
1312
- const body = req.body;
1313
- try {
1314
- if (!isSafeProviderSessionId(sessionId)) {
1315
- res.status(400).json({ error: `${spec.label} 会话 ID 格式无效。` });
1316
- return;
1317
- }
1318
- const cwd = body.cwd?.trim();
1319
- if (!cwd) {
1320
- res.status(400).json({ error: "无法确定工作目录 (cwd),无法恢复。" });
1321
- return;
1322
- }
1323
- const snapshot = structured.createSession({
1324
- cwd,
1325
- mode: parseExecutionMode(body.mode, defaultMode),
1326
- provider: spec.provider,
1327
- runner: spec.runner,
1328
- worktreeEnabled: body.worktreeEnabled === true,
1329
- claudeSessionId: sessionId,
1330
- workspaceId: resolveWorkspaceIdForNewSession(storage, cwd),
1331
- ...parseSessionCreationOrigin(body),
1332
- });
1333
- onSessionCreated?.(cwd);
1334
- res.status(201).json({ resumedClaudeSessionId: sessionId, ...sessionResponseDTO(snapshot) });
1335
- }
1336
- catch (error) {
1337
- res.status(400).json({ error: getErrorMessage(error, `无法按 ${spec.label} 会话 ID 恢复会话。`) });
1338
- }
1339
- };
1340
- app.post("/api/grok-sessions/:sessionId/resume", (req, res) => {
1341
- resumeNamedStructuredSession(req, res, {
1342
- provider: "grok",
1343
- runner: "grok-cli-headless",
1344
- label: "Grok",
1345
- });
1346
- });
1347
- app.post("/api/pi-sessions/:sessionId/resume", (req, res) => {
1348
- resumeNamedStructuredSession(req, res, {
1349
- provider: "pi",
1350
- runner: "pi-cli-json",
1351
- label: "Pi",
1352
- });
1201
+ registerStructuredResumeRoutes(app, {
1202
+ processes,
1203
+ structured,
1204
+ storage,
1205
+ defaultMode,
1206
+ onSessionCreated,
1207
+ toDetailDTO: sessionResponseDTO,
1353
1208
  });
1354
1209
  app.post("/api/sessions/:id/resize", (req, res) => {
1355
1210
  const body = req.body;
@@ -1362,7 +1217,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1362
1217
  res.json(sessionResponseDTO(snapshot));
1363
1218
  }
1364
1219
  catch (error) {
1365
- res.status(400).json({ error: getErrorMessage(error, "无法调整终端大小。") });
1220
+ sendRouteError(res, error, "无法调整终端大小。");
1366
1221
  }
1367
1222
  });
1368
1223
  app.post("/api/sessions/:id/approve-permission", (req, res) => {
@@ -1379,7 +1234,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1379
1234
  res.json(sessionResponseDTO(processes.approvePermission(req.params.id)));
1380
1235
  }
1381
1236
  catch (error) {
1382
- res.status(400).json({ error: getErrorMessage(error, "无法批准该授权请求。") });
1237
+ sendRouteError(res, error, "无法批准该授权请求。");
1383
1238
  }
1384
1239
  });
1385
1240
  app.post("/api/sessions/:id/deny-permission", (req, res) => {
@@ -1396,7 +1251,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1396
1251
  res.json(sessionResponseDTO(processes.denyPermission(req.params.id)));
1397
1252
  }
1398
1253
  catch (error) {
1399
- res.status(400).json({ error: getErrorMessage(error, "无法拒绝该授权请求。") });
1254
+ sendRouteError(res, error, "无法拒绝该授权请求。");
1400
1255
  }
1401
1256
  });
1402
1257
  app.post("/api/sessions/:id/toggle-auto-approve", (req, res) => {
@@ -1413,7 +1268,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1413
1268
  res.json(sessionResponseDTO(processes.toggleAutoApprove(req.params.id)));
1414
1269
  }
1415
1270
  catch (error) {
1416
- res.status(400).json({ error: getErrorMessage(error, "无法切换自动批准状态。") });
1271
+ sendRouteError(res, error, "无法切换自动批准状态。");
1417
1272
  }
1418
1273
  });
1419
1274
  app.post("/api/sessions/:id/escalations/:requestId/resolve", (req, res) => {
@@ -1432,7 +1287,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1432
1287
  res.json(sessionResponseDTO(processes.resolveEscalation(req.params.id, requestId, resolution)));
1433
1288
  }
1434
1289
  catch (error) {
1435
- res.status(400).json({ error: getErrorMessage(error, "无法处理该授权请求。") });
1290
+ sendRouteError(res, error, "无法处理该授权请求。");
1436
1291
  }
1437
1292
  });
1438
1293
  app.post("/api/sessions/:id/stop", (req, res) => {
@@ -1444,7 +1299,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1444
1299
  res.json(sessionResponseDTO(processes.stop(req.params.id)));
1445
1300
  }
1446
1301
  catch (error) {
1447
- res.status(400).json({ error: getErrorMessage(error, "无法停止会话。") });
1302
+ sendRouteError(res, error, "无法停止会话。");
1448
1303
  }
1449
1304
  });
1450
1305
  app.delete("/api/sessions/:id", (req, res) => {
@@ -1453,7 +1308,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1453
1308
  res.json({ ok: true });
1454
1309
  }
1455
1310
  catch (error) {
1456
- res.status(400).json({ error: getErrorMessage(error, "无法删除会话。") });
1311
+ sendRouteError(res, error, "无法删除会话。");
1457
1312
  }
1458
1313
  });
1459
1314
  }
@@ -1547,7 +1402,7 @@ export function registerClaudeHistoryRoutes(app, processes, storage) {
1547
1402
  res.json({ ok: true, deleted });
1548
1403
  }
1549
1404
  catch (error) {
1550
- res.status(500).json({ error: getErrorMessage(error, "无法删除该目录下的历史会话。") });
1405
+ sendRouteError(res, error, "无法删除该目录下的历史会话。", 500);
1551
1406
  }
1552
1407
  });
1553
1408
  registerHistoryMutations("codex", "threadId", "Codex");
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import { buildChildEnv } from "./env-utils.js";
4
4
  import { getErrorMessage } from "./error-utils.js";
5
5
  import { asyncRoute } from "./express-async.js";
6
+ import { sendRouteError } from "./server-request.js";
6
7
  import { getProviderDefaultModels, PREFERENCE_KEYS, saveConfig, validateCommitAiConfig, writePreferenceToStorage, } from "./config.js";
7
8
  import { DEPLOYMENT_CONFIG_KEYS } from "./runtime-config.js";
8
9
  import { callSystemAiText, discoverCliSystemAiConfigs, mergeSystemAiConfigs, normalizeSystemAiConfig, } from "./system-ai.js";
@@ -203,7 +204,7 @@ export function registerSettingsRoutes(app, deps) {
203
204
  }
204
205
  }
205
206
  catch (error) {
206
- res.status(400).json({ error: getErrorMessage(error, "测试线路配置无效。") });
207
+ sendRouteError(res, error, "测试线路配置无效。");
207
208
  return;
208
209
  }
209
210
  const startedAt = Date.now();
@@ -352,7 +353,7 @@ export function registerSettingsRoutes(app, deps) {
352
353
  validateCommitAiConfig(candidateConfig);
353
354
  }
354
355
  catch (error) {
355
- res.status(400).json({ error: getErrorMessage(error, "配置校验失败。") });
356
+ sendRouteError(res, error, "配置校验失败。");
356
357
  return;
357
358
  }
358
359
  if (!touchedDeployField && stagedPreferences.length === 0) {
@@ -387,7 +388,7 @@ export function registerSettingsRoutes(app, deps) {
387
388
  }
388
389
  catch { /* preserve original */ }
389
390
  }
390
- res.status(500).json({ error: getErrorMessage(error, "保存配置失败。") });
391
+ sendRouteError(res, error, "保存配置失败。", 500);
391
392
  }
392
393
  }));
393
394
  app.get("/api/models", (_req, res) => {
@@ -423,7 +424,7 @@ export function registerSettingsRoutes(app, deps) {
423
424
  });
424
425
  }
425
426
  catch (error) {
426
- res.status(500).json({ error: getErrorMessage(error, "刷新模型列表失败。") });
427
+ sendRouteError(res, error, "刷新模型列表失败。", 500);
427
428
  }
428
429
  }));
429
430
  app.post("/api/settings/upload-cert", requireAdmin, asyncRoute(async (req, res) => {
@@ -442,7 +443,7 @@ export function registerSettingsRoutes(app, deps) {
442
443
  res.json({ ok: true, restartRequired: true });
443
444
  }
444
445
  catch (error) {
445
- res.status(500).json({ error: getErrorMessage(error, "保存证书失败。") });
446
+ sendRouteError(res, error, "保存证书失败。", 500);
446
447
  }
447
448
  }));
448
449
  }