@co0ontty/wand 4.42.1 → 4.43.0-beta.ga1bd9c8

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.
Files changed (36) hide show
  1. package/dist/build-info.json +4 -4
  2. package/dist/cli.js +2 -2
  3. package/dist/distribution-manager.d.ts +8 -0
  4. package/dist/distribution-manager.js +41 -2
  5. package/dist/missions.d.ts +4 -5
  6. package/dist/missions.js +28 -59
  7. package/dist/password-manager.d.ts +4 -0
  8. package/dist/password-manager.js +37 -0
  9. package/dist/process-manager.js +15 -7
  10. package/dist/provider-history-scanner.js +5 -0
  11. package/dist/server-file-routes.js +12 -6
  12. package/dist/server-mission-routes.js +2 -2
  13. package/dist/server-session-routes.d.ts +4 -11
  14. package/dist/server-session-routes.js +102 -77
  15. package/dist/server-update-routes.d.ts +1 -0
  16. package/dist/server-update-routes.js +3 -1
  17. package/dist/server-workspace-routes.js +17 -7
  18. package/dist/server.js +44 -13
  19. package/dist/session-transport.d.ts +2 -0
  20. package/dist/session-transport.js +5 -0
  21. package/dist/storage.d.ts +10 -0
  22. package/dist/storage.js +41 -6
  23. package/dist/structured-session-manager.js +3 -1
  24. package/dist/terminal-daemon-client.d.ts +20 -0
  25. package/dist/terminal-daemon-client.js +112 -7
  26. package/dist/types.d.ts +3 -1
  27. package/dist/web-ui/content/scripts.js +72 -89
  28. package/dist/web-ui/content/styles.css +1 -1
  29. package/dist/web-ui/content/vendor/xterm/xterm.bundle.js +10 -10
  30. package/dist/web-ui/embedded-assets.d.ts +2 -2
  31. package/dist/web-ui/embedded-assets.js +5 -5
  32. package/dist/workspace-binding.d.ts +29 -0
  33. package/dist/workspace-binding.js +111 -0
  34. package/dist/ws-broadcast.d.ts +3 -1
  35. package/dist/ws-broadcast.js +5 -2
  36. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  {
2
- "commit": "f1421de43ef92ef52af6727276871ae4bdec207e",
3
- "builtAt": "2026-08-13T13:38:41.979Z",
4
- "version": "4.42.1",
5
- "channel": "stable"
2
+ "commit": "a1bd9c8e9124a3e7fe47bd5c1e270115e4828481",
3
+ "builtAt": "2026-08-22T02:42:33.618Z",
4
+ "version": "4.43.0-beta.ga1bd9c8",
5
+ "channel": "beta"
6
6
  }
package/dist/cli.js CHANGED
@@ -225,10 +225,10 @@ Commands:
225
225
 
226
226
  Agent runtime:
227
227
  wand session:list List sessions as JSON
228
+ wand inbox:list List mission inbox items as JSON
228
229
  wand session:read <id> Read a complete session snapshot
229
230
  wand session:send <id> <text>
230
231
  wand session:wait <id> [--timeout 900]
231
- wand inbox:list List global Agent Inbox items
232
232
  wand mission:list List parallel tasks
233
233
  wand mission:create --prompt <text> --cwd <path> --providers claude,codex
234
234
  wand mission:diff <mission-id> <attempt-id>
@@ -262,6 +262,7 @@ async function runAgentCliCommand(command, args, configPath) {
262
262
  return value;
263
263
  };
264
264
  switch (command) {
265
+ case "inbox:list": return output(await api.get("/api/inbox"));
265
266
  case "session:list": return output(await api.get("/api/sessions"));
266
267
  case "session:read": {
267
268
  const id = required(args[1], "wand session:read <id>");
@@ -286,7 +287,6 @@ async function runAgentCliCommand(command, args, configPath) {
286
287
  await new Promise((resolve) => setTimeout(resolve, 500));
287
288
  }
288
289
  }
289
- case "inbox:list": return output(await api.get("/api/inbox"));
290
290
  case "mission:list": return output(await api.get("/api/missions"));
291
291
  case "mission:create": {
292
292
  const prompt = required(readFlagValue(args, "--prompt"), "wand mission:create --prompt <text> --cwd <path> --providers claude,codex");
@@ -16,6 +16,8 @@ export interface ResolvedDistributionAsset {
16
16
  size: number;
17
17
  source: "local" | "github";
18
18
  releaseNotes?: string;
19
+ /** 本地分发文件的 SHA-256(hex);GitHub 来源不提供,客户端据此跳过校验。 */
20
+ sha256?: string;
19
21
  }
20
22
  export interface DistributionSettings {
21
23
  androidApk: Record<string, unknown>;
@@ -45,6 +47,12 @@ export declare class DistributionManager {
45
47
  resolveLatestDmg(): Promise<ResolvedDistributionAsset | null>;
46
48
  resolveMacosDownload(): Promise<LocalDistributionAsset | null>;
47
49
  getSettings(): Promise<DistributionSettings>;
50
+ private readonly sha256Cache;
51
+ /**
52
+ * 计算本地分发文件的 SHA-256(hex)。按 path + size + mtime 缓存:更新检查
53
+ * 可能被多个客户端频繁触发,避免每次都全量读盘。
54
+ */
55
+ private computeLocalFileSha256;
48
56
  private refreshConfig;
49
57
  private resolveLocalAsset;
50
58
  private readLocalAsset;
@@ -1,3 +1,5 @@
1
+ import { createHash } from "node:crypto";
2
+ import { createReadStream } from "node:fs";
1
3
  import { mkdir, readdir, readFile, stat } from "node:fs/promises";
2
4
  import path from "node:path";
3
5
  import { compareApkInstallOrder, compareSemver, extractSemver } from "./version-utils.js";
@@ -48,10 +50,27 @@ export class DistributionManager {
48
50
  source: "local",
49
51
  } : null;
50
52
  const github = githubApk ? { ...githubApk, source: "github" } : null;
53
+ let winner;
51
54
  if (local && github) {
52
- return compareApkInstallOrder(github.version, local.version) > 0 ? github : local;
55
+ winner = compareApkInstallOrder(github.version, local.version) > 0 ? github : local;
53
56
  }
54
- return local ?? github;
57
+ else {
58
+ winner = local ?? github;
59
+ }
60
+ // 本地分发的 APK 附带 SHA-256,客户端下载后校验(GitHub 来源走正规 TLS,不提供)。
61
+ if (winner?.source === "local" && localApk) {
62
+ try {
63
+ const fileStat = await stat(localApk.filePath);
64
+ winner = {
65
+ ...winner,
66
+ sha256: await this.computeLocalFileSha256(localApk.filePath, fileStat.size, fileStat.mtimeMs),
67
+ };
68
+ }
69
+ catch {
70
+ // 哈希计算失败不阻断更新检查;客户端拿到空 sha256 会跳过校验。
71
+ }
72
+ }
73
+ return winner;
55
74
  }
56
75
  async resolveAndroidDownload(channel = "beta") {
57
76
  await this.refreshConfig();
@@ -109,6 +128,26 @@ export class DistributionManager {
109
128
  macosDmg: this.buildSettings("dmg", dmgDir, this.options.config.macos?.enabled === true, localDmg, githubDmg),
110
129
  };
111
130
  }
131
+ sha256Cache = new Map();
132
+ /**
133
+ * 计算本地分发文件的 SHA-256(hex)。按 path + size + mtime 缓存:更新检查
134
+ * 可能被多个客户端频繁触发,避免每次都全量读盘。
135
+ */
136
+ async computeLocalFileSha256(filePath, size, mtimeMs) {
137
+ const key = `${size}:${mtimeMs}`;
138
+ const cached = this.sha256Cache.get(filePath);
139
+ if (cached && cached.key === key)
140
+ return cached.digest;
141
+ const digest = await new Promise((resolve, reject) => {
142
+ const hash = createHash("sha256");
143
+ const stream = createReadStream(filePath);
144
+ stream.on("error", reject);
145
+ stream.on("data", (chunk) => hash.update(chunk));
146
+ stream.on("end", () => resolve(hash.digest("hex")));
147
+ });
148
+ this.sha256Cache.set(filePath, { key, digest });
149
+ return digest;
150
+ }
112
151
  async refreshConfig() {
113
152
  let raw;
114
153
  try {
@@ -4,8 +4,8 @@ import type { StructuredSessionManager } from "./structured-session-manager.js";
4
4
  import type { WandStorage } from "./storage.js";
5
5
  import type { ProcessEvent } from "./types.js";
6
6
  /**
7
- * Deep task-orchestration module. Callers create missions, observe one inbox,
8
- * and review diffs without coordinating session/worktree/storage details.
7
+ * Deep task-orchestration module. Callers create missions and review diffs
8
+ * without coordinating session/worktree/storage details.
9
9
  */
10
10
  export declare class Missions {
11
11
  private readonly storage;
@@ -14,17 +14,16 @@ export declare class Missions {
14
14
  constructor(storage: WandStorage, structured: StructuredSessionManager, sessions: SessionRegistry);
15
15
  list(): MissionDetails[];
16
16
  get(id: string): MissionDetails | null;
17
+ create(input: CreateMissionInput): MissionDetails;
18
+ ingest(event: ProcessEvent): void;
17
19
  inbox(): AgentActivityItem[];
18
20
  markInboxRead(sessionId?: string): void;
19
- create(input: CreateMissionInput): MissionDetails;
20
- ingest(event: ProcessEvent): AgentActivityItem | null;
21
21
  diff(missionId: string, attemptId: string): MissionDiff;
22
22
  addReviewComment(missionId: string, attemptId: string, input: CreateReviewCommentInput): MissionReviewComment;
23
23
  sendReview(missionId: string, attemptId: string, commentIds?: string[]): MissionReviewComment[];
24
24
  resolveReview(missionId: string, attemptId: string, commentIds: string[]): MissionReviewComment[];
25
25
  archive(id: string): MissionDetails;
26
26
  private dispatchAttempt;
27
- private seedInbox;
28
27
  private details;
29
28
  private getIncludingArchived;
30
29
  private requireAttempt;
package/dist/missions.js CHANGED
@@ -94,8 +94,8 @@ function reviewPrompt(comments) {
94
94
  ].join("\n");
95
95
  }
96
96
  /**
97
- * Deep task-orchestration module. Callers create missions, observe one inbox,
98
- * and review diffs without coordinating session/worktree/storage details.
97
+ * Deep task-orchestration module. Callers create missions and review diffs
98
+ * without coordinating session/worktree/storage details.
99
99
  */
100
100
  export class Missions {
101
101
  storage;
@@ -113,13 +113,6 @@ export class Missions {
113
113
  const mission = this.storage.getMission(id);
114
114
  return mission ? this.details(mission) : null;
115
115
  }
116
- inbox() {
117
- this.seedInbox();
118
- return this.storage.listAgentActivity();
119
- }
120
- markInboxRead(sessionId) {
121
- this.storage.markAgentActivityRead(sessionId);
122
- }
123
116
  create(input) {
124
117
  const prompt = input.prompt?.trim();
125
118
  if (!prompt)
@@ -156,38 +149,41 @@ export class Missions {
156
149
  }
157
150
  ingest(event) {
158
151
  if (!event.sessionId || event.sessionId === "__system__")
159
- return null;
152
+ return;
160
153
  const snapshot = this.sessions.getLatest(event.sessionId);
161
154
  if (!snapshot)
162
- return null;
155
+ return;
163
156
  const attempt = this.storage.getMissionAttemptBySession(event.sessionId);
164
- const mission = attempt ? this.storage.getMission(attempt.missionId) : null;
157
+ if (!attempt)
158
+ return;
165
159
  const state = activityState(snapshot, event);
166
160
  const updatedAt = nowIso();
167
- const item = {
168
- sessionId: snapshot.id,
169
- missionId: attempt?.missionId ?? null,
170
- attemptId: attempt?.id ?? null,
161
+ this.storage.saveMissionAttempt({
162
+ ...attempt,
163
+ state: state,
164
+ summary: sessionSummary(snapshot),
165
+ error: state === "failed" ? snapshot.structuredState?.lastError ?? "任务执行失败" : null,
166
+ updatedAt,
167
+ });
168
+ this.storage.upsertAgentActivity({
169
+ sessionId: event.sessionId,
170
+ missionId: attempt.missionId,
171
+ attemptId: attempt.id,
171
172
  state,
172
- title: mission?.title || snapshot.title || `${snapshot.provider ?? "agent"} 会话`,
173
+ title: snapshot.title?.trim() || snapshot.summary?.trim() || firstPromptLine(this.storage.getMission(attempt.missionId)?.prompt ?? ""),
173
174
  summary: sessionSummary(snapshot),
174
- provider: snapshot.provider ?? snapshot.structuredState?.provider ?? null,
175
- cwd: snapshot.cwd || null,
175
+ provider: snapshot.provider ?? attempt.provider,
176
+ cwd: snapshot.cwd,
176
177
  updatedAt,
177
178
  readAt: null,
178
- };
179
- this.storage.upsertAgentActivity(item);
180
- if (attempt) {
181
- this.storage.saveMissionAttempt({
182
- ...attempt,
183
- state: state,
184
- summary: item.summary,
185
- error: state === "failed" ? snapshot.structuredState?.lastError ?? "任务执行失败" : null,
186
- updatedAt,
187
- });
188
- this.refreshMissionStatus(attempt.missionId);
189
- }
190
- return item;
179
+ });
180
+ this.refreshMissionStatus(attempt.missionId);
181
+ }
182
+ inbox() {
183
+ return this.storage.listAgentActivity();
184
+ }
185
+ markInboxRead(sessionId) {
186
+ this.storage.markAgentActivityRead(sessionId);
191
187
  }
192
188
  diff(missionId, attemptId) {
193
189
  const attempt = this.requireAttempt(missionId, attemptId);
@@ -277,11 +273,6 @@ export class Missions {
277
273
  updatedAt: nowIso(),
278
274
  };
279
275
  this.storage.saveMissionAttempt(attempt);
280
- this.storage.upsertAgentActivity({
281
- sessionId: session.id, missionId: mission.id, attemptId, state: "working",
282
- title: mission.title, summary: `已分派给 ${provider}`, provider, cwd: session.cwd,
283
- updatedAt: attempt.updatedAt, readAt: null,
284
- });
285
276
  const completion = this.structured.sendMessage(session.id, mission.prompt);
286
277
  completion.catch((error) => {
287
278
  console.error(`[Missions] Attempt ${attemptId} failed after dispatch:`, error);
@@ -296,28 +287,6 @@ export class Missions {
296
287
  });
297
288
  }
298
289
  }
299
- seedInbox() {
300
- const existing = new Set(this.storage.listAgentActivity().map((item) => item.sessionId));
301
- for (const snapshot of this.sessions.listSlim()) {
302
- if (existing.has(snapshot.id))
303
- continue;
304
- const attempt = this.storage.getMissionAttemptBySession(snapshot.id);
305
- const mission = attempt ? this.storage.getMission(attempt.missionId) : null;
306
- const at = nowIso();
307
- this.storage.upsertAgentActivity({
308
- sessionId: snapshot.id,
309
- missionId: attempt?.missionId ?? null,
310
- attemptId: attempt?.id ?? null,
311
- state: activityState(snapshot),
312
- title: mission?.title || snapshot.title || `${snapshot.provider ?? "agent"} 会话`,
313
- summary: sessionSummary(snapshot),
314
- provider: snapshot.provider ?? snapshot.structuredState?.provider ?? null,
315
- cwd: snapshot.cwd || null,
316
- updatedAt: snapshot.endedAt || snapshot.startedAt || at,
317
- readAt: at,
318
- });
319
- }
320
- }
321
290
  details(mission) {
322
291
  return {
323
292
  ...mission,
@@ -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";
@@ -369,7 +369,11 @@ function listOpenCodeSessionCandidates() {
369
369
  let db = null;
370
370
  try {
371
371
  db = new DatabaseSync(dbPath, { readOnly: true });
372
- const rows = db.prepare("SELECT id, directory, time_created, time_updated FROM session ORDER BY time_updated DESC LIMIT 500").all();
372
+ const sessionColumns = db.prepare("PRAGMA table_info(session)").all();
373
+ const rootSessionFilter = sessionColumns.some((column) => column.name === "parent_id")
374
+ ? "WHERE parent_id IS NULL"
375
+ : "";
376
+ const rows = db.prepare(`SELECT id, directory, time_created, time_updated FROM session ${rootSessionFilter} ORDER BY time_updated DESC LIMIT 500`).all();
373
377
  return rows.flatMap((row) => {
374
378
  if (typeof row.id !== "string" || typeof row.directory !== "string")
375
379
  return [];
@@ -968,12 +972,16 @@ export class ProcessManager extends EventEmitter {
968
972
  let priorMessages = [];
969
973
  let inheritedSessionSource;
970
974
  let inheritedAutomationId;
975
+ let inheritedWorkspaceId;
976
+ let inheritedWorkspaceTaskId;
971
977
  if (opts?.reuseId) {
972
978
  const oldRecord = this.sessions.get(id);
973
979
  if (oldRecord) {
974
980
  priorMessages = oldRecord.ptyBridge?.getMessages() ?? oldRecord.messages ?? [];
975
981
  inheritedSessionSource = oldRecord.sessionSource;
976
982
  inheritedAutomationId = oldRecord.automationId;
983
+ inheritedWorkspaceId = oldRecord.workspaceId;
984
+ inheritedWorkspaceTaskId = oldRecord.workspaceTaskId;
977
985
  this.cleanupRecord(oldRecord);
978
986
  this.sessions.delete(id);
979
987
  }
@@ -982,6 +990,8 @@ export class ProcessManager extends EventEmitter {
982
990
  priorMessages = stored?.messages ?? [];
983
991
  inheritedSessionSource = stored?.sessionSource;
984
992
  inheritedAutomationId = stored?.automationId;
993
+ inheritedWorkspaceId = stored?.workspaceId;
994
+ inheritedWorkspaceTaskId = stored?.workspaceTaskId;
985
995
  }
986
996
  this.terminalHost.forget(id);
987
997
  }
@@ -1032,8 +1042,8 @@ export class ProcessManager extends EventEmitter {
1032
1042
  id,
1033
1043
  sessionSource: opts?.sessionSource ?? inheritedSessionSource ?? "interactive",
1034
1044
  automationId: opts?.automationId ?? inheritedAutomationId,
1035
- workspaceId: opts?.workspaceId,
1036
- workspaceTaskId: opts?.workspaceTaskId,
1045
+ workspaceId: opts?.workspaceId ?? inheritedWorkspaceId,
1046
+ workspaceTaskId: opts?.workspaceTaskId ?? inheritedWorkspaceTaskId,
1037
1047
  provider,
1038
1048
  command,
1039
1049
  cwd: resolvedCwd,
@@ -2352,10 +2362,8 @@ export class ProcessManager extends EventEmitter {
2352
2362
  const escapedModel = trimmedModel.replace(/'/g, "'\\''");
2353
2363
  result += ` --model '${escapedModel}'`;
2354
2364
  }
2355
- const variant = thinkingEffortToOpenCodeVariant(thinkingEffort ?? null);
2356
- if (variant && !/--variant(?:\s|=)/.test(result)) {
2357
- result += ` --variant '${variant.replace(/'/g, "'\\''")}'`;
2358
- }
2365
+ // thinkingEffort → --variant 仅适用于 `opencode run`(结构化 runner);
2366
+ // 交互 TUI(裸 `opencode`)没有该选项,注入会导致 CLI 直接报错退出。
2359
2367
  if ((mode === "managed" || mode === "full-access" || mode === "auto-edit") && !/--auto(?:\s|$)/.test(result)) {
2360
2368
  result += " --auto";
2361
2369
  }
@@ -434,6 +434,10 @@ export class ProviderHistoryScanner {
434
434
  let database = null;
435
435
  try {
436
436
  database = new DatabaseSync(this.openCodeDatabasePath, { readOnly: true });
437
+ const sessionColumns = database.prepare("PRAGMA table_info(session)").all();
438
+ const rootSessionFilter = sessionColumns.some((column) => column.name === "parent_id")
439
+ ? "WHERE session.parent_id IS NULL"
440
+ : "";
437
441
  const rows = database.prepare(`
438
442
  SELECT
439
443
  session.id AS id,
@@ -445,6 +449,7 @@ export class ProviderHistoryScanner {
445
449
  SUM(CASE WHEN json_extract(message.data, '$.role') = 'assistant' THEN 1 ELSE 0 END) AS assistant_count
446
450
  FROM session
447
451
  LEFT JOIN message ON message.session_id = session.id
452
+ ${rootSessionFilter}
448
453
  GROUP BY session.id
449
454
  ORDER BY session.time_updated DESC
450
455
  LIMIT 1000
@@ -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) {
@@ -9,8 +9,8 @@ export function registerMissionRoutes(app, missions) {
9
9
  res.json({ items: missions.inbox() });
10
10
  });
11
11
  app.post("/api/inbox/read", (req, res) => {
12
- const sessionId = typeof req.body?.sessionId === "string" ? req.body.sessionId : undefined;
13
- missions.markInboxRead(sessionId);
12
+ const sessionId = typeof req.body?.sessionId === "string" ? req.body.sessionId.trim() : "";
13
+ missions.markInboxRead(sessionId || undefined);
14
14
  res.json({ ok: true });
15
15
  });
16
16
  app.get("/api/missions", (_req, res) => {
@@ -37,13 +37,6 @@ export type SessionListPageEntry = {
37
37
  key: string;
38
38
  sortTimestamp: number;
39
39
  session: ReturnType<typeof toSessionListItemDTO>;
40
- } | {
41
- type: "recoverable";
42
- key: string;
43
- sortTimestamp: number;
44
- history: ProviderHistorySession & {
45
- provider: "claude" | "codex" | "opencode" | "qoder";
46
- };
47
40
  };
48
41
  export interface SessionListPage {
49
42
  entries: SessionListPageEntry[];
@@ -60,9 +53,9 @@ export interface SessionDirectoryTreeResponse {
60
53
  /** Full directory-tree revision, including custom workspace names. */
61
54
  treeRevision: string;
62
55
  }
63
- export declare function buildSessionListEntries(sessions: SessionSnapshot[], claudeHistory: ProviderHistorySession[], codexHistory: ProviderHistorySession[], hiddenHistoryIds: Set<string>, openCodeHistory?: ProviderHistorySession[], qoderHistory?: ProviderHistorySession[]): SessionListPageEntry[];
64
- export declare function buildSessionListPage(sessions: SessionSnapshot[], claudeHistory: ProviderHistorySession[], codexHistory: ProviderHistorySession[], hiddenHistoryIds: Set<string>, offset: number, limit: number, openCodeHistory?: ProviderHistorySession[], qoderHistory?: ProviderHistorySession[]): SessionListPage;
65
- export declare function buildSessionDirectoryTree(sessions: SessionSnapshot[], claudeHistory: ProviderHistorySession[], codexHistory: ProviderHistorySession[], hiddenHistoryIds: Set<string>, openCodeHistory?: ProviderHistorySession[], qoderHistory?: ProviderHistorySession[], customNames?: ReadonlyMap<string, string>): SessionDirectoryTreeResponse;
56
+ export declare function buildSessionListEntries(sessions: SessionSnapshot[]): SessionListPageEntry[];
57
+ export declare function buildSessionListPage(sessions: SessionSnapshot[], offset: number, limit: number): SessionListPage;
58
+ export declare function buildSessionDirectoryTree(sessions: SessionSnapshot[], customNames?: ReadonlyMap<string, string>): SessionDirectoryTreeResponse;
66
59
  /**
67
60
  * Provider history is scanned by ProcessManager, but structured sessions live
68
61
  * in StructuredSessionManager. Annotate against the combined session list so a
@@ -71,5 +64,5 @@ export declare function buildSessionDirectoryTree(sessions: SessionSnapshot[], c
71
64
  */
72
65
  export declare function markManagedProviderHistory<T extends ProviderHistorySession>(history: T[], sessions: SessionSnapshot[], provider: "claude" | "codex" | "opencode" | "qoder"): T[];
73
66
  export declare function registerSessionRoutes(app: Express, processes: ProcessManager, structured: StructuredSessionManager, storage: WandStorage, defaultMode: ExecutionMode, config: WandConfig, sessions: SessionRegistry, onSessionCreated?: (cwd: string | undefined | null) => void): void;
74
- export declare function registerClaudeHistoryRoutes(app: Express, processes: ProcessManager, _structured: StructuredSessionManager, storage: WandStorage, sessionRegistry: SessionRegistry): void;
67
+ export declare function registerClaudeHistoryRoutes(app: Express, processes: ProcessManager, _structured: StructuredSessionManager, storage: WandStorage, _sessionRegistry: SessionRegistry): void;
75
68
  export {};