@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.
@@ -1,6 +1,6 @@
1
1
  {
2
- "commit": "c05b4f15f592f2c4f335df60c46208a7e0ea82fc",
3
- "builtAt": "2026-08-22T00:47:50.129Z",
4
- "version": "4.43.0",
5
- "channel": "stable"
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,
@@ -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;
@@ -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, isPathWithinBase, normalizeFolderPath } from "./middleware/path-safety.js";
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 : process.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
- const allowedBase = process.cwd();
502
- const resolvedCwd = path.resolve(allowedBase, cwd);
503
- if (!isPathWithinBase(resolvedCwd, allowedBase)) {
504
- res.status(403).json({ error: "访问被拒绝:路径必须在项目目录内。" });
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", (_req, res) => {
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(400).json({ error: "结构化会话不需要终端权限操作。" });
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(400).json({ error: "结构化会话不需要终端权限操作。" });
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(400).json({ error: "结构化会话不需要切换终端自动批准。" });
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.json(structured.resolveEscalation(req.params.id, requestId, resolution));
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
- // Kept as an empty compatibility endpoint for older native clients. Wand no
1427
- // longer imports provider-native history into its session list.
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([]);
@@ -6,6 +6,7 @@ import type { WandConfig } from "./types.js";
6
6
  interface SettingsDistributionPayload {
7
7
  androidApk: Record<string, unknown>;
8
8
  macosDmg: Record<string, unknown>;
9
+ iosIpa: Record<string, unknown>;
9
10
  }
10
11
  interface SettingsBuildInfo {
11
12
  commit: string | null;
@@ -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
- return { androidApk, macosDmg };
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 === "stable" ? "stable" : "beta";
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;