@co0ontty/wand 4.70.0 → 4.71.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.json +3 -3
- package/dist/milestone-scope.d.ts +6 -0
- package/dist/milestone-scope.js +20 -0
- package/dist/missions.js +3 -0
- package/dist/provider-cli-updater.d.ts +15 -1
- package/dist/provider-cli-updater.js +141 -8
- package/dist/server-session-routes.js +3 -0
- package/dist/server-task-routes.d.ts +5 -2
- package/dist/server-task-routes.js +94 -36
- package/dist/server-update-routes.js +1 -1
- package/dist/server-workspace-routes.js +3 -2
- package/dist/server.js +4 -1
- package/dist/storage.d.ts +7 -1
- package/dist/storage.js +41 -9
- package/dist/structured-client-protocol.js +72 -1
- package/dist/structured-session-manager.js +1 -1
- package/dist/task-types.d.ts +20 -2
- package/dist/task-types.js +9 -0
- package/dist/wand-task-sync.d.ts +10 -0
- package/dist/wand-task-sync.js +47 -18
- package/dist/web-ui/content/scripts.js +67 -65
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/embedded-assets.js +2 -2
- package/dist/web-ui/sidebar-layout.d.ts +30 -0
- package/dist/web-ui/sidebar-layout.js +28 -0
- package/package.json +2 -1
package/dist/build-info.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"commit": "
|
|
3
|
-
"builtAt": "2026-09-
|
|
4
|
-
"version": "4.
|
|
2
|
+
"commit": "fad4d4b5db4c4be548f414c6de249f84700daa33",
|
|
3
|
+
"builtAt": "2026-09-19T07:12:02.224Z",
|
|
4
|
+
"version": "4.71.0",
|
|
5
5
|
"channel": "stable"
|
|
6
6
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { WandStorage } from "./storage.js";
|
|
2
|
+
/**
|
|
3
|
+
* 把里程碑 id 收敛到「任务所在工作区能用」的范围;不兼容就返回 null(解绑)。
|
|
4
|
+
* 调用方负责先校验 id 是否存在。
|
|
5
|
+
*/
|
|
6
|
+
export declare function scopedMilestoneId(storage: WandStorage, milestoneId: string | null | undefined, workspaceId: string | null | undefined): string | null;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// 迭代(里程碑)按工作区归属,规则只有一条:
|
|
2
|
+
// 项目里的任务只能挂「本项目的迭代」或「全局迭代(没绑工作区的)」。
|
|
3
|
+
// 没归属项目的任务(未分配 / 隐藏的全局工作区)不受限,避免把独立任务限制死。
|
|
4
|
+
// 移动端只会 PATCH `workspaceId`,不会自己清里程碑,所以这条规则必须由服务端兜住。
|
|
5
|
+
/**
|
|
6
|
+
* 把里程碑 id 收敛到「任务所在工作区能用」的范围;不兼容就返回 null(解绑)。
|
|
7
|
+
* 调用方负责先校验 id 是否存在。
|
|
8
|
+
*/
|
|
9
|
+
export function scopedMilestoneId(storage, milestoneId, workspaceId) {
|
|
10
|
+
if (!milestoneId)
|
|
11
|
+
return null;
|
|
12
|
+
const milestone = storage.getWandMilestone(milestoneId);
|
|
13
|
+
// 里程碑没了 / 是全局迭代:原样保留。
|
|
14
|
+
if (!milestone || !milestone.workspaceId)
|
|
15
|
+
return milestoneId;
|
|
16
|
+
// 任务不在项目里:不限。
|
|
17
|
+
if (!workspaceId)
|
|
18
|
+
return milestoneId;
|
|
19
|
+
return milestone.workspaceId === workspaceId ? milestoneId : null;
|
|
20
|
+
}
|
package/dist/missions.js
CHANGED
|
@@ -3,6 +3,7 @@ import { existsSync, statSync } from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { buildMissionDiff } from "./mission-diff.js";
|
|
5
5
|
import { isSessionProvider } from "./session-provider.js";
|
|
6
|
+
import { syncWorkspaceTaskToBoard } from "./wand-task-sync.js";
|
|
6
7
|
const MAX_ATTEMPTS = 6;
|
|
7
8
|
function nowIso() {
|
|
8
9
|
return new Date().toISOString();
|
|
@@ -321,6 +322,8 @@ export class Missions {
|
|
|
321
322
|
sessionSource: "automation",
|
|
322
323
|
automationId: mission.id,
|
|
323
324
|
});
|
|
325
|
+
// 关联任务的派发会话立刻落到任务卡片上,不等下一次看板列表的兜底同步。
|
|
326
|
+
syncWorkspaceTaskToBoard(this.storage, mission.taskId);
|
|
324
327
|
attempt = {
|
|
325
328
|
...attempt,
|
|
326
329
|
sessionId: session.id,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type ProviderCliId = "claude" | "codex" | "opencode" | "qoder" | "pi";
|
|
1
|
+
export type ProviderCliId = "claude" | "codex" | "opencode" | "grok" | "qoder" | "pi";
|
|
2
2
|
export interface ProviderCliUpdateStatus {
|
|
3
3
|
id: ProviderCliId;
|
|
4
4
|
label: string;
|
|
@@ -31,7 +31,21 @@ export interface ProviderCliUpdaterOptions {
|
|
|
31
31
|
onLog?: (line: string) => void;
|
|
32
32
|
}
|
|
33
33
|
export declare function parseProviderCliVersion(output: string): string | null;
|
|
34
|
+
/**
|
|
35
|
+
* Homebrew 托管的 CLI 官方更新器会拒绍动手(实测 `claude update` 只打印
|
|
36
|
+
* "managed by Homebrew" 并不改版本),所以直接给出正确的 brew 命令。
|
|
37
|
+
*/
|
|
38
|
+
export declare function homebrewUpdateHint(executable: string | null): string | null;
|
|
39
|
+
/** 从 `/opt/homebrew/Caskroom/<cask>/…` / `/Cellar/<formula>/…` 反解 Homebrew 包名。 */
|
|
40
|
+
export declare function parseHomebrewInstall(executable: string | null): {
|
|
41
|
+
kind: "cask" | "formula";
|
|
42
|
+
name: string;
|
|
43
|
+
} | null;
|
|
34
44
|
export declare function providerCliUpdateAvailable(currentVersion: string | null, latestVersion: string | null): boolean;
|
|
45
|
+
/** 从 `brew info --json=v2` 输出里取 cask version / formula stable 版本。 */
|
|
46
|
+
export declare function parseBrewInfoVersion(output: string, kind: "cask" | "formula"): string | null;
|
|
47
|
+
/** `grok update --check --json` 的 JSON 行里取 `latestVersion`;日志混排也能挑出来。 */
|
|
48
|
+
export declare function parseCliLatestVersionJson(output: string): string | null;
|
|
35
49
|
export declare function checkProviderCliUpdates(options?: ProviderCliUpdaterOptions): Promise<ProviderCliUpdateStatus[]>;
|
|
36
50
|
export declare function updateProviderClis(statuses: ProviderCliUpdateStatus[], ids?: ProviderCliId[], options?: ProviderCliUpdaterOptions): Promise<ProviderCliUpdateResult[]>;
|
|
37
51
|
/** Re-check the active PATH after updating so duplicate installs cannot masquerade as success. */
|
|
@@ -36,6 +36,15 @@ const PROVIDER_CLI_SPECS = [
|
|
|
36
36
|
versionArgs: ["--version"],
|
|
37
37
|
updateArgs: ["upgrade"],
|
|
38
38
|
},
|
|
39
|
+
{
|
|
40
|
+
id: "grok",
|
|
41
|
+
label: "Grok CLI",
|
|
42
|
+
command: "grok",
|
|
43
|
+
// Grok CLI 没有 npm 发布渠道,最新版只能问它自己。
|
|
44
|
+
versionArgs: ["--version"],
|
|
45
|
+
updateArgs: ["update"],
|
|
46
|
+
cliLatestArgs: ["update", "--check", "--json"],
|
|
47
|
+
},
|
|
39
48
|
{
|
|
40
49
|
id: "qoder",
|
|
41
50
|
label: "Qoder CLI",
|
|
@@ -48,7 +57,9 @@ const PROVIDER_CLI_SPECS = [
|
|
|
48
57
|
id: "pi",
|
|
49
58
|
label: "Pi CLI",
|
|
50
59
|
command: "pi",
|
|
51
|
-
|
|
60
|
+
// pi 已从 @mariozechner/pi-coding-agent 改名到 @earendil-works/pi-coding-agent,
|
|
61
|
+
// 继续查旧包会拿到低于本机安装版本的“最新版”,把更新判断彻底带偏。
|
|
62
|
+
npmPackage: "@earendil-works/pi-coding-agent",
|
|
52
63
|
versionArgs: ["--version"],
|
|
53
64
|
updateArgs: ["update", "self"],
|
|
54
65
|
},
|
|
@@ -79,10 +90,32 @@ function resolveInstallKind(executable, id, version) {
|
|
|
79
90
|
return "npm";
|
|
80
91
|
if (normalized.includes("/cellar/") || normalized.includes("/caskroom/") || normalized.includes("/homebrew/"))
|
|
81
92
|
return "brew";
|
|
82
|
-
if (normalized.includes("/.claude/") || normalized.includes("/.codex/") || normalized.includes("/.opencode/") || normalized.includes("/.qoder/"))
|
|
93
|
+
if (normalized.includes("/.claude/") || normalized.includes("/.codex/") || normalized.includes("/.opencode/") || normalized.includes("/.grok/") || normalized.includes("/.qoder/") || normalized.includes("/.qoder-cn/"))
|
|
83
94
|
return "native";
|
|
84
95
|
return "unknown";
|
|
85
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Homebrew 托管的 CLI 官方更新器会拒绍动手(实测 `claude update` 只打印
|
|
99
|
+
* "managed by Homebrew" 并不改版本),所以直接给出正确的 brew 命令。
|
|
100
|
+
*/
|
|
101
|
+
export function homebrewUpdateHint(executable) {
|
|
102
|
+
const install = parseHomebrewInstall(executable);
|
|
103
|
+
return install ? `brew upgrade ${install.name}` : null;
|
|
104
|
+
}
|
|
105
|
+
/** 从 `/opt/homebrew/Caskroom/<cask>/…` / `/Cellar/<formula>/…` 反解 Homebrew 包名。 */
|
|
106
|
+
export function parseHomebrewInstall(executable) {
|
|
107
|
+
if (!executable)
|
|
108
|
+
return null;
|
|
109
|
+
let resolved = executable;
|
|
110
|
+
try {
|
|
111
|
+
resolved = realpathSync(executable);
|
|
112
|
+
}
|
|
113
|
+
catch { /* keep original */ }
|
|
114
|
+
const matched = /\/(Caskroom|Cellar)\/([^/]+)\//i.exec(resolved.replace(/\\/g, "/"));
|
|
115
|
+
if (!matched)
|
|
116
|
+
return null;
|
|
117
|
+
return { kind: matched[1].toLowerCase() === "caskroom" ? "cask" : "formula", name: matched[2] };
|
|
118
|
+
}
|
|
86
119
|
function isUpdateSupported(id, version) {
|
|
87
120
|
return !(id === "opencode" && version !== null && /^0\.0\./.test(version));
|
|
88
121
|
}
|
|
@@ -107,7 +140,18 @@ async function readInstalledVersion(spec, options) {
|
|
|
107
140
|
return { executable, version: null, error: getErrorMessage(error, "读取版本失败。") };
|
|
108
141
|
}
|
|
109
142
|
}
|
|
110
|
-
async function readLatestVersion(spec, options) {
|
|
143
|
+
async function readLatestVersion(spec, options, status) {
|
|
144
|
+
if (spec.cliLatestArgs)
|
|
145
|
+
return readLatestVersionFromCli(spec, options);
|
|
146
|
+
// Homebrew 安装跟 npm 的 latest 不是同一个发布渠道(cask 通常落后一两个版本),
|
|
147
|
+
// 拿 npm 比会永久显示“有更新”。
|
|
148
|
+
if (status?.installKind === "brew") {
|
|
149
|
+
const install = parseHomebrewInstall(status.executable);
|
|
150
|
+
if (install)
|
|
151
|
+
return readBrewLatestVersion(install, options);
|
|
152
|
+
}
|
|
153
|
+
if (!spec.npmPackage)
|
|
154
|
+
return { version: null, error: "该 CLI 没有配置版本来源。" };
|
|
111
155
|
const npm = options.env?.WAND_NPM_BIN || process.env.WAND_NPM_BIN || (process.platform === "win32" ? "npm.cmd" : "npm");
|
|
112
156
|
try {
|
|
113
157
|
const result = await runCommand(npm, ["view", `${spec.npmPackage}@latest`, "version"], options.registryTimeoutMs ?? REGISTRY_TIMEOUT_MS, options);
|
|
@@ -118,12 +162,85 @@ async function readLatestVersion(spec, options) {
|
|
|
118
162
|
return { version: null, error: getErrorMessage(error, "无法连接 npm registry。") };
|
|
119
163
|
}
|
|
120
164
|
}
|
|
165
|
+
/** `brew info --json=v2` 的 cask/formula 最新版。 */
|
|
166
|
+
async function readBrewLatestVersion(install, options) {
|
|
167
|
+
const env = resolveChildEnv(options);
|
|
168
|
+
const brew = whichSync("brew", { env, timeoutMs: options.versionTimeoutMs ?? VERSION_TIMEOUT_MS });
|
|
169
|
+
if (!brew)
|
|
170
|
+
return { version: null, error: "未找到 brew 命令,无法检查 Homebrew 最新版。" };
|
|
171
|
+
try {
|
|
172
|
+
const result = await runCommand(brew, ["info", "--json=v2", install.kind === "cask" ? "--cask" : "--formula", install.name], options.registryTimeoutMs ?? REGISTRY_TIMEOUT_MS, options);
|
|
173
|
+
const version = parseBrewInfoVersion(result.stdout, install.kind);
|
|
174
|
+
return version ? { version } : { version: null, error: "brew info 未返回版本。" };
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
return { version: null, error: getErrorMessage(error, "无法运行 brew info。") };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/** 从 `brew info --json=v2` 输出里取 cask version / formula stable 版本。 */
|
|
181
|
+
export function parseBrewInfoVersion(output, kind) {
|
|
182
|
+
let parsed;
|
|
183
|
+
try {
|
|
184
|
+
parsed = JSON.parse(output);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
const root = parsed && typeof parsed === "object" ? parsed : {};
|
|
190
|
+
const list = kind === "cask" ? root.casks : root.formulae;
|
|
191
|
+
const entry = Array.isArray(list) && list.length && list[0] && typeof list[0] === "object"
|
|
192
|
+
? list[0]
|
|
193
|
+
: null;
|
|
194
|
+
if (!entry)
|
|
195
|
+
return null;
|
|
196
|
+
if (kind === "cask")
|
|
197
|
+
return typeof entry.version === "string" ? extractSemver(entry.version) : null;
|
|
198
|
+
const versions = entry.versions && typeof entry.versions === "object" ? entry.versions : {};
|
|
199
|
+
return typeof versions.stable === "string" ? extractSemver(versions.stable) : null;
|
|
200
|
+
}
|
|
201
|
+
/** `grok update --check --json` 的 JSON 行里取 `latestVersion`;日志混排也能挑出来。 */
|
|
202
|
+
export function parseCliLatestVersionJson(output) {
|
|
203
|
+
for (const line of output.split("\n")) {
|
|
204
|
+
const trimmed = line.trim();
|
|
205
|
+
if (!trimmed.startsWith("{") || !trimmed.endsWith("}"))
|
|
206
|
+
continue;
|
|
207
|
+
let parsed;
|
|
208
|
+
try {
|
|
209
|
+
parsed = JSON.parse(trimmed);
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const latest = parsed && typeof parsed === "object"
|
|
215
|
+
? parsed.latestVersion
|
|
216
|
+
: null;
|
|
217
|
+
const version = typeof latest === "string" ? extractSemver(latest) : null;
|
|
218
|
+
if (version)
|
|
219
|
+
return version;
|
|
220
|
+
}
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
async function readLatestVersionFromCli(spec, options) {
|
|
224
|
+
const env = resolveChildEnv(options);
|
|
225
|
+
const executable = whichSync(spec.command, { env, timeoutMs: options.versionTimeoutMs ?? VERSION_TIMEOUT_MS });
|
|
226
|
+
// 没装就没有“最新版”可比,交给上面按未安装展示。
|
|
227
|
+
if (!executable)
|
|
228
|
+
return { version: null };
|
|
229
|
+
try {
|
|
230
|
+
const result = await runCommand(executable, spec.cliLatestArgs, options.registryTimeoutMs ?? REGISTRY_TIMEOUT_MS, options);
|
|
231
|
+
const version = parseCliLatestVersionJson(`${result.stdout}\n${result.stderr}`);
|
|
232
|
+
return version ? { version } : { version: null, error: "CLI 未返回可解析的最新版本。" };
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
return { version: null, error: getErrorMessage(error, "无法检查 CLI 最新版本。") };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
121
238
|
export async function checkProviderCliUpdates(options = {}) {
|
|
122
239
|
return Promise.all(PROVIDER_CLI_SPECS.map(async (spec) => {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
240
|
+
// 串行:查最新版要先用 installKind 判断是不是 Homebrew 安装(其它 5 个 CLI 仍并发)。
|
|
241
|
+
const installed = await readInstalledVersion(spec, options);
|
|
242
|
+
const installKind = resolveInstallKind(installed.executable, spec.id, installed.version);
|
|
243
|
+
const latest = await readLatestVersion(spec, options, { executable: installed.executable, installKind });
|
|
127
244
|
const updateSupported = isUpdateSupported(spec.id, installed.version);
|
|
128
245
|
const errors = [installed.error, latest.error].filter(Boolean);
|
|
129
246
|
if (!updateSupported) {
|
|
@@ -139,7 +256,7 @@ export async function checkProviderCliUpdates(options = {}) {
|
|
|
139
256
|
latestVersion: latest.version,
|
|
140
257
|
updateAvailable: providerCliUpdateAvailable(installed.version, latest.version),
|
|
141
258
|
updateSupported,
|
|
142
|
-
installKind
|
|
259
|
+
installKind,
|
|
143
260
|
...(errors.length ? { error: errors.join(";") } : {}),
|
|
144
261
|
};
|
|
145
262
|
}));
|
|
@@ -192,6 +309,22 @@ export async function updateProviderClis(statuses, ids, options = {}) {
|
|
|
192
309
|
continue;
|
|
193
310
|
}
|
|
194
311
|
const executable = status.executable;
|
|
312
|
+
// Homebrew 托管的安装不能靠 CLI 自更新(它会直接拒绍),别浪费一次必然失败的子进程。
|
|
313
|
+
if (status.installKind === "brew") {
|
|
314
|
+
const hint = homebrewUpdateHint(executable);
|
|
315
|
+
results.push({
|
|
316
|
+
id: spec.id,
|
|
317
|
+
label: spec.label,
|
|
318
|
+
ok: false,
|
|
319
|
+
skipped: true,
|
|
320
|
+
fromVersion: status.currentVersion,
|
|
321
|
+
toVersion: status.latestVersion,
|
|
322
|
+
message: hint
|
|
323
|
+
? `${spec.label} 由 Homebrew 安装,请运行 \`${hint}\` 更新。`
|
|
324
|
+
: `${spec.label} 由 Homebrew 安装,请用 \`brew upgrade\` 更新。`,
|
|
325
|
+
});
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
195
328
|
options.onLog?.(`[CLI Update] ${spec.label}: ${status.currentVersion} -> ${status.latestVersion}`);
|
|
196
329
|
try {
|
|
197
330
|
const output = await runCommand(executable, spec.updateArgs, options.updateTimeoutMs ?? UPDATE_TIMEOUT_MS, options);
|
|
@@ -16,6 +16,7 @@ import { sendRouteError } from "./server-request.js";
|
|
|
16
16
|
import { registerStructuredResumeRoutes } from "./server-resume-routes.js";
|
|
17
17
|
import { addHiddenSessionIds, removeHiddenSessionIds } from "./session-registry.js";
|
|
18
18
|
import { enrichStructuredMessages, WAND_PROTOCOL_VERSION } from "./structured-client-protocol.js";
|
|
19
|
+
import { syncWorkspaceTaskToBoard } from "./wand-task-sync.js";
|
|
19
20
|
import { buildDirectoryTree, normalizeSessionDirectory, } from "./session-directory-tree.js";
|
|
20
21
|
import { projectCwdForSession, renameWorkspaceDirectory, resolveWorkspaceIdForNewSession, } from "./workspace-binding.js";
|
|
21
22
|
export function parseExecutionMode(value, fallback) {
|
|
@@ -446,6 +447,8 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
446
447
|
...origin,
|
|
447
448
|
});
|
|
448
449
|
onSessionCreated?.(snapshot.cwd);
|
|
450
|
+
// 会话一落地就挂到任务的看板卡片上,不等下一次看板列表的兜底同步。
|
|
451
|
+
syncWorkspaceTaskToBoard(storage, snapshot.workspaceTaskId);
|
|
449
452
|
const prompt = body.prompt?.trim();
|
|
450
453
|
if (prompt) {
|
|
451
454
|
const finished = await structured.sendMessage(snapshot.id, prompt);
|
|
@@ -3,7 +3,8 @@ 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 {
|
|
6
|
+
import type { ProcessManager } from "./process-manager.js";
|
|
7
|
+
import type { WandTaskAgent, WandTaskAgentKind, WandTaskAgentMode } from "./task-types.js";
|
|
7
8
|
import type { WandConfig } from "./types.js";
|
|
8
9
|
/** 任务面板上次选用的 CLI 工具 / 模型 / 思考深度;从未保存过时回落到 Claude 默认。 */
|
|
9
10
|
export declare function readTaskBoardLastAgent(storage: WandStorage): WandTaskAgent;
|
|
@@ -12,6 +13,8 @@ export interface TaskRouteDependencies {
|
|
|
12
13
|
storage: WandStorage;
|
|
13
14
|
sessions?: SessionRegistry;
|
|
14
15
|
structured?: StructuredSessionManager;
|
|
16
|
+
/** PTY 会话创建入口;派发 `kind: "pty"` 的任务时使用。测试里可换成同步桩。 */
|
|
17
|
+
processes?: ProcessManager;
|
|
15
18
|
config?: WandConfig;
|
|
16
19
|
/** 可注入的任务标题生成器;测试里换成同步桩,避免真的起 CLI。 */
|
|
17
20
|
generateTitle?: typeof generateWandTaskTitle;
|
|
@@ -21,7 +24,7 @@ export interface TaskRouteDependencies {
|
|
|
21
24
|
* 老客户端(Android / iOS 尚未发 mode)不传 mode,PATCH / dispatch 时传任务当前值,
|
|
22
25
|
* 避免把用户在 Web 上选的工作模式悄悄复位成标准。
|
|
23
26
|
*/
|
|
24
|
-
export declare function parseTaskAgent(value: unknown, fallbackMode?: WandTaskAgentMode): WandTaskAgent | null;
|
|
27
|
+
export declare function parseTaskAgent(value: unknown, fallbackMode?: WandTaskAgentMode, fallbackKind?: WandTaskAgentKind): WandTaskAgent | null;
|
|
25
28
|
/** 等待排队中的标题生成全部结束;只有测试用,避免用例轮询。 */
|
|
26
29
|
export declare function whenWandTaskTitlesSettled(): Promise<void>;
|
|
27
30
|
export interface AutoTaskTitleOptions {
|
|
@@ -3,17 +3,18 @@ import { asyncRoute } from "./express-async.js";
|
|
|
3
3
|
import { getErrorMessage } from "./error-utils.js";
|
|
4
4
|
import { bodyObject, sendRouteError, text } from "./server-request.js";
|
|
5
5
|
import { parseWandTaskAgent } from "./storage.js";
|
|
6
|
+
import { scopedMilestoneId } from "./milestone-scope.js";
|
|
6
7
|
import { generateWandTaskTitle, provisionalTaskTitleFromDescription, TASK_TITLE_MAX_LENGTH } from "./task-title.js";
|
|
7
8
|
import { resolveSystemAiContext } from "./session-ai-context.js";
|
|
8
|
-
import { DEFAULT_WAND_TASK_AGENT_MODE, isWandTaskAgentMode, normalizeWandTaskAgentMode, WAND_MILESTONE_NAME_MAX_LENGTH } from "./task-types.js";
|
|
9
|
-
import { archiveBoardTask, ensureWorkspaceTaskForBoardTask, isAutoNameableBoardTask, moveSessionToWorkspaceTask, syncClosedBoardTask, syncUngroupedSessionsToBoard, taskAutoNameSignature, taskAutoNameSourceText } from "./wand-task-sync.js";
|
|
10
|
-
import { isSessionProvider } from "./session-provider.js";
|
|
9
|
+
import { DEFAULT_WAND_TASK_AGENT_KIND, DEFAULT_WAND_TASK_AGENT_MODE, DEFAULT_WAND_TASK_PRIORITY, isWandTaskAgentKind, isWandTaskAgentMode, normalizeWandTaskAgentMode, WAND_MILESTONE_NAME_MAX_LENGTH } from "./task-types.js";
|
|
10
|
+
import { archiveBoardTask, ensureWorkspaceTaskForBoardTask, isAutoNameableBoardTask, moveSessionToWorkspaceTask, syncClosedBoardTask, syncUngroupedSessionsToBoard, syncWorkspaceTaskToBoard, taskAutoNameSignature, taskAutoNameSourceText } from "./wand-task-sync.js";
|
|
11
|
+
import { isSessionProvider, providerCliCommand } from "./session-provider.js";
|
|
11
12
|
const STATUSES = new Set(["todo", "doing", "done", "archived"]);
|
|
12
13
|
const PRIORITIES = new Set(["none", "low", "medium", "high", "urgent"]);
|
|
13
14
|
const AGENT_EFFORTS = new Set(["off", "standard", "deep", "max"]);
|
|
14
15
|
const TASK_BOARD_LAST_AGENT_KEY = "pref:taskBoardLastAgent";
|
|
15
16
|
function defaultTaskBoardAgent() {
|
|
16
|
-
return { provider: "claude", model: "default", thinkingEffort: "off", mode: DEFAULT_WAND_TASK_AGENT_MODE };
|
|
17
|
+
return { provider: "claude", model: "default", thinkingEffort: "off", mode: DEFAULT_WAND_TASK_AGENT_MODE, kind: DEFAULT_WAND_TASK_AGENT_KIND };
|
|
17
18
|
}
|
|
18
19
|
/** 任务面板上次选用的 CLI 工具 / 模型 / 思考深度;从未保存过时回落到 Claude 默认。 */
|
|
19
20
|
export function readTaskBoardLastAgent(storage) {
|
|
@@ -33,6 +34,7 @@ export function writeTaskBoardLastAgent(storage, agent) {
|
|
|
33
34
|
model: agent.model,
|
|
34
35
|
thinkingEffort: agent.thinkingEffort,
|
|
35
36
|
mode: agent.mode,
|
|
37
|
+
kind: agent.kind,
|
|
36
38
|
});
|
|
37
39
|
}
|
|
38
40
|
function dateValue(value) {
|
|
@@ -68,12 +70,23 @@ function milestoneIdFrom(storage, value) {
|
|
|
68
70
|
throw new Error("未找到该里程碑。");
|
|
69
71
|
return id;
|
|
70
72
|
}
|
|
73
|
+
/** 新建里程碑时归属的工作区;null 表示全局(未指定项目)。 */
|
|
74
|
+
function milestoneWorkspaceIdFrom(storage, value) {
|
|
75
|
+
if (value === null || value === undefined)
|
|
76
|
+
return null;
|
|
77
|
+
const workspaceId = text(value);
|
|
78
|
+
if (!workspaceId)
|
|
79
|
+
return null;
|
|
80
|
+
if (!storage.getWorkspace(workspaceId))
|
|
81
|
+
throw new Error("项目不存在。");
|
|
82
|
+
return workspaceId;
|
|
83
|
+
}
|
|
71
84
|
/**
|
|
72
85
|
* 任务派发配置:provider / model / thinkingEffort 必须合法;mode 缺省时用 `fallbackMode`。
|
|
73
86
|
* 老客户端(Android / iOS 尚未发 mode)不传 mode,PATCH / dispatch 时传任务当前值,
|
|
74
87
|
* 避免把用户在 Web 上选的工作模式悄悄复位成标准。
|
|
75
88
|
*/
|
|
76
|
-
export function parseTaskAgent(value, fallbackMode = DEFAULT_WAND_TASK_AGENT_MODE) {
|
|
89
|
+
export function parseTaskAgent(value, fallbackMode = DEFAULT_WAND_TASK_AGENT_MODE, fallbackKind = DEFAULT_WAND_TASK_AGENT_KIND) {
|
|
77
90
|
if (value === null)
|
|
78
91
|
return null;
|
|
79
92
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
@@ -93,7 +106,11 @@ export function parseTaskAgent(value, fallbackMode = DEFAULT_WAND_TASK_AGENT_MOD
|
|
|
93
106
|
throw new Error("工作模式无效。");
|
|
94
107
|
// Codex 之类只认一种模式的 provider 在这里夹到有效值,落库值与实际执行保持一致。
|
|
95
108
|
const mode = normalizeWandTaskAgentMode(provider, rawMode);
|
|
96
|
-
|
|
109
|
+
// kind 与 mode 同样兼容老客户端:不传就沿用任务当前值,避免把 PTY 悄悄复位成结构化。
|
|
110
|
+
const rawKind = body.kind === undefined || body.kind === null || body.kind === "" ? fallbackKind : body.kind;
|
|
111
|
+
if (!isWandTaskAgentKind(rawKind))
|
|
112
|
+
throw new Error("会话形态无效。");
|
|
113
|
+
return { provider, model, thinkingEffort, mode, kind: rawKind };
|
|
97
114
|
}
|
|
98
115
|
function taskDto({ storage, sessions }, task) {
|
|
99
116
|
if (!task)
|
|
@@ -214,7 +231,7 @@ function taskTitleAiOptions(config) {
|
|
|
214
231
|
return resolveSystemAiContext(defaultSession, config);
|
|
215
232
|
}
|
|
216
233
|
export function registerTaskRoutes(app, deps) {
|
|
217
|
-
const { storage, sessions, structured, config } = deps;
|
|
234
|
+
const { storage, sessions, structured, processes, config } = deps;
|
|
218
235
|
const dto = (task) => taskDto(deps, task);
|
|
219
236
|
app.get("/api/wand-tasks", (req, res) => {
|
|
220
237
|
try {
|
|
@@ -237,11 +254,13 @@ export function registerTaskRoutes(app, deps) {
|
|
|
237
254
|
app.get("/api/wand-task-agent-defaults", (_req, res) => {
|
|
238
255
|
res.json(readTaskBoardLastAgent(storage));
|
|
239
256
|
});
|
|
240
|
-
// ──
|
|
241
|
-
app.get("/api/wand-milestones", (
|
|
257
|
+
// ── 里程碑(迭代):归属工作区,任务面板的「新建任务」按工作区过滤 ──
|
|
258
|
+
app.get("/api/wand-milestones", (req, res) => {
|
|
259
|
+
// 不传 workspaceId 返回全部;传了就只给「该工作区 + 全局」的里程碑。
|
|
260
|
+
const workspaceId = typeof req.query.workspaceId === "string" ? req.query.workspaceId : null;
|
|
242
261
|
const counts = storage.countWandTasksByMilestone();
|
|
243
262
|
res.json({
|
|
244
|
-
milestones: storage.listWandMilestones().map((milestone) => ({
|
|
263
|
+
milestones: storage.listWandMilestones(workspaceId).map((milestone) => ({
|
|
245
264
|
...milestone,
|
|
246
265
|
taskCount: counts[milestone.id] ?? 0,
|
|
247
266
|
})),
|
|
@@ -251,11 +270,13 @@ export function registerTaskRoutes(app, deps) {
|
|
|
251
270
|
try {
|
|
252
271
|
const body = bodyObject(req.body);
|
|
253
272
|
const name = milestoneNameFrom(body.name);
|
|
254
|
-
const
|
|
273
|
+
const workspaceId = milestoneWorkspaceIdFrom(storage, body.workspaceId);
|
|
274
|
+
// 重名只在「同一个工作区可见的里程碑」里查:不同工作区可以各有同名迭代。
|
|
275
|
+
const duplicate = storage.listWandMilestones(workspaceId)
|
|
255
276
|
.find((milestone) => milestone.name.toLowerCase() === name.toLowerCase());
|
|
256
277
|
if (duplicate)
|
|
257
278
|
throw new Error(`里程碑「${duplicate.name}」已存在。`);
|
|
258
|
-
const created = storage.createWandMilestone({ name, dueDate: dateValue(body.dueDate) ?? null });
|
|
279
|
+
const created = storage.createWandMilestone({ name, dueDate: dateValue(body.dueDate) ?? null, workspaceId });
|
|
259
280
|
res.status(201).json({ ...created, taskCount: 0 });
|
|
260
281
|
}
|
|
261
282
|
catch (error) {
|
|
@@ -273,12 +294,21 @@ export function registerTaskRoutes(app, deps) {
|
|
|
273
294
|
const patch = {};
|
|
274
295
|
if (body.name !== undefined) {
|
|
275
296
|
const name = milestoneNameFrom(body.name);
|
|
276
|
-
const duplicate = storage.listWandMilestones()
|
|
297
|
+
const duplicate = storage.listWandMilestones(current.workspaceId)
|
|
277
298
|
.find((milestone) => milestone.id !== current.id && milestone.name.toLowerCase() === name.toLowerCase());
|
|
278
299
|
if (duplicate)
|
|
279
300
|
throw new Error(`里程碑「${duplicate.name}」已存在。`);
|
|
280
301
|
patch.name = name;
|
|
281
302
|
}
|
|
303
|
+
if (body.workspaceId !== undefined) {
|
|
304
|
+
// 允许改挂工作区(例如新建任务时目录对应的项目还没建,提交后再绑上)。
|
|
305
|
+
const workspaceId = milestoneWorkspaceIdFrom(storage, body.workspaceId);
|
|
306
|
+
const duplicate = storage.listWandMilestones(workspaceId)
|
|
307
|
+
.find((milestone) => milestone.id !== current.id && milestone.name.toLowerCase() === current.name.toLowerCase());
|
|
308
|
+
if (duplicate)
|
|
309
|
+
throw new Error(`里程碑「${duplicate.name}」已存在。`);
|
|
310
|
+
patch.workspaceId = workspaceId;
|
|
311
|
+
}
|
|
282
312
|
if (body.dueDate !== undefined)
|
|
283
313
|
patch.dueDate = dateValue(body.dueDate) ?? null;
|
|
284
314
|
const updated = storage.updateWandMilestone(current.id, patch);
|
|
@@ -320,13 +350,13 @@ export function registerTaskRoutes(app, deps) {
|
|
|
320
350
|
const titleSource = providedTitle ? "user" : "auto";
|
|
321
351
|
const title = providedTitle || provisionalTaskTitleFromDescription(description) || "新建任务";
|
|
322
352
|
const status = STATUSES.has(body.status) ? body.status : "todo";
|
|
323
|
-
const priority = PRIORITIES.has(body.priority) ? body.priority :
|
|
353
|
+
const priority = PRIORITIES.has(body.priority) ? body.priority : DEFAULT_WAND_TASK_PRIORITY;
|
|
324
354
|
const agent = body.agent === undefined ? null : parseTaskAgent(body.agent);
|
|
325
355
|
if (agent)
|
|
326
356
|
writeTaskBoardLastAgent(storage, agent);
|
|
327
357
|
const labels = labelsFrom(body.labels);
|
|
328
358
|
const dueDate = dateValue(body.dueDate) ?? null;
|
|
329
|
-
const milestoneId = milestoneIdFrom(storage, body.milestoneId);
|
|
359
|
+
const milestoneId = scopedMilestoneId(storage, milestoneIdFrom(storage, body.milestoneId), workspaceId);
|
|
330
360
|
const task = storage.transaction(() => {
|
|
331
361
|
const card = storage.createWandTask({
|
|
332
362
|
workspaceId,
|
|
@@ -391,11 +421,16 @@ export function registerTaskRoutes(app, deps) {
|
|
|
391
421
|
if (workspaceId && !storage.getWorkspace(workspaceId))
|
|
392
422
|
throw new Error("项目不存在。");
|
|
393
423
|
patch.workspaceId = workspaceId || null;
|
|
424
|
+
// 换项目后原迭代不再属于新工作区就一并摘掉;移动端只 PATCH workspaceId,不会自己清。
|
|
425
|
+
const milestoneId = body.milestoneId !== undefined
|
|
426
|
+
? patch.milestoneId
|
|
427
|
+
: storage.getWandTask(req.params.id)?.milestoneId ?? null;
|
|
428
|
+
patch.milestoneId = scopedMilestoneId(storage, milestoneId, patch.workspaceId);
|
|
394
429
|
}
|
|
395
430
|
if (body.agent !== undefined) {
|
|
396
|
-
// 老客户端不传 mode
|
|
431
|
+
// 老客户端不传 mode / kind:沿用任务当前值,而不是复位成标准 / 结构化。
|
|
397
432
|
const current = storage.getWandTask(req.params.id);
|
|
398
|
-
patch.agent = parseTaskAgent(body.agent, current?.agent?.mode);
|
|
433
|
+
patch.agent = parseTaskAgent(body.agent, current?.agent?.mode, current?.agent?.kind);
|
|
399
434
|
if (patch.agent)
|
|
400
435
|
writeTaskBoardLastAgent(storage, patch.agent);
|
|
401
436
|
}
|
|
@@ -457,8 +492,9 @@ export function registerTaskRoutes(app, deps) {
|
|
|
457
492
|
res.json({ ok: true });
|
|
458
493
|
});
|
|
459
494
|
/**
|
|
460
|
-
* 用选定的 CLI
|
|
461
|
-
*
|
|
495
|
+
* 用选定的 CLI 工具开一个会话(`agent.kind` 为 structured 时是结构化对话,pty 时是原始
|
|
496
|
+
* CLI 终端),把这次派发的 prompt 作为首条消息 / 初始输入,并把会话绑定回该任务。
|
|
497
|
+
* cwd 取任务所属项目目录;未指定项目时用全局默认目录。
|
|
462
498
|
*/
|
|
463
499
|
app.post("/api/wand-tasks/:id/dispatch", asyncRoute(async (req, res) => {
|
|
464
500
|
try {
|
|
@@ -467,14 +503,18 @@ export function registerTaskRoutes(app, deps) {
|
|
|
467
503
|
res.status(404).json({ error: "未找到该任务。" });
|
|
468
504
|
return;
|
|
469
505
|
}
|
|
470
|
-
if (!
|
|
471
|
-
throw new Error("
|
|
506
|
+
if (!config)
|
|
507
|
+
throw new Error("当前服务未启用派发,无法派发 Agent。");
|
|
472
508
|
const body = bodyObject(req.body);
|
|
473
509
|
const agent = body.agent === undefined
|
|
474
510
|
? task.agent ?? parseWandTaskAgent(JSON.stringify(body))
|
|
475
|
-
: parseTaskAgent(body.agent, task.agent?.mode);
|
|
511
|
+
: parseTaskAgent(body.agent, task.agent?.mode, task.agent?.kind);
|
|
476
512
|
if (!agent)
|
|
477
513
|
throw new Error("请先为该任务选择 CLI 工具。");
|
|
514
|
+
if (agent.kind === "structured" && !structured)
|
|
515
|
+
throw new Error("当前服务未启用结构化会话,无法派发 Agent。");
|
|
516
|
+
if (agent.kind === "pty" && !processes)
|
|
517
|
+
throw new Error("当前服务未启用终端会话,无法派发 Agent。");
|
|
478
518
|
writeTaskBoardLastAgent(storage, agent);
|
|
479
519
|
if (body.workspaceId !== undefined) {
|
|
480
520
|
const workspaceId = body.workspaceId === null ? null : text(body.workspaceId) || null;
|
|
@@ -494,28 +534,43 @@ export function registerTaskRoutes(app, deps) {
|
|
|
494
534
|
throw new Error("请输入提示词。");
|
|
495
535
|
const prompt = requestedPrompt || task.description.trim() || task.title || "执行此任务";
|
|
496
536
|
const model = agent.model === "default" ? "" : agent.model;
|
|
497
|
-
const
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
provider
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
537
|
+
const provider = agent.provider;
|
|
538
|
+
const resolvedModel = model || getDefaultModelForProvider(config, provider) || undefined;
|
|
539
|
+
const session = agent.kind === "pty"
|
|
540
|
+
? await processes.start(providerCliCommand(provider), cwd, agent.mode, prompt, {
|
|
541
|
+
provider,
|
|
542
|
+
model: resolvedModel,
|
|
543
|
+
thinkingEffort: agent.thinkingEffort,
|
|
544
|
+
sessionSource: "automation",
|
|
545
|
+
automationId: `wand-task:${task.id}`,
|
|
546
|
+
workspaceId: group.workspaceId,
|
|
547
|
+
workspaceTaskId: group.id,
|
|
548
|
+
})
|
|
549
|
+
: structured.createSession({
|
|
550
|
+
cwd,
|
|
551
|
+
mode: agent.mode,
|
|
552
|
+
provider,
|
|
553
|
+
model: resolvedModel,
|
|
554
|
+
thinkingEffort: agent.thinkingEffort,
|
|
555
|
+
worktreeEnabled: false,
|
|
556
|
+
sessionSource: "automation",
|
|
557
|
+
automationId: `wand-task:${task.id}`,
|
|
558
|
+
workspaceId: group.workspaceId,
|
|
559
|
+
workspaceTaskId: group.id,
|
|
560
|
+
});
|
|
509
561
|
storage.updateWandTask(task.id, { agent, status: task.status === "todo" ? "doing" : task.status });
|
|
510
562
|
storage.bindWandTaskSession(task.id, session.id);
|
|
511
|
-
|
|
512
|
-
|
|
563
|
+
if (agent.kind !== "pty") {
|
|
564
|
+
const completion = structured.sendMessage(session.id, prompt);
|
|
565
|
+
completion.catch((error) => console.error(`[WandTask] Agent dispatch failed for ${task.id}:`, error));
|
|
566
|
+
}
|
|
513
567
|
res.status(202).json({
|
|
514
568
|
ok: true,
|
|
515
569
|
taskId: task.id,
|
|
516
570
|
session: {
|
|
517
571
|
id: session.id,
|
|
518
572
|
provider: session.provider,
|
|
573
|
+
sessionKind: session.sessionKind ?? "pty",
|
|
519
574
|
model: session.selectedModel,
|
|
520
575
|
thinkingEffort: session.thinkingEffort,
|
|
521
576
|
mode: session.mode,
|
|
@@ -528,6 +583,9 @@ export function registerTaskRoutes(app, deps) {
|
|
|
528
583
|
}
|
|
529
584
|
}));
|
|
530
585
|
app.get("/api/wand-tasks/:id", asyncRoute(async (req, res) => {
|
|
586
|
+
// 单卡读取不跑全量同步,但至少把这张卡片自己的任务归属对上:
|
|
587
|
+
// 原生客户端 / 详情面板只拉单卡时,也不会看到一张没有会话的卡片。
|
|
588
|
+
syncWorkspaceTaskToBoard(storage, storage.getWandTask(req.params.id)?.workspaceTaskId);
|
|
531
589
|
const task = dto(storage.getWandTask(req.params.id));
|
|
532
590
|
if (!task) {
|
|
533
591
|
res.status(404).json({ error: "未找到该任务。" });
|
|
@@ -361,7 +361,7 @@ export function registerAdminUpdateRoutes(app, deps) {
|
|
|
361
361
|
return;
|
|
362
362
|
}
|
|
363
363
|
const rawIds = Array.isArray(req.body?.ids) ? req.body.ids : [];
|
|
364
|
-
const ids = rawIds.filter((value) => value === "claude" || value === "codex" || value === "opencode" || value === "qoder" || value === "pi");
|
|
364
|
+
const ids = rawIds.filter((value) => value === "claude" || value === "codex" || value === "opencode" || value === "grok" || value === "qoder" || value === "pi");
|
|
365
365
|
state.providerCliUpdateInFlight = true;
|
|
366
366
|
try {
|
|
367
367
|
const before = await refreshProviderCliUpdateState(state, config);
|
|
@@ -9,6 +9,7 @@ import { checkSessionWorktreeMergeabilityAsync, cleanupWorktreeSync, prepareSess
|
|
|
9
9
|
import { collectSessionTopicBlocklist } from "./session-topic.js";
|
|
10
10
|
import { resolveSessionDisplayTitle } from "./session-transport.js";
|
|
11
11
|
import { attachUnboundSessionsToWorkspace, backfillSessionWorkspaces, normalizeProjectCwd, projectCwdForSession, isGlobalWorkspace, syncDirectoryNameForWorkspace } from "./workspace-binding.js";
|
|
12
|
+
import { scopedMilestoneId } from "./milestone-scope.js";
|
|
12
13
|
import { archiveBoardTaskForWorkspaceTask, archiveWorkspaceTask, ensureBoardTaskForWorkspaceTask, isUnnamedWorkspaceTaskName, moveSessionToWorkspaceTask, syncSidebarTasksFromBoard, UNNAMED_WORKSPACE_TASK_NAME } from "./wand-task-sync.js";
|
|
13
14
|
import { isSessionProvider } from "./session-provider.js";
|
|
14
15
|
import { firstLayoutTabId } from "./layout-tree.js";
|
|
@@ -118,12 +119,12 @@ function createTaskForWorkspace(storage, workspace, body) {
|
|
|
118
119
|
// 留空(或老客户端回传的占位名)走自动命名:先用提示词总结,再交给模型改写。
|
|
119
120
|
const named = name.length > 0 && !isUnnamedWorkspaceTaskName(name);
|
|
120
121
|
const provisional = named ? "" : provisionalTaskTitleFromDescription(description);
|
|
121
|
-
//
|
|
122
|
+
// 迭代按工作区归属:项目里的任务只能挂本项目或全局的迭代(详见 milestone-scope.ts)。
|
|
122
123
|
const requestedMilestoneId = typeof body.milestoneId === "string" ? body.milestoneId.trim() : "";
|
|
123
124
|
if (requestedMilestoneId && !storage.getWandMilestone(requestedMilestoneId)) {
|
|
124
125
|
throw new Error("未找到该里程碑。");
|
|
125
126
|
}
|
|
126
|
-
const milestoneId = requestedMilestoneId || null;
|
|
127
|
+
const milestoneId = scopedMilestoneId(storage, requestedMilestoneId || null, isGlobalWorkspace(workspace) ? null : workspace.id);
|
|
127
128
|
let mountedCwd;
|
|
128
129
|
if (body.cwd !== undefined && body.cwd !== null && String(body.cwd).trim()) {
|
|
129
130
|
mountedCwd = resolveWorkspaceCwd(body.cwd);
|