@co0ontty/wand 4.43.0 → 4.45.0-beta.g56ec050
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/cli.js +3 -0
- package/dist/config.js +24 -0
- package/dist/distribution-manager.d.ts +3 -0
- package/dist/distribution-manager.js +46 -3
- package/dist/missions.d.ts +3 -1
- package/dist/missions.js +18 -0
- package/dist/password-manager.d.ts +4 -0
- package/dist/password-manager.js +37 -0
- package/dist/server-file-routes.js +12 -6
- package/dist/server-mission-routes.js +4 -2
- package/dist/server-session-routes.js +73 -13
- package/dist/server-settings-routes.d.ts +1 -0
- package/dist/server-settings-routes.js +3 -1
- package/dist/server-update-routes.d.ts +3 -0
- package/dist/server-update-routes.js +18 -1
- package/dist/server.js +37 -9
- package/dist/session-transport.d.ts +2 -0
- package/dist/session-transport.js +5 -0
- package/dist/storage.d.ts +2 -0
- package/dist/storage.js +17 -5
- package/dist/structured-session-manager.js +3 -1
- package/dist/types.d.ts +7 -1
- package/dist/web-ui/content/scripts.js +70 -70
- 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 +3 -1
- package/dist/ws-broadcast.js +5 -2
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"commit": "
|
|
3
|
-
"builtAt": "2026-08-
|
|
4
|
-
"version": "4.
|
|
5
|
-
"channel": "
|
|
2
|
+
"commit": "56ec050abeeaae4b9fdebe6234682e02fd73b5e5",
|
|
3
|
+
"builtAt": "2026-08-22T05:18:43.717Z",
|
|
4
|
+
"version": "4.45.0-beta.g56ec050",
|
|
5
|
+
"channel": "beta"
|
|
6
6
|
}
|
package/dist/cli.js
CHANGED
|
@@ -177,6 +177,7 @@ async function main() {
|
|
|
177
177
|
case "session:read":
|
|
178
178
|
case "session:send":
|
|
179
179
|
case "session:wait":
|
|
180
|
+
case "inbox:list":
|
|
180
181
|
case "mission:list":
|
|
181
182
|
case "mission:create":
|
|
182
183
|
case "mission:diff":
|
|
@@ -224,6 +225,7 @@ Commands:
|
|
|
224
225
|
|
|
225
226
|
Agent runtime:
|
|
226
227
|
wand session:list List sessions as JSON
|
|
228
|
+
wand inbox:list List mission inbox items as JSON
|
|
227
229
|
wand session:read <id> Read a complete session snapshot
|
|
228
230
|
wand session:send <id> <text>
|
|
229
231
|
wand session:wait <id> [--timeout 900]
|
|
@@ -260,6 +262,7 @@ async function runAgentCliCommand(command, args, configPath) {
|
|
|
260
262
|
return value;
|
|
261
263
|
};
|
|
262
264
|
switch (command) {
|
|
265
|
+
case "inbox:list": return output(await api.get("/api/inbox"));
|
|
263
266
|
case "session:list": return output(await api.get("/api/sessions"));
|
|
264
267
|
case "session:read": {
|
|
265
268
|
const id = required(args[1], "wand session:read <id>");
|
package/dist/config.js
CHANGED
|
@@ -70,6 +70,7 @@ export const defaultConfig = () => ({
|
|
|
70
70
|
language: "",
|
|
71
71
|
android: defaultAndroidApkConfig(),
|
|
72
72
|
macos: defaultMacosDmgConfig(),
|
|
73
|
+
ios: defaultIosIpaConfig(),
|
|
73
74
|
cardDefaults: defaultCardExpandDefaults(),
|
|
74
75
|
defaultModel: "",
|
|
75
76
|
defaultCodexModel: "",
|
|
@@ -594,6 +595,28 @@ function normalizeMacosDmgConfig(input) {
|
|
|
594
595
|
: defaults.currentDmgFile,
|
|
595
596
|
};
|
|
596
597
|
}
|
|
598
|
+
function defaultIosIpaConfig() {
|
|
599
|
+
return {
|
|
600
|
+
enabled: false,
|
|
601
|
+
ipaDir: "ios",
|
|
602
|
+
currentIpaFile: "",
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
function normalizeIosIpaConfig(input) {
|
|
606
|
+
if (!input || typeof input !== "object")
|
|
607
|
+
return undefined;
|
|
608
|
+
const defaults = defaultIosIpaConfig();
|
|
609
|
+
const iosInput = input;
|
|
610
|
+
return {
|
|
611
|
+
enabled: typeof iosInput.enabled === "boolean" ? iosInput.enabled : defaults.enabled,
|
|
612
|
+
ipaDir: typeof iosInput.ipaDir === "string" && iosInput.ipaDir.trim()
|
|
613
|
+
? iosInput.ipaDir.trim()
|
|
614
|
+
: defaults.ipaDir,
|
|
615
|
+
currentIpaFile: typeof iosInput.currentIpaFile === "string"
|
|
616
|
+
? iosInput.currentIpaFile.trim()
|
|
617
|
+
: defaults.currentIpaFile,
|
|
618
|
+
};
|
|
619
|
+
}
|
|
597
620
|
function normalizeStructuredChatPersona(input) {
|
|
598
621
|
if (!input || typeof input !== "object")
|
|
599
622
|
return undefined;
|
|
@@ -664,6 +687,7 @@ function mergeWithDefaults(input) {
|
|
|
664
687
|
: crypto.randomBytes(32).toString("hex"),
|
|
665
688
|
android: normalizeAndroidApkConfig(input.android) ?? defaults.android,
|
|
666
689
|
macos: normalizeMacosDmgConfig(input.macos) ?? defaults.macos,
|
|
690
|
+
ios: normalizeIosIpaConfig(input.ios) ?? defaults.ios,
|
|
667
691
|
cardDefaults: normalizeCardDefaults(input.cardDefaults),
|
|
668
692
|
defaultProvider: input.defaultProvider === "codex" || input.defaultProvider === "opencode" || input.defaultProvider === "grok" || input.defaultProvider === "qoder" || input.defaultProvider === "pi" ? input.defaultProvider : "claude",
|
|
669
693
|
defaultSessionKind: input.defaultSessionKind === "pty" ? "pty" : "structured",
|
|
@@ -22,6 +22,7 @@ export interface ResolvedDistributionAsset {
|
|
|
22
22
|
export interface DistributionSettings {
|
|
23
23
|
androidApk: Record<string, unknown>;
|
|
24
24
|
macosDmg: Record<string, unknown>;
|
|
25
|
+
iosIpa: Record<string, unknown>;
|
|
25
26
|
}
|
|
26
27
|
export interface DistributionManagerOptions {
|
|
27
28
|
configDir: string;
|
|
@@ -46,6 +47,8 @@ export declare class DistributionManager {
|
|
|
46
47
|
resolveAndroidDownload(channel?: ApkUpdateChannel): Promise<LocalDistributionAsset | null>;
|
|
47
48
|
resolveLatestDmg(): Promise<ResolvedDistributionAsset | null>;
|
|
48
49
|
resolveMacosDownload(): Promise<LocalDistributionAsset | null>;
|
|
50
|
+
resolveLatestIpa(): Promise<ResolvedDistributionAsset | null>;
|
|
51
|
+
resolveIosDownload(): Promise<LocalDistributionAsset | null>;
|
|
49
52
|
getSettings(): Promise<DistributionSettings>;
|
|
50
53
|
private readonly sha256Cache;
|
|
51
54
|
/**
|
|
@@ -114,18 +114,49 @@ export class DistributionManager {
|
|
|
114
114
|
compareVersions: compareSemver,
|
|
115
115
|
});
|
|
116
116
|
}
|
|
117
|
+
async resolveLatestIpa() {
|
|
118
|
+
const localIpa = await this.resolveIosDownload();
|
|
119
|
+
if (localIpa?.version) {
|
|
120
|
+
return {
|
|
121
|
+
version: localIpa.version,
|
|
122
|
+
downloadUrl: localIpa.downloadUrl,
|
|
123
|
+
fileName: localIpa.fileName,
|
|
124
|
+
size: localIpa.size,
|
|
125
|
+
source: "local",
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const github = await this.fetchGitHubAsset(".ipa");
|
|
129
|
+
return github ? { ...github, source: "github" } : null;
|
|
130
|
+
}
|
|
131
|
+
async resolveIosDownload() {
|
|
132
|
+
await this.refreshConfig();
|
|
133
|
+
const { config, configDir } = this.options;
|
|
134
|
+
if (config.ios?.enabled !== true)
|
|
135
|
+
return null;
|
|
136
|
+
return this.resolveLocalAsset({
|
|
137
|
+
directory: resolveConfiguredDir(configDir, config.ios.ipaDir, "ios"),
|
|
138
|
+
extension: ".ipa",
|
|
139
|
+
configuredFile: config.ios.currentIpaFile,
|
|
140
|
+
downloadUrl: "/ios/download",
|
|
141
|
+
compareVersions: compareSemver,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
117
144
|
async getSettings() {
|
|
118
|
-
const [localApk, githubApk, localDmg, githubDmg] = await Promise.all([
|
|
145
|
+
const [localApk, githubApk, localDmg, githubDmg, localIpa, githubIpa] = await Promise.all([
|
|
119
146
|
this.resolveAndroidDownload("beta"),
|
|
120
147
|
this.fetchGitHubAsset(".apk"),
|
|
121
148
|
this.resolveMacosDownload(),
|
|
122
149
|
this.fetchGitHubAsset(".dmg"),
|
|
150
|
+
this.resolveIosDownload(),
|
|
151
|
+
this.fetchGitHubAsset(".ipa"),
|
|
123
152
|
]);
|
|
124
153
|
const apkDir = resolveConfiguredDir(this.options.configDir, this.options.config.android?.apkDir, "android");
|
|
125
154
|
const dmgDir = resolveConfiguredDir(this.options.configDir, this.options.config.macos?.dmgDir, "macos");
|
|
155
|
+
const ipaDir = resolveConfiguredDir(this.options.configDir, this.options.config.ios?.ipaDir, "ios");
|
|
126
156
|
return {
|
|
127
157
|
androidApk: this.buildSettings("apk", apkDir, this.options.config.android?.enabled === true, localApk, githubApk),
|
|
128
158
|
macosDmg: this.buildSettings("dmg", dmgDir, this.options.config.macos?.enabled === true, localDmg, githubDmg),
|
|
159
|
+
iosIpa: this.buildSettings("ipa", ipaDir, this.options.config.ios?.enabled === true, localIpa, githubIpa),
|
|
129
160
|
};
|
|
130
161
|
}
|
|
131
162
|
sha256Cache = new Map();
|
|
@@ -181,6 +212,18 @@ export class DistributionManager {
|
|
|
181
212
|
config.macos.currentDmgFile = typeof macos.currentDmgFile === "string" ? macos.currentDmgFile.trim() : "";
|
|
182
213
|
}
|
|
183
214
|
}
|
|
215
|
+
const ios = asRecord(raw.ios);
|
|
216
|
+
if (ios) {
|
|
217
|
+
config.ios = { ...(config.ios ?? {}) };
|
|
218
|
+
if (typeof ios.enabled === "boolean")
|
|
219
|
+
config.ios.enabled = ios.enabled;
|
|
220
|
+
if (Object.hasOwn(ios, "ipaDir")) {
|
|
221
|
+
config.ios.ipaDir = typeof ios.ipaDir === "string" && ios.ipaDir.trim() ? ios.ipaDir.trim() : "ios";
|
|
222
|
+
}
|
|
223
|
+
if (Object.hasOwn(ios, "currentIpaFile")) {
|
|
224
|
+
config.ios.currentIpaFile = typeof ios.currentIpaFile === "string" ? ios.currentIpaFile.trim() : "";
|
|
225
|
+
}
|
|
226
|
+
}
|
|
184
227
|
}
|
|
185
228
|
async resolveLocalAsset(options) {
|
|
186
229
|
await mkdir(options.directory, { recursive: true });
|
|
@@ -295,8 +338,8 @@ export class DistributionManager {
|
|
|
295
338
|
: github
|
|
296
339
|
? { ...github, updatedAt: null, source: "github" }
|
|
297
340
|
: null;
|
|
298
|
-
const hasKey = kind === "apk" ? "hasApk" : "hasDmg";
|
|
299
|
-
const dirKey = kind === "apk" ? "apkDir" : "dmgDir";
|
|
341
|
+
const hasKey = kind === "apk" ? "hasApk" : kind === "dmg" ? "hasDmg" : "hasIpa";
|
|
342
|
+
const dirKey = kind === "apk" ? "apkDir" : kind === "dmg" ? "dmgDir" : "ipaDir";
|
|
300
343
|
return {
|
|
301
344
|
enabled,
|
|
302
345
|
[dirKey]: directory,
|
package/dist/missions.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CreateMissionInput, CreateReviewCommentInput, MissionDetails, MissionDiff, MissionReviewComment } from "./mission-types.js";
|
|
1
|
+
import type { AgentActivityItem, CreateMissionInput, CreateReviewCommentInput, MissionDetails, MissionDiff, MissionReviewComment } from "./mission-types.js";
|
|
2
2
|
import type { SessionRegistry } from "./session-registry.js";
|
|
3
3
|
import type { StructuredSessionManager } from "./structured-session-manager.js";
|
|
4
4
|
import type { WandStorage } from "./storage.js";
|
|
@@ -16,6 +16,8 @@ export declare class Missions {
|
|
|
16
16
|
get(id: string): MissionDetails | null;
|
|
17
17
|
create(input: CreateMissionInput): MissionDetails;
|
|
18
18
|
ingest(event: ProcessEvent): void;
|
|
19
|
+
inbox(): AgentActivityItem[];
|
|
20
|
+
markInboxRead(sessionId?: string): void;
|
|
19
21
|
diff(missionId: string, attemptId: string): MissionDiff;
|
|
20
22
|
addReviewComment(missionId: string, attemptId: string, input: CreateReviewCommentInput): MissionReviewComment;
|
|
21
23
|
sendReview(missionId: string, attemptId: string, commentIds?: string[]): MissionReviewComment[];
|
package/dist/missions.js
CHANGED
|
@@ -165,8 +165,26 @@ export class Missions {
|
|
|
165
165
|
error: state === "failed" ? snapshot.structuredState?.lastError ?? "任务执行失败" : null,
|
|
166
166
|
updatedAt,
|
|
167
167
|
});
|
|
168
|
+
this.storage.upsertAgentActivity({
|
|
169
|
+
sessionId: event.sessionId,
|
|
170
|
+
missionId: attempt.missionId,
|
|
171
|
+
attemptId: attempt.id,
|
|
172
|
+
state,
|
|
173
|
+
title: snapshot.title?.trim() || snapshot.summary?.trim() || firstPromptLine(this.storage.getMission(attempt.missionId)?.prompt ?? ""),
|
|
174
|
+
summary: sessionSummary(snapshot),
|
|
175
|
+
provider: snapshot.provider ?? attempt.provider,
|
|
176
|
+
cwd: snapshot.cwd,
|
|
177
|
+
updatedAt,
|
|
178
|
+
readAt: null,
|
|
179
|
+
});
|
|
168
180
|
this.refreshMissionStatus(attempt.missionId);
|
|
169
181
|
}
|
|
182
|
+
inbox() {
|
|
183
|
+
return this.storage.listAgentActivity();
|
|
184
|
+
}
|
|
185
|
+
markInboxRead(sessionId) {
|
|
186
|
+
this.storage.markAgentActivityRead(sessionId);
|
|
187
|
+
}
|
|
170
188
|
diff(missionId, attemptId) {
|
|
171
189
|
const attempt = this.requireAttempt(missionId, attemptId);
|
|
172
190
|
if (!attempt.worktreePath || !attempt.baseRef)
|
|
@@ -77,6 +77,10 @@ export interface PasswordGeneratorOptions {
|
|
|
77
77
|
digits?: boolean;
|
|
78
78
|
symbols?: boolean;
|
|
79
79
|
}
|
|
80
|
+
/** AES-256-GCM at-rest wrapper. Legacy plaintext values pass through on read. */
|
|
81
|
+
export declare function encryptVaultSecret(plaintext: string, secret: string): string;
|
|
82
|
+
export declare function decryptVaultSecret(value: string | undefined, secret: string | null): string | undefined;
|
|
83
|
+
export declare function isEncryptedVaultSecret(value: string | undefined): boolean;
|
|
80
84
|
export declare function generatePassword(options?: PasswordGeneratorOptions): string;
|
|
81
85
|
export declare function generateTotpCode(secret: string, timeMs?: number, digits?: number, period?: number): string;
|
|
82
86
|
export declare function decodeTotpSecret(secret: string): Buffer;
|
package/dist/password-manager.js
CHANGED
|
@@ -240,6 +240,43 @@ export function buildPasswordSecurityReport(items, now = Date.now()) {
|
|
|
240
240
|
issues: issues.sort((a, b) => issueRank(b.severity) - issueRank(a.severity)),
|
|
241
241
|
};
|
|
242
242
|
}
|
|
243
|
+
const VAULT_CIPHER_PREFIX = "enc:v1:";
|
|
244
|
+
function deriveVaultKey(secret) {
|
|
245
|
+
return crypto.createHash("sha256").update(secret, "utf8").digest();
|
|
246
|
+
}
|
|
247
|
+
/** AES-256-GCM at-rest wrapper. Legacy plaintext values pass through on read. */
|
|
248
|
+
export function encryptVaultSecret(plaintext, secret) {
|
|
249
|
+
if (!plaintext || !secret)
|
|
250
|
+
return plaintext;
|
|
251
|
+
if (plaintext.startsWith(VAULT_CIPHER_PREFIX))
|
|
252
|
+
return plaintext;
|
|
253
|
+
const iv = crypto.randomBytes(12);
|
|
254
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", deriveVaultKey(secret), iv);
|
|
255
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
256
|
+
return `${VAULT_CIPHER_PREFIX}${iv.toString("base64url")}.${cipher.getAuthTag().toString("base64url")}.${encrypted.toString("base64url")}`;
|
|
257
|
+
}
|
|
258
|
+
export function decryptVaultSecret(value, secret) {
|
|
259
|
+
if (!value)
|
|
260
|
+
return undefined;
|
|
261
|
+
if (!value.startsWith(VAULT_CIPHER_PREFIX))
|
|
262
|
+
return value;
|
|
263
|
+
if (!secret)
|
|
264
|
+
return undefined;
|
|
265
|
+
try {
|
|
266
|
+
const [ivB64, tagB64, dataB64] = value.slice(VAULT_CIPHER_PREFIX.length).split(".");
|
|
267
|
+
if (!ivB64 || !tagB64 || !dataB64)
|
|
268
|
+
return undefined;
|
|
269
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", deriveVaultKey(secret), Buffer.from(ivB64, "base64url"));
|
|
270
|
+
decipher.setAuthTag(Buffer.from(tagB64, "base64url"));
|
|
271
|
+
return Buffer.concat([decipher.update(Buffer.from(dataB64, "base64url")), decipher.final()]).toString("utf8");
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
export function isEncryptedVaultSecret(value) {
|
|
278
|
+
return typeof value === "string" && value.startsWith(VAULT_CIPHER_PREFIX);
|
|
279
|
+
}
|
|
243
280
|
export function generatePassword(options = {}) {
|
|
244
281
|
const length = clampInteger(options.length ?? 20, 8, 80);
|
|
245
282
|
const lower = "abcdefghijkmnopqrstuvwxyz";
|
|
@@ -7,7 +7,7 @@ import process from "node:process";
|
|
|
7
7
|
import { promisify } from "node:util";
|
|
8
8
|
import { getErrorMessage } from "./error-utils.js";
|
|
9
9
|
import { asyncRoute } from "./express-async.js";
|
|
10
|
-
import { isBlockedFolderPath,
|
|
10
|
+
import { isBlockedFolderPath, normalizeFolderPath } from "./middleware/path-safety.js";
|
|
11
11
|
import { parseBoundedInteger } from "./request-limits.js";
|
|
12
12
|
const execAsync = promisify(exec);
|
|
13
13
|
const DIRECTORY_MAX_ITEMS = 200;
|
|
@@ -493,15 +493,21 @@ export function registerFileRoutes(app, deps) {
|
|
|
493
493
|
}));
|
|
494
494
|
app.get("/api/file-search", asyncRoute(async (req, res) => {
|
|
495
495
|
const query = typeof req.query.q === "string" ? req.query.q.trim().slice(0, 256) : "";
|
|
496
|
-
const cwd = typeof req.query.cwd === "string" ? req.query.cwd :
|
|
496
|
+
const cwd = typeof req.query.cwd === "string" ? req.query.cwd : defaultCwd;
|
|
497
497
|
const maxDepth = parseBoundedInteger(req.query.depth, 5, 0, 8);
|
|
498
498
|
const maxResults = parseBoundedInteger(req.query.limit, 50, 1, 200);
|
|
499
499
|
const ignoredDirectories = new Set([".git", "node_modules", ".next", "dist", "build", "coverage", ".wand-uploads"]);
|
|
500
500
|
const maxVisitedEntries = 20_000;
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
501
|
+
let resolvedCwd;
|
|
502
|
+
try {
|
|
503
|
+
resolvedCwd = normalizeFolderPath(cwd);
|
|
504
|
+
}
|
|
505
|
+
catch {
|
|
506
|
+
res.status(400).json({ error: "无效的搜索目录。" });
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
if (isBlockedFolderPath(resolvedCwd)) {
|
|
510
|
+
res.status(403).json({ error: "访问被拒绝:不能搜索系统目录。" });
|
|
505
511
|
return;
|
|
506
512
|
}
|
|
507
513
|
if (!query) {
|
|
@@ -6,9 +6,11 @@ function sendMissionError(res, error) {
|
|
|
6
6
|
}
|
|
7
7
|
export function registerMissionRoutes(app, missions) {
|
|
8
8
|
app.get("/api/inbox", (_req, res) => {
|
|
9
|
-
res.json({ items:
|
|
9
|
+
res.json({ items: missions.inbox() });
|
|
10
10
|
});
|
|
11
|
-
app.post("/api/inbox/read", (
|
|
11
|
+
app.post("/api/inbox/read", (req, res) => {
|
|
12
|
+
const sessionId = typeof req.body?.sessionId === "string" ? req.body.sessionId.trim() : "";
|
|
13
|
+
missions.markInboxRead(sessionId || undefined);
|
|
12
14
|
res.json({ ok: true });
|
|
13
15
|
});
|
|
14
16
|
app.get("/api/missions", (_req, res) => {
|
|
@@ -433,6 +433,16 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
433
433
|
res.status(409).json({ error: "会话列表已更新,请重新加载。", revision: page.revision });
|
|
434
434
|
return;
|
|
435
435
|
}
|
|
436
|
+
if (offset === 0 && requestedRevision && requestedRevision === page.revision) {
|
|
437
|
+
res.json({
|
|
438
|
+
unchanged: true,
|
|
439
|
+
entries: [],
|
|
440
|
+
offset: 0,
|
|
441
|
+
total: page.total,
|
|
442
|
+
revision: page.revision,
|
|
443
|
+
});
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
436
446
|
res.json(page);
|
|
437
447
|
}
|
|
438
448
|
catch (error) {
|
|
@@ -1067,7 +1077,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1067
1077
|
return;
|
|
1068
1078
|
}
|
|
1069
1079
|
const newSnapshot = await startResumedPtySession(processes, storage, existingSession, sessionId, defaultMode, body);
|
|
1070
|
-
res.status(201).json(newSnapshot);
|
|
1080
|
+
res.status(201).json(sessionResponseDTO(newSnapshot));
|
|
1071
1081
|
}
|
|
1072
1082
|
catch (error) {
|
|
1073
1083
|
res.status(400).json({ error: getErrorMessage(error, "无法恢复会话。") });
|
|
@@ -1316,6 +1326,50 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1316
1326
|
res.status(400).json({ error: getErrorMessage(error, "无法按 Qoder 会话 ID 恢复会话。") });
|
|
1317
1327
|
}
|
|
1318
1328
|
});
|
|
1329
|
+
const resumeNamedStructuredSession = (req, res, spec) => {
|
|
1330
|
+
const sessionId = String(req.params.sessionId || "").trim();
|
|
1331
|
+
const body = req.body;
|
|
1332
|
+
try {
|
|
1333
|
+
if (!isSafeProviderSessionId(sessionId)) {
|
|
1334
|
+
res.status(400).json({ error: `${spec.label} 会话 ID 格式无效。` });
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
const cwd = body.cwd?.trim();
|
|
1338
|
+
if (!cwd) {
|
|
1339
|
+
res.status(400).json({ error: "无法确定工作目录 (cwd),无法恢复。" });
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
const snapshot = structured.createSession({
|
|
1343
|
+
cwd,
|
|
1344
|
+
mode: parseExecutionMode(body.mode, defaultMode),
|
|
1345
|
+
provider: spec.provider,
|
|
1346
|
+
runner: spec.runner,
|
|
1347
|
+
worktreeEnabled: body.worktreeEnabled === true,
|
|
1348
|
+
claudeSessionId: sessionId,
|
|
1349
|
+
workspaceId: resolveWorkspaceIdForNewSession(storage, cwd),
|
|
1350
|
+
...parseSessionCreationOrigin(body),
|
|
1351
|
+
});
|
|
1352
|
+
onSessionCreated?.(cwd);
|
|
1353
|
+
res.status(201).json({ resumedClaudeSessionId: sessionId, ...sessionResponseDTO(snapshot) });
|
|
1354
|
+
}
|
|
1355
|
+
catch (error) {
|
|
1356
|
+
res.status(400).json({ error: getErrorMessage(error, `无法按 ${spec.label} 会话 ID 恢复会话。`) });
|
|
1357
|
+
}
|
|
1358
|
+
};
|
|
1359
|
+
app.post("/api/grok-sessions/:sessionId/resume", (req, res) => {
|
|
1360
|
+
resumeNamedStructuredSession(req, res, {
|
|
1361
|
+
provider: "grok",
|
|
1362
|
+
runner: "grok-cli-headless",
|
|
1363
|
+
label: "Grok",
|
|
1364
|
+
});
|
|
1365
|
+
});
|
|
1366
|
+
app.post("/api/pi-sessions/:sessionId/resume", (req, res) => {
|
|
1367
|
+
resumeNamedStructuredSession(req, res, {
|
|
1368
|
+
provider: "pi",
|
|
1369
|
+
runner: "pi-cli-json",
|
|
1370
|
+
label: "Pi",
|
|
1371
|
+
});
|
|
1372
|
+
});
|
|
1319
1373
|
app.post("/api/sessions/:id/resize", (req, res) => {
|
|
1320
1374
|
const body = req.body;
|
|
1321
1375
|
try {
|
|
@@ -1333,7 +1387,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1333
1387
|
app.post("/api/sessions/:id/approve-permission", (req, res) => {
|
|
1334
1388
|
try {
|
|
1335
1389
|
if (sessions.ownerOf(req.params.id) === "structured") {
|
|
1336
|
-
res.status(
|
|
1390
|
+
res.status(404).json({ error: "结构化会话没有运行时授权请求。" });
|
|
1337
1391
|
return;
|
|
1338
1392
|
}
|
|
1339
1393
|
const snapshot = sessions.get(req.params.id);
|
|
@@ -1341,7 +1395,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1341
1395
|
res.status(400).json({ error: "Codex provider 不支持权限批准操作。" });
|
|
1342
1396
|
return;
|
|
1343
1397
|
}
|
|
1344
|
-
res.json(processes.approvePermission(req.params.id));
|
|
1398
|
+
res.json(sessionResponseDTO(processes.approvePermission(req.params.id)));
|
|
1345
1399
|
}
|
|
1346
1400
|
catch (error) {
|
|
1347
1401
|
res.status(400).json({ error: getErrorMessage(error, "无法批准该授权请求。") });
|
|
@@ -1350,7 +1404,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1350
1404
|
app.post("/api/sessions/:id/deny-permission", (req, res) => {
|
|
1351
1405
|
try {
|
|
1352
1406
|
if (sessions.ownerOf(req.params.id) === "structured") {
|
|
1353
|
-
res.status(
|
|
1407
|
+
res.status(404).json({ error: "结构化会话没有运行时授权请求。" });
|
|
1354
1408
|
return;
|
|
1355
1409
|
}
|
|
1356
1410
|
const snapshot = sessions.get(req.params.id);
|
|
@@ -1358,7 +1412,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1358
1412
|
res.status(400).json({ error: "Codex provider 不支持权限拒绝操作。" });
|
|
1359
1413
|
return;
|
|
1360
1414
|
}
|
|
1361
|
-
res.json(processes.denyPermission(req.params.id));
|
|
1415
|
+
res.json(sessionResponseDTO(processes.denyPermission(req.params.id)));
|
|
1362
1416
|
}
|
|
1363
1417
|
catch (error) {
|
|
1364
1418
|
res.status(400).json({ error: getErrorMessage(error, "无法拒绝该授权请求。") });
|
|
@@ -1367,7 +1421,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1367
1421
|
app.post("/api/sessions/:id/toggle-auto-approve", (req, res) => {
|
|
1368
1422
|
try {
|
|
1369
1423
|
if (sessions.ownerOf(req.params.id) === "structured") {
|
|
1370
|
-
res.status(
|
|
1424
|
+
res.status(404).json({ error: "结构化会话没有运行时授权请求。" });
|
|
1371
1425
|
return;
|
|
1372
1426
|
}
|
|
1373
1427
|
const snapshot = sessions.get(req.params.id);
|
|
@@ -1375,7 +1429,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1375
1429
|
res.status(400).json({ error: "Codex provider 不支持自动批准切换。" });
|
|
1376
1430
|
return;
|
|
1377
1431
|
}
|
|
1378
|
-
res.json(processes.toggleAutoApprove(req.params.id));
|
|
1432
|
+
res.json(sessionResponseDTO(processes.toggleAutoApprove(req.params.id)));
|
|
1379
1433
|
}
|
|
1380
1434
|
catch (error) {
|
|
1381
1435
|
res.status(400).json({ error: getErrorMessage(error, "无法切换自动批准状态。") });
|
|
@@ -1391,10 +1445,10 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1391
1445
|
return;
|
|
1392
1446
|
}
|
|
1393
1447
|
if (sessions.ownerOf(req.params.id) === "structured") {
|
|
1394
|
-
res.
|
|
1448
|
+
res.status(404).json({ error: "结构化会话没有运行时授权请求。" });
|
|
1395
1449
|
return;
|
|
1396
1450
|
}
|
|
1397
|
-
res.json(processes.resolveEscalation(req.params.id, requestId, resolution));
|
|
1451
|
+
res.json(sessionResponseDTO(processes.resolveEscalation(req.params.id, requestId, resolution)));
|
|
1398
1452
|
}
|
|
1399
1453
|
catch (error) {
|
|
1400
1454
|
res.status(400).json({ error: getErrorMessage(error, "无法处理该授权请求。") });
|
|
@@ -1403,10 +1457,10 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1403
1457
|
app.post("/api/sessions/:id/stop", (req, res) => {
|
|
1404
1458
|
try {
|
|
1405
1459
|
if (sessions.ownerOf(req.params.id) === "structured") {
|
|
1406
|
-
res.json(structured.stop(req.params.id));
|
|
1460
|
+
res.json(sessionResponseDTO(structured.stop(req.params.id)));
|
|
1407
1461
|
return;
|
|
1408
1462
|
}
|
|
1409
|
-
res.json(processes.stop(req.params.id));
|
|
1463
|
+
res.json(sessionResponseDTO(processes.stop(req.params.id)));
|
|
1410
1464
|
}
|
|
1411
1465
|
catch (error) {
|
|
1412
1466
|
res.status(400).json({ error: getErrorMessage(error, "无法停止会话。") });
|
|
@@ -1423,8 +1477,9 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1423
1477
|
});
|
|
1424
1478
|
}
|
|
1425
1479
|
export function registerClaudeHistoryRoutes(app, processes, _structured, storage, _sessionRegistry) {
|
|
1426
|
-
//
|
|
1427
|
-
// longer imports provider-native history into its session
|
|
1480
|
+
// Intentional compatibility stub: older native clients still GET these
|
|
1481
|
+
// endpoints. Wand no longer imports provider-native history into its session
|
|
1482
|
+
// list, so the supported response remains an empty array.
|
|
1428
1483
|
app.get("/api/claude-history", (_req, res) => {
|
|
1429
1484
|
res.json([]);
|
|
1430
1485
|
});
|
|
@@ -1595,6 +1650,11 @@ export function registerClaudeHistoryRoutes(app, processes, _structured, storage
|
|
|
1595
1650
|
remove: (ids) => processes.deleteQoderHistoryFiles(ids),
|
|
1596
1651
|
},
|
|
1597
1652
|
];
|
|
1653
|
+
for (const provider of ["grok", "pi"]) {
|
|
1654
|
+
app.get(`/api/${provider}-history`, (_req, res) => {
|
|
1655
|
+
res.json([]);
|
|
1656
|
+
});
|
|
1657
|
+
}
|
|
1598
1658
|
for (const config of externalHistoryProviders) {
|
|
1599
1659
|
app.get(`/api/${config.provider}-history`, (_req, res) => {
|
|
1600
1660
|
res.json([]);
|
|
@@ -47,11 +47,13 @@ function publicSystemAi(systemAi) {
|
|
|
47
47
|
function publicDistributionInfo(distribution) {
|
|
48
48
|
const androidApk = { ...distribution.androidApk };
|
|
49
49
|
const macosDmg = { ...distribution.macosDmg };
|
|
50
|
+
const iosIpa = { ...distribution.iosIpa };
|
|
50
51
|
// These server filesystem paths are useful to administrators, but the About
|
|
51
52
|
// panel only needs versions, sizes, and download URLs.
|
|
52
53
|
delete androidApk.apkDir;
|
|
53
54
|
delete macosDmg.dmgDir;
|
|
54
|
-
|
|
55
|
+
delete iosIpa.ipaDir;
|
|
56
|
+
return { androidApk, macosDmg, iosIpa };
|
|
55
57
|
}
|
|
56
58
|
export function registerSettingsRoutes(app, deps) {
|
|
57
59
|
const { storage, config, runtimeConfig, configPath, configDir, requireAdmin, requireAdminOrSessionPreferences, } = deps;
|
|
@@ -23,6 +23,8 @@ export interface PublicUpdateRoutesDependencies {
|
|
|
23
23
|
resolveAndroidDownload(channel: "stable" | "beta"): Promise<DownloadAsset | null>;
|
|
24
24
|
resolveLatestDmg(): Promise<ResolvedUpdateAsset | null>;
|
|
25
25
|
resolveMacosDownload(): Promise<DownloadAsset | null>;
|
|
26
|
+
resolveLatestIpa(): Promise<ResolvedUpdateAsset | null>;
|
|
27
|
+
resolveIosDownload(): Promise<DownloadAsset | null>;
|
|
26
28
|
}
|
|
27
29
|
export declare function registerPublicUpdateRoutes(app: Express, deps: PublicUpdateRoutesDependencies): void;
|
|
28
30
|
export declare class ServerUpdateState {
|
|
@@ -46,6 +48,7 @@ export interface AdminUpdateRoutesDependencies {
|
|
|
46
48
|
getDistributionSettings(): Promise<{
|
|
47
49
|
androidApk: Record<string, unknown>;
|
|
48
50
|
macosDmg: Record<string, unknown>;
|
|
51
|
+
iosIpa: Record<string, unknown>;
|
|
49
52
|
}>;
|
|
50
53
|
modelCatalog: ModelCatalogService;
|
|
51
54
|
getUpdateChannel(): UpdateChannel;
|
|
@@ -36,7 +36,7 @@ export function registerPublicUpdateRoutes(app, deps) {
|
|
|
36
36
|
});
|
|
37
37
|
}));
|
|
38
38
|
app.get("/android/download", asyncRoute(async (req, res) => {
|
|
39
|
-
const channel = req.query.channel === "
|
|
39
|
+
const channel = req.query.channel === "beta" ? "beta" : "stable";
|
|
40
40
|
const asset = await deps.resolveAndroidDownload(channel);
|
|
41
41
|
if (!asset) {
|
|
42
42
|
res.status(404).json({ error: "当前没有可下载的 APK 文件。" });
|
|
@@ -86,6 +86,20 @@ export function registerPublicUpdateRoutes(app, deps) {
|
|
|
86
86
|
readErrorMessage: "读取 DMG 文件失败。",
|
|
87
87
|
});
|
|
88
88
|
}));
|
|
89
|
+
app.get("/ios/download", asyncRoute(async (req, res) => {
|
|
90
|
+
const asset = await deps.resolveIosDownload();
|
|
91
|
+
if (!asset) {
|
|
92
|
+
res.status(404).json({ error: "当前没有可下载的 IPA 文件。" });
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
streamFileWithRange(req, res, {
|
|
96
|
+
filePath: asset.filePath,
|
|
97
|
+
size: asset.size,
|
|
98
|
+
contentType: "application/octet-stream",
|
|
99
|
+
disposition: `attachment; filename="${encodeURIComponent(asset.fileName)}"`,
|
|
100
|
+
readErrorMessage: "读取 IPA 文件失败。",
|
|
101
|
+
});
|
|
102
|
+
}));
|
|
89
103
|
}
|
|
90
104
|
export class ServerUpdateState {
|
|
91
105
|
providerCliUpdateCache = null;
|
|
@@ -106,6 +120,9 @@ export function registerAdminUpdateRoutes(app, deps) {
|
|
|
106
120
|
app.get("/api/macos-dmg", requireAdmin, asyncRoute(async (_req, res) => {
|
|
107
121
|
res.json((await deps.getDistributionSettings()).macosDmg);
|
|
108
122
|
}));
|
|
123
|
+
app.get("/api/ios-ipa", requireAdmin, asyncRoute(async (_req, res) => {
|
|
124
|
+
res.json((await deps.getDistributionSettings()).iosIpa);
|
|
125
|
+
}));
|
|
109
126
|
app.get("/api/provider-cli-updates", requireAdmin, asyncRoute(async (req, res) => {
|
|
110
127
|
try {
|
|
111
128
|
const force = req.query.refresh === "1" || !state.providerCliUpdateCache;
|