@co0ontty/wand 4.88.0 → 4.89.0-beta.gde0b3cd

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 (79) hide show
  1. package/dist/agent-dispatch.d.ts +5 -0
  2. package/dist/agent-dispatch.js +8 -0
  3. package/dist/ai-team-runner.d.ts +1 -1
  4. package/dist/ai-team-runner.js +13 -5
  5. package/dist/ai-team-types.js +3 -2
  6. package/dist/build-info.json +4 -4
  7. package/dist/commit-task-archive.d.ts +3 -0
  8. package/dist/commit-task-archive.js +15 -0
  9. package/dist/config.js +1 -0
  10. package/dist/daemon-maintenance-state.d.ts +2 -0
  11. package/dist/daemon-maintenance-targets.d.ts +1 -0
  12. package/dist/daemon-maintenance-targets.js +26 -0
  13. package/dist/daemon-maintenance.d.ts +7 -0
  14. package/dist/daemon-maintenance.js +69 -0
  15. package/dist/decision-expert-employee.d.ts +1 -1
  16. package/dist/decision-expert-employee.js +3 -3
  17. package/dist/decision-expert-service.d.ts +6 -5
  18. package/dist/decision-expert-service.js +75 -49
  19. package/dist/decision-types.d.ts +2 -1
  20. package/dist/employee-text.d.ts +28 -0
  21. package/dist/employee-text.js +124 -0
  22. package/dist/git-quick-commit.d.ts +13 -4
  23. package/dist/git-quick-commit.js +112 -33
  24. package/dist/local-model-setup.d.ts +7 -1
  25. package/dist/local-model-setup.js +54 -7
  26. package/dist/local-model-types.d.ts +2 -1
  27. package/dist/process-manager.d.ts +4 -1
  28. package/dist/process-manager.js +5 -6
  29. package/dist/render-daemon-client.d.ts +2 -0
  30. package/dist/render-daemon-client.js +6 -0
  31. package/dist/server-daemon-maintenance-routes.d.ts +8 -0
  32. package/dist/server-daemon-maintenance-routes.js +19 -0
  33. package/dist/server-employee-routes.d.ts +2 -0
  34. package/dist/server-employee-routes.js +19 -45
  35. package/dist/server-file-routes.js +61 -25
  36. package/dist/server-local-model-routes.js +12 -0
  37. package/dist/server-session-routes.d.ts +2 -1
  38. package/dist/server-session-routes.js +13 -10
  39. package/dist/server-speech-routes.d.ts +2 -0
  40. package/dist/server-speech-routes.js +7 -1
  41. package/dist/server-task-routes.d.ts +3 -0
  42. package/dist/server-task-routes.js +13 -10
  43. package/dist/server.js +38 -11
  44. package/dist/session-ai-context.d.ts +6 -4
  45. package/dist/session-ai-context.js +10 -9
  46. package/dist/silicon-employee-dispatch.d.ts +4 -6
  47. package/dist/silicon-employee-dispatch.js +9 -6
  48. package/dist/speech-polisher-employee.d.ts +6 -0
  49. package/dist/speech-polisher-employee.js +23 -0
  50. package/dist/speech-polisher-identity.d.ts +4 -0
  51. package/dist/speech-polisher-identity.js +4 -0
  52. package/dist/speech-polisher-service.d.ts +31 -0
  53. package/dist/speech-polisher-service.js +88 -0
  54. package/dist/speech-service.d.ts +3 -1
  55. package/dist/speech-service.js +14 -3
  56. package/dist/speech-support.d.ts +6 -0
  57. package/dist/speech-support.js +26 -0
  58. package/dist/speech-types.d.ts +5 -0
  59. package/dist/storage.d.ts +2 -0
  60. package/dist/storage.js +18 -1
  61. package/dist/structured-session-manager.d.ts +3 -2
  62. package/dist/structured-session-manager.js +11 -7
  63. package/dist/system-employee.d.ts +6 -4
  64. package/dist/system-employee.js +8 -5
  65. package/dist/types.d.ts +3 -1
  66. package/dist/user-memory.d.ts +2 -0
  67. package/dist/user-memory.js +1 -1
  68. package/dist/web-ui/content/ai-teams.js +13 -13
  69. package/dist/web-ui/content/scripts.js +324 -212
  70. package/dist/web-ui/content/styles.css +1 -1
  71. package/dist/web-ui/content/tailwind.css +1 -1
  72. package/dist/web-ui/embedded-assets.d.ts +1 -0
  73. package/dist/web-ui/embedded-assets.js +4 -3
  74. package/dist/web-ui/index.js +3 -3
  75. package/dist/web-ui/provider-identity.d.ts +2 -0
  76. package/dist/web-ui/provider-identity.js +7 -1
  77. package/dist/web-ui/scripts.d.ts +2 -0
  78. package/dist/web-ui/scripts.js +5 -0
  79. package/package.json +1 -1
@@ -7,6 +7,7 @@ import { LAYA_FILES, LAYA_REPOSITORY, layaFileUrl } from "./laya-model-manifest.
7
7
  import { ModelFileStore } from "./model-file-store.js";
8
8
  import { runModelSetup } from "./model-setup-process.js";
9
9
  import { speechModel } from "./speech-models.js";
10
+ import { speechSupport } from "./speech-support.js";
10
11
  import { systemEnvValue } from "./env-utils.js";
11
12
  import { decisionHardware } from "./decision-hardware.js";
12
13
  import { DECISION_EXPERT_ID } from "./decision-expert-identity.js";
@@ -20,6 +21,8 @@ export class LocalModelSetupService {
20
21
  jobs = new Map();
21
22
  sequence = 0;
22
23
  disposed = false;
24
+ speechEnableRevision = 0;
25
+ supportCache = null;
23
26
  constructor(deps) {
24
27
  this.deps = deps;
25
28
  this.files = new ModelFileStore(deps.fetch);
@@ -55,6 +58,7 @@ export class LocalModelSetupService {
55
58
  const hardware = decisionHardware();
56
59
  const operation = this.operations.get("laya") ?? null;
57
60
  const selected = speech.models.find((value) => value.id === speech.settings.model);
61
+ const support = await this.speechSupport(speech.runtime.available);
58
62
  return {
59
63
  laya: { kind: "laya", label: "LAYA 本地决策", supported,
60
64
  reason: !supported ? hardware.message
@@ -62,12 +66,38 @@ export class LocalModelSetupService {
62
66
  enabled: decision.enabled, model: LAYA_REPOSITORY, modelSize: this.manifest.reduce((sum, file) => sum + file.size, 0),
63
67
  downloaded: model.downloaded, runtimeAvailable: !!python, initialized: decision.state === "ready", busy: this.jobs.has("laya") || decision.queued > 0 || decision.state === "loading",
64
68
  operation: operation ? { ...operation } : null, hardware, decisionEmployeeId: DECISION_EXPERT_ID },
65
- speech: { kind: "speech", label: "服务端语音识别", supported: ["darwin", "linux", "win32"].includes(process.platform), reason: speech.reason,
69
+ speech: { kind: "speech", label: "服务端语音识别", supported: support.supported, supportReason: support.reason, reason: speech.reason,
66
70
  enabled: speech.settings.enabled, model: speech.settings.model, modelSize: selected?.size ?? 0,
67
71
  downloaded: selected?.downloaded ?? false, runtimeAvailable: speech.runtime.available, initialized: speech.initialized === true,
68
72
  busy: speech.busy || this.jobs.has("speech"), operation: this.operations.has("speech") ? { ...this.operations.get("speech") } : null },
69
73
  };
70
74
  }
75
+ speechSupport(runtimeAvailable) {
76
+ if (!this.supportCache || this.supportCache.expires < Date.now() || this.supportCache.runtimeAvailable !== runtimeAvailable) {
77
+ this.supportCache = { expires: Date.now() + 30_000, runtimeAvailable, value: (this.deps.speechSupport ?? speechSupport)(runtimeAvailable) };
78
+ }
79
+ return this.supportCache.value;
80
+ }
81
+ async setSpeechEnabled(enabled) {
82
+ const revision = ++this.speechEnableRevision;
83
+ const settingsRevision = this.deps.speech.settingsRevision();
84
+ if (this.disposed)
85
+ throw new DecisionError("UNAVAILABLE", "模型管理已关闭。", 503);
86
+ if (!enabled) {
87
+ this.cancel("speech");
88
+ this.deps.speech.configure({ enabled: false });
89
+ return;
90
+ }
91
+ const status = await this.deps.speech.status();
92
+ const support = await this.speechSupport(status.runtime.available);
93
+ if (revision !== this.speechEnableRevision || this.disposed || settingsRevision !== this.deps.speech.settingsRevision())
94
+ return;
95
+ if (!support.supported)
96
+ throw new DecisionError("UNSUPPORTED", support.reason || "本机不支持语音识别。", 409);
97
+ if (status.busy || status.download?.phase === "downloading")
98
+ throw new DecisionError("BUSY", "语音识别或模型下载正在进行,请稍后启用。", 409);
99
+ this.start("speech", "activate", {});
100
+ }
71
101
  parse(kind, raw) {
72
102
  if (!raw || typeof raw !== "object" || Array.isArray(raw))
73
103
  throw new DecisionError("INVALID_REQUEST", "模型操作参数无效。");
@@ -90,6 +120,8 @@ export class LocalModelSetupService {
90
120
  throw new DecisionError("UNAVAILABLE", "模型管理已关闭。", 503);
91
121
  if (kind === "laya" && !this.supported())
92
122
  throw new DecisionError("UNSUPPORTED", `LAYA-MLX 需要 Apple Silicon macOS 与 Metal;${decisionHardware().message}`, 409);
123
+ if (kind === "laya" && action === "activate")
124
+ throw new DecisionError("INVALID_REQUEST", "一键启用仅用于语音识别。");
93
125
  const input = this.parse(kind, raw);
94
126
  if (this.jobs.has(kind))
95
127
  throw new DecisionError("BUSY", "该模型已有安装任务,请等待或取消。", 409);
@@ -119,6 +151,8 @@ export class LocalModelSetupService {
119
151
  timer.unref();
120
152
  const signal = abort.signal;
121
153
  const phase = (value, message) => this.update(kind, id, { phase: value, message });
154
+ let activationMaintenance = false;
155
+ const settingsRevision = this.deps.speech.settingsRevision();
122
156
  try {
123
157
  if (kind === "laya") {
124
158
  let model = await this.layaModel();
@@ -170,7 +204,7 @@ export class LocalModelSetupService {
170
204
  }
171
205
  else {
172
206
  const selected = input.model ?? this.deps.speech.settings().model;
173
- if (action === "download") {
207
+ if (action === "download" || action === "activate") {
174
208
  phase("downloading", "下载并校验语音模型");
175
209
  await this.deps.speech.startDownload(selected);
176
210
  while (true) {
@@ -188,7 +222,12 @@ export class LocalModelSetupService {
188
222
  });
189
223
  }
190
224
  }
191
- else {
225
+ if (action !== "download") {
226
+ if (action === "activate") {
227
+ signal.throwIfAborted();
228
+ this.deps.speech.acquireMaintenance();
229
+ activationMaintenance = true;
230
+ }
192
231
  const status = await this.deps.speech.status();
193
232
  signal.throwIfAborted();
194
233
  const backend = input.backend === "auto" || !input.backend ? process.platform === "darwin" ? "metal" : "cpu" : input.backend;
@@ -207,22 +246,30 @@ export class LocalModelSetupService {
207
246
  await rm(work, { recursive: true, force: true }).catch(() => { });
208
247
  }
209
248
  }
210
- phase("initializing", "加载所选语音模型并用合成静音检查,未启用识别或重启服务");
211
- await this.deps.speech.initializeModel(selected, signal);
249
+ phase("initializing", action === "activate" ? "正在检查语音模型,完成后自动启用。" : "加载所选语音模型并用合成静音检查,未启用识别或重启服务");
250
+ await this.deps.speech.initializeModel(selected, signal, action === "activate" ? "auto" : undefined);
251
+ if (action === "activate") {
252
+ signal.throwIfAborted();
253
+ if (settingsRevision !== this.deps.speech.settingsRevision())
254
+ throw new DecisionError("SETTINGS_CHANGED", "语音设置已由其他操作修改,本次启用已取消。");
255
+ this.deps.speech.configure({ enabled: true, acceleration: "auto" });
256
+ }
212
257
  }
213
258
  }
214
259
  signal.throwIfAborted();
215
- phase("completed", action === "download" ? "模型下载及完整性校验完成;可继续初始化" : "运行时和模型初始化检查完成;启用状态保持不变");
260
+ phase("completed", action === "activate" ? "服务端语音识别已启用。" : action === "download" ? "模型下载及完整性校验完成;可继续初始化" : "运行时和模型初始化检查完成;启用状态保持不变");
216
261
  }
217
262
  catch (error) {
218
263
  const cancelled = signal.aborted;
219
264
  const message = cancelled ? "安装任务已取消,完整文件保留,可重试" : error instanceof DecisionError || (error instanceof Error && /请先|校验|自定义|独立/.test(error.message))
220
265
  ? error.message : "安装或初始化失败,请检查网络、编译工具/Python版本、磁盘空间和平台能力,再重试。";
221
266
  this.update(kind, id, { phase: cancelled ? "cancelled" : "failed", message, ...(cancelled ? {} : { error: message }) });
222
- if (kind === "speech" && action === "download" && cancelled)
267
+ if (kind === "speech" && (action === "download" || action === "activate") && cancelled)
223
268
  this.deps.speech.cancelDownload();
224
269
  }
225
270
  finally {
271
+ if (activationMaintenance)
272
+ this.deps.speech.releaseMaintenance();
226
273
  clearTimeout(timer);
227
274
  }
228
275
  }
@@ -3,7 +3,7 @@ export type LocalModelKind = "laya" | "speech";
3
3
  export type ModelSetupPhase = "downloading" | "verifying" | "runtime" | "initializing" | "completed" | "failed" | "cancelled";
4
4
  export interface ModelSetupOperation {
5
5
  id: number;
6
- action: "download" | "initialize";
6
+ action: "download" | "initialize" | "activate";
7
7
  phase: ModelSetupPhase;
8
8
  message: string;
9
9
  received: number;
@@ -14,6 +14,7 @@ export interface LocalModelStatus {
14
14
  kind: LocalModelKind;
15
15
  label: string;
16
16
  supported: boolean;
17
+ supportReason?: string | null;
17
18
  reason: string | null;
18
19
  enabled: boolean;
19
20
  model: string;
@@ -3,6 +3,7 @@ import { EventEmitter } from "node:events";
3
3
  import { WandStorage } from "./storage.js";
4
4
  import { ExecutionMode, ProcessEventHandler, SessionProvider, SessionSnapshot, SessionSource, WandConfig } from "./types.js";
5
5
  import { type PermissionResolution } from "./claude-pty-bridge.js";
6
+ import type { EmployeeTextDeps } from "./employee-text.js";
6
7
  import { type PtyTerminalSnapshot, type PtyHistoryPage } from "./pty-terminal-state.js";
7
8
  import { samplePtyForegrounds } from "./pty-foreground.js";
8
9
  import { type TerminalHost } from "./terminal-host.js";
@@ -34,6 +35,7 @@ export declare const PTY_TURN_IDLE_MS = 3000;
34
35
  */
35
36
  export declare const PTY_FOREGROUND_SAMPLE_MS = 1000;
36
37
  export interface ProcessManagerOptions {
38
+ free?: EmployeeTextDeps["free"];
37
39
  /** Overrides the non-Claude PTY turn quiet window (tests use a short one). */
38
40
  ptyTurnIdleMs?: number;
39
41
  /** Overrides the foreground-process-group sampling interval (tests use a short one). */
@@ -55,6 +57,7 @@ export declare class ProcessManager extends EventEmitter {
55
57
  private readonly ptyForegroundSampleMs;
56
58
  /** Foreground probe; injectable so tests can drive it without a real process table. */
57
59
  private readonly probePtyForegrounds;
60
+ private readonly free;
58
61
  private readonly resolveDecisionEvaluate;
59
62
  /** 24h archive scan timer */
60
63
  private archiveTimer;
@@ -177,7 +180,7 @@ export declare class ProcessManager extends EventEmitter {
177
180
  */
178
181
  setSessionModel(id: string, model: string | null): SessionSnapshot;
179
182
  /**
180
- * 归档会先停掉还在跑的终端,但保留会话 ID 和 provider session id,恢复时用它们续上。
183
+ * 归档只写标记,保留正在运行的终端、会话 ID 和 provider session id。
181
184
  * 取消归档只清标记;重新拉起由恢复入口负责。
182
185
  */
183
186
  setSessionArchived(id: string, archived: boolean): SessionSnapshot;
@@ -563,6 +563,7 @@ export class ProcessManager extends EventEmitter {
563
563
  ptyForegroundSampleMs;
564
564
  /** Foreground probe; injectable so tests can drive it without a real process table. */
565
565
  probePtyForegrounds;
566
+ free;
566
567
  resolveDecisionEvaluate;
567
568
  /** 24h archive scan timer */
568
569
  archiveTimer = null;
@@ -595,6 +596,7 @@ export class ProcessManager extends EventEmitter {
595
596
  this.ptyTurnIdleMs = options.ptyTurnIdleMs ?? PTY_TURN_IDLE_MS;
596
597
  this.ptyForegroundSampleMs = options.ptyForegroundSampleMs ?? PTY_FOREGROUND_SAMPLE_MS;
597
598
  this.probePtyForegrounds = options.samplePtyForegrounds ?? samplePtyForegrounds;
599
+ this.free = options.free;
598
600
  this.resolveDecisionEvaluate = options.resolveDecisionEvaluate ?? (() => undefined);
599
601
  this.terminalHost = terminalHost ?? new InProcessTerminalHost();
600
602
  this.logger = new SessionLogger(configDir || path.join(process.env.HOME || process.cwd(), ".wand"), config.shortcutLogMaxBytes);
@@ -1229,7 +1231,7 @@ export class ProcessManager extends EventEmitter {
1229
1231
  ai: resolveSystemAiContext({
1230
1232
  provider: groupProvider, runner: undefined, command: providerCliCommand(groupProvider),
1231
1233
  structuredState: undefined, selectedModel: null, thinkingEffort: opts?.thinkingEffort,
1232
- }, this.config, this.storage.getSystemSiliconEmployee()),
1234
+ }, this.config, this.storage.getSystemSiliconEmployee(), this.free),
1233
1235
  cwd: resolveSessionCwd(cwd, this.config.defaultCwd),
1234
1236
  language: this.config.language,
1235
1237
  });
@@ -1775,13 +1777,10 @@ export class ProcessManager extends EventEmitter {
1775
1777
  return this.snapshot(record);
1776
1778
  }
1777
1779
  /**
1778
- * 归档会先停掉还在跑的终端,但保留会话 ID 和 provider session id,恢复时用它们续上。
1780
+ * 归档只写标记,保留正在运行的终端、会话 ID 和 provider session id。
1779
1781
  * 取消归档只清标记;重新拉起由恢复入口负责。
1780
1782
  */
1781
1783
  setSessionArchived(id, archived) {
1782
- if (archived && this.mustGet(id).status === "running") {
1783
- this.stop(id);
1784
- }
1785
1784
  const record = this.mustGet(id);
1786
1785
  record.archived = archived;
1787
1786
  record.archivedAt = archived ? (record.archivedAt ?? new Date().toISOString()) : null;
@@ -2354,7 +2353,7 @@ export class ProcessManager extends EventEmitter {
2354
2353
  input: prompt,
2355
2354
  cwd: record.cwd,
2356
2355
  language: this.config.language,
2357
- ai: resolveSystemAiContext(record, this.config, this.storage.getSystemSiliconEmployee()),
2356
+ ai: resolveSystemAiContext(record, this.config, this.storage.getSystemSiliconEmployee(), this.free),
2358
2357
  readNativeTitle: () => readNativeSessionTitle(resolveSessionProvider(record), record.claudeSessionId, record.cwd),
2359
2358
  hasNativeTitle,
2360
2359
  onGenerating: (generating) => {
@@ -108,6 +108,8 @@ export declare class RenderDaemonClient implements TerminalHost {
108
108
  sessions: TerminalSessionState[] | null;
109
109
  }>;
110
110
  requestShutdownDrain(): Promise<void>;
111
+ /** Explicit confirmed maintenance only; does not shut down the daemon itself. */
112
+ interruptMaintenanceSessions(): Promise<void>;
111
113
  connect(): Promise<void>;
112
114
  private open;
113
115
  /**
@@ -233,6 +233,12 @@ export class RenderDaemonClient {
233
233
  async requestShutdownDrain() {
234
234
  await this.request("shutdown", { mode: "drain" });
235
235
  }
236
+ /** Explicit confirmed maintenance only; does not shut down the daemon itself. */
237
+ async interruptMaintenanceSessions() {
238
+ const sessions = await this.listSessions();
239
+ await Promise.all(sessions.filter(session => session.status === "running")
240
+ .map(session => this.request("kill", { sessionId: session.sessionId, signal: "SIGTERM" })));
241
+ }
236
242
  connect() {
237
243
  if (this.disposed)
238
244
  return Promise.reject(new Error("Render client disposed"));
@@ -0,0 +1,8 @@
1
+ import type { Express, Request, RequestHandler } from "express";
2
+ import type { DaemonMaintenance } from "./daemon-maintenance.js";
3
+ export declare function registerDaemonMaintenanceRoutes(app: Express, options: {
4
+ requireAdmin: RequestHandler;
5
+ canForceUpdate(req: Request): boolean;
6
+ maintenance: Pick<DaemonMaintenance, "status" | "forceUpdate">;
7
+ log(error: unknown): void;
8
+ }): void;
@@ -0,0 +1,19 @@
1
+ export function registerDaemonMaintenanceRoutes(app, options) {
2
+ const { maintenance } = options;
3
+ app.get("/api/daemon-maintenance", (req, res) => {
4
+ res.set("Cache-Control", "no-store").json({ ...maintenance.status(), canForceUpdate: options.canForceUpdate(req) });
5
+ });
6
+ app.post("/api/daemon-maintenance/force-update", options.requireAdmin, async (req, res) => {
7
+ if (req.body?.confirmInterrupt !== true) {
8
+ res.status(400).json({ error: "请先确认中断正在执行的任务。" });
9
+ return;
10
+ }
11
+ try {
12
+ res.set("Cache-Control", "no-store").json(await maintenance.forceUpdate());
13
+ }
14
+ catch (error) {
15
+ options.log(error);
16
+ res.status(409).json({ error: "强制更新未完成,执行可能仍在停止或组件暂不可用。系统会在空闲后自动重试。", ...maintenance.status() });
17
+ }
18
+ });
19
+ }
@@ -1,5 +1,6 @@
1
1
  import type { Express } from "express";
2
2
  import { type SiliconEmployee, type SiliconEmployeeDraft } from "./ai-team-types.js";
3
+ import type { OpenRouterFreeModelsService } from "./openrouter-free-models.js";
3
4
  import type { WandStorage } from "./storage.js";
4
5
  import type { WandTaskAgent } from "./task-types.js";
5
6
  import type { WandConfig } from "./types.js";
@@ -17,6 +18,7 @@ export declare function registerSiliconEmployeeRoutes(app: Express, deps: {
17
18
  storage: WandStorage;
18
19
  notifyEmployeeChanged?: (employeeId: string) => void;
19
20
  config?: WandConfig;
21
+ free?: Pick<OpenRouterFreeModelsService, "resolveForCall">;
20
22
  /** 测试注入点:默认按系统 AI 配置真实调用模型。 */
21
23
  generateDraft?: (request: EmployeeDraftRequest) => Promise<SiliconEmployeeDraft>;
22
24
  /** 测试注入点:CLI 可用性探测。 */
@@ -1,8 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { AI_TEAM_MAX_CANDIDATES, SILICON_EMPLOYEE_AVATAR_MAX_CHARS, agentKey, isBuiltinSiliconEmployee, parseSiliconEmployeeTags, siliconEmployeeTags, } from "./ai-team-types.js";
3
3
  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
4
  import { asyncRoute } from "./express-async.js";
7
5
  import { bodyObject, sendRouteError, text } from "./server-request.js";
8
6
  import { parseTaskAgent } from "./server-task-routes.js";
@@ -14,7 +12,7 @@ import { SESSION_PROVIDERS } from "./session-provider.js";
14
12
  import { DEFAULT_WAND_TASK_AGENT_KIND } from "./task-types.js";
15
13
  import { EMPLOYEE_KNOWLEDGE_MAX_ENTRIES } from "./employee-knowledge-types.js";
16
14
  /** 员工起草没有会话上下文,按「默认 provider」解析,与任务标题 / 提示词优化一致。 */
17
- function employeeDraftAiOptions(storage, config) {
15
+ function employeeDraftAiOptions(storage, config, free) {
18
16
  if (!config)
19
17
  return {};
20
18
  const provider = config.defaultProvider ?? "claude";
@@ -25,7 +23,7 @@ function employeeDraftAiOptions(storage, config) {
25
23
  command: provider === "qoder" ? "qodercli" : provider,
26
24
  selectedModel: null,
27
25
  thinkingEffort: config.defaultThinkingEffort,
28
- }, config, storage.getSystemSiliconEmployee());
26
+ }, config, storage.getSystemSiliconEmployee(), free);
29
27
  }
30
28
  function sendEmployeeError(res, error, context) {
31
29
  const message = getErrorMessage(error, "硅基员工操作失败。");
@@ -99,23 +97,23 @@ export function parseSystemEmployeeAgents(value, existing) {
99
97
  if (body.systemKey !== undefined && body.systemKey !== existing.systemKey) {
100
98
  throw new Error("该员工是内置的,身份不可修改。");
101
99
  }
102
- if (!Array.isArray(body.agents))
100
+ return parseEmployeeAgents(body.agents);
101
+ }
102
+ /** One validation owner for every employee candidate list, including legacy single agents. */
103
+ function parseEmployeeAgents(value) {
104
+ if (!Array.isArray(value))
103
105
  throw new Error("请提供执行候选数组(CLI / SDK)。");
104
- if (body.agents.length === 0)
106
+ if (value.length === 0)
105
107
  throw new Error("至少需要一个执行候选。");
106
- if (body.agents.length > AI_TEAM_MAX_CANDIDATES) {
108
+ if (value.length > AI_TEAM_MAX_CANDIDATES) {
107
109
  throw new Error(`最多 ${AI_TEAM_MAX_CANDIDATES} 个执行候选。`);
108
110
  }
109
- const agents = body.agents.map((rawAgent) => {
111
+ const agents = value.map((rawAgent) => {
110
112
  const parsed = parseTaskAgent(rawAgent ?? {});
111
113
  if (!parsed)
112
114
  throw new Error("请选择有效的 CLI 工具或 Wand Agent。");
113
115
  if (parsed.kind !== "structured")
114
116
  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
117
  return parsed;
120
118
  });
121
119
  if (new Set(agents.map(agentKey)).size !== agents.length) {
@@ -132,40 +130,16 @@ export function parseSiliconEmployeeInput(value, existing, now) {
132
130
  const prompt = text(body.prompt);
133
131
  if (prompt.length > 20_000)
134
132
  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) {
133
+ let candidates;
134
+ if (Array.isArray(body.agents))
135
+ candidates = body.agents;
136
+ else if (body.agents !== undefined && body.agents !== null)
152
137
  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 {
138
+ else if (body.agent !== undefined && body.agent !== null)
139
+ candidates = [body.agent];
140
+ else
163
141
  throw new Error("至少需要一个执行候选。");
164
- }
165
- const seenKeys = new Set(agents.map(agentKey));
166
- if (seenKeys.size !== agents.length) {
167
- throw new Error("执行候选存在重复。");
168
- }
142
+ const agents = parseEmployeeAgents(candidates);
169
143
  const avatar = parseAvatar(body.avatar, name);
170
144
  const tags = parseSiliconEmployeeTags(body.tags === undefined ? existing?.tags ?? [] : body.tags);
171
145
  const id = existing?.id ?? `e_${randomUUID().replace(/-/g, "")}`;
@@ -203,7 +177,7 @@ export function registerSiliconEmployeeRoutes(app, deps) {
203
177
  const available = availableEmployeeProviders(resolveDefaults().provider, isProviderAvailable);
204
178
  return available.length ? available : [...SESSION_PROVIDERS];
205
179
  };
206
- const generateDraft = deps.generateDraft ?? ((request) => generateSiliconEmployeeDraft(request.expectation, employeeDraftAiOptions(storage, config), {
180
+ const generateDraft = deps.generateDraft ?? ((request) => generateSiliconEmployeeDraft(request.expectation, employeeDraftAiOptions(storage, config, deps.free), {
207
181
  cwd: config?.defaultCwd || process.cwd(),
208
182
  language: config?.language ?? "",
209
183
  existingNames: request.existingNames,
@@ -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 {};