@co0ontty/wand 4.60.0 → 4.61.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/server.js CHANGED
@@ -6,7 +6,6 @@ import { stat } from "node:fs/promises";
6
6
  import { createServer as createHttpServer } from "node:http";
7
7
  import { createServer as createHttpsServer } from "node:https";
8
8
  import { spawn } from "node:child_process";
9
- import os from "node:os";
10
9
  import path from "node:path";
11
10
  import process from "node:process";
12
11
  import { WebSocketServer } from "ws";
@@ -22,6 +21,9 @@ import { resolveSessionAiContext, resolveSystemAiContext } from "./session-ai-co
22
21
  import { StructuredSessionManager } from "./structured-session-manager.js";
23
22
  import { recordRecentPath, registerFileRoutes } from "./server-file-routes.js";
24
23
  import { registerSettingsRoutes } from "./server-settings-routes.js";
24
+ import { appTokenLoginPayload, buildStructuredChatPersonaPayload, isBrowserExtensionOrigin, resolveAppConnectCode, resolveStructuredChatAvatarPath, verifyAppToken, } from "./server-app-connect.js";
25
+ import { firstHeaderValue, sendRouteError } from "./server-request.js";
26
+ import { registerVaultRoutes } from "./server-vault-routes.js";
25
27
  import { registerGithubRoutes } from "./server-github-routes.js";
26
28
  import { registerTaskRoutes } from "./server-task-routes.js";
27
29
  import { getGithubConnectorStatus } from "./github-connector.js";
@@ -44,7 +46,6 @@ import { toSessionDetailDTO } from "./session-transport.js";
44
46
  import { registerUploadRoutes } from "./upload-routes.js";
45
47
  import { optimizePrompt, PromptOptimizeError } from "./prompt-optimizer.js";
46
48
  import { resolveDatabasePath, WandStorage } from "./storage.js";
47
- import { buildPasswordSecurityReport, generatePassword, generateTotpCode, normalizePasswordItemType, } from "./password-manager.js";
48
49
  import { deepRepairRuntimePath, formatPathRepairSummary, repairRuntimePath } from "./path-repair.js";
49
50
  import { DistributionManager } from "./distribution-manager.js";
50
51
  import { isLogBusActive, wandTuiLog } from "./tui/log-bus.js";
@@ -100,83 +101,6 @@ function readBuildInfo() {
100
101
  const BUILD_INFO = readBuildInfo();
101
102
  const DISPLAY_VERSION = BUILD_INFO.version || PKG_VERSION;
102
103
  const SERVER_INSTANCE_ID = crypto.randomUUID();
103
- function isExternalAvatarSource(value) {
104
- return /^(https?:|data:)/i.test(value);
105
- }
106
- function normalizePersonaName(value) {
107
- if (typeof value !== "string")
108
- return undefined;
109
- const trimmed = value.trim();
110
- return trimmed || undefined;
111
- }
112
- function normalizePersonaAvatar(value) {
113
- if (typeof value !== "string")
114
- return undefined;
115
- const trimmed = value.trim();
116
- return trimmed || undefined;
117
- }
118
- function resolveStructuredChatPersona(config) {
119
- const persona = config.structuredChatPersona;
120
- if (!persona)
121
- return undefined;
122
- const userName = normalizePersonaName(persona.user?.name);
123
- const userAvatar = normalizePersonaAvatar(persona.user?.avatar);
124
- const assistantName = normalizePersonaName(persona.assistant?.name);
125
- const assistantAvatar = normalizePersonaAvatar(persona.assistant?.avatar);
126
- if (!userName && !userAvatar && !assistantName && !assistantAvatar) {
127
- return undefined;
128
- }
129
- return {
130
- user: userName || userAvatar ? { name: userName, avatar: userAvatar } : undefined,
131
- assistant: assistantName || assistantAvatar ? { name: assistantName, avatar: assistantAvatar } : undefined,
132
- };
133
- }
134
- function resolveStructuredChatAvatarPath(configPath, config, role) {
135
- const avatar = role === "user"
136
- ? config.structuredChatPersona?.user?.avatar
137
- : config.structuredChatPersona?.assistant?.avatar;
138
- if (!avatar || isExternalAvatarSource(avatar)) {
139
- return null;
140
- }
141
- const configDir = resolveConfigDir(configPath);
142
- return path.isAbsolute(avatar) ? avatar : path.resolve(configDir, avatar);
143
- }
144
- async function buildStructuredChatPersonaPayload(configPath, config) {
145
- const persona = resolveStructuredChatPersona(config);
146
- if (!persona)
147
- return undefined;
148
- const buildRole = async (role) => {
149
- const roleConfig = role === "user" ? persona.user : persona.assistant;
150
- if (!roleConfig)
151
- return undefined;
152
- let avatar = roleConfig.avatar;
153
- if (avatar && !isExternalAvatarSource(avatar)) {
154
- const resolvedPath = resolveStructuredChatAvatarPath(configPath, config, role);
155
- if (!resolvedPath) {
156
- avatar = undefined;
157
- }
158
- else {
159
- try {
160
- const fileStat = await stat(resolvedPath);
161
- avatar = fileStat.isFile() ? `/api/structured-chat-avatar/${role}` : undefined;
162
- }
163
- catch {
164
- avatar = undefined;
165
- }
166
- }
167
- }
168
- if (!roleConfig.name && !avatar)
169
- return undefined;
170
- return {
171
- name: roleConfig.name,
172
- avatar,
173
- };
174
- };
175
- const [user, assistant] = await Promise.all([buildRole("user"), buildRole("assistant")]);
176
- if (!user && !assistant)
177
- return undefined;
178
- return { user, assistant };
179
- }
180
104
  // ── Auth helpers ──
181
105
  const requestPrincipals = new WeakMap();
182
106
  function buildRequireAuth(useHttps, storage, config, authService) {
@@ -259,167 +183,6 @@ function authenticateBearerAppToken(req, storage, config) {
259
183
  return null;
260
184
  }
261
185
  }
262
- function resolveRequestServerUrl(req, config, useHttps) {
263
- // 配置了公开 origin 就优先用它:TLS 在 L4 反代(nginx stream / NPM TCP stream)终止时
264
- // node 只看到明文 HTTP,既没有 X-Forwarded-Proto 也没有正确的 scheme,猜不出来。
265
- const configuredOrigin = normalizePublicOrigin(config.publicOrigin);
266
- if (configuredOrigin)
267
- return resolveAppConnectOrigin(configuredOrigin, config);
268
- const requestProtocol = getPublicRequestProtocol(req, useHttps ? "https" : "http");
269
- const requestHost = getPublicRequestHost(req, config);
270
- const originHeader = firstHeaderValue(req.headers.origin);
271
- const browserOrigin = isBrowserExtensionOrigin(originHeader)
272
- ? undefined
273
- : normalizePublicOrigin(originHeader);
274
- return resolveAppConnectOrigin(browserOrigin ?? `${requestProtocol}://${requestHost}`, config);
275
- }
276
- function appTokenLoginPayload(req, storage, config, useHttps) {
277
- return {
278
- appToken: generateAppToken(getEffectivePassword(storage, config), config.appSecret ?? ""),
279
- serverUrl: resolveRequestServerUrl(req, config, useHttps),
280
- };
281
- }
282
- // ── App connection token helpers ──
283
- function generateAppToken(password, secret) {
284
- return crypto.createHmac("sha256", secret).update(password).digest("hex");
285
- }
286
- function verifyAppToken(token, password, secret) {
287
- const expected = generateAppToken(password, secret);
288
- return crypto.timingSafeEqual(Buffer.from(token, "hex"), Buffer.from(expected, "hex"));
289
- }
290
- function encodeConnectCode(url, token) {
291
- return Buffer.from(`${url}#${token}`).toString("base64");
292
- }
293
- function firstHeaderValue(value) {
294
- if (Array.isArray(value))
295
- return value[0];
296
- return value;
297
- }
298
- function firstHeaderListValue(value) {
299
- return firstHeaderValue(value)?.split(",")[0]?.trim();
300
- }
301
- function unquoteHeaderValue(value) {
302
- const trimmed = value.trim();
303
- if (trimmed.length >= 2 && trimmed.startsWith("\"") && trimmed.endsWith("\"")) {
304
- return trimmed.slice(1, -1);
305
- }
306
- return trimmed;
307
- }
308
- function getForwardedParam(req, key) {
309
- const forwarded = firstHeaderListValue(req.headers.forwarded);
310
- if (!forwarded)
311
- return undefined;
312
- const targetKey = key.toLowerCase();
313
- for (const part of forwarded.split(";")) {
314
- const eqIdx = part.indexOf("=");
315
- if (eqIdx < 1)
316
- continue;
317
- const partKey = part.slice(0, eqIdx).trim().toLowerCase();
318
- if (partKey !== targetKey)
319
- continue;
320
- return unquoteHeaderValue(part.slice(eqIdx + 1));
321
- }
322
- return undefined;
323
- }
324
- function normalizePublicProtocol(value) {
325
- const proto = value?.trim().toLowerCase();
326
- if (proto === "http" || proto === "https")
327
- return proto;
328
- return undefined;
329
- }
330
- function getPublicRequestProtocol(req, fallback) {
331
- return (normalizePublicProtocol(firstHeaderListValue(req.headers["x-forwarded-proto"]))
332
- ?? normalizePublicProtocol(getForwardedParam(req, "proto"))
333
- ?? (firstHeaderListValue(req.headers["x-forwarded-ssl"])?.toLowerCase() === "on" ? "https" : undefined)
334
- ?? (firstHeaderListValue(req.headers["x-forwarded-scheme"])?.toLowerCase() === "https" ? "https" : undefined)
335
- ?? fallback);
336
- }
337
- function getPublicRequestHost(req, config) {
338
- return (firstHeaderListValue(req.headers["x-forwarded-host"])
339
- ?? getForwardedParam(req, "host")
340
- ?? req.headers.host
341
- ?? `${config.host}:${config.port}`);
342
- }
343
- function firstQueryStringValue(value) {
344
- if (typeof value === "string")
345
- return value;
346
- if (Array.isArray(value))
347
- return firstQueryStringValue(value[0]);
348
- return undefined;
349
- }
350
- function normalizePublicOrigin(value) {
351
- const raw = value?.trim();
352
- if (!raw)
353
- return undefined;
354
- try {
355
- const parsed = new URL(raw);
356
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
357
- return undefined;
358
- if (!parsed.hostname || parsed.username || parsed.password)
359
- return undefined;
360
- return parsed.origin;
361
- }
362
- catch {
363
- return undefined;
364
- }
365
- }
366
- function isBrowserExtensionOrigin(value) {
367
- if (!value)
368
- return false;
369
- return /^chrome-extension:\/\/[a-z]{32}$/i.test(value)
370
- || /^moz-extension:\/\/[0-9a-f-]+$/i.test(value)
371
- || /^safari-web-extension:\/\//i.test(value);
372
- }
373
- function isPrivateIpv4(address) {
374
- if (address.startsWith("10.") || address.startsWith("192.168."))
375
- return true;
376
- const match = address.match(/^172\.(\d+)\./);
377
- return match ? Number(match[1]) >= 16 && Number(match[1]) <= 31 : false;
378
- }
379
- function preferredLanIpv4() {
380
- const candidates = [];
381
- for (const [name, entries] of Object.entries(os.networkInterfaces())) {
382
- for (const entry of entries ?? []) {
383
- const family = entry.family;
384
- if (entry.internal || (family !== "IPv4" && family !== 4))
385
- continue;
386
- let score = isPrivateIpv4(entry.address) ? 10 : 0;
387
- if (/^(en|eth|wlan)\d+$/i.test(name))
388
- score += 10;
389
- candidates.push({ address: entry.address, score });
390
- }
391
- }
392
- candidates.sort((a, b) => b.score - a.score || a.address.localeCompare(b.address));
393
- return candidates[0]?.address;
394
- }
395
- /**
396
- * App 连接码用于另一台设备。设置页若从 localhost 打开,直接编码浏览器 origin
397
- * 会让客户端连接它自己;监听 0.0.0.0 时自动换成本机优先 LAN IPv4。
398
- */
399
- function resolveAppConnectOrigin(origin, config) {
400
- if (config.host !== "0.0.0.0")
401
- return origin;
402
- try {
403
- const parsed = new URL(origin);
404
- const hostname = parsed.hostname.toLowerCase();
405
- const isLocalOnly = hostname === "localhost"
406
- || hostname === "127.0.0.1"
407
- || hostname === "[::1]"
408
- || hostname === "::1"
409
- || hostname === "0.0.0.0"
410
- || hostname === "[::]";
411
- if (!isLocalOnly)
412
- return parsed.origin;
413
- const lanIp = preferredLanIpv4();
414
- if (!lanIp)
415
- return parsed.origin;
416
- parsed.hostname = lanIp;
417
- return parsed.origin;
418
- }
419
- catch {
420
- return origin;
421
- }
422
- }
423
186
  // ── Startup error handling ──
424
187
  process.on("uncaughtException", (err) => {
425
188
  wandError("服务器异常", err.message, "请检查配置是否正确,或尝试重启服务。");
@@ -703,7 +466,9 @@ export async function startServer(config, configPath, options = {}) {
703
466
  res.json({
704
467
  ok: true,
705
468
  principal,
706
- ...(client === "browser-extension" ? appTokenLoginPayload(req, storage, config, useHttps) : {}),
469
+ ...(client === "browser-extension"
470
+ ? appTokenLoginPayload(req, config, useHttps, getEffectivePassword(storage, config))
471
+ : {}),
707
472
  });
708
473
  });
709
474
  app.post("/api/logout", (req, res) => {
@@ -824,128 +589,17 @@ export async function startServer(config, configPath, options = {}) {
824
589
  serverInstanceId: SERVER_INSTANCE_ID,
825
590
  });
826
591
  }));
827
- // ── Browser extension password vault endpoints ──
592
+ // ── Claude skills & browser extension vault ──
828
593
  app.get("/api/claude-skills", (req, res) => {
829
594
  try {
830
595
  const cwd = resolveSessionCwd(typeof req.query.cwd === "string" ? req.query.cwd : undefined, config.defaultCwd);
831
596
  res.json({ skills: listClaudeSkills(cwd) });
832
597
  }
833
598
  catch (error) {
834
- res.status(400).json({ error: getErrorMessage(error, "无法读取 skills。") });
835
- }
836
- });
837
- app.get("/api/browser-extension/status", (req, res) => {
838
- res.json({
839
- ok: true,
840
- serverUrl: resolveRequestServerUrl(req, config, useHttps),
841
- features: {
842
- loginAutofill: true,
843
- saveLogins: true,
844
- federatedLoginMemory: true,
845
- passwordGenerator: true,
846
- totp: true,
847
- cardsAndIdentities: true,
848
- vaults: true,
849
- securityReport: true,
850
- passkeys: "webauthn-proxy",
851
- },
852
- });
853
- });
854
- app.get("/api/browser-extension/vaults", (_req, res) => {
855
- res.json({ vaults: storage.listPasswordVaults() });
856
- });
857
- app.post("/api/browser-extension/vaults", (req, res) => {
858
- try {
859
- const vault = storage.createPasswordVault(req.body.name);
860
- res.status(201).json({ vault });
861
- }
862
- catch (error) {
863
- res.status(400).json({ error: getErrorMessage(error, "无法创建 vault。") });
599
+ sendRouteError(res, error, "无法读取 skills。");
864
600
  }
865
601
  });
866
- app.get("/api/browser-extension/items", (req, res) => {
867
- const filter = {
868
- q: firstQueryStringValue(req.query.q),
869
- url: firstQueryStringValue(req.query.url),
870
- vaultId: firstQueryStringValue(req.query.vaultId),
871
- type: req.query.type ? normalizePasswordItemType(firstQueryStringValue(req.query.type)) : undefined,
872
- limit: req.query.limit ? Number(firstQueryStringValue(req.query.limit)) : undefined,
873
- };
874
- res.json({ items: storage.listPasswordItems(filter) });
875
- });
876
- app.post("/api/browser-extension/items", (req, res) => {
877
- try {
878
- const item = storage.createPasswordItem(req.body);
879
- res.status(201).json({ item });
880
- }
881
- catch (error) {
882
- res.status(400).json({ error: getErrorMessage(error, "无法保存条目。") });
883
- }
884
- });
885
- app.get("/api/browser-extension/items/:id", (req, res) => {
886
- const item = storage.getPasswordItem(req.params.id);
887
- if (!item) {
888
- res.status(404).json({ error: "条目不存在。" });
889
- return;
890
- }
891
- res.json({ item });
892
- });
893
- app.put("/api/browser-extension/items/:id", (req, res) => {
894
- try {
895
- const item = storage.updatePasswordItem(req.params.id, req.body);
896
- if (!item) {
897
- res.status(404).json({ error: "条目不存在。" });
898
- return;
899
- }
900
- res.json({ item });
901
- }
902
- catch (error) {
903
- res.status(400).json({ error: getErrorMessage(error, "无法更新条目。") });
904
- }
905
- });
906
- app.delete("/api/browser-extension/items/:id", (req, res) => {
907
- if (!storage.deletePasswordItem(req.params.id)) {
908
- res.status(404).json({ error: "条目不存在。" });
909
- return;
910
- }
911
- res.json({ ok: true });
912
- });
913
- app.post("/api/browser-extension/items/:id/use", (req, res) => {
914
- const item = storage.touchPasswordItem(req.params.id);
915
- if (!item) {
916
- res.status(404).json({ error: "条目不存在。" });
917
- return;
918
- }
919
- res.json({ item });
920
- });
921
- app.get("/api/browser-extension/generator/password", (req, res) => {
922
- res.json({
923
- password: generatePassword({
924
- length: Number(firstQueryStringValue(req.query.length)),
925
- digits: firstQueryStringValue(req.query.digits) !== "false",
926
- symbols: firstQueryStringValue(req.query.symbols) !== "false",
927
- }),
928
- });
929
- });
930
- app.post("/api/browser-extension/totp/preview", (req, res) => {
931
- try {
932
- const { secret, digits, period } = req.body;
933
- if (!secret) {
934
- res.status(400).json({ error: "缺少 TOTP secret。" });
935
- return;
936
- }
937
- res.json({
938
- code: generateTotpCode(secret, Date.now(), digits ?? 6, period ?? 30),
939
- period: period ?? 30,
940
- });
941
- }
942
- catch (error) {
943
- res.status(400).json({ error: getErrorMessage(error, "无法生成 TOTP。") });
944
- }
945
- });
946
- app.get("/api/browser-extension/security-report", (_req, res) => {
947
- res.json({ report: buildPasswordSecurityReport(storage.listPasswordItems({ includeArchived: false, limit: 200 })) });
948
- });
602
+ registerVaultRoutes(app, { storage, config, useHttps });
949
603
  // ── Settings endpoints ──
950
604
  const getDistributionSettings = () => distributionManager.getSettings();
951
605
  registerSettingsRoutes(app, {
@@ -963,16 +617,7 @@ export async function startServer(config, configPath, options = {}) {
963
617
  getDistributionSettings,
964
618
  getGithubConnector: () => getGithubConnectorStatus(storage),
965
619
  modelCatalog,
966
- resolveAppConnectCode: (req) => {
967
- const effectivePassword = getEffectivePassword(storage, config);
968
- const configuredOrigin = normalizePublicOrigin(config.publicOrigin);
969
- const requestProtocol = getPublicRequestProtocol(req, useHttps ? "https" : "http");
970
- const requestHost = getPublicRequestHost(req, config);
971
- const browserOrigin = normalizePublicOrigin(firstQueryStringValue(req.query.origin));
972
- const serverUrl = resolveAppConnectOrigin(configuredOrigin ?? browserOrigin ?? `${requestProtocol}://${requestHost}`, config);
973
- const token = generateAppToken(effectivePassword, config.appSecret ?? "");
974
- return { code: encodeConnectCode(serverUrl, token), url: serverUrl };
975
- },
620
+ resolveAppConnectCode: (req) => resolveAppConnectCode(req, config, useHttps, getEffectivePassword(storage, config)),
976
621
  });
977
622
  registerGithubRoutes(app, { storage, requireAdmin, sessions: sessionRegistry });
978
623
  // 任务管理与 Missions 一样只需登录:原生 connected-app 也要能列/建/派发。
@@ -1042,7 +687,7 @@ export async function startServer(config, configPath, options = {}) {
1042
687
  res.status(status).json({ error: error.message, errorCode: error.code });
1043
688
  return;
1044
689
  }
1045
- res.status(500).json({ error: getErrorMessage(error, "提示词优化失败。") });
690
+ sendRouteError(res, error, "提示词优化失败。", 500);
1046
691
  }
1047
692
  }));
1048
693
  registerFileRoutes(app, { storage, defaultCwd: config.defaultCwd });
@@ -1112,7 +757,7 @@ export async function startServer(config, configPath, options = {}) {
1112
757
  res.status(201).json(toSessionDetailDTO(snapshot));
1113
758
  }
1114
759
  catch (error) {
1115
- res.status(400).json({ error: getErrorMessage(error, "无法启动命令。请检查命令是否安装。") });
760
+ sendRouteError(res, error, "无法启动命令。请检查命令是否安装。");
1116
761
  }
1117
762
  }));
1118
763
  // ── WebSocket broadcast layer ──
package/dist/storage.d.ts CHANGED
@@ -84,6 +84,7 @@ export declare class WandStorage {
84
84
  cwd: string;
85
85
  worktree: SessionSnapshot["worktree"];
86
86
  }>;
87
+ /** 侧栏任务顺序:新创建在前。GET /api/tasks 按此顺序填充目录组。 */
87
88
  listWorkspaceTasks(workspaceId: string): WorkspaceTask[];
88
89
  getWorkspaceTask(id: string): WorkspaceTask | null;
89
90
  createWorkspaceTask(input: {
@@ -92,11 +93,13 @@ export declare class WandStorage {
92
93
  worktree?: WorkspaceTaskWorktree | null;
93
94
  cwd?: string | null;
94
95
  status?: WorkspaceTaskStatus;
96
+ milestoneId?: string | null;
95
97
  }): WorkspaceTask;
96
98
  updateWorkspaceTask(id: string, patch: {
97
99
  name?: string;
98
100
  status?: WorkspaceTaskStatus;
99
101
  worktree?: WorkspaceTaskWorktree | null;
102
+ milestoneId?: string | null;
100
103
  }): void;
101
104
  saveWorkspaceTaskLayout(id: string, layout: TaskWindowLayout | null, expectedRevision?: number): {
102
105
  ok: true;
@@ -112,6 +115,7 @@ export declare class WandStorage {
112
115
  cascade?: boolean;
113
116
  }): void;
114
117
  tasksAggregateFingerprint(): string;
118
+ /** GET /api/wand-tasks 的顺序:新创建在前。客户端按此顺序渲染,不再本地排序。 */
115
119
  listWandTasks(workspaceId?: string | null): import("./task-types.js").WandTask[];
116
120
  private mapWandTaskRow;
117
121
  getWandTask(id: string): import("./task-types.js").WandTask | null;
@@ -127,10 +131,25 @@ export declare class WandStorage {
127
131
  priority?: import("./task-types.js").WandTaskPriority;
128
132
  labels?: string[];
129
133
  dueDate?: string | null;
134
+ milestoneId?: string | null;
130
135
  agent?: import("./task-types.js").WandTaskAgent | null;
131
136
  }): import("./task-types.js").WandTask;
132
- updateWandTask(id: string, patch: Partial<Pick<import("./task-types.js").WandTask, "workspaceId" | "workspaceTaskId" | "title" | "titleSource" | "description" | "status" | "priority" | "labels" | "dueDate" | "sortOrder" | "agent">>): import("./task-types.js").WandTask | null;
137
+ updateWandTask(id: string, patch: Partial<Pick<import("./task-types.js").WandTask, "workspaceId" | "workspaceTaskId" | "title" | "titleSource" | "description" | "status" | "priority" | "labels" | "dueDate" | "milestoneId" | "sortOrder" | "agent">>): import("./task-types.js").WandTask | null;
133
138
  deleteWandTask(id: string): void;
139
+ listWandMilestones(): import("./task-types.js").WandTaskMilestone[];
140
+ getWandMilestone(id: string): import("./task-types.js").WandTaskMilestone | null;
141
+ createWandMilestone(input: {
142
+ name: string;
143
+ dueDate?: string | null;
144
+ }): import("./task-types.js").WandTaskMilestone;
145
+ updateWandMilestone(id: string, patch: {
146
+ name?: string;
147
+ dueDate?: string | null;
148
+ }): import("./task-types.js").WandTaskMilestone | null;
149
+ /** 删除里程碑只解绑任务(milestone_id 置空),绝不删除任务。 */
150
+ deleteWandMilestone(id: string): boolean;
151
+ /** 各里程碑下的任务数(看板卡片),用于下拉里的数量提示。 */
152
+ countWandTasksByMilestone(): Record<string, number>;
134
153
  listWandTaskSessionIds(taskId: string): string[];
135
154
  listBoundWandTaskSessionIds(): string[];
136
155
  bindWandTaskSession(taskId: string, sessionId: string): void;