@co0ontty/wand 4.89.0-beta.gde0b3cd → 4.89.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.
Files changed (70) hide show
  1. package/dist/agent-dispatch.d.ts +0 -5
  2. package/dist/agent-dispatch.js +0 -8
  3. package/dist/ai-team-runner.d.ts +1 -1
  4. package/dist/ai-team-runner.js +5 -13
  5. package/dist/ai-team-types.js +2 -3
  6. package/dist/build-info.json +4 -4
  7. package/dist/commit-task-archive.d.ts +0 -3
  8. package/dist/commit-task-archive.js +0 -15
  9. package/dist/config.js +0 -1
  10. package/dist/decision-expert-employee.d.ts +1 -1
  11. package/dist/decision-expert-employee.js +3 -3
  12. package/dist/decision-expert-service.d.ts +5 -6
  13. package/dist/decision-expert-service.js +49 -75
  14. package/dist/decision-types.d.ts +1 -2
  15. package/dist/git-quick-commit.d.ts +4 -13
  16. package/dist/git-quick-commit.js +33 -112
  17. package/dist/local-model-setup.d.ts +1 -7
  18. package/dist/local-model-setup.js +7 -54
  19. package/dist/local-model-types.d.ts +1 -2
  20. package/dist/process-manager.d.ts +1 -4
  21. package/dist/process-manager.js +6 -5
  22. package/dist/server-employee-routes.d.ts +0 -2
  23. package/dist/server-employee-routes.js +45 -19
  24. package/dist/server-file-routes.js +25 -61
  25. package/dist/server-local-model-routes.js +0 -12
  26. package/dist/server-session-routes.d.ts +1 -2
  27. package/dist/server-session-routes.js +10 -13
  28. package/dist/server-speech-routes.d.ts +0 -2
  29. package/dist/server-speech-routes.js +1 -7
  30. package/dist/server-task-routes.d.ts +0 -3
  31. package/dist/server-task-routes.js +10 -13
  32. package/dist/server.js +8 -20
  33. package/dist/session-ai-context.d.ts +4 -6
  34. package/dist/session-ai-context.js +9 -10
  35. package/dist/silicon-employee-dispatch.d.ts +6 -4
  36. package/dist/silicon-employee-dispatch.js +6 -9
  37. package/dist/speech-service.d.ts +1 -3
  38. package/dist/speech-service.js +3 -14
  39. package/dist/speech-types.d.ts +0 -5
  40. package/dist/storage.d.ts +0 -2
  41. package/dist/storage.js +1 -18
  42. package/dist/structured-session-manager.d.ts +2 -3
  43. package/dist/structured-session-manager.js +7 -11
  44. package/dist/system-employee.d.ts +4 -6
  45. package/dist/system-employee.js +5 -8
  46. package/dist/types.d.ts +1 -3
  47. package/dist/user-memory.d.ts +0 -2
  48. package/dist/user-memory.js +1 -1
  49. package/dist/web-ui/content/ai-teams.js +10 -10
  50. package/dist/web-ui/content/scripts.js +204 -316
  51. package/dist/web-ui/content/styles.css +1 -1
  52. package/dist/web-ui/content/tailwind.css +1 -1
  53. package/dist/web-ui/embedded-assets.d.ts +0 -1
  54. package/dist/web-ui/embedded-assets.js +3 -4
  55. package/dist/web-ui/index.js +3 -3
  56. package/dist/web-ui/provider-identity.d.ts +0 -2
  57. package/dist/web-ui/provider-identity.js +1 -7
  58. package/dist/web-ui/scripts.d.ts +0 -2
  59. package/dist/web-ui/scripts.js +0 -5
  60. package/package.json +1 -1
  61. package/dist/employee-text.d.ts +0 -28
  62. package/dist/employee-text.js +0 -124
  63. package/dist/speech-polisher-employee.d.ts +0 -6
  64. package/dist/speech-polisher-employee.js +0 -23
  65. package/dist/speech-polisher-identity.d.ts +0 -4
  66. package/dist/speech-polisher-identity.js +0 -4
  67. package/dist/speech-polisher-service.d.ts +0 -31
  68. package/dist/speech-polisher-service.js +0 -88
  69. package/dist/speech-support.d.ts +0 -6
  70. package/dist/speech-support.js +0 -26
@@ -1,9 +1,5 @@
1
- import { existsSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs";
2
- import { randomUUID } from "node:crypto";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
1
+ import { existsSync } from "node:fs";
5
2
  import { spawn } from "node:child_process";
6
- import { abortableEmployeeText, prepareEmployeeTextCandidate } from "./employee-text.js";
7
3
  import { buildChildEnv } from "./env-utils.js";
8
4
  import { buildLanguageDirective } from "./language-prompt.js";
9
5
  import { isSessionProvider, providerCliInstalled } from "./session-provider.js";
@@ -310,7 +306,7 @@ async function callClaudeText(request, cwd, language, opts) {
310
306
  const stdout = await runCliText("claude", args, request.prompt, {
311
307
  cwd,
312
308
  timeoutMs: cliTextTimeoutMs(CLAUDE_MESSAGE_TIMEOUT_MS, opts),
313
- inheritEnv: opts.inheritEnv, signal: opts.signal,
309
+ inheritEnv: opts.inheritEnv,
314
310
  });
315
311
  const text = stdout.trim();
316
312
  if (!text)
@@ -542,8 +538,6 @@ function buildGrokTextArgs(request, opts, allowTools = false) {
542
538
  args.push(...systemPromptArgsFrom("grok", request.system));
543
539
  if (allowTools)
544
540
  args.push("--always-approve");
545
- else
546
- args.push("--tools", "", "--disallowed-tools", "run_terminal_cmd,read_file,search_replace,grep,list_dir,web_search,web_fetch,todo_write,task,Agent", "--deny", "MCPTool", "--disable-web-search", "--no-subagents");
547
541
  return args;
548
542
  }
549
543
  /**
@@ -574,8 +568,6 @@ function buildQoderTextArgs(request, opts, allowTools = false) {
574
568
  args.push(...systemPromptArgsFrom("qoder", request.system));
575
569
  if (allowTools)
576
570
  args.push("--permission-mode", "bypass_permissions");
577
- else
578
- args.push("--tools", "", "--strict-mcp-config");
579
571
  return args;
580
572
  }
581
573
  /** provider 自己的系统提示开关 + 规则文本;没有开关的 provider 返回空。 */
@@ -591,48 +583,35 @@ function contentWithSystemPrompt(provider, request) {
591
583
  return systemPromptFlag(provider) ? request.prompt : composeSystemFallback(request.system, request.prompt);
592
584
  }
593
585
  function runCliText(command, args, prompt, opts) {
594
- opts.signal?.throwIfAborted();
595
586
  return new Promise((resolve, reject) => {
596
- const baseEnv = buildChildEnv(opts.inheritEnv !== false);
597
587
  const child = spawn(command, args, {
598
588
  cwd: opts.cwd,
599
- env: opts.env ? opts.env(baseEnv) : baseEnv,
589
+ env: buildChildEnv(opts.inheritEnv !== false),
600
590
  stdio: ["pipe", "pipe", "pipe"],
601
591
  });
602
592
  let stdout = "";
603
593
  let stderr = "";
604
594
  let settled = false;
605
- const cancelled = () => {
606
- if (settled)
607
- return;
608
- settled = true;
609
- clearTimeout(timeout);
610
- child.kill("SIGTERM");
611
- reject(opts.signal?.reason ?? new Error("AI 文本请求已取消。"));
612
- };
613
- const cleanup = () => { clearTimeout(timeout); opts.signal?.removeEventListener("abort", cancelled); };
614
595
  const timeout = setTimeout(() => {
615
596
  settled = true;
616
- opts.signal?.removeEventListener("abort", cancelled);
617
597
  child.kill("SIGTERM");
618
598
  reject(new QuickCommitError(`${command} 调用超时。`, "CLAUDE_TIMEOUT"));
619
599
  }, opts.timeoutMs);
620
- opts.signal?.addEventListener("abort", cancelled, { once: true });
621
600
  child.stdout?.on("data", (chunk) => { stdout += chunk.toString(); });
622
601
  child.stderr?.on("data", (chunk) => { stderr += chunk.toString(); });
623
602
  child.on("error", (error) => {
624
603
  if (settled)
625
604
  return;
626
605
  settled = true;
627
- cleanup();
606
+ clearTimeout(timeout);
628
607
  const code = error.code === "ENOENT" ? "CLAUDE_CLI_MISSING" : "CLAUDE_CLI_FAILED";
629
608
  reject(new QuickCommitError(error.code === "ENOENT" ? `未找到 ${command} CLI。` : `${command} CLI 失败:${error.message}`, code));
630
609
  });
631
610
  child.on("close", (code) => {
632
- cleanup();
633
611
  if (settled)
634
612
  return;
635
613
  settled = true;
614
+ clearTimeout(timeout);
636
615
  if (code === 0) {
637
616
  resolve(stdout);
638
617
  return;
@@ -653,7 +632,7 @@ function runCliText(command, args, prompt, opts) {
653
632
  if (settled || !expectsStdin)
654
633
  return;
655
634
  settled = true;
656
- cleanup();
635
+ clearTimeout(timeout);
657
636
  child.kill("SIGTERM");
658
637
  reject(new QuickCommitError(`${command} CLI stdin failed: ${error.code ?? "write error"}`, "CLAUDE_CLI_FAILED"));
659
638
  });
@@ -675,7 +654,7 @@ async function callCodexText(request, cwd, opts) {
675
654
  const stdout = await runCliText("codex", args, contentWithSystemPrompt("codex", request), {
676
655
  cwd,
677
656
  timeoutMs: cliTextTimeoutMs(CODEX_MESSAGE_TIMEOUT_MS, opts),
678
- inheritEnv: opts.inheritEnv, signal: opts.signal,
657
+ inheritEnv: opts.inheritEnv,
679
658
  });
680
659
  const text = extractCodexText(stdout);
681
660
  if (!text) {
@@ -684,8 +663,7 @@ async function callCodexText(request, cwd, opts) {
684
663
  return text;
685
664
  }
686
665
  async function callOpenCodeText(request, cwd, opts) {
687
- const agent = `wand-text-${randomUUID()}`;
688
- const args = ["run", "--format", "json", "--pure", "--agent", agent];
666
+ const args = ["run", "--format", "json"];
689
667
  const model = opts.model?.trim();
690
668
  if (model && model !== "default")
691
669
  args.push("--model", model);
@@ -695,18 +673,7 @@ async function callOpenCodeText(request, cwd, opts) {
695
673
  const stdout = await runCliText("opencode", args, contentWithSystemPrompt("opencode", request), {
696
674
  cwd,
697
675
  timeoutMs: cliTextTimeoutMs(CODEX_MESSAGE_TIMEOUT_MS, opts),
698
- inheritEnv: opts.inheritEnv, signal: opts.signal,
699
- // A dedicated per-invocation agent and scalar deny replace rather than merge
700
- // wildcard/specific allow rules. Preserve inherited provider/model config.
701
- env: base => {
702
- const configured = base.OPENCODE_CONFIG_CONTENT ? JSON.parse(base.OPENCODE_CONFIG_CONTENT) : {};
703
- if (!configured || typeof configured !== "object" || Array.isArray(configured))
704
- throw new Error("OpenCode 内联配置无效。");
705
- return { ...base, OPENCODE_PERMISSION: JSON.stringify("deny"), OPENCODE_CONFIG_CONTENT: JSON.stringify({
706
- ...configured, permission: "deny", autoupdate: false, share: "disabled",
707
- agent: { ...configured.agent, [agent]: { mode: "primary", permission: "deny", prompt: "Return only the requested text. Do not use tools." } },
708
- }) };
709
- },
676
+ inheritEnv: opts.inheritEnv,
710
677
  });
711
678
  const text = extractOpenCodeText(stdout);
712
679
  if (!text)
@@ -717,7 +684,7 @@ async function callGrokText(request, cwd, opts) {
717
684
  const stdout = await runCliText("grok", buildGrokTextArgs(request, opts), "", {
718
685
  cwd,
719
686
  timeoutMs: cliTextTimeoutMs(CODEX_MESSAGE_TIMEOUT_MS, opts),
720
- inheritEnv: opts.inheritEnv, signal: opts.signal,
687
+ inheritEnv: opts.inheritEnv,
721
688
  });
722
689
  const text = extractGrokText(stdout);
723
690
  if (!text)
@@ -728,7 +695,7 @@ async function callQoderText(request, cwd, opts) {
728
695
  const stdout = await runCliText("qodercli", buildQoderTextArgs(request, opts), "", {
729
696
  cwd,
730
697
  timeoutMs: cliTextTimeoutMs(CODEX_MESSAGE_TIMEOUT_MS, opts),
731
- inheritEnv: opts.inheritEnv, signal: opts.signal,
698
+ inheritEnv: opts.inheritEnv,
732
699
  });
733
700
  const text = extractQoderText(stdout);
734
701
  if (!text)
@@ -737,42 +704,25 @@ async function callQoderText(request, cwd, opts) {
737
704
  }
738
705
  async function callPiText(request, cwd, opts) {
739
706
  const stdout = await runCliText("pi", buildPiTextArgs(request, opts), "", {
740
- cwd, timeoutMs: cliTextTimeoutMs(CODEX_MESSAGE_TIMEOUT_MS, opts), inheritEnv: opts.inheritEnv, signal: opts.signal,
707
+ cwd, timeoutMs: cliTextTimeoutMs(CODEX_MESSAGE_TIMEOUT_MS, opts), inheritEnv: opts.inheritEnv,
741
708
  });
742
709
  const text = extractPiText(stdout);
743
710
  if (!text)
744
711
  throw new QuickCommitError("Pi 返回了空的 commit message。", "EMPTY_AI_MESSAGE");
745
712
  return text;
746
713
  }
747
- export function assertGeminiTextCapability() {
748
- // Gemini ignores supplemental admin policy when a central policy exists.
749
- // Refuse that unsupported text-only path rather than launching without deny.
750
- const systemPolicies = process.platform === "darwin" ? "/Library/Application Support/GeminiCli/policies"
751
- : process.platform === "win32" ? join(process.env.ProgramData ?? "C:\\ProgramData", "gemini-cli", "policies") : "/etc/gemini-cli/policies";
752
- if (existsSync(systemPolicies) && readdirSync(systemPolicies).some(file => file.endsWith(".toml"))) {
753
- throw new QuickCommitError("Gemini 中央策略阻止注入纯文本工具边界,请配置可用的文本候选。", "AI_FALLBACK_FAILED");
754
- }
755
- }
756
714
  async function callGeminiText(request, cwd, opts) {
757
- assertGeminiTextCapability();
758
- const root = mkdtempSync(join(tmpdir(), "wand-gemini-text-"));
759
- try {
760
- const policy = join(root, "text-only.toml");
761
- writeFileSync(policy, '[[rule]]\ntoolName = "*"\ndecision = "deny"\npriority = 999\n', { mode: 0o600 });
762
- const args = [...buildGeminiTextArgs(opts), "--admin-policy", policy, "--extensions", "none", "--allowed-mcp-server-names", ""];
763
- const stdout = await runCliText("gemini", args, contentWithSystemPrompt("gemini", request), {
764
- cwd, timeoutMs: cliTextTimeoutMs(CODEX_MESSAGE_TIMEOUT_MS, opts), inheritEnv: opts.inheritEnv, signal: opts.signal,
765
- });
766
- const text = extractGeminiText(stdout);
767
- if (!text)
768
- throw new QuickCommitError("Gemini 返回了空的 commit message。", "EMPTY_AI_MESSAGE");
769
- return text;
770
- }
771
- finally {
772
- rmSync(root, { recursive: true, force: true });
773
- }
715
+ const stdout = await runCliText("gemini", buildGeminiTextArgs(opts), contentWithSystemPrompt("gemini", request), {
716
+ cwd,
717
+ timeoutMs: cliTextTimeoutMs(CODEX_MESSAGE_TIMEOUT_MS, opts),
718
+ inheritEnv: opts.inheritEnv,
719
+ });
720
+ const text = extractGeminiText(stdout);
721
+ if (!text)
722
+ throw new QuickCommitError("Gemini 返回了空的 commit message。", "EMPTY_AI_MESSAGE");
723
+ return text;
774
724
  }
775
- export async function callCliAiText(request, cwd, language, opts) {
725
+ async function callCliAiText(request, cwd, language, opts) {
776
726
  const provider = defaultProvider(opts.provider);
777
727
  if (provider === "codex") {
778
728
  return callCodexText(request, cwd, opts);
@@ -824,56 +774,28 @@ function singleCandidate(opts) {
824
774
  * 只有一条候选时保持原行为(错误原样抛出,不摘要)。
825
775
  * 仅用于一次性文本生成;执行工具的任务不能使用这条无条件重试路径。
826
776
  */
827
- async function callTextCandidates(request, cwd, language, opts, parseOutput) {
828
- opts.signal?.throwIfAborted();
777
+ async function callCliCandidates(request, cwd, language, opts, parseOutput) {
829
778
  const { resolveModelGroupModels } = await import("./model-groups.js");
830
- const modelGroups = opts.modelGroups ?? opts.employeeText?.config.modelGroups;
831
- const seen = new Set();
832
779
  const chain = (opts.cliCandidates?.length ? opts.cliCandidates : [singleCandidate(opts)])
833
- .flatMap((candidate) => resolveModelGroupModels(modelGroups, candidate.provider, candidate.model, { preferDefault: !candidate.model || candidate.model === "default" })
834
- .map((model) => ({ ...candidate, model: model || undefined })))
835
- .filter(candidate => {
836
- const key = JSON.stringify([candidate.provider, candidate.engine ?? "cli", candidate.model ?? "", candidate.thinkingEffort ?? opts.thinkingEffort]);
837
- if (seen.has(key))
838
- return false;
839
- seen.add(key);
840
- return true;
841
- });
780
+ .flatMap((candidate) => resolveModelGroupModels(opts.modelGroups, candidate.provider, candidate.model, { preferDefault: !candidate.model || candidate.model === "default" })
781
+ .map((model) => ({ ...candidate, model: model || undefined })));
842
782
  if (opts.employeeChannelOnly && !opts.cliCandidates?.length) {
843
- throw new QuickCommitError("系统员工没有可用的执行候选。", "AI_FALLBACK_FAILED");
783
+ throw new QuickCommitError("系统员工没有可用的 CLI 候选,已跳过 Wand Agent。", "AI_FALLBACK_FAILED");
844
784
  }
845
785
  const deadline = Date.now() + Math.max(1, opts.budgetMs ?? CLI_CHAIN_BUDGET_MS);
846
- const call = async (candidate) => {
847
- if (candidate.engine !== "sdk")
848
- return callCliAiText(request, cwd, language, { ...candidateOptions(opts, candidate), deadline });
849
- const controller = new AbortController();
850
- const timer = setTimeout(() => controller.abort(new QuickCommitError("Wand Agent 调用超时。", "CLAUDE_TIMEOUT")), Math.max(1, deadline - Date.now()));
851
- const signal = opts.signal ? AbortSignal.any([opts.signal, controller.signal]) : controller.signal;
852
- try {
853
- const generate = await abortableEmployeeText(prepareEmployeeTextCandidate({
854
- ...opts.employeeText,
855
- config: { ...opts.employeeText?.config, defaultCwd: cwd, inheritEnv: opts.inheritEnv ?? opts.employeeText?.config.inheritEnv, modelGroups },
856
- }, { ...candidate, model: candidate.model ?? "default", thinkingEffort: candidate.thinkingEffort ?? opts.thinkingEffort ?? "off", mode: "default", kind: "structured" }, request, signal), signal);
857
- signal.throwIfAborted();
858
- return await abortableEmployeeText(generate(), signal);
859
- }
860
- finally {
861
- clearTimeout(timer);
862
- }
863
- };
864
- if (chain.length === 1)
865
- return parseOutput(await call(chain[0]));
866
- const installed = chain.filter((candidate) => candidate.engine === "sdk" || providerCliInstalled(candidate.provider));
786
+ if (chain.length === 1) {
787
+ return parseOutput(await callCliAiText(request, cwd, language, { ...candidateOptions(opts, chain[0]), deadline }));
788
+ }
789
+ const installed = chain.filter((candidate) => providerCliInstalled(candidate.provider));
867
790
  const attempts = installed.length ? installed : chain;
868
791
  const errors = [];
869
792
  for (const candidate of attempts) {
870
- opts.signal?.throwIfAborted();
871
793
  if (Date.now() >= deadline) {
872
794
  errors.push("已超过等待上限");
873
795
  break;
874
796
  }
875
797
  try {
876
- const text = await call(candidate);
798
+ const text = await callCliAiText(request, cwd, language, { ...candidateOptions(opts, candidate), deadline });
877
799
  const result = await parseOutput(text);
878
800
  if (candidate !== attempts[0]) {
879
801
  console.info(`[SystemAi] 候选 ${chain.indexOf(candidate) + 1}/${chain.length} (${candidate.provider}) 成功`);
@@ -881,17 +803,16 @@ async function callTextCandidates(request, cwd, language, opts, parseOutput) {
881
803
  return result;
882
804
  }
883
805
  catch (error) {
884
- opts.signal?.throwIfAborted();
885
806
  errors.push(`${candidate.provider}: ${getGitErrorMessage(error)}`);
886
807
  // 不记录提示词、路径、CLI 原始响应或可能带鉴权信息的错误正文。
887
808
  const code = error instanceof QuickCommitError ? error.code : "INVALID_AI_RESULT";
888
809
  console.warn(`[SystemAi] 候选 ${chain.indexOf(candidate) + 1}/${chain.length} (${candidate.provider}) 失败: ${code}`);
889
810
  }
890
811
  }
891
- throw new QuickCommitError(`所有执行候选均失败:${errors.join(";")}`, "AI_FALLBACK_FAILED");
812
+ throw new QuickCommitError(`所有 CLI 候选均失败:${errors.join(";")}`, "AI_FALLBACK_FAILED");
892
813
  }
893
814
  export async function callConfiguredAiText(request, cwd, language, opts, parseOutput = (raw) => raw) {
894
- return callTextCandidates(withOpsPersona(request, opts.opsPersona), cwd, language, opts, parseOutput);
815
+ return callCliCandidates(withOpsPersona(request, opts.opsPersona), cwd, language, opts, parseOutput);
895
816
  }
896
817
  /** Read the unstaged + staged tree without touching the index. */
897
818
  async function collectWorkingTreeDiff(cwd) {
@@ -3,7 +3,6 @@ import type { DecisionService } from "./decision-service.js";
3
3
  import { type ModelFile } from "./laya-model-manifest.js";
4
4
  import { runModelSetup } from "./model-setup-process.js";
5
5
  import type { SpeechService } from "./speech-service.js";
6
- import { type SpeechSupport } from "./speech-support.js";
7
6
  import type { LocalModelKind, LocalModelsStatus } from "./local-model-types.js";
8
7
  export interface LocalModelSetupDeps {
9
8
  configDir: string;
@@ -18,7 +17,6 @@ export interface LocalModelSetupDeps {
18
17
  run?: typeof runModelSetup;
19
18
  checkRuntime?: (python: string, signal: AbortSignal) => Promise<boolean>;
20
19
  timeoutMs?: number;
21
- speechSupport?: (runtimeAvailable: boolean) => Promise<SpeechSupport>;
22
20
  }
23
21
  /** Deployment jobs only; existing inference owners retain their protocols, rate limits and capabilities. */
24
22
  export declare class LocalModelSetupService {
@@ -30,18 +28,14 @@ export declare class LocalModelSetupService {
30
28
  private readonly jobs;
31
29
  private sequence;
32
30
  private disposed;
33
- private speechEnableRevision;
34
- private supportCache;
35
31
  constructor(deps: LocalModelSetupDeps);
36
32
  private supported;
37
33
  private managedModel;
38
34
  private layaModel;
39
35
  private python;
40
36
  status(): Promise<LocalModelsStatus>;
41
- private speechSupport;
42
- setSpeechEnabled(enabled: boolean): Promise<void>;
43
37
  private parse;
44
- start(kind: LocalModelKind, action: "download" | "initialize" | "activate", raw: unknown): void;
38
+ start(kind: LocalModelKind, action: "download" | "initialize", raw: unknown): void;
45
39
  private update;
46
40
  private work;
47
41
  private checkRuntime;
@@ -7,7 +7,6 @@ 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";
11
10
  import { systemEnvValue } from "./env-utils.js";
12
11
  import { decisionHardware } from "./decision-hardware.js";
13
12
  import { DECISION_EXPERT_ID } from "./decision-expert-identity.js";
@@ -21,8 +20,6 @@ export class LocalModelSetupService {
21
20
  jobs = new Map();
22
21
  sequence = 0;
23
22
  disposed = false;
24
- speechEnableRevision = 0;
25
- supportCache = null;
26
23
  constructor(deps) {
27
24
  this.deps = deps;
28
25
  this.files = new ModelFileStore(deps.fetch);
@@ -58,7 +55,6 @@ export class LocalModelSetupService {
58
55
  const hardware = decisionHardware();
59
56
  const operation = this.operations.get("laya") ?? null;
60
57
  const selected = speech.models.find((value) => value.id === speech.settings.model);
61
- const support = await this.speechSupport(speech.runtime.available);
62
58
  return {
63
59
  laya: { kind: "laya", label: "LAYA 本地决策", supported,
64
60
  reason: !supported ? hardware.message
@@ -66,38 +62,12 @@ export class LocalModelSetupService {
66
62
  enabled: decision.enabled, model: LAYA_REPOSITORY, modelSize: this.manifest.reduce((sum, file) => sum + file.size, 0),
67
63
  downloaded: model.downloaded, runtimeAvailable: !!python, initialized: decision.state === "ready", busy: this.jobs.has("laya") || decision.queued > 0 || decision.state === "loading",
68
64
  operation: operation ? { ...operation } : null, hardware, decisionEmployeeId: DECISION_EXPERT_ID },
69
- speech: { kind: "speech", label: "服务端语音识别", supported: support.supported, supportReason: support.reason, reason: speech.reason,
65
+ speech: { kind: "speech", label: "服务端语音识别", supported: ["darwin", "linux", "win32"].includes(process.platform), reason: speech.reason,
70
66
  enabled: speech.settings.enabled, model: speech.settings.model, modelSize: selected?.size ?? 0,
71
67
  downloaded: selected?.downloaded ?? false, runtimeAvailable: speech.runtime.available, initialized: speech.initialized === true,
72
68
  busy: speech.busy || this.jobs.has("speech"), operation: this.operations.has("speech") ? { ...this.operations.get("speech") } : null },
73
69
  };
74
70
  }
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
- }
101
71
  parse(kind, raw) {
102
72
  if (!raw || typeof raw !== "object" || Array.isArray(raw))
103
73
  throw new DecisionError("INVALID_REQUEST", "模型操作参数无效。");
@@ -120,8 +90,6 @@ export class LocalModelSetupService {
120
90
  throw new DecisionError("UNAVAILABLE", "模型管理已关闭。", 503);
121
91
  if (kind === "laya" && !this.supported())
122
92
  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", "一键启用仅用于语音识别。");
125
93
  const input = this.parse(kind, raw);
126
94
  if (this.jobs.has(kind))
127
95
  throw new DecisionError("BUSY", "该模型已有安装任务,请等待或取消。", 409);
@@ -151,8 +119,6 @@ export class LocalModelSetupService {
151
119
  timer.unref();
152
120
  const signal = abort.signal;
153
121
  const phase = (value, message) => this.update(kind, id, { phase: value, message });
154
- let activationMaintenance = false;
155
- const settingsRevision = this.deps.speech.settingsRevision();
156
122
  try {
157
123
  if (kind === "laya") {
158
124
  let model = await this.layaModel();
@@ -204,7 +170,7 @@ export class LocalModelSetupService {
204
170
  }
205
171
  else {
206
172
  const selected = input.model ?? this.deps.speech.settings().model;
207
- if (action === "download" || action === "activate") {
173
+ if (action === "download") {
208
174
  phase("downloading", "下载并校验语音模型");
209
175
  await this.deps.speech.startDownload(selected);
210
176
  while (true) {
@@ -222,12 +188,7 @@ export class LocalModelSetupService {
222
188
  });
223
189
  }
224
190
  }
225
- if (action !== "download") {
226
- if (action === "activate") {
227
- signal.throwIfAborted();
228
- this.deps.speech.acquireMaintenance();
229
- activationMaintenance = true;
230
- }
191
+ else {
231
192
  const status = await this.deps.speech.status();
232
193
  signal.throwIfAborted();
233
194
  const backend = input.backend === "auto" || !input.backend ? process.platform === "darwin" ? "metal" : "cpu" : input.backend;
@@ -246,30 +207,22 @@ export class LocalModelSetupService {
246
207
  await rm(work, { recursive: true, force: true }).catch(() => { });
247
208
  }
248
209
  }
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
- }
210
+ phase("initializing", "加载所选语音模型并用合成静音检查,未启用识别或重启服务");
211
+ await this.deps.speech.initializeModel(selected, signal);
257
212
  }
258
213
  }
259
214
  signal.throwIfAborted();
260
- phase("completed", action === "activate" ? "服务端语音识别已启用。" : action === "download" ? "模型下载及完整性校验完成;可继续初始化" : "运行时和模型初始化检查完成;启用状态保持不变");
215
+ phase("completed", action === "download" ? "模型下载及完整性校验完成;可继续初始化" : "运行时和模型初始化检查完成;启用状态保持不变");
261
216
  }
262
217
  catch (error) {
263
218
  const cancelled = signal.aborted;
264
219
  const message = cancelled ? "安装任务已取消,完整文件保留,可重试" : error instanceof DecisionError || (error instanceof Error && /请先|校验|自定义|独立/.test(error.message))
265
220
  ? error.message : "安装或初始化失败,请检查网络、编译工具/Python版本、磁盘空间和平台能力,再重试。";
266
221
  this.update(kind, id, { phase: cancelled ? "cancelled" : "failed", message, ...(cancelled ? {} : { error: message }) });
267
- if (kind === "speech" && (action === "download" || action === "activate") && cancelled)
222
+ if (kind === "speech" && action === "download" && cancelled)
268
223
  this.deps.speech.cancelDownload();
269
224
  }
270
225
  finally {
271
- if (activationMaintenance)
272
- this.deps.speech.releaseMaintenance();
273
226
  clearTimeout(timer);
274
227
  }
275
228
  }
@@ -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" | "activate";
6
+ action: "download" | "initialize";
7
7
  phase: ModelSetupPhase;
8
8
  message: string;
9
9
  received: number;
@@ -14,7 +14,6 @@ export interface LocalModelStatus {
14
14
  kind: LocalModelKind;
15
15
  label: string;
16
16
  supported: boolean;
17
- supportReason?: string | null;
18
17
  reason: string | null;
19
18
  enabled: boolean;
20
19
  model: string;
@@ -3,7 +3,6 @@ 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";
7
6
  import { type PtyTerminalSnapshot, type PtyHistoryPage } from "./pty-terminal-state.js";
8
7
  import { samplePtyForegrounds } from "./pty-foreground.js";
9
8
  import { type TerminalHost } from "./terminal-host.js";
@@ -35,7 +34,6 @@ export declare const PTY_TURN_IDLE_MS = 3000;
35
34
  */
36
35
  export declare const PTY_FOREGROUND_SAMPLE_MS = 1000;
37
36
  export interface ProcessManagerOptions {
38
- free?: EmployeeTextDeps["free"];
39
37
  /** Overrides the non-Claude PTY turn quiet window (tests use a short one). */
40
38
  ptyTurnIdleMs?: number;
41
39
  /** Overrides the foreground-process-group sampling interval (tests use a short one). */
@@ -57,7 +55,6 @@ export declare class ProcessManager extends EventEmitter {
57
55
  private readonly ptyForegroundSampleMs;
58
56
  /** Foreground probe; injectable so tests can drive it without a real process table. */
59
57
  private readonly probePtyForegrounds;
60
- private readonly free;
61
58
  private readonly resolveDecisionEvaluate;
62
59
  /** 24h archive scan timer */
63
60
  private archiveTimer;
@@ -180,7 +177,7 @@ export declare class ProcessManager extends EventEmitter {
180
177
  */
181
178
  setSessionModel(id: string, model: string | null): SessionSnapshot;
182
179
  /**
183
- * 归档只写标记,保留正在运行的终端、会话 ID 和 provider session id。
180
+ * 归档会先停掉还在跑的终端,但保留会话 ID 和 provider session id,恢复时用它们续上。
184
181
  * 取消归档只清标记;重新拉起由恢复入口负责。
185
182
  */
186
183
  setSessionArchived(id: string, archived: boolean): SessionSnapshot;
@@ -563,7 +563,6 @@ 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;
567
566
  resolveDecisionEvaluate;
568
567
  /** 24h archive scan timer */
569
568
  archiveTimer = null;
@@ -596,7 +595,6 @@ export class ProcessManager extends EventEmitter {
596
595
  this.ptyTurnIdleMs = options.ptyTurnIdleMs ?? PTY_TURN_IDLE_MS;
597
596
  this.ptyForegroundSampleMs = options.ptyForegroundSampleMs ?? PTY_FOREGROUND_SAMPLE_MS;
598
597
  this.probePtyForegrounds = options.samplePtyForegrounds ?? samplePtyForegrounds;
599
- this.free = options.free;
600
598
  this.resolveDecisionEvaluate = options.resolveDecisionEvaluate ?? (() => undefined);
601
599
  this.terminalHost = terminalHost ?? new InProcessTerminalHost();
602
600
  this.logger = new SessionLogger(configDir || path.join(process.env.HOME || process.cwd(), ".wand"), config.shortcutLogMaxBytes);
@@ -1231,7 +1229,7 @@ export class ProcessManager extends EventEmitter {
1231
1229
  ai: resolveSystemAiContext({
1232
1230
  provider: groupProvider, runner: undefined, command: providerCliCommand(groupProvider),
1233
1231
  structuredState: undefined, selectedModel: null, thinkingEffort: opts?.thinkingEffort,
1234
- }, this.config, this.storage.getSystemSiliconEmployee(), this.free),
1232
+ }, this.config, this.storage.getSystemSiliconEmployee()),
1235
1233
  cwd: resolveSessionCwd(cwd, this.config.defaultCwd),
1236
1234
  language: this.config.language,
1237
1235
  });
@@ -1777,10 +1775,13 @@ export class ProcessManager extends EventEmitter {
1777
1775
  return this.snapshot(record);
1778
1776
  }
1779
1777
  /**
1780
- * 归档只写标记,保留正在运行的终端、会话 ID 和 provider session id。
1778
+ * 归档会先停掉还在跑的终端,但保留会话 ID 和 provider session id,恢复时用它们续上。
1781
1779
  * 取消归档只清标记;重新拉起由恢复入口负责。
1782
1780
  */
1783
1781
  setSessionArchived(id, archived) {
1782
+ if (archived && this.mustGet(id).status === "running") {
1783
+ this.stop(id);
1784
+ }
1784
1785
  const record = this.mustGet(id);
1785
1786
  record.archived = archived;
1786
1787
  record.archivedAt = archived ? (record.archivedAt ?? new Date().toISOString()) : null;
@@ -2353,7 +2354,7 @@ export class ProcessManager extends EventEmitter {
2353
2354
  input: prompt,
2354
2355
  cwd: record.cwd,
2355
2356
  language: this.config.language,
2356
- ai: resolveSystemAiContext(record, this.config, this.storage.getSystemSiliconEmployee(), this.free),
2357
+ ai: resolveSystemAiContext(record, this.config, this.storage.getSystemSiliconEmployee()),
2357
2358
  readNativeTitle: () => readNativeSessionTitle(resolveSessionProvider(record), record.claudeSessionId, record.cwd),
2358
2359
  hasNativeTitle,
2359
2360
  onGenerating: (generating) => {
@@ -1,6 +1,5 @@
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";
4
3
  import type { WandStorage } from "./storage.js";
5
4
  import type { WandTaskAgent } from "./task-types.js";
6
5
  import type { WandConfig } from "./types.js";
@@ -18,7 +17,6 @@ export declare function registerSiliconEmployeeRoutes(app: Express, deps: {
18
17
  storage: WandStorage;
19
18
  notifyEmployeeChanged?: (employeeId: string) => void;
20
19
  config?: WandConfig;
21
- free?: Pick<OpenRouterFreeModelsService, "resolveForCall">;
22
20
  /** 测试注入点:默认按系统 AI 配置真实调用模型。 */
23
21
  generateDraft?: (request: EmployeeDraftRequest) => Promise<SiliconEmployeeDraft>;
24
22
  /** 测试注入点:CLI 可用性探测。 */