@co0ontty/wand 4.63.0 → 4.64.0-beta.g3402a64

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 (83) hide show
  1. package/dist/build-info.d.ts +18 -0
  2. package/dist/build-info.js +13 -0
  3. package/dist/build-info.json +4 -4
  4. package/dist/config.js +6 -11
  5. package/dist/env-utils.d.ts +5 -0
  6. package/dist/env-utils.js +4 -0
  7. package/dist/git-quick-commit.js +7 -11
  8. package/dist/ios-ota.js +8 -29
  9. package/dist/layout-tree.d.ts +6 -0
  10. package/dist/layout-tree.js +9 -0
  11. package/dist/missions.js +2 -2
  12. package/dist/models.js +2 -5
  13. package/dist/path-repair.js +2 -1
  14. package/dist/process-manager.js +10 -20
  15. package/dist/provider-cli-updater.js +3 -6
  16. package/dist/pty-shell-launch.js +3 -5
  17. package/dist/server-app-connect.js +11 -46
  18. package/dist/server-github-routes.js +12 -11
  19. package/dist/server-local-preview-routes.js +6 -1
  20. package/dist/server-request.d.ts +4 -2
  21. package/dist/server-request.js +25 -4
  22. package/dist/server-session-routes.js +14 -78
  23. package/dist/server-settings-routes.d.ts +2 -6
  24. package/dist/server-settings-routes.js +3 -8
  25. package/dist/server-task-routes.d.ts +9 -6
  26. package/dist/server-task-routes.js +50 -43
  27. package/dist/server-update-routes.d.ts +2 -5
  28. package/dist/server-update-routes.js +42 -9
  29. package/dist/server-workspace-routes.js +4 -12
  30. package/dist/server.js +33 -99
  31. package/dist/session-ai-context.js +6 -38
  32. package/dist/session-provider.d.ts +16 -0
  33. package/dist/session-provider.js +49 -0
  34. package/dist/session-registry.d.ts +10 -0
  35. package/dist/session-registry.js +41 -14
  36. package/dist/session-topic.js +3 -11
  37. package/dist/shell-quote.d.ts +9 -0
  38. package/dist/shell-quote.js +13 -0
  39. package/dist/signal-utils.d.ts +10 -0
  40. package/dist/signal-utils.js +38 -0
  41. package/dist/storage.js +17 -55
  42. package/dist/structured-client-protocol.js +4 -8
  43. package/dist/structured-codex-protocol.js +1 -6
  44. package/dist/structured-content.d.ts +2 -0
  45. package/dist/structured-content.js +6 -0
  46. package/dist/structured-exec-host.d.ts +0 -2
  47. package/dist/structured-exec-host.js +2 -11
  48. package/dist/structured-exec-pump.js +2 -12
  49. package/dist/structured-grok-adapter.js +1 -5
  50. package/dist/structured-opencode-adapter.js +1 -5
  51. package/dist/structured-pi-adapter.js +9 -11
  52. package/dist/structured-provider-common.d.ts +11 -3
  53. package/dist/structured-provider-common.js +16 -23
  54. package/dist/structured-qoder-adapter.js +3 -8
  55. package/dist/structured-session-manager.js +6 -17
  56. package/dist/task-title.js +4 -12
  57. package/dist/task-types.d.ts +23 -1
  58. package/dist/task-types.js +26 -0
  59. package/dist/terminal-daemon-protocol.d.ts +0 -6
  60. package/dist/terminal-daemon-server.js +1 -9
  61. package/dist/text-utils.d.ts +7 -0
  62. package/dist/text-utils.js +15 -0
  63. package/dist/tui/attach.js +17 -93
  64. package/dist/tui/commands.d.ts +2 -0
  65. package/dist/tui/commands.js +3 -2
  66. package/dist/tui/index.js +28 -115
  67. package/dist/tui/runtime-utils.d.ts +7 -0
  68. package/dist/tui/runtime-utils.js +33 -0
  69. package/dist/tui/service-actions.d.ts +31 -0
  70. package/dist/tui/service-actions.js +86 -0
  71. package/dist/tui/snapshot.js +1 -8
  72. package/dist/update-helper.js +6 -35
  73. package/dist/web-ui/content/scripts.js +397 -497
  74. package/dist/web-ui/content/styles.css +1 -1
  75. package/dist/web-ui/content/tailwind.css +7210 -0
  76. package/dist/web-ui/embedded-assets.js +2 -2
  77. package/dist/web-ui/index.js +3 -3
  78. package/dist/web-ui/provider-identity.d.ts +4 -0
  79. package/dist/web-ui/provider-identity.js +9 -0
  80. package/dist/web-ui/styles.js +24 -7
  81. package/dist/workspace-binding.d.ts +2 -0
  82. package/dist/workspace-binding.js +4 -3
  83. package/package.json +9 -12
@@ -8,11 +8,13 @@ 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 { inferProviderFromCommand, isSessionProvider, providerCliCommand, SESSION_PROVIDERS } from "./session-provider.js";
11
12
  import { buildProviderResumeCommand, isProviderSessionId } from "./resume-policy.js";
12
13
  import { parseBoundedInteger } from "./request-limits.js";
13
14
  import { asyncRoute } from "./express-async.js";
14
15
  import { sendRouteError } from "./server-request.js";
15
16
  import { registerStructuredResumeRoutes } from "./server-resume-routes.js";
17
+ import { addHiddenSessionIds, removeHiddenSessionIds } from "./session-registry.js";
16
18
  import { enrichStructuredMessages, WAND_PROTOCOL_VERSION } from "./structured-client-protocol.js";
17
19
  import { buildDirectoryTree, normalizeSessionDirectory, } from "./session-directory-tree.js";
18
20
  import { projectCwdForSession, renameWorkspaceDirectory, resolveWorkspaceIdForNewSession, } from "./workspace-binding.js";
@@ -101,56 +103,6 @@ function getInputDebugMeta(error) {
101
103
  }
102
104
  return { error };
103
105
  }
104
- function getHiddenClaudeSessionIds(storage) {
105
- const raw = storage.getConfigValue("hidden_claude_session_ids");
106
- if (!raw)
107
- return new Set();
108
- try {
109
- const parsed = JSON.parse(raw);
110
- return new Set(Array.isArray(parsed) ? parsed.filter((value) => typeof value === "string") : []);
111
- }
112
- catch {
113
- return new Set();
114
- }
115
- }
116
- function saveHiddenClaudeSessionIds(storage, ids) {
117
- storage.setConfigValue("hidden_claude_session_ids", JSON.stringify(Array.from(ids)));
118
- }
119
- function addToHiddenClaudeSessionIds(storage, ids) {
120
- if (ids.length === 0)
121
- return;
122
- const raw = storage.getConfigValue("hidden_claude_session_ids");
123
- let hidden;
124
- try {
125
- const parsed = raw ? JSON.parse(raw) : [];
126
- hidden = new Set(Array.isArray(parsed) ? parsed.filter((value) => typeof value === "string") : []);
127
- }
128
- catch {
129
- hidden = new Set();
130
- }
131
- let changed = false;
132
- for (const id of ids) {
133
- if (!hidden.has(id)) {
134
- hidden.add(id);
135
- changed = true;
136
- }
137
- }
138
- if (changed) {
139
- storage.setConfigValue("hidden_claude_session_ids", JSON.stringify(Array.from(hidden)));
140
- }
141
- }
142
- function removeFromHiddenClaudeSessionIds(storage, ids) {
143
- if (ids.length === 0)
144
- return;
145
- const hidden = getHiddenClaudeSessionIds(storage);
146
- let changed = false;
147
- for (const id of ids) {
148
- changed = hidden.delete(id) || changed;
149
- }
150
- if (changed) {
151
- saveHiddenClaudeSessionIds(storage, hidden);
152
- }
153
- }
154
106
  /**
155
107
  * A Wand-managed session and its provider-native history represent the same
156
108
  * conversation. Delete both in one operation so the native history cannot
@@ -169,11 +121,8 @@ export function deleteSessionWithProviderHistory(processes, structured, storage,
169
121
  return;
170
122
  const provider = snapshot.provider
171
123
  ?? snapshot.structuredState?.provider
172
- ?? (/^codex\b/i.test(snapshot.command.trim())
173
- ? "codex"
174
- : /^opencode\b/i.test(snapshot.command.trim())
175
- ? "opencode"
176
- : "claude");
124
+ ?? inferProviderFromCommand(snapshot.command)
125
+ ?? "claude";
177
126
  if (provider === "claude") {
178
127
  processes.deleteClaudeHistoryFiles([{
179
128
  claudeSessionId: providerSessionId,
@@ -194,7 +143,7 @@ export function deleteSessionWithProviderHistory(processes, structured, storage,
194
143
  }
195
144
  // History deletion is intentionally best-effort at the filesystem layer.
196
145
  // Keep a tombstone as a fallback for permission errors or process tail writes.
197
- addToHiddenClaudeSessionIds(storage, [providerSessionId]);
146
+ addHiddenSessionIds(storage, [providerSessionId]);
198
147
  }
199
148
  function sessionSortTimestamp(snapshot) {
200
149
  const timestamp = Date.parse(snapshot.startedAt);
@@ -313,22 +262,10 @@ function getWorktreeMergePayload(error, fallback) {
313
262
  function resolvePtyResumeProvider(snapshot) {
314
263
  if (snapshot.provider)
315
264
  return snapshot.provider;
316
- const command = snapshot.command.trim();
317
- if (/^codex\b/.test(command))
318
- return "codex";
319
- if (/^opencode\b/.test(command))
320
- return "opencode";
321
- if (/^grok\b/.test(command))
322
- return "grok";
323
- if (/^qodercli\b/.test(command))
324
- return "qoder";
325
- if (/^pi\b/.test(command))
326
- return "pi";
327
- return "claude";
265
+ return inferProviderFromCommand(snapshot.command) ?? "claude";
328
266
  }
329
267
  function isPtyProviderCommand(provider, command) {
330
- const executable = provider === "qoder" ? "qodercli" : provider;
331
- return new RegExp(`^${executable}\\b`, "i").test(command.trim());
268
+ return new RegExp(`^${providerCliCommand(provider)}\\b`, "i").test(command.trim());
332
269
  }
333
270
  async function startResumedPtySession(processes, storage, existingSession, sessionId, defaultMode, body, initialInput) {
334
271
  if ((existingSession.sessionKind ?? "pty") !== "pty") {
@@ -383,8 +320,7 @@ function canAutoResumePtyForInput(snapshot, input) {
383
320
  return Boolean(snapshot
384
321
  && (snapshot.sessionKind ?? "pty") === "pty"
385
322
  && snapshot.status !== "running"
386
- && (snapshot.provider === "claude" || snapshot.provider === "codex" || snapshot.provider === "opencode" || snapshot.provider === "grok" || snapshot.provider === "qoder" || snapshot.provider === "pi"
387
- || /^(?:claude|codex|opencode|grok|qodercli)\b/.test(snapshot.command.trim()))
323
+ && (isSessionProvider(snapshot.provider) || Boolean(inferProviderFromCommand(snapshot.command)))
388
324
  && snapshot.claudeSessionId
389
325
  && input);
390
326
  }
@@ -484,11 +420,11 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
484
420
  app.post("/api/structured-sessions", asyncRoute(async (req, res) => {
485
421
  const body = req.body;
486
422
  try {
487
- if (body.provider && body.provider !== "claude" && body.provider !== "codex" && body.provider !== "opencode" && body.provider !== "grok" && body.provider !== "qoder" && body.provider !== "pi") {
423
+ if (body.provider && !isSessionProvider(body.provider)) {
488
424
  res.status(400).json({ error: "结构化会话当前仅支持 Claude、Codex、OpenCode、Grok、Qoder 或 Pi provider。" });
489
425
  return;
490
426
  }
491
- const provider = body.provider === "codex" || body.provider === "opencode" || body.provider === "grok" || body.provider === "qoder" || body.provider === "pi" ? body.provider : "claude";
427
+ const provider = isSessionProvider(body.provider) ? body.provider : "claude";
492
428
  const rawModel = typeof body.model === "string" ? body.model.trim() : "";
493
429
  const origin = parseSessionCreationOrigin(body);
494
430
  const cwd = resolveSessionCwd(body.cwd, config.defaultCwd);
@@ -1315,7 +1251,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1315
1251
  export function registerClaudeHistoryRoutes(app, processes, storage) {
1316
1252
  // Older clients still GET these endpoints. Wand no longer imports provider-native
1317
1253
  // history into its session list, so the supported list response stays empty.
1318
- for (const provider of ["claude", "codex", "opencode", "qoder", "grok", "pi"]) {
1254
+ for (const provider of SESSION_PROVIDERS) {
1319
1255
  app.get(`/api/${provider}-history`, (_req, res) => {
1320
1256
  res.json([]);
1321
1257
  });
@@ -1351,8 +1287,8 @@ export function registerClaudeHistoryRoutes(app, processes, storage) {
1351
1287
  const toDelete = ids.filter((id) => existing.has(id));
1352
1288
  const toHide = ids.filter((id) => !existing.has(id));
1353
1289
  const deleted = historyFiles[provider].remove(toDelete);
1354
- removeFromHiddenClaudeSessionIds(storage, toDelete);
1355
- addToHiddenClaudeSessionIds(storage, toHide);
1290
+ removeHiddenSessionIds(storage, toDelete);
1291
+ addHiddenSessionIds(storage, toHide);
1356
1292
  return deleted + toHide.length;
1357
1293
  };
1358
1294
  const registerHistoryMutations = (provider, param, label) => {
@@ -1398,7 +1334,7 @@ export function registerClaudeHistoryRoutes(app, processes, storage) {
1398
1334
  .filter((session) => session.claudeSessionId && session.cwd === cwd)
1399
1335
  .map((session) => ({ claudeSessionId: session.claudeSessionId, cwd: session.cwd }));
1400
1336
  const deleted = processes.deleteClaudeHistoryFiles(toDelete);
1401
- removeFromHiddenClaudeSessionIds(storage, toDelete.map((session) => session.claudeSessionId));
1337
+ removeHiddenSessionIds(storage, toDelete.map((session) => session.claudeSessionId));
1402
1338
  res.json({ ok: true, deleted });
1403
1339
  }
1404
1340
  catch (error) {
@@ -1,4 +1,5 @@
1
1
  import type { Express, Request, RequestHandler } from "express";
2
+ import { type BuildInfo } from "./build-info.js";
2
3
  import type { ModelCatalogService } from "./models.js";
3
4
  import { type RuntimeConfigState } from "./runtime-config.js";
4
5
  import type { WandStorage } from "./storage.js";
@@ -9,11 +10,6 @@ interface SettingsDistributionPayload {
9
10
  macosDmg: Record<string, unknown>;
10
11
  iosIpa: Record<string, unknown>;
11
12
  }
12
- interface SettingsBuildInfo {
13
- commit: string | null;
14
- builtAt: string | null;
15
- channel: string | null;
16
- }
17
13
  export interface ServerSettingsRoutesDependencies {
18
14
  storage: WandStorage;
19
15
  config: WandConfig;
@@ -28,7 +24,7 @@ export interface ServerSettingsRoutesDependencies {
28
24
  nodeVersion: string;
29
25
  repoUrl: string;
30
26
  };
31
- buildInfo: SettingsBuildInfo;
27
+ buildInfo: BuildInfo;
32
28
  getCachedUpdateInfo(): {
33
29
  updateAvailable: boolean;
34
30
  latest: string | null;
@@ -1,5 +1,6 @@
1
1
  import { existsSync, writeFileSync } from "node:fs";
2
2
  import path from "node:path";
3
+ import { buildInfoPayload } from "./build-info.js";
3
4
  import { buildChildEnv } from "./env-utils.js";
4
5
  import { getErrorMessage } from "./error-utils.js";
5
6
  import { asyncRoute } from "./express-async.js";
@@ -70,10 +71,7 @@ export function registerSettingsRoutes(app, deps) {
70
71
  updateAvailable: cachedUpdate?.updateAvailable ?? false,
71
72
  latestVersion: cachedUpdate?.latest ?? null,
72
73
  updateChannel: deps.getUpdateChannel(),
73
- build: {
74
- ...deps.buildInfo,
75
- shortCommit: deps.buildInfo.commit ? deps.buildInfo.commit.slice(0, 7) : null,
76
- },
74
+ build: buildInfoPayload(deps.buildInfo),
77
75
  ...distribution,
78
76
  });
79
77
  }));
@@ -96,10 +94,7 @@ export function registerSettingsRoutes(app, deps) {
96
94
  updateAvailable: cachedUpdate?.updateAvailable ?? false,
97
95
  latestVersion: cachedUpdate?.latest ?? null,
98
96
  updateChannel: deps.getUpdateChannel(),
99
- build: {
100
- ...deps.buildInfo,
101
- shortCommit: deps.buildInfo.commit ? deps.buildInfo.commit.slice(0, 7) : null,
102
- },
97
+ build: buildInfoPayload(deps.buildInfo),
103
98
  autoUpdate: {
104
99
  web: storage.getConfigValue("autoUpdateWeb") === "true",
105
100
  apk: storage.getConfigValue("autoUpdateApk") === "true",
@@ -1,9 +1,9 @@
1
- import type { Express, RequestHandler } from "express";
1
+ import type { Express } from "express";
2
2
  import { type WandStorage } from "./storage.js";
3
3
  import { generateWandTaskTitle } from "./task-title.js";
4
4
  import type { SessionRegistry } from "./session-registry.js";
5
5
  import type { StructuredSessionManager } from "./structured-session-manager.js";
6
- import type { WandTaskAgent } from "./task-types.js";
6
+ import type { WandTaskAgent, WandTaskAgentMode } from "./task-types.js";
7
7
  import type { WandConfig } from "./types.js";
8
8
  /** 任务面板上次选用的 CLI 工具 / 模型 / 思考深度;从未保存过时回落到 Claude 默认。 */
9
9
  export declare function readTaskBoardLastAgent(storage: WandStorage): WandTaskAgent;
@@ -13,12 +13,15 @@ export interface TaskRouteDependencies {
13
13
  sessions?: SessionRegistry;
14
14
  structured?: StructuredSessionManager;
15
15
  config?: WandConfig;
16
- requireAdmin?: RequestHandler;
17
16
  /** 可注入的任务标题生成器;测试里换成同步桩,避免真的起 CLI。 */
18
17
  generateTitle?: typeof generateWandTaskTitle;
19
18
  }
20
- /** 任务派发配置:provider / model / thinkingEffort 三者必须同时给出且合法。 */
21
- export declare function parseTaskAgent(value: unknown): WandTaskAgent | null;
19
+ /**
20
+ * 任务派发配置:provider / model / thinkingEffort 必须合法;mode 缺省时用 `fallbackMode`。
21
+ * 老客户端(Android / iOS 尚未发 mode)不传 mode,PATCH / dispatch 时传任务当前值,
22
+ * 避免把用户在 Web 上选的工作模式悄悄复位成标准。
23
+ */
24
+ export declare function parseTaskAgent(value: unknown, fallbackMode?: WandTaskAgentMode): WandTaskAgent | null;
22
25
  /** 等待排队中的标题生成全部结束;只有测试用,避免用例轮询。 */
23
26
  export declare function whenWandTaskTitlesSettled(): Promise<void>;
24
- export declare function registerTaskRoutes(app: Express, deps: TaskRouteDependencies | WandStorage, sessionsLegacy?: SessionRegistry, requireAdminLegacy?: RequestHandler): void;
27
+ export declare function registerTaskRoutes(app: Express, deps: TaskRouteDependencies): void;
@@ -5,15 +5,15 @@ import { bodyObject, sendRouteError, text } from "./server-request.js";
5
5
  import { parseWandTaskAgent } from "./storage.js";
6
6
  import { generateWandTaskTitle, provisionalTaskTitleFromDescription, TASK_TITLE_MAX_LENGTH } from "./task-title.js";
7
7
  import { resolveSystemAiContext } from "./session-ai-context.js";
8
- import { WAND_MILESTONE_NAME_MAX_LENGTH } from "./task-types.js";
8
+ import { DEFAULT_WAND_TASK_AGENT_MODE, isWandTaskAgentMode, normalizeWandTaskAgentMode, WAND_MILESTONE_NAME_MAX_LENGTH } from "./task-types.js";
9
9
  import { archiveBoardTask, syncClosedBoardTask, syncUngroupedSessionsToBoard } from "./wand-task-sync.js";
10
+ import { isSessionProvider } from "./session-provider.js";
10
11
  const STATUSES = new Set(["todo", "doing", "done", "archived"]);
11
12
  const PRIORITIES = new Set(["none", "low", "medium", "high", "urgent"]);
12
- const AGENT_PROVIDERS = new Set(["claude", "codex", "opencode", "grok", "qoder", "pi"]);
13
13
  const AGENT_EFFORTS = new Set(["off", "standard", "deep", "max"]);
14
14
  const TASK_BOARD_LAST_AGENT_KEY = "pref:taskBoardLastAgent";
15
15
  function defaultTaskBoardAgent() {
16
- return { provider: "claude", model: "default", thinkingEffort: "off" };
16
+ return { provider: "claude", model: "default", thinkingEffort: "off", mode: DEFAULT_WAND_TASK_AGENT_MODE };
17
17
  }
18
18
  /** 任务面板上次选用的 CLI 工具 / 模型 / 思考深度;从未保存过时回落到 Claude 默认。 */
19
19
  export function readTaskBoardLastAgent(storage) {
@@ -32,6 +32,7 @@ export function writeTaskBoardLastAgent(storage, agent) {
32
32
  provider: agent.provider,
33
33
  model: agent.model,
34
34
  thinkingEffort: agent.thinkingEffort,
35
+ mode: agent.mode,
35
36
  });
36
37
  }
37
38
  function dateValue(value) {
@@ -67,15 +68,19 @@ function milestoneIdFrom(storage, value) {
67
68
  throw new Error("未找到该里程碑。");
68
69
  return id;
69
70
  }
70
- /** 任务派发配置:provider / model / thinkingEffort 三者必须同时给出且合法。 */
71
- export function parseTaskAgent(value) {
71
+ /**
72
+ * 任务派发配置:provider / model / thinkingEffort 必须合法;mode 缺省时用 `fallbackMode`。
73
+ * 老客户端(Android / iOS 尚未发 mode)不传 mode,PATCH / dispatch 时传任务当前值,
74
+ * 避免把用户在 Web 上选的工作模式悄悄复位成标准。
75
+ */
76
+ export function parseTaskAgent(value, fallbackMode = DEFAULT_WAND_TASK_AGENT_MODE) {
72
77
  if (value === null)
73
78
  return null;
74
79
  if (!value || typeof value !== "object" || Array.isArray(value))
75
80
  throw new Error("Agent 配置必须是对象。");
76
81
  const body = value;
77
82
  const provider = text(body.provider);
78
- if (!AGENT_PROVIDERS.has(provider))
83
+ if (!isSessionProvider(provider))
79
84
  throw new Error("请选择有效的 CLI 工具。");
80
85
  const model = text(body.model) || "default";
81
86
  if (model.length > 128)
@@ -83,7 +88,12 @@ export function parseTaskAgent(value) {
83
88
  const thinkingEffort = (text(body.thinkingEffort) || "off");
84
89
  if (!AGENT_EFFORTS.has(thinkingEffort))
85
90
  throw new Error("思考深度无效。");
86
- return { provider, model, thinkingEffort };
91
+ const rawMode = body.mode === undefined || body.mode === null || body.mode === "" ? fallbackMode : body.mode;
92
+ if (!isWandTaskAgentMode(rawMode))
93
+ throw new Error("工作模式无效。");
94
+ // Codex 之类只认一种模式的 provider 在这里夹到有效值,落库值与实际执行保持一致。
95
+ const mode = normalizeWandTaskAgentMode(provider, rawMode);
96
+ return { provider, model, thinkingEffort, mode };
87
97
  }
88
98
  function taskDto({ storage, sessions }, task) {
89
99
  if (!task)
@@ -115,11 +125,10 @@ function taskSessionSummary(session) {
115
125
  cwd: session.cwd,
116
126
  model: session.selectedModel || session.structuredState?.model || "",
117
127
  thinkingEffort: session.thinkingEffort || "off",
128
+ // 会话实际跑的执行模式;旧会话没有该字段时留空,前端回落到任务上的配置。
129
+ mode: session.mode || "",
118
130
  };
119
131
  }
120
- function sendTaskError(res, error, fallback) {
121
- sendRouteError(res, error, fallback);
122
- }
123
132
  /**
124
133
  * 用户没写标题时,先用描述首行当占位标题,再由后台模型总结一个更好的。
125
134
  * 全程不阻塞创建请求:失败只是保留占位标题,不返回错误、也不覆盖用户后来改的标题。
@@ -166,15 +175,10 @@ function taskTitleAiOptions(config) {
166
175
  // resolveSystemAiContext 会在直连 API 可用且已启用时优先走 API,否则回落到 CLI。
167
176
  return resolveSystemAiContext(defaultSession, config);
168
177
  }
169
- export function registerTaskRoutes(app, deps, sessionsLegacy, requireAdminLegacy) {
170
- // 兼容旧的 positional 调用;新代码统一用对象形式的依赖。
171
- const resolved = deps && typeof deps === "object" && "storage" in deps
172
- ? deps
173
- : { storage: deps, sessions: sessionsLegacy, requireAdmin: requireAdminLegacy };
174
- const { storage, sessions, structured, config } = resolved;
175
- const guard = resolved.requireAdmin ?? ((_req, _res, next) => next());
176
- const dto = (task) => taskDto(resolved, task);
177
- app.get("/api/wand-tasks", guard, (req, res) => {
178
+ export function registerTaskRoutes(app, deps) {
179
+ const { storage, sessions, structured, config } = deps;
180
+ const dto = (task) => taskDto(deps, task);
181
+ app.get("/api/wand-tasks", (req, res) => {
178
182
  try {
179
183
  syncUngroupedSessionsToBoard(storage);
180
184
  }
@@ -185,11 +189,11 @@ export function registerTaskRoutes(app, deps, sessionsLegacy, requireAdminLegacy
185
189
  // 数组顺序即展示顺序:created_at 倒序,新创建的在上面。
186
190
  res.json(storage.listWandTasks(workspaceId).map((task) => dto(task)));
187
191
  });
188
- app.get("/api/wand-task-agent-defaults", guard, (_req, res) => {
192
+ app.get("/api/wand-task-agent-defaults", (_req, res) => {
189
193
  res.json(readTaskBoardLastAgent(storage));
190
194
  });
191
195
  // ── 里程碑:跨项目全局列表,所有任务面板的「新建任务」共用 ──
192
- app.get("/api/wand-milestones", guard, (_req, res) => {
196
+ app.get("/api/wand-milestones", (_req, res) => {
193
197
  const counts = storage.countWandTasksByMilestone();
194
198
  res.json({
195
199
  milestones: storage.listWandMilestones().map((milestone) => ({
@@ -198,7 +202,7 @@ export function registerTaskRoutes(app, deps, sessionsLegacy, requireAdminLegacy
198
202
  })),
199
203
  });
200
204
  });
201
- app.post("/api/wand-milestones", guard, (req, res) => {
205
+ app.post("/api/wand-milestones", (req, res) => {
202
206
  try {
203
207
  const body = bodyObject(req.body);
204
208
  const name = milestoneNameFrom(body.name);
@@ -210,10 +214,10 @@ export function registerTaskRoutes(app, deps, sessionsLegacy, requireAdminLegacy
210
214
  res.status(201).json({ ...created, taskCount: 0 });
211
215
  }
212
216
  catch (error) {
213
- sendTaskError(res, error, "无法创建里程碑。");
217
+ sendRouteError(res, error, "无法创建里程碑。");
214
218
  }
215
219
  });
216
- app.patch("/api/wand-milestones/:id", guard, (req, res) => {
220
+ app.patch("/api/wand-milestones/:id", (req, res) => {
217
221
  try {
218
222
  const body = bodyObject(req.body);
219
223
  const current = storage.getWandMilestone(req.params.id);
@@ -236,15 +240,15 @@ export function registerTaskRoutes(app, deps, sessionsLegacy, requireAdminLegacy
236
240
  res.json({ ...updated, taskCount: storage.countWandTasksByMilestone()[current.id] ?? 0 });
237
241
  }
238
242
  catch (error) {
239
- sendTaskError(res, error, "无法更新里程碑。");
243
+ sendRouteError(res, error, "无法更新里程碑。");
240
244
  }
241
245
  });
242
- app.delete("/api/wand-milestones/:id", guard, (req, res) => {
246
+ app.delete("/api/wand-milestones/:id", (req, res) => {
243
247
  // 只解绑任务,不删任务;已经删过的 id 幂等返回 ok。
244
248
  storage.deleteWandMilestone(req.params.id);
245
249
  res.json({ ok: true });
246
250
  });
247
- app.put("/api/wand-task-agent-defaults", guard, (req, res) => {
251
+ app.put("/api/wand-task-agent-defaults", (req, res) => {
248
252
  try {
249
253
  const agent = parseTaskAgent(req.body);
250
254
  if (!agent)
@@ -253,10 +257,10 @@ export function registerTaskRoutes(app, deps, sessionsLegacy, requireAdminLegacy
253
257
  res.json(agent);
254
258
  }
255
259
  catch (error) {
256
- sendTaskError(res, error, "无法保存 Agent 默认选项。");
260
+ sendRouteError(res, error, "无法保存 Agent 默认选项。");
257
261
  }
258
262
  });
259
- app.post("/api/wand-tasks", guard, (req, res) => {
263
+ app.post("/api/wand-tasks", (req, res) => {
260
264
  try {
261
265
  const body = bodyObject(req.body);
262
266
  const workspaceId = body.workspaceId === null ? null : text(body.workspaceId) || null;
@@ -293,16 +297,16 @@ export function registerTaskRoutes(app, deps, sessionsLegacy, requireAdminLegacy
293
297
  scheduleWandTaskTitleGeneration(storage, task.id, description, {
294
298
  cwd: workspaceId ? storage.getWorkspace(workspaceId)?.cwd : undefined,
295
299
  config,
296
- generateTitle: resolved.generateTitle ?? generateWandTaskTitle,
300
+ generateTitle: deps.generateTitle ?? generateWandTaskTitle,
297
301
  });
298
302
  }
299
303
  res.status(201).json(dto(task));
300
304
  }
301
305
  catch (error) {
302
- sendTaskError(res, error, "无法创建任务。");
306
+ sendRouteError(res, error, "无法创建任务。");
303
307
  }
304
308
  });
305
- app.patch("/api/wand-tasks/:id", guard, (req, res) => {
309
+ app.patch("/api/wand-tasks/:id", (req, res) => {
306
310
  try {
307
311
  const body = bodyObject(req.body);
308
312
  const patch = {};
@@ -339,7 +343,9 @@ export function registerTaskRoutes(app, deps, sessionsLegacy, requireAdminLegacy
339
343
  patch.workspaceId = workspaceId || null;
340
344
  }
341
345
  if (body.agent !== undefined) {
342
- patch.agent = parseTaskAgent(body.agent);
346
+ // 老客户端不传 mode:沿用任务当前的工作模式,而不是复位成标准。
347
+ const current = storage.getWandTask(req.params.id);
348
+ patch.agent = parseTaskAgent(body.agent, current?.agent?.mode);
343
349
  if (patch.agent)
344
350
  writeTaskBoardLastAgent(storage, patch.agent);
345
351
  }
@@ -356,10 +362,10 @@ export function registerTaskRoutes(app, deps, sessionsLegacy, requireAdminLegacy
356
362
  res.json(dto(storage.getWandTask(task.id) ?? task));
357
363
  }
358
364
  catch (error) {
359
- sendTaskError(res, error, "无法更新任务。");
365
+ sendRouteError(res, error, "无法更新任务。");
360
366
  }
361
367
  });
362
- app.delete("/api/wand-tasks/:id", guard, (req, res) => {
368
+ app.delete("/api/wand-tasks/:id", (req, res) => {
363
369
  const archived = archiveBoardTask(storage, req.params.id);
364
370
  if (!archived) {
365
371
  res.status(404).json({ error: "未找到该任务。" });
@@ -367,7 +373,7 @@ export function registerTaskRoutes(app, deps, sessionsLegacy, requireAdminLegacy
367
373
  }
368
374
  res.json({ ok: true, ...dto(archived) });
369
375
  });
370
- app.post("/api/wand-tasks/:id/sessions", guard, (req, res) => {
376
+ app.post("/api/wand-tasks/:id/sessions", (req, res) => {
371
377
  try {
372
378
  const sessionId = text(bodyObject(req.body).sessionId);
373
379
  if (!sessionId)
@@ -378,10 +384,10 @@ export function registerTaskRoutes(app, deps, sessionsLegacy, requireAdminLegacy
378
384
  res.status(201).json(dto(storage.getWandTask(req.params.id)));
379
385
  }
380
386
  catch (error) {
381
- sendTaskError(res, error, "无法绑定会话。");
387
+ sendRouteError(res, error, "无法绑定会话。");
382
388
  }
383
389
  });
384
- app.delete("/api/wand-tasks/:id/sessions/:sessionId", guard, (req, res) => {
390
+ app.delete("/api/wand-tasks/:id/sessions/:sessionId", (req, res) => {
385
391
  storage.unbindWandTaskSession(req.params.id, req.params.sessionId);
386
392
  res.json({ ok: true });
387
393
  });
@@ -389,7 +395,7 @@ export function registerTaskRoutes(app, deps, sessionsLegacy, requireAdminLegacy
389
395
  * 用选定的 CLI 工具开一个结构化会话,把这次派发的 prompt 作为首条消息,
390
396
  * 并把会话绑定回该任务。cwd 取任务所属项目目录;未指定项目时用全局默认目录。
391
397
  */
392
- app.post("/api/wand-tasks/:id/dispatch", guard, asyncRoute(async (req, res) => {
398
+ app.post("/api/wand-tasks/:id/dispatch", asyncRoute(async (req, res) => {
393
399
  try {
394
400
  let task = storage.getWandTask(req.params.id);
395
401
  if (!task) {
@@ -401,7 +407,7 @@ export function registerTaskRoutes(app, deps, sessionsLegacy, requireAdminLegacy
401
407
  const body = bodyObject(req.body);
402
408
  const agent = body.agent === undefined
403
409
  ? task.agent ?? parseWandTaskAgent(JSON.stringify(body))
404
- : parseTaskAgent(body.agent);
410
+ : parseTaskAgent(body.agent, task.agent?.mode);
405
411
  if (!agent)
406
412
  throw new Error("请先为该任务选择 CLI 工具。");
407
413
  writeTaskBoardLastAgent(storage, agent);
@@ -423,7 +429,7 @@ export function registerTaskRoutes(app, deps, sessionsLegacy, requireAdminLegacy
423
429
  const model = agent.model === "default" ? "" : agent.model;
424
430
  const session = structured.createSession({
425
431
  cwd,
426
- mode: "agent",
432
+ mode: agent.mode,
427
433
  provider: agent.provider,
428
434
  model: model || getDefaultModelForProvider(config, agent.provider) || undefined,
429
435
  thinkingEffort: agent.thinkingEffort,
@@ -445,15 +451,16 @@ export function registerTaskRoutes(app, deps, sessionsLegacy, requireAdminLegacy
445
451
  provider: session.provider,
446
452
  model: session.selectedModel,
447
453
  thinkingEffort: session.thinkingEffort,
454
+ mode: session.mode,
448
455
  cwd: session.cwd,
449
456
  },
450
457
  });
451
458
  }
452
459
  catch (error) {
453
- sendTaskError(res, error, "无法派发 Agent。");
460
+ sendRouteError(res, error, "无法派发 Agent。");
454
461
  }
455
462
  }));
456
- app.get("/api/wand-tasks/:id", guard, asyncRoute(async (req, res) => {
463
+ app.get("/api/wand-tasks/:id", asyncRoute(async (req, res) => {
457
464
  const task = dto(storage.getWandTask(req.params.id));
458
465
  if (!task) {
459
466
  res.status(404).json({ error: "未找到该任务。" });
@@ -1,4 +1,5 @@
1
1
  import type { Express, RequestHandler } from "express";
2
+ import { type BuildInfo } from "./build-info.js";
2
3
  import type { GitHubApkDownload } from "./distribution-manager.js";
3
4
  import type { ModelCatalogService } from "./models.js";
4
5
  import type { PackageUpdateInfo, UpdateChannel } from "./npm-update-utils.js";
@@ -63,11 +64,7 @@ export interface AdminUpdateRoutesDependencies {
63
64
  modelCatalog: ModelCatalogService;
64
65
  getUpdateChannel(): UpdateChannel;
65
66
  checkLatestPackageVersion(channel: UpdateChannel, forceRefresh?: boolean): Promise<PackageUpdateInfo>;
66
- buildInfo: {
67
- commit: string | null;
68
- builtAt: string | null;
69
- channel: string | null;
70
- };
67
+ buildInfo: BuildInfo;
71
68
  serverInstanceId: string;
72
69
  emitSystemNotification(data: Record<string, unknown>): void;
73
70
  }
@@ -1,8 +1,9 @@
1
1
  import { Readable } from "node:stream";
2
2
  import { pipeline } from "node:stream/promises";
3
+ import { buildInfoPayload } from "./build-info.js";
3
4
  import { getErrorMessage } from "./error-utils.js";
4
5
  import { asyncRoute } from "./express-async.js";
5
- import { sendRouteError, stringQuery } from "./server-request.js";
6
+ import { sendRouteError, text } from "./server-request.js";
6
7
  import { checkProviderCliUpdates, updateProviderClis, verifyProviderCliUpdateResults, } from "./provider-cli-updater.js";
7
8
  import { absoluteUrl, buildIosInstallPage, buildIosOtaManifest, buildItmsServicesUrl, collectIosOtaBlockers, formatIosAssetSize, inspectIpa, normalizePublicOrigin, publicOriginFromRequest, } from "./ios-ota.js";
8
9
  import { streamFileWithRange } from "./server-file-routes.js";
@@ -54,7 +55,7 @@ async function proxyGitHubApk(_req, res, asset, fetchImpl = fetch) {
54
55
  * 用 400 + 固定的英文文案(老客户端按字符串匹配)。
55
56
  */
56
57
  function requireCurrentVersion(req, res) {
57
- const currentVersion = stringQuery(req.query.currentVersion);
58
+ const currentVersion = text(req.query.currentVersion);
58
59
  if (!currentVersion) {
59
60
  res.status(400).json({ error: "Missing currentVersion query parameter." });
60
61
  return null;
@@ -385,13 +386,7 @@ export function registerAdminUpdateRoutes(app, deps) {
385
386
  app.get("/api/check-update", requireAdmin, asyncRoute(async (_req, res) => {
386
387
  try {
387
388
  const info = await deps.checkLatestPackageVersion(deps.getUpdateChannel(), true);
388
- res.json({
389
- ...info,
390
- build: {
391
- ...deps.buildInfo,
392
- shortCommit: deps.buildInfo.commit ? deps.buildInfo.commit.slice(0, 7) : null,
393
- },
394
- });
389
+ res.json({ ...info, build: buildInfoPayload(deps.buildInfo) });
395
390
  }
396
391
  catch (error) {
397
392
  sendRouteError(res, error, "检查更新失败。", 500);
@@ -450,4 +445,42 @@ export function registerAdminUpdateRoutes(app, deps) {
450
445
  state.updateInFlight = false;
451
446
  }
452
447
  }));
448
+ // ── 自动更新开关(Web / APK / DMG / provider CLI) ──
449
+ const readAutoUpdatePrefs = () => ({
450
+ web: storage.getConfigValue("autoUpdateWeb") === "true",
451
+ apk: storage.getConfigValue("autoUpdateApk") === "true",
452
+ dmg: storage.getConfigValue("autoUpdateDmg") === "true",
453
+ cli: storage.getConfigValue("autoUpdateProviderClis") === "true",
454
+ });
455
+ app.get("/api/auto-update", requireAdmin, (_req, res) => {
456
+ res.json(readAutoUpdatePrefs());
457
+ });
458
+ app.post("/api/auto-update", requireAdmin, (req, res) => {
459
+ const body = (req.body ?? {});
460
+ const writes = [];
461
+ if (typeof body.web === "boolean")
462
+ writes.push(["autoUpdateWeb", String(body.web)]);
463
+ if (typeof body.apk === "boolean")
464
+ writes.push(["autoUpdateApk", String(body.apk)]);
465
+ if (typeof body.dmg === "boolean")
466
+ writes.push(["autoUpdateDmg", String(body.dmg)]);
467
+ if (typeof body.cli === "boolean")
468
+ writes.push(["autoUpdateProviderClis", String(body.cli)]);
469
+ for (const [key, value] of writes)
470
+ storage.setConfigValue(key, value);
471
+ res.json(readAutoUpdatePrefs());
472
+ });
473
+ // ── 更新通道(stable / beta) ──
474
+ app.get("/api/update-channel", requireAdmin, (_req, res) => {
475
+ res.json({
476
+ channel: deps.getUpdateChannel(),
477
+ build: buildInfoPayload(deps.buildInfo),
478
+ });
479
+ });
480
+ app.post("/api/update-channel", requireAdmin, (req, res) => {
481
+ const body = (req.body ?? {});
482
+ const channel = body.channel === "beta" ? "beta" : "stable";
483
+ storage.setConfigValue("updateChannel", channel);
484
+ res.json({ channel });
485
+ });
453
486
  }