@co0ontty/wand 4.89.0 → 4.89.2-beta.gd071c20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/dist/agent-dispatch.d.ts +5 -0
  2. package/dist/agent-dispatch.js +8 -0
  3. package/dist/ai-team-employee-binding.d.ts +1 -0
  4. package/dist/ai-team-employee-binding.js +1 -1
  5. package/dist/ai-team-runner.d.ts +1 -1
  6. package/dist/ai-team-runner.js +13 -5
  7. package/dist/ai-team-types.d.ts +2 -2
  8. package/dist/ai-team-types.js +3 -2
  9. package/dist/build-info.json +4 -4
  10. package/dist/commit-task-archive.d.ts +3 -0
  11. package/dist/commit-task-archive.js +15 -0
  12. package/dist/config.js +1 -0
  13. package/dist/daemon-maintenance-targets.js +1 -1
  14. package/dist/decision-expert-employee.d.ts +1 -1
  15. package/dist/decision-expert-employee.js +3 -3
  16. package/dist/decision-expert-service.d.ts +6 -5
  17. package/dist/decision-expert-service.js +75 -49
  18. package/dist/decision-types.d.ts +2 -1
  19. package/dist/default-employee.d.ts +5 -1
  20. package/dist/default-employee.js +17 -5
  21. package/dist/employee-text.d.ts +28 -0
  22. package/dist/employee-text.js +124 -0
  23. package/dist/fixed-employee-avatar.d.ts +19 -0
  24. package/dist/fixed-employee-avatar.js +13 -0
  25. package/dist/git-quick-commit.d.ts +13 -4
  26. package/dist/git-quick-commit.js +112 -33
  27. package/dist/local-model-setup.d.ts +7 -1
  28. package/dist/local-model-setup.js +54 -7
  29. package/dist/local-model-types.d.ts +2 -1
  30. package/dist/plush-avatar.d.ts +100 -0
  31. package/dist/plush-avatar.js +96 -0
  32. package/dist/process-manager.d.ts +4 -1
  33. package/dist/process-manager.js +9 -7
  34. package/dist/server-employee-routes.d.ts +6 -2
  35. package/dist/server-employee-routes.js +55 -53
  36. package/dist/server-file-routes.js +61 -25
  37. package/dist/server-local-model-routes.js +12 -0
  38. package/dist/server-session-routes.d.ts +2 -1
  39. package/dist/server-session-routes.js +13 -10
  40. package/dist/server-speech-routes.d.ts +2 -0
  41. package/dist/server-speech-routes.js +7 -1
  42. package/dist/server-task-routes.d.ts +3 -0
  43. package/dist/server-task-routes.js +19 -10
  44. package/dist/server.js +25 -8
  45. package/dist/session-ai-context.d.ts +6 -4
  46. package/dist/session-ai-context.js +10 -9
  47. package/dist/silicon-employee-dispatch.d.ts +4 -6
  48. package/dist/silicon-employee-dispatch.js +9 -6
  49. package/dist/speech-polisher-employee.d.ts +6 -0
  50. package/dist/speech-polisher-employee.js +23 -0
  51. package/dist/speech-polisher-identity.d.ts +4 -0
  52. package/dist/speech-polisher-identity.js +4 -0
  53. package/dist/speech-polisher-service.d.ts +31 -0
  54. package/dist/speech-polisher-service.js +88 -0
  55. package/dist/speech-service.d.ts +3 -1
  56. package/dist/speech-service.js +15 -4
  57. package/dist/speech-support.d.ts +6 -0
  58. package/dist/speech-support.js +26 -0
  59. package/dist/speech-types.d.ts +5 -0
  60. package/dist/storage.d.ts +13 -4
  61. package/dist/storage.js +84 -32
  62. package/dist/structured-session-manager.d.ts +3 -2
  63. package/dist/structured-session-manager.js +11 -7
  64. package/dist/system-employee.d.ts +6 -4
  65. package/dist/system-employee.js +13 -10
  66. package/dist/types.d.ts +3 -1
  67. package/dist/user-memory.d.ts +2 -0
  68. package/dist/user-memory.js +1 -1
  69. package/dist/wand-task-sync.js +13 -2
  70. package/dist/web-ui/content/ai-teams.js +13 -13
  71. package/dist/web-ui/content/plush-avatar.js +4220 -0
  72. package/dist/web-ui/content/scripts.js +572 -209
  73. package/dist/web-ui/content/styles.css +1 -1
  74. package/dist/web-ui/content/tailwind.css +1 -1
  75. package/dist/web-ui/embedded-assets.d.ts +2 -0
  76. package/dist/web-ui/embedded-assets.js +5 -3
  77. package/dist/web-ui/index.js +3 -3
  78. package/dist/web-ui/provider-identity.d.ts +2 -0
  79. package/dist/web-ui/provider-identity.js +7 -1
  80. package/dist/web-ui/scripts.d.ts +7 -0
  81. package/dist/web-ui/scripts.js +30 -5
  82. package/package.json +3 -1
@@ -1,8 +1,7 @@
1
+ import { isFixedAvatarEmployee } from "./fixed-employee-avatar.js";
1
2
  import { randomUUID } from "node:crypto";
2
3
  import { AI_TEAM_MAX_CANDIDATES, SILICON_EMPLOYEE_AVATAR_MAX_CHARS, agentKey, isBuiltinSiliconEmployee, parseSiliconEmployeeTags, siliconEmployeeTags, } from "./ai-team-types.js";
3
4
  import { getErrorMessage } from "./error-utils.js";
4
- import { DECISION_EXPERT_KEY, isLocalDecisionModel } from "./decision-expert-identity.js";
5
- import { OPENROUTER_FREE_SELECTOR, isOpenRouterFreeSelector } from "./openrouter-free-selection.js";
6
5
  import { asyncRoute } from "./express-async.js";
7
6
  import { bodyObject, sendRouteError, text } from "./server-request.js";
8
7
  import { parseTaskAgent } from "./server-task-routes.js";
@@ -11,10 +10,11 @@ import { employeeCliAvailable } from "./silicon-employee-dispatch.js";
11
10
  import { EMPLOYEE_CREATION_DEFAULTS_PREF, resolveSiliconEmployeeDefaults, } from "./silicon-employee-defaults.js";
12
11
  import { availableEmployeeProviders, generateSiliconEmployeeDraft, SiliconEmployeeDraftError, validateStructuredEmployeeDraft, } from "./silicon-employee-draft.js";
13
12
  import { SESSION_PROVIDERS } from "./session-provider.js";
13
+ import { parsePlushAvatar, parsePlushCatAvatar } from "./plush-avatar.js";
14
14
  import { DEFAULT_WAND_TASK_AGENT_KIND } from "./task-types.js";
15
15
  import { EMPLOYEE_KNOWLEDGE_MAX_ENTRIES } from "./employee-knowledge-types.js";
16
16
  /** 员工起草没有会话上下文,按「默认 provider」解析,与任务标题 / 提示词优化一致。 */
17
- function employeeDraftAiOptions(storage, config) {
17
+ function employeeDraftAiOptions(storage, config, free) {
18
18
  if (!config)
19
19
  return {};
20
20
  const provider = config.defaultProvider ?? "claude";
@@ -25,7 +25,7 @@ function employeeDraftAiOptions(storage, config) {
25
25
  command: provider === "qoder" ? "qodercli" : provider,
26
26
  selectedModel: null,
27
27
  thinkingEffort: config.defaultThinkingEffort,
28
- }, config, storage.getSystemSiliconEmployee());
28
+ }, config, storage.getSystemSiliconEmployee(), free);
29
29
  }
30
30
  function sendEmployeeError(res, error, context) {
31
31
  const message = getErrorMessage(error, "硅基员工操作失败。");
@@ -59,9 +59,14 @@ function boundedText(value, label, min, max) {
59
59
  return result;
60
60
  }
61
61
  function parseAvatar(value, name) {
62
+ if (value !== undefined && value !== null && typeof value !== "string") {
63
+ throw new Error(`员工「${name}」的头像格式无效。`);
64
+ }
62
65
  const avatar = text(value);
63
66
  if (!avatar || /^cat:\d{1,2}$/.test(avatar))
64
67
  return avatar;
68
+ if (parsePlushAvatar(avatar) || parsePlushCatAvatar(avatar))
69
+ return avatar;
65
70
  if (!/^data:image\/(png|jpeg|webp);base64,[A-Za-z0-9+/=]+$/.test(avatar)) {
66
71
  throw new Error(`员工「${name}」的头像格式无效。`);
67
72
  }
@@ -70,7 +75,7 @@ function parseAvatar(value, name) {
70
75
  }
71
76
  return avatar;
72
77
  }
73
- /** 内置员工的锁定字段:名字 / 职责 / 人设 / 头像写死在服务端,只有候选可改。 */
78
+ /** Other built-ins keep profile locks; the two stable cat identities lock only avatar. */
74
79
  const SYSTEM_EMPLOYEE_LOCKED_FIELDS = [
75
80
  { key: "name", label: "名字" },
76
81
  { key: "duty", label: "职责" },
@@ -78,14 +83,14 @@ const SYSTEM_EMPLOYEE_LOCKED_FIELDS = [
78
83
  { key: "avatar", label: "头像" },
79
84
  ];
80
85
  /**
81
- * 内置员工只接受 agents;其余字段即使前端遗漏地发上来也必须与服务端一致,
82
- * 不一致直接拒绝(静默忽略会让客户端以为改成功了)。
86
+ * 其他内置员工只接受 agents;固定猫头员工另外接受资料的部分更新。
87
+ * 锁定字段即使由旧客户端或直接 API 发来,也必须一致;不一致明确拒绝。
83
88
  */
84
89
  export function parseSystemEmployeeAgents(value, existing) {
85
90
  const body = bodyObject(value);
86
- for (const field of SYSTEM_EMPLOYEE_LOCKED_FIELDS) {
91
+ for (const field of SYSTEM_EMPLOYEE_LOCKED_FIELDS.filter((field) => !isFixedAvatarEmployee(existing) || field.key === "avatar")) {
87
92
  const incoming = body[field.key];
88
- if (incoming === undefined || incoming === null)
93
+ if (incoming === undefined || (incoming === null && !isFixedAvatarEmployee(existing)))
89
94
  continue;
90
95
  const current = existing[field.key];
91
96
  const isSame = typeof incoming === "string" && incoming === current;
@@ -96,26 +101,41 @@ export function parseSystemEmployeeAgents(value, existing) {
96
101
  if (body.tags !== undefined && JSON.stringify(body.tags) !== JSON.stringify(siliconEmployeeTags(existing))) {
97
102
  throw new Error("该员工是内置的,标签不可修改。");
98
103
  }
104
+ if (isFixedAvatarEmployee(existing) && body.id !== undefined && body.id !== existing.id)
105
+ throw new Error("该员工是内置的,身份不可修改。");
99
106
  if (body.systemKey !== undefined && body.systemKey !== existing.systemKey) {
100
107
  throw new Error("该员工是内置的,身份不可修改。");
101
108
  }
102
- if (!Array.isArray(body.agents))
109
+ return isFixedAvatarEmployee(existing) && body.agents === undefined ? existing.agents : parseEmployeeAgents(body.agents);
110
+ }
111
+ /** Partial edits preserve omitted fields and the raw saved prompt, not a generated read projection. */
112
+ export function parseFixedAvatarEmployeeInput(value, existing, now) {
113
+ const body = bodyObject(value);
114
+ const agents = parseSystemEmployeeAgents(body, existing);
115
+ const name = body.name === undefined ? existing.name : boundedText(body.name, "员工名字", 1, 40);
116
+ const duty = body.duty === undefined ? existing.duty : text(body.duty);
117
+ const prompt = body.prompt === undefined ? existing.prompt : text(body.prompt);
118
+ if (duty.length > 2000)
119
+ throw new Error("员工职责不能超过 2000 个字符。");
120
+ if (prompt.length > 20_000)
121
+ throw new Error("员工设定 Prompt 不能超过 20000 个字符。");
122
+ return { ...existing, name, duty, prompt, agents, updatedAt: now };
123
+ }
124
+ /** One validation owner for every employee candidate list, including legacy single agents. */
125
+ function parseEmployeeAgents(value) {
126
+ if (!Array.isArray(value))
103
127
  throw new Error("请提供执行候选数组(CLI / SDK)。");
104
- if (body.agents.length === 0)
128
+ if (value.length === 0)
105
129
  throw new Error("至少需要一个执行候选。");
106
- if (body.agents.length > AI_TEAM_MAX_CANDIDATES) {
130
+ if (value.length > AI_TEAM_MAX_CANDIDATES) {
107
131
  throw new Error(`最多 ${AI_TEAM_MAX_CANDIDATES} 个执行候选。`);
108
132
  }
109
- const agents = body.agents.map((rawAgent) => {
133
+ const agents = value.map((rawAgent) => {
110
134
  const parsed = parseTaskAgent(rawAgent ?? {});
111
135
  if (!parsed)
112
136
  throw new Error("请选择有效的 CLI 工具或 Wand Agent。");
113
137
  if (parsed.kind !== "structured")
114
138
  throw new Error("硅基员工只支持结构化会话。");
115
- if (existing.systemKey === DECISION_EXPERT_KEY && (parsed.provider !== "pi" || parsed.engine !== "sdk"
116
- || !(isLocalDecisionModel(parsed.model) || parsed.model === OPENROUTER_FREE_SELECTOR || isOpenRouterFreeSelector(parsed.model)))) {
117
- throw new Error("决策专家仅支持 LAYA 或 Wand 免费分组的决策候选;请使用 Wand 内部执行入口。");
118
- }
119
139
  return parsed;
120
140
  });
121
141
  if (new Set(agents.map(agentKey)).size !== agents.length) {
@@ -132,40 +152,16 @@ export function parseSiliconEmployeeInput(value, existing, now) {
132
152
  const prompt = text(body.prompt);
133
153
  if (prompt.length > 20_000)
134
154
  throw new Error("员工设定 Prompt 不能超过 20000 个字符。");
135
- let agents;
136
- if (Array.isArray(body.agents)) {
137
- if (body.agents.length === 0)
138
- throw new Error("至少需要一个执行候选。");
139
- if (body.agents.length > AI_TEAM_MAX_CANDIDATES) {
140
- throw new Error(`最多 ${AI_TEAM_MAX_CANDIDATES} 个执行候选。`);
141
- }
142
- agents = body.agents.map((rawAgent) => {
143
- const parsed = parseTaskAgent(rawAgent ?? {});
144
- if (!parsed)
145
- throw new Error("请选择有效的 CLI 工具或 Wand Agent。");
146
- if (parsed.kind !== "structured")
147
- throw new Error("硅基员工只支持结构化会话。");
148
- return parsed;
149
- });
150
- }
151
- else if (body.agents !== undefined && body.agents !== null) {
155
+ let candidates;
156
+ if (Array.isArray(body.agents))
157
+ candidates = body.agents;
158
+ else if (body.agents !== undefined && body.agents !== null)
152
159
  throw new Error("候选执行配置必须是数组。");
153
- }
154
- else if (body.agent !== undefined && body.agent !== null) {
155
- const singleAgent = parseTaskAgent(body.agent);
156
- if (!singleAgent)
157
- throw new Error("请选择有效的 CLI 工具或 Wand Agent。");
158
- if (singleAgent.kind !== "structured")
159
- throw new Error("硅基员工只支持结构化会话。");
160
- agents = [singleAgent];
161
- }
162
- else {
160
+ else if (body.agent !== undefined && body.agent !== null)
161
+ candidates = [body.agent];
162
+ else
163
163
  throw new Error("至少需要一个执行候选。");
164
- }
165
- const seenKeys = new Set(agents.map(agentKey));
166
- if (seenKeys.size !== agents.length) {
167
- throw new Error("执行候选存在重复。");
168
- }
164
+ const agents = parseEmployeeAgents(candidates);
169
165
  const avatar = parseAvatar(body.avatar, name);
170
166
  const tags = parseSiliconEmployeeTags(body.tags === undefined ? existing?.tags ?? [] : body.tags);
171
167
  const id = existing?.id ?? `e_${randomUUID().replace(/-/g, "")}`;
@@ -203,7 +199,7 @@ export function registerSiliconEmployeeRoutes(app, deps) {
203
199
  const available = availableEmployeeProviders(resolveDefaults().provider, isProviderAvailable);
204
200
  return available.length ? available : [...SESSION_PROVIDERS];
205
201
  };
206
- const generateDraft = deps.generateDraft ?? ((request) => generateSiliconEmployeeDraft(request.expectation, employeeDraftAiOptions(storage, config), {
202
+ const generateDraft = deps.generateDraft ?? ((request) => generateSiliconEmployeeDraft(request.expectation, employeeDraftAiOptions(storage, config, deps.free), {
207
203
  cwd: config?.defaultCwd || process.cwd(),
208
204
  language: config?.language ?? "",
209
205
  existingNames: request.existingNames,
@@ -354,9 +350,15 @@ export function registerSiliconEmployeeRoutes(app, deps) {
354
350
  if (!existing)
355
351
  throw new Error(`硅基员工「${req.params.id}」不存在。`);
356
352
  const now = new Date().toISOString();
357
- const employee = isBuiltinSiliconEmployee(existing)
358
- ? { ...existing, agents: parseSystemEmployeeAgents(req.body, existing), updatedAt: now }
359
- : parseSiliconEmployeeInput(req.body, existing, now);
353
+ const definition = storage.getSiliconEmployeeDefinition(existing.id);
354
+ const body = bodyObject(req.body);
355
+ // An unchanged projected prompt is an echo, not a request to persist generated preferences.
356
+ const input = body.prompt === existing.prompt ? { ...body, prompt: definition.prompt } : body;
357
+ const employee = isFixedAvatarEmployee(definition)
358
+ ? parseFixedAvatarEmployeeInput(input, definition, now)
359
+ : isBuiltinSiliconEmployee(existing)
360
+ ? { ...existing, agents: parseSystemEmployeeAgents(input, existing), updatedAt: now }
361
+ : parseSiliconEmployeeInput(input, existing, now);
360
362
  storage.saveSiliconEmployee(employee);
361
363
  notifyEmployee(employee.id);
362
364
  res.json(employee);
@@ -10,6 +10,7 @@ import { asyncRoute } from "./express-async.js";
10
10
  import { sendRouteError } from "./server-request.js";
11
11
  import { isBlockedFolderPath, isPathWithinBase, normalizeFolderPath } from "./middleware/path-safety.js";
12
12
  import { parseBoundedInteger } from "./request-limits.js";
13
+ import { runGitRawAsync } from "./git-utils.js";
13
14
  const execAsync = promisify(exec);
14
15
  const DIRECTORY_MAX_ITEMS = 200;
15
16
  const MAX_TEXT_PREVIEW_SIZE = 512 * 1024;
@@ -593,7 +594,10 @@ export function registerFileRoutes(app, deps) {
593
594
  const cwd = typeof req.query.cwd === "string" ? req.query.cwd : defaultCwd;
594
595
  const maxDepth = parseBoundedInteger(req.query.depth, 5, 0, 8);
595
596
  const maxResults = parseBoundedInteger(req.query.limit, 50, 1, 200);
596
- const ignoredDirectories = new Set([".git", "node_modules", ".next", "dist", "build", "coverage", ".wand-uploads", ".wand-team"]);
597
+ const includeGenerated = req.query.includeGenerated === "true";
598
+ // Internal metadata remains excluded even when the user opts into generated files.
599
+ const ignoredDirectories = new Set([".git", ".wand", ".wand-uploads", ".wand-team"]);
600
+ const generatedDirectories = new Set(["node_modules", ".next", "dist", "build", "coverage"]);
597
601
  const maxVisitedEntries = 20_000;
598
602
  let resolvedCwd;
599
603
  try {
@@ -612,38 +616,70 @@ export function registerFileRoutes(app, deps) {
612
616
  return;
613
617
  }
614
618
  try {
619
+ let gitIgnored = new Set();
620
+ try {
621
+ const ignored = await runGitRawAsync(["ls-files", "--others", "--ignored", "--exclude-standard", "--directory", "-z"], resolvedCwd, { timeout: 2_000, maxBuffer: 2 * 1024 * 1024 });
622
+ gitIgnored = new Set(ignored.split("\0").filter(Boolean).map((entry) => path.resolve(resolvedCwd, entry.replace(/\/$/, ""))));
623
+ }
624
+ catch {
625
+ // Ordinary directories and hosts without Git still retain the existing known-directory exclusions.
626
+ }
615
627
  const results = [];
628
+ const deferred = [];
616
629
  const queryLower = query.toLowerCase();
617
630
  let visitedEntries = 0;
618
- async function searchDir(dirPath, currentDepth) {
619
- if (currentDepth > maxDepth || results.length >= maxResults || visitedEntries >= maxVisitedEntries)
620
- return;
621
- const entries = await readdir(dirPath, { withFileTypes: true });
622
- for (const entry of entries) {
623
- if (results.length >= maxResults || visitedEntries >= maxVisitedEntries)
624
- break;
625
- visitedEntries += 1;
626
- if (entry.isDirectory() && ignoredDirectories.has(entry.name))
631
+ async function searchDir(dirPath, currentDepth, generated = false) {
632
+ const directories = [{ path: dirPath, depth: currentDepth }];
633
+ for (let index = 0; index < directories.length && visitedEntries < maxVisitedEntries; index += 1) {
634
+ const directory = directories[index];
635
+ if (directory.depth > maxDepth)
627
636
  continue;
628
- const entryPath = path.join(dirPath, entry.name);
629
- const matchIndex = entry.name.toLowerCase().indexOf(queryLower);
630
- if (matchIndex !== -1) {
631
- results.push({
632
- path: entryPath,
633
- name: entry.name,
634
- type: entry.isDirectory() ? "dir" : "file",
635
- matchScore: matchIndex,
636
- });
637
+ const entries = await readdir(directory.path, { withFileTypes: true });
638
+ for (const entry of entries) {
639
+ if (visitedEntries >= maxVisitedEntries)
640
+ break;
641
+ visitedEntries += 1;
642
+ if (entry.isDirectory() && ignoredDirectories.has(entry.name))
643
+ continue;
644
+ const entryPath = path.join(directory.path, entry.name);
645
+ if (!generated && entry.isDirectory() && generatedDirectories.has(entry.name)) {
646
+ if (includeGenerated)
647
+ deferred.push({ path: entryPath, name: entry.name, depth: directory.depth + 1 });
648
+ continue;
649
+ }
650
+ if (!generated && gitIgnored.has(entryPath))
651
+ continue;
652
+ const matchIndex = entry.name.toLowerCase().indexOf(queryLower);
653
+ if (matchIndex !== -1) {
654
+ results.push({
655
+ path: entryPath,
656
+ name: entry.name,
657
+ type: entry.isDirectory() ? "dir" : "file",
658
+ matchScore: matchIndex,
659
+ generated,
660
+ depth: directory.depth,
661
+ });
662
+ }
663
+ if (entry.isDirectory() && directory.depth < maxDepth)
664
+ directories.push({ path: entryPath, depth: directory.depth + 1 });
637
665
  }
638
- if (entry.isDirectory())
639
- await searchDir(entryPath, currentDepth + 1);
640
666
  }
641
667
  }
642
668
  await searchDir(resolvedCwd, 0);
643
- results.sort((a, b) => a.matchScore !== b.matchScore
644
- ? a.matchScore - b.matchScore
645
- : a.name.localeCompare(b.name));
646
- res.json({ results: results.slice(0, maxResults), query, cwd: resolvedCwd });
669
+ // Complete the project-file pass before dependencies can consume the bounded result budget.
670
+ for (const directory of deferred) {
671
+ if (visitedEntries >= maxVisitedEntries)
672
+ break;
673
+ const matchScore = directory.name.toLowerCase().indexOf(queryLower);
674
+ if (matchScore !== -1)
675
+ results.push({ path: directory.path, name: directory.name, type: "dir", matchScore, generated: true, depth: directory.depth - 1 });
676
+ await searchDir(directory.path, directory.depth, true);
677
+ }
678
+ results.sort((a, b) => Number(a.generated) - Number(b.generated)
679
+ || Number(b.name.toLowerCase() === queryLower) - Number(a.name.toLowerCase() === queryLower)
680
+ || a.depth - b.depth || a.matchScore - b.matchScore || a.name.localeCompare(b.name));
681
+ res.json({ results: results.slice(0, maxResults), query, cwd: resolvedCwd,
682
+ truncated: results.length > maxResults || visitedEntries >= maxVisitedEntries });
647
683
  }
648
684
  catch (error) {
649
685
  sendRouteError(res, error, "搜索失败。可能原因:路径不存在或权限不足。");
@@ -51,6 +51,18 @@ export function registerLocalModelRoutes(app, deps) {
51
51
  next(error);
52
52
  }
53
53
  });
54
+ app.patch("/api/local-models/speech/settings", deps.requireAdmin, async (req, res, next) => {
55
+ try {
56
+ if (!req.body || Array.isArray(req.body) || Object.keys(req.body).length !== 1 || typeof req.body.enabled !== "boolean") {
57
+ throw new DecisionError("INVALID_REQUEST", "仅允许修改语音启用状态。");
58
+ }
59
+ await deps.models.setSpeechEnabled(req.body.enabled);
60
+ res.status(202).json(await deps.models.status());
61
+ }
62
+ catch (error) {
63
+ next(error);
64
+ }
65
+ });
54
66
  const errors = (error, _req, res, _next) => {
55
67
  if (error instanceof DecisionError || error instanceof SpeechError) {
56
68
  res.status(error.status).json({ error: error.message, code: error.code });
@@ -3,6 +3,7 @@ import { ProcessManager } from "./process-manager.js";
3
3
  import { StructuredSessionManager } from "./structured-session-manager.js";
4
4
  import { WandStorage } from "./storage.js";
5
5
  import { ExecutionMode, ProcessEvent, SessionSnapshot, SessionSource, WandConfig } from "./types.js";
6
+ import type { OpenRouterFreeModelsService } from "./openrouter-free-models.js";
6
7
  import { toSessionListItemDTO } from "./session-transport.js";
7
8
  import { type ProviderHistoryDeleter, SessionRegistry } from "./session-registry.js";
8
9
  import { type PiRecommendationRuntime } from "./server-pi-recommendation-routes.js";
@@ -46,6 +47,6 @@ export interface SessionDirectoryTreeResponse {
46
47
  }
47
48
  export declare function buildSessionListPage(sessions: SessionSnapshot[], offset: number, limit: number): SessionListPage;
48
49
  export declare function buildSessionDirectoryTree(sessions: SessionSnapshot[], customNames?: ReadonlyMap<string, string>): SessionDirectoryTreeResponse;
49
- export declare function registerSessionRoutes(app: Express, processes: ProcessManager, structured: StructuredSessionManager, storage: WandStorage, defaultMode: ExecutionMode, config: WandConfig, sessions: SessionRegistry, onSessionCreated?: (cwd: string | undefined | null) => void, onCompletionViewed?: (event: ProcessEvent) => void, decisions?: PiRecommendationRuntime): void;
50
+ export declare function registerSessionRoutes(app: Express, processes: ProcessManager, structured: StructuredSessionManager, storage: WandStorage, defaultMode: ExecutionMode, config: WandConfig, sessions: SessionRegistry, onSessionCreated?: (cwd: string | undefined | null) => void, onCompletionViewed?: (event: ProcessEvent) => void, decisions?: PiRecommendationRuntime, free?: Pick<OpenRouterFreeModelsService, "resolveForCall">): void;
50
51
  export declare function registerClaudeHistoryRoutes(app: Express, processes: ProcessManager, storage: WandStorage): void;
51
52
  export {};
@@ -11,7 +11,7 @@ import { resolveSessionCwd } from "./session-cwd.js";
11
11
  import { resolveSystemAiContext } from "./session-ai-context.js";
12
12
  import { getGitStatusAsync, QuickCommitError, runQuickCommitWithFallback, runTagHead, runPush, generateCommitMessageOnly, } from "./git-quick-commit.js";
13
13
  import { getErrorMessage } from "./error-utils.js";
14
- import { archiveCommitTasks } from "./commit-task-archive.js";
14
+ import { archiveCommitTasks, archiveCommitStandaloneSessions } from "./commit-task-archive.js";
15
15
  import { buildIterationCommitContext, COMMIT_CONTEXT_MODE_PREF_KEY, isCommitContextMode, readCommitContextMode, resolveCommitContextInput, } from "./iteration-log.js";
16
16
  import { inferProviderFromCommand, isSessionProvider, providerCliCommand, SESSION_PROVIDERS } from "./session-provider.js";
17
17
  import { selectEmployeeCandidate } from "./silicon-employee-dispatch.js";
@@ -366,7 +366,7 @@ function canMergeSession(snapshot) {
366
366
  function isMergeActionAllowed(snapshot) {
367
367
  return snapshot.status !== "running";
368
368
  }
369
- export function registerSessionRoutes(app, processes, structured, storage, defaultMode, config, sessions, onSessionCreated, onCompletionViewed, decisions) {
369
+ export function registerSessionRoutes(app, processes, structured, storage, defaultMode, config, sessions, onSessionCreated, onCompletionViewed, decisions, free) {
370
370
  registerPiRecommendationRoute(app, { structured, sessions, config, decisions });
371
371
  const wantsCompactTools = (req) => req.get("X-Wand-Tool-Projection") === "compact" || req.query.compactTools === "1";
372
372
  const sessionResponseDTO = (snapshot, req) => {
@@ -497,9 +497,9 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
497
497
  res.status(400).json({ error: "请选择有效的执行引擎。" });
498
498
  return;
499
499
  }
500
- const requestedEngine = (employee && !requestedProvider) || !rawEngine
501
- ? undefined
502
- : rawEngine;
500
+ const requestedEngine = employee && !requestedProvider
501
+ ? employeeAgent?.engine ?? "cli"
502
+ : rawEngine ? rawEngine : undefined;
503
503
  if (requestedEngine === "sdk" && provider !== "pi") {
504
504
  res.status(400).json({ error: "Wand Agent 只支持 Pi 结构化会话。" });
505
505
  return;
@@ -1100,7 +1100,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1100
1100
  }
1101
1101
  const body = (req.body ?? {});
1102
1102
  try {
1103
- const ai = resolveSystemAiContext(snapshot, config, storage.getSystemSiliconEmployee());
1103
+ const ai = resolveSystemAiContext(snapshot, config, storage.getSystemSiliconEmployee(), free);
1104
1104
  // 自动 message + 迭代模式:拿本轮提示词当输入;提交成功后标记已用掉。
1105
1105
  const context = await iterationCommitInput(storage, snapshot, body);
1106
1106
  const result = await runQuickCommitWithFallback({
@@ -1120,6 +1120,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1120
1120
  ? storage.markIterationPromptsConsumed(context.entryIds, result.commit.hash)
1121
1121
  : 0;
1122
1122
  let archivedTaskIds = [];
1123
+ const archivedSessionIds = [];
1123
1124
  let archiveError;
1124
1125
  if (body.archiveRelatedTasks === true && result.ok && result.commit?.hash) {
1125
1126
  try {
@@ -1129,15 +1130,17 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1129
1130
  archivedTaskIds = archiveCommitTasks(storage, snapshot, context.entryIds, {
1130
1131
  workspaceTaskId: openedTaskId || null,
1131
1132
  });
1133
+ archiveCommitStandaloneSessions(storage, sessions, snapshot, archivedSessionIds);
1132
1134
  }
1133
1135
  catch (error) {
1134
- archiveError = getErrorMessage(error, "归档关联任务失败。");
1136
+ archiveError = getErrorMessage(error, "归档任务与会话失败。");
1135
1137
  console.error("[QuickCommit] Failed to archive related tasks:", archiveError);
1136
1138
  }
1137
1139
  }
1138
1140
  res.json({
1139
1141
  ...result,
1140
1142
  archivedTaskIds,
1143
+ archivedSessionIds,
1141
1144
  ...(archiveError ? { archiveError } : {}),
1142
1145
  commitContext: {
1143
1146
  source: commitMessageFromIteration(body) && context.digest ? "iteration" : "diff",
@@ -1199,7 +1202,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1199
1202
  }
1200
1203
  const body = (req.body ?? {});
1201
1204
  try {
1202
- const ai = resolveSystemAiContext(snapshot, config, storage.getSystemSiliconEmployee());
1205
+ const ai = resolveSystemAiContext(snapshot, config, storage.getSystemSiliconEmployee(), free);
1203
1206
  const context = await iterationCommitInput(storage, snapshot, body);
1204
1207
  const result = await generateCommitMessageOnly(snapshot.cwd, config.language ?? "", {
1205
1208
  ...ai,
@@ -1236,7 +1239,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1236
1239
  }
1237
1240
  const body = (req.body ?? {});
1238
1241
  try {
1239
- const ai = resolveSystemAiContext(snapshot, config, storage.getSystemSiliconEmployee());
1242
+ const ai = resolveSystemAiContext(snapshot, config, storage.getSystemSiliconEmployee(), free);
1240
1243
  const result = await runTagHead({
1241
1244
  cwd: snapshot.cwd,
1242
1245
  language: config.language ?? "",
@@ -1733,7 +1736,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1733
1736
  sendRouteError(res, error, "无法停止会话。");
1734
1737
  }
1735
1738
  });
1736
- // 归档会停止会话,但保留 Wand 会话 ID 和 provider session id。恢复时用这个 ID 续上。
1739
+ // 归档只隐藏会话,保留运行进程、Wand 会话 ID 与 provider session id。
1737
1740
  app.post("/api/sessions/:id/archive", (req, res) => {
1738
1741
  try {
1739
1742
  const updated = sessions.setArchived(req.params.id, true);
@@ -1,8 +1,10 @@
1
1
  import { type Express, type RequestHandler } from "express";
2
2
  import { type SpeechService } from "./speech-service.js";
3
+ import type { SpeechPolisherService } from "./speech-polisher-service.js";
3
4
  /** Mounted behind /api authentication. Clients may transcribe, only admins configure/download. */
4
5
  export declare function registerSpeechRoutes(app: Express, deps: {
5
6
  speech: SpeechService;
6
7
  requireSessions: RequestHandler;
7
8
  requireAdmin: RequestHandler;
9
+ polisher?: Pick<SpeechPolisherService, "polish">;
8
10
  }): void;
@@ -53,6 +53,7 @@ export function registerSpeechRoutes(app, deps) {
53
53
  next();
54
54
  };
55
55
  app.post("/api/speech/transcribe", admission, express.raw({ type: "audio/wav", limit: SPEECH_MAX_WAV_BYTES }), async (req, res, next) => {
56
+ const started = Date.now();
56
57
  const abort = new AbortController();
57
58
  const disconnected = () => { if (!res.writableEnded)
58
59
  abort.abort(); };
@@ -61,8 +62,13 @@ export function registerSpeechRoutes(app, deps) {
61
62
  if (!Buffer.isBuffer(req.body))
62
63
  throw new SpeechError("INVALID_AUDIO", "未收到 WAV 音频。");
63
64
  const result = await deps.speech.transcribe(req.body, abort.signal);
65
+ const polished = deps.polisher ? await deps.polisher.polish(result.text, abort.signal, Math.min(45_000, 123_000 - (Date.now() - started))).catch(() => {
66
+ abort.signal.throwIfAborted();
67
+ return { text: result.text, originalText: result.text, optimized: false,
68
+ optimizationError: "口述整理暂不可用,已保留原始转写。" };
69
+ }) : null;
64
70
  if (!res.destroyed)
65
- res.json(result);
71
+ res.json({ ...result, ...(polished ?? {}) });
66
72
  }
67
73
  catch (error) {
68
74
  if (!res.destroyed)
@@ -1,6 +1,7 @@
1
1
  import type { Express } from "express";
2
2
  import { type WandStorage } from "./storage.js";
3
3
  import { generateWandTaskTitle } from "./task-title.js";
4
+ import type { OpenRouterFreeModelsService } from "./openrouter-free-models.js";
4
5
  import type { SessionRegistry } from "./session-registry.js";
5
6
  import type { StructuredSessionManager } from "./structured-session-manager.js";
6
7
  import type { ProcessManager } from "./process-manager.js";
@@ -17,6 +18,7 @@ export interface TaskRouteDependencies {
17
18
  /** PTY 会话创建入口;派发 `kind: "pty"` 的任务时使用。测试里可换成同步桩。 */
18
19
  processes?: ProcessManager;
19
20
  config?: WandConfig;
21
+ free?: Pick<OpenRouterFreeModelsService, "resolveForCall">;
20
22
  aiTeams?: AiTeamRunner;
21
23
  /** 可注入的任务标题生成器;测试里换成同步桩,避免真的起 CLI。 */
22
24
  generateTitle?: typeof generateWandTaskTitle;
@@ -33,6 +35,7 @@ export declare function parseTaskAgent(value: unknown, fallbackMode?: WandTaskAg
33
35
  export declare function whenWandTaskTitlesSettled(): Promise<void>;
34
36
  export interface AutoTaskTitleOptions {
35
37
  config?: WandConfig;
38
+ free?: Pick<OpenRouterFreeModelsService, "resolveForCall">;
36
39
  /** 可注入的标题生成器;测试里换成同步桩,避免真的起 CLI。 */
37
40
  generateTitle?: typeof generateWandTaskTitle;
38
41
  /** 未从任务所属工作区解析到目录时的兜底 cwd。 */
@@ -41,6 +41,7 @@ export function writeTaskBoardLastAgent(storage, agent) {
41
41
  thinkingEffort: agent.thinkingEffort,
42
42
  mode: agent.mode,
43
43
  kind: agent.kind,
44
+ ...(agent.engine ? { engine: agent.engine } : {}),
44
45
  });
45
46
  }
46
47
  function parseExecutionSubject(storage, value) {
@@ -250,7 +251,11 @@ function scheduleWandTaskTitleGeneration(storage, taskId, source, signature, opt
250
251
  const cwd = options.cwd || options.config?.defaultCwd || process.cwd();
251
252
  const run = async () => {
252
253
  try {
253
- const title = await generateTitle(source, cwd, options.config?.language ?? "", taskTitleAiOptions(options.config, storage));
254
+ const pending = storage.getWandTask(taskId);
255
+ // 排队期间可能移走 Agent,只剩空白终端;此时也不应再启动总结。
256
+ if (!pending || !taskAutoNameSourceText(storage, pending))
257
+ return;
258
+ const title = await generateTitle(source, cwd, options.config?.language ?? "", taskTitleAiOptions(options.config, storage, options.free));
254
259
  const current = storage.getWandTask(taskId);
255
260
  // 用户已经自己写了标题(原生端 / 面板编辑)就不要覆盖。
256
261
  if (!current || current.titleSource !== "auto")
@@ -258,6 +263,8 @@ function scheduleWandTaskTitleGeneration(storage, taskId, source, signature, opt
258
263
  // 命名输入已经变了(新增会话 / 会话内容更新):这次是过期结果,交给下一轮。
259
264
  if (current.autoTitleSignature !== signature)
260
265
  return;
266
+ if (!taskAutoNameSourceText(storage, current))
267
+ return;
261
268
  const clipped = title.slice(0, TASK_TITLE_MAX_LENGTH);
262
269
  if (!clipped || clipped === current.title)
263
270
  return;
@@ -304,11 +311,12 @@ export function refreshAutoBoardTaskTitles(storage, options = {}) {
304
311
  cwd,
305
312
  config: options.config,
306
313
  generateTitle: options.generateTitle,
314
+ free: options.free,
307
315
  });
308
316
  }
309
317
  }
310
318
  /** 自动生成标题没有会话上下文,按「默认 provider + 默认模型」解析,与提示词优化一致。 */
311
- function taskTitleAiOptions(config, storage) {
319
+ function taskTitleAiOptions(config, storage, free) {
312
320
  if (!config)
313
321
  return {};
314
322
  const provider = config.defaultProvider ?? "claude";
@@ -320,8 +328,7 @@ function taskTitleAiOptions(config, storage) {
320
328
  selectedModel: null,
321
329
  thinkingEffort: config.defaultThinkingEffort,
322
330
  };
323
- // resolveSystemAiContext 按系统运维员工的 CLI 候选链解析(首个已安装的优先)。
324
- return resolveSystemAiContext(defaultSession, config, storage?.getSystemSiliconEmployee() ?? null);
331
+ return resolveSystemAiContext(defaultSession, config, storage?.getSystemSiliconEmployee() ?? null, free);
325
332
  }
326
333
  export function registerTaskRoutes(app, deps) {
327
334
  const { storage, sessions, structured, processes, config, aiTeams } = deps;
@@ -329,7 +336,7 @@ export function registerTaskRoutes(app, deps) {
329
336
  app.get("/api/wand-tasks", (req, res) => {
330
337
  try {
331
338
  // 侧栏建的任务 / 后续新增的会话都会在这里补上自动标题。
332
- refreshAutoBoardTaskTitles(storage, { config, generateTitle: deps.generateTitle });
339
+ refreshAutoBoardTaskTitles(storage, { config, generateTitle: deps.generateTitle, free: deps.free });
333
340
  }
334
341
  catch (error) {
335
342
  console.error("[WandTask] Failed to refresh auto task titles:", getErrorMessage(error));
@@ -483,13 +490,15 @@ export function registerTaskRoutes(app, deps) {
483
490
  agent: assignedAgent,
484
491
  executionSubject,
485
492
  });
486
- if (agent)
493
+ // Task execution settings stay local unless the caller explicitly saves future defaults.
494
+ if (body.rememberAgentDefaults === true && agent)
487
495
  writeTaskBoardLastAgent(storage, agent);
488
496
  if (titleSource === "auto") {
489
497
  refreshAutoBoardTaskTitles(storage, {
490
498
  cwd: workspaceId ? storage.getWorkspace(workspaceId)?.cwd : undefined,
491
499
  config,
492
500
  generateTitle: deps.generateTitle,
501
+ free: deps.free,
493
502
  });
494
503
  }
495
504
  res.status(201).json(dto(storage.getWandTask(task.id) ?? task));
@@ -583,7 +592,7 @@ export function registerTaskRoutes(app, deps) {
583
592
  res.status(404).json({ error: "未找到该任务。" });
584
593
  return;
585
594
  }
586
- if (body.agent !== undefined && patch.agent)
595
+ if (body.rememberAgentDefaults === true && patch.agent)
587
596
  writeTaskBoardLastAgent(storage, patch.agent);
588
597
  res.json(dto(storage.getWandTask(task.id) ?? task));
589
598
  }
@@ -741,12 +750,12 @@ export function registerTaskRoutes(app, deps) {
741
750
  if (subject?.type === "employee" && (!employee || employee.archivedAt)) {
742
751
  throw new Error("硅基员工不存在或已归档。");
743
752
  }
744
- const selected = employee ? selectEmployeeCandidate(employee, undefined, { skipSdk: true }) : null;
753
+ const selected = employee ? selectEmployeeCandidate(employee) : null;
745
754
  const cliDefault = subject?.type === "cli"
746
755
  ? parseTaskAgent({ ...cliAgentForSubject(subject, task.agent), ...(body.kind === "pty" ? { kind: "pty" } : {}) })
747
756
  : null;
748
757
  const fallbackCandidate = !subject && body.agent === undefined && !task.agent
749
- && !body.provider ? selectEmployeeCandidate(defaultRoleForCli(storage, config.defaultProvider), undefined, { skipSdk: true }).agent : null;
758
+ && !body.provider ? selectEmployeeCandidate(defaultRoleForCli(storage, config.defaultProvider)).agent : null;
750
759
  const fallbackAgent = fallbackCandidate && body.kind === "pty"
751
760
  ? { ...fallbackCandidate, kind: "pty" } : fallbackCandidate;
752
761
  const agent = selected?.agent ?? (body.agent === undefined
@@ -780,7 +789,7 @@ export function registerTaskRoutes(app, deps) {
780
789
  executionSubject: usedSubject,
781
790
  status: task.status === "todo" ? "doing" : task.status,
782
791
  });
783
- if (!employee)
792
+ if (body.rememberAgentDefaults === true && !employee)
784
793
  writeTaskBoardLastAgent(storage, agent);
785
794
  // 派发也算一轮迭代里的改动意图:直接把任务的标题 / 描述记进迭代记录。
786
795
  recordIterationPromptForTask(storage, {