@co0ontty/wand 4.63.0 → 4.64.0
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.d.ts +18 -0
- package/dist/build-info.js +13 -0
- package/dist/build-info.json +3 -3
- package/dist/config.js +6 -11
- package/dist/env-utils.d.ts +5 -0
- package/dist/env-utils.js +4 -0
- package/dist/git-quick-commit.js +7 -11
- package/dist/ios-ota.js +8 -29
- package/dist/layout-tree.d.ts +6 -0
- package/dist/layout-tree.js +9 -0
- package/dist/missions.js +2 -2
- package/dist/models.js +2 -5
- package/dist/path-repair.js +2 -1
- package/dist/process-manager.js +10 -20
- package/dist/provider-cli-updater.js +3 -6
- package/dist/pty-shell-launch.js +3 -5
- package/dist/server-app-connect.js +11 -46
- package/dist/server-github-routes.js +12 -11
- package/dist/server-local-preview-routes.js +6 -1
- package/dist/server-request.d.ts +4 -2
- package/dist/server-request.js +25 -4
- package/dist/server-session-routes.js +14 -78
- package/dist/server-settings-routes.d.ts +2 -6
- package/dist/server-settings-routes.js +3 -8
- package/dist/server-task-routes.d.ts +9 -6
- package/dist/server-task-routes.js +50 -43
- package/dist/server-update-routes.d.ts +2 -5
- package/dist/server-update-routes.js +42 -9
- package/dist/server-workspace-routes.js +4 -12
- package/dist/server.js +33 -99
- package/dist/session-ai-context.js +6 -38
- package/dist/session-provider.d.ts +16 -0
- package/dist/session-provider.js +49 -0
- package/dist/session-registry.d.ts +10 -0
- package/dist/session-registry.js +41 -14
- package/dist/session-topic.js +3 -11
- package/dist/shell-quote.d.ts +9 -0
- package/dist/shell-quote.js +13 -0
- package/dist/signal-utils.d.ts +10 -0
- package/dist/signal-utils.js +38 -0
- package/dist/storage.js +17 -55
- package/dist/structured-client-protocol.js +4 -8
- package/dist/structured-codex-protocol.js +1 -6
- package/dist/structured-content.d.ts +2 -0
- package/dist/structured-content.js +6 -0
- package/dist/structured-exec-host.d.ts +0 -2
- package/dist/structured-exec-host.js +2 -11
- package/dist/structured-exec-pump.js +2 -12
- package/dist/structured-grok-adapter.js +1 -5
- package/dist/structured-opencode-adapter.js +1 -5
- package/dist/structured-pi-adapter.js +9 -11
- package/dist/structured-provider-common.d.ts +11 -3
- package/dist/structured-provider-common.js +16 -23
- package/dist/structured-qoder-adapter.js +3 -8
- package/dist/structured-session-manager.js +6 -17
- package/dist/task-title.js +4 -12
- package/dist/task-types.d.ts +23 -1
- package/dist/task-types.js +26 -0
- package/dist/terminal-daemon-protocol.d.ts +0 -6
- package/dist/terminal-daemon-server.js +1 -9
- package/dist/text-utils.d.ts +7 -0
- package/dist/text-utils.js +15 -0
- package/dist/tui/attach.js +17 -93
- package/dist/tui/commands.d.ts +2 -0
- package/dist/tui/commands.js +3 -2
- package/dist/tui/index.js +28 -115
- package/dist/tui/runtime-utils.d.ts +7 -0
- package/dist/tui/runtime-utils.js +33 -0
- package/dist/tui/service-actions.d.ts +31 -0
- package/dist/tui/service-actions.js +86 -0
- package/dist/tui/snapshot.js +1 -8
- package/dist/update-helper.js +6 -35
- package/dist/web-ui/content/scripts.js +397 -497
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/content/tailwind.css +7210 -0
- package/dist/web-ui/embedded-assets.js +2 -2
- package/dist/web-ui/index.js +3 -3
- package/dist/web-ui/provider-identity.d.ts +4 -0
- package/dist/web-ui/provider-identity.js +9 -0
- package/dist/web-ui/styles.js +24 -7
- package/dist/workspace-binding.d.ts +2 -0
- package/dist/workspace-binding.js +4 -3
- 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
|
-
?? (
|
|
173
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
1355
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
/**
|
|
21
|
-
|
|
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
|
|
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
|
-
/**
|
|
71
|
-
|
|
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 (!
|
|
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
|
-
|
|
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
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
|
|
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",
|
|
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",
|
|
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",
|
|
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
|
-
|
|
217
|
+
sendRouteError(res, error, "无法创建里程碑。");
|
|
214
218
|
}
|
|
215
219
|
});
|
|
216
|
-
app.patch("/api/wand-milestones/:id",
|
|
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
|
-
|
|
243
|
+
sendRouteError(res, error, "无法更新里程碑。");
|
|
240
244
|
}
|
|
241
245
|
});
|
|
242
|
-
app.delete("/api/wand-milestones/:id",
|
|
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",
|
|
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
|
-
|
|
260
|
+
sendRouteError(res, error, "无法保存 Agent 默认选项。");
|
|
257
261
|
}
|
|
258
262
|
});
|
|
259
|
-
app.post("/api/wand-tasks",
|
|
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:
|
|
300
|
+
generateTitle: deps.generateTitle ?? generateWandTaskTitle,
|
|
297
301
|
});
|
|
298
302
|
}
|
|
299
303
|
res.status(201).json(dto(task));
|
|
300
304
|
}
|
|
301
305
|
catch (error) {
|
|
302
|
-
|
|
306
|
+
sendRouteError(res, error, "无法创建任务。");
|
|
303
307
|
}
|
|
304
308
|
});
|
|
305
|
-
app.patch("/api/wand-tasks/:id",
|
|
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
|
-
|
|
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
|
-
|
|
365
|
+
sendRouteError(res, error, "无法更新任务。");
|
|
360
366
|
}
|
|
361
367
|
});
|
|
362
|
-
app.delete("/api/wand-tasks/:id",
|
|
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",
|
|
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
|
-
|
|
387
|
+
sendRouteError(res, error, "无法绑定会话。");
|
|
382
388
|
}
|
|
383
389
|
});
|
|
384
|
-
app.delete("/api/wand-tasks/:id/sessions/:sessionId",
|
|
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",
|
|
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:
|
|
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
|
-
|
|
460
|
+
sendRouteError(res, error, "无法派发 Agent。");
|
|
454
461
|
}
|
|
455
462
|
}));
|
|
456
|
-
app.get("/api/wand-tasks/:id",
|
|
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,
|
|
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 =
|
|
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
|
}
|