@co0ontty/wand 4.60.0 → 4.61.0-beta.gb3ecfc4
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/claude-pty-bridge.js +4 -0
- package/dist/mission-types.d.ts +4 -0
- package/dist/missions.js +4 -0
- package/dist/server-app-connect.d.ts +40 -0
- package/dist/server-app-connect.js +241 -0
- package/dist/server-file-routes.js +11 -10
- package/dist/server-github-routes.js +12 -30
- package/dist/server-mission-routes.js +4 -2
- package/dist/server-request.d.ts +34 -0
- package/dist/server-request.js +61 -0
- package/dist/server-resume-routes.d.ts +25 -0
- package/dist/server-resume-routes.js +124 -0
- package/dist/server-session-routes.js +37 -182
- package/dist/server-settings-routes.js +6 -5
- package/dist/server-task-routes.js +93 -15
- package/dist/server-update-routes.js +23 -16
- package/dist/server-vault-routes.d.ts +16 -0
- package/dist/server-vault-routes.js +125 -0
- package/dist/server-workspace-routes.js +44 -8
- package/dist/server.js +12 -367
- package/dist/storage.d.ts +20 -1
- package/dist/storage.js +112 -16
- package/dist/structured-session-manager.js +33 -44
- package/dist/task-types.d.ts +19 -1
- package/dist/task-types.js +6 -1
- package/dist/types.d.ts +6 -0
- package/dist/wand-task-sync.d.ts +5 -3
- package/dist/wand-task-sync.js +30 -12
- package/dist/web-ui/content/scripts.js +76 -73
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/embedded-assets.js +2 -2
- package/dist/workspace-binding.d.ts +8 -1
- package/dist/workspace-binding.js +38 -0
- package/package.json +1 -1
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
446
|
+
sendRouteError(res, error, "保存证书失败。", 500);
|
|
446
447
|
}
|
|
447
448
|
}));
|
|
448
449
|
}
|