@co0ontty/wand 4.89.0 → 4.89.2-beta.g9f64cf7

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
@@ -0,0 +1,124 @@
1
+ import { normalizeContext } from "@earendil-works/pi-ai";
2
+ import { coreHarnessRuntime, loadPiAi, resolveCoreModel } from "./harness-engine.js";
3
+ import { agentKey } from "./ai-team-types.js";
4
+ import { getDefaultModelForProvider } from "./config.js";
5
+ import { freeModelOrder, resolveModelGroupModels } from "./model-groups.js";
6
+ import { isOpenRouterFreeSelector, OPENROUTER_FREE_ROUTING } from "./openrouter-free-models.js";
7
+ import { providerCliInstalled } from "./session-provider.js";
8
+ import { thinkingEffortToPiLevel } from "./structured-provider-common.js";
9
+ import { classifyStructuredFailure } from "./structured-failure.js";
10
+ /** Expand each configured slot once; equivalent leaves execute once in first occurrence order. */
11
+ export function employeeTextCandidates(agents, config) {
12
+ const seen = new Set();
13
+ return agents.flatMap((candidate, index) => {
14
+ let models;
15
+ try {
16
+ const model = candidate.model === "default" || !candidate.model ? getDefaultModelForProvider(config, candidate.provider) : candidate.model;
17
+ models = resolveModelGroupModels(config.modelGroups, candidate.provider, model, { preferDefault: !candidate.model || candidate.model === "default" });
18
+ }
19
+ catch {
20
+ return [];
21
+ } // A removed/empty group has accepted no input and is unavailable.
22
+ return models.flatMap(model => {
23
+ const agent = { ...candidate, model, engine: candidate.engine ?? "cli" };
24
+ const key = agentKey(agent);
25
+ if (seen.has(key))
26
+ return [];
27
+ seen.add(key);
28
+ return [{ agent, index }];
29
+ });
30
+ });
31
+ }
32
+ export async function abortableEmployeeText(pending, signal) {
33
+ let abort;
34
+ try {
35
+ return await Promise.race([pending, new Promise((_resolve, reject) => {
36
+ abort = () => reject(signal.reason);
37
+ signal.addEventListener("abort", abort, { once: true });
38
+ if (signal.aborted)
39
+ abort();
40
+ })]);
41
+ }
42
+ finally {
43
+ if (abort)
44
+ signal.removeEventListener("abort", abort);
45
+ }
46
+ }
47
+ export class EmployeeCandidateRejected extends Error {
48
+ failure;
49
+ constructor(failure) {
50
+ super("SDK 候选在执行前被拒绝。");
51
+ this.failure = failure;
52
+ }
53
+ }
54
+ function sdkText(message, status) {
55
+ if (message.stopReason === "error") {
56
+ const rejection = status === 401 || status === 403 ? "authentication"
57
+ : status === 402 ? "quota" : status === 429 ? "rate-limit" : status === 404 ? "model-unavailable" : undefined;
58
+ const progressed = message.content.length > 0 || Object.values(message.usage.cost).some(value => value > 0)
59
+ || message.usage.input > 0 || message.usage.output > 0;
60
+ const failure = classifyStructuredFailure({ state: { blocks: [], result: "", sessionId: null }, exitCode: 1,
61
+ signal: null, stderr: "", primaryError: "SDK 文字生成失败。", rejection,
62
+ inputAccepted: rejection && !progressed ? false : undefined }, { progress: progressed });
63
+ if (failure?.retryable)
64
+ throw new EmployeeCandidateRejected(failure);
65
+ throw new Error("SDK文字生成失败。");
66
+ }
67
+ if (message.stopReason === "aborted" || message.content.some(part => part.type === "toolCall"))
68
+ throw new Error("SDK文字生成失败。");
69
+ return message.content.filter(part => part.type === "text").map(part => part.text).join("");
70
+ }
71
+ /** Prepare a configured, no-tool adapter before any user input is accepted. */
72
+ export async function prepareEmployeeTextCandidate(deps, agent, request, signal) {
73
+ signal.throwIfAborted();
74
+ const config = deps.config;
75
+ if (agent.engine !== "sdk") {
76
+ if (!providerCliInstalled(agent.provider) || isOpenRouterFreeSelector(agent.model))
77
+ throw new Error("CLI候选不可用。");
78
+ const { callCliAiText, assertGeminiTextCapability } = await import("./git-quick-commit.js");
79
+ if (agent.provider === "gemini")
80
+ assertGeminiTextCapability();
81
+ // One adapter call, never re-enter the candidate chain.
82
+ return async () => {
83
+ signal.throwIfAborted();
84
+ return callCliAiText(request, config.defaultCwd ?? process.cwd(), "", { provider: agent.provider, model: agent.model,
85
+ thinkingEffort: agent.thinkingEffort, inheritEnv: config.inheritEnv, signal, deadline: Date.now() + 20_000 });
86
+ };
87
+ }
88
+ if (agent.provider !== "pi")
89
+ throw new Error("Wand Agent只支持Pi provider。");
90
+ const context = normalizeContext({ systemPrompt: request.system,
91
+ messages: [{ role: "user", content: request.prompt, timestamp: Date.now() }], tools: [] });
92
+ const reasoning = thinkingEffortToPiLevel(agent.thinkingEffort ?? "off");
93
+ if (isOpenRouterFreeSelector(agent.model)) {
94
+ if (!deps.free)
95
+ throw new Error("Wand 免费分组尚未连接执行服务。");
96
+ const order = freeModelOrder(config.modelGroups);
97
+ const checked = await deps.free.resolveForCall(agent.model, signal, order.length ? { allowedSelectors: order } : {});
98
+ const { streamSimple } = await import("@earendil-works/pi-ai/api/openai-completions");
99
+ return async () => {
100
+ signal.throwIfAborted();
101
+ let status;
102
+ const message = await streamSimple(checked.model, context, { apiKey: checked.apiKey, signal,
103
+ maxRetries: 0, fetch: async (input, init) => { const response = await fetch(input, init); status = response.status; return response; },
104
+ maxTokens: Math.min(4096, checked.model.maxTokens), reasoning: reasoning === "off" ? undefined : reasoning,
105
+ toolChoice: "none", onPayload: payload => ({ ...payload, provider: OPENROUTER_FREE_ROUTING }) }).result();
106
+ const text = sdkText(message, status);
107
+ deps.onUsage?.({ inputTokens: message.usage.input, outputTokens: message.usage.output, model: `${checked.model.provider}/${checked.model.id}` });
108
+ return text;
109
+ };
110
+ }
111
+ const resolved = await resolveCoreModel(config.harness, agent.model, { cwd: config.defaultCwd });
112
+ if (!resolved)
113
+ throw new Error("Wand Agent模型不可用。");
114
+ const runtime = await coreHarnessRuntime(config.harness), pi = await loadPiAi();
115
+ const level = pi.clampThinkingLevel(resolved.model, (reasoning ?? "off"));
116
+ return async () => {
117
+ signal.throwIfAborted();
118
+ const message = await runtime.streamSimple(resolved.model, context, { signal, maxRetries: 0, maxTokens: Math.min(4096, resolved.model.maxTokens),
119
+ reasoning: level === "off" ? undefined : level, toolChoice: "none" }).result();
120
+ const text = sdkText(message);
121
+ deps.onUsage?.({ inputTokens: message.usage.input, outputTokens: message.usage.output, model: `${resolved.model.provider}/${resolved.model.id}` });
122
+ return text;
123
+ };
124
+ }
@@ -0,0 +1,19 @@
1
+ export declare const FIXED_EMPLOYEE_AVATARS: readonly [{
2
+ readonly id: "e_wand_default";
3
+ readonly systemKey: "wand-default";
4
+ readonly avatar: "plush-cat:v1:silver";
5
+ }, {
6
+ readonly id: "e_wand_ops";
7
+ readonly systemKey: "wand-ops";
8
+ readonly avatar: "plush-cat:v1:orange";
9
+ }];
10
+ /** Stable identity pairs only: names and source employees never determine policy. */
11
+ export declare function fixedEmployeeAvatar(identity: {
12
+ id?: string | null;
13
+ systemKey?: string | null;
14
+ }): string | null;
15
+ /** The avatar exception does not change any other built-in employee's edit policy. */
16
+ export declare function isFixedAvatarEmployee(identity: {
17
+ id?: string | null;
18
+ systemKey?: string | null;
19
+ }): boolean;
@@ -0,0 +1,13 @@
1
+ import { DEFAULT_EMPLOYEE_ID, DEFAULT_EMPLOYEE_KEY, SYSTEM_EMPLOYEE_ID, SYSTEM_EMPLOYEE_KEY } from "./ai-team-types.js";
2
+ export const FIXED_EMPLOYEE_AVATARS = [
3
+ { id: DEFAULT_EMPLOYEE_ID, systemKey: DEFAULT_EMPLOYEE_KEY, avatar: "plush-cat:v1:silver" },
4
+ { id: SYSTEM_EMPLOYEE_ID, systemKey: SYSTEM_EMPLOYEE_KEY, avatar: "plush-cat:v1:orange" },
5
+ ];
6
+ /** Stable identity pairs only: names and source employees never determine policy. */
7
+ export function fixedEmployeeAvatar(identity) {
8
+ return FIXED_EMPLOYEE_AVATARS.find((entry) => entry.id === identity.id && entry.systemKey === identity.systemKey)?.avatar ?? null;
9
+ }
10
+ /** The avatar exception does not change any other built-in employee's edit policy. */
11
+ export function isFixedAvatarEmployee(identity) {
12
+ return fixedEmployeeAvatar(identity) !== null;
13
+ }
@@ -1,3 +1,4 @@
1
+ import { type EmployeeTextDeps } from "./employee-text.js";
1
2
  import type { AiTextRequest } from "./types.js";
2
3
  import { GitStatusResult, PushResult, QuickCommitResult, SessionProvider, SessionSnapshot, TagHeadResult } from "./types.js";
3
4
  export type QuickCommitErrorCode = "CWD_MISSING" | "NO_CWD" | "NOT_A_GIT_REPO" | "NO_COMMIT" | "NOTHING_TO_COMMIT" | "NOTHING_TO_PUSH" | "EMPTY_MESSAGE" | "EMPTY_TAG" | "EMPTY_AI_MESSAGE" | "AI_FALLBACK_FAILED" | "INVALID_AI_TAG" | "TAG_EXISTS" | "GIT_ADD_FAILED" | "GIT_DIFF_FAILED" | "GIT_COMMIT_FAILED" | "GIT_TAG_FAILED" | "CLAUDE_CLI_MISSING" | "CLAUDE_CLI_FAILED" | "CLAUDE_TIMEOUT";
@@ -26,12 +27,15 @@ export interface QuickCommitAiOptions {
26
27
  inheritEnv?: boolean;
27
28
  /** 内置「系统运维」员工的角色设定:作为系统提示前缀注入。 */
28
29
  opsPersona?: string;
29
- /** CLI 降级链(按顺序);未设置时只用 provider/model 这一次调用。 */
30
+ /** 员工候选链(按顺序);未设置时只用 provider/model 这一次调用。 */
30
31
  cliCandidates?: import("./types.js").AiCliCandidate[];
31
- /** 系统应用必须经员工渠道;没有可用 CLI 候选时直接失败,不退回默认 provider。 */
32
+ /** SDK 使用当前实例配置与现有免费分组服务,不创建其他执行渠道。 */
33
+ employeeText?: EmployeeTextDeps;
34
+ /** 系统应用必须经员工渠道;没有可用候选时直接失败,不退回默认 provider。 */
32
35
  employeeChannelOnly?: boolean;
33
36
  /** 整条候选链的时间上限;默认 CLI_CHAIN_BUDGET_MS。第一条消息里的调用要短得多。 */
34
37
  budgetMs?: number;
38
+ signal?: AbortSignal;
35
39
  }
36
40
  export declare class QuickCommitError extends Error {
37
41
  readonly code: QuickCommitErrorCode;
@@ -47,11 +51,16 @@ export interface CommitInputOptions {
47
51
  iterationDigest?: string;
48
52
  includeDiff?: boolean;
49
53
  }
54
+ type CliAiTextOptions = QuickCommitAiOptions & {
55
+ deadline?: number;
56
+ };
57
+ export declare function assertGeminiTextCapability(): void;
58
+ export declare function callCliAiText(request: AiTextRequest, cwd: string, language: string, opts: CliAiTextOptions): Promise<string>;
50
59
  /** 角色设定走系统提示前缀;任务自己的输出格式与约束必须排在后面。 */
51
60
  export declare function withOpsPersona(request: AiTextRequest, persona?: string): AiTextRequest;
52
61
  /**
53
- * Run a lightweight AI request through the system employee's CLI chain.
54
- * 所有 Wand 自有文本调用都走本机已安装的 CLI,没有任何直连 API 分支。
62
+ * Run a lightweight AI request through the system employee's configured chain.
63
+ * provider 与引擎由候选决定;SDK 与 CLI 均保留各自适配和身份。
55
64
  * parseOutput 在候选链内解析/校验结果;失败后继续下一条,而非提前宣布成功。
56
65
  */
57
66
  export declare function callConfiguredAiText(request: AiTextRequest, cwd: string, language: string, opts: QuickCommitAiOptions): Promise<string>;
@@ -1,5 +1,9 @@
1
- import { existsSync } from "node:fs";
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";
2
5
  import { spawn } from "node:child_process";
6
+ import { abortableEmployeeText, prepareEmployeeTextCandidate } from "./employee-text.js";
3
7
  import { buildChildEnv } from "./env-utils.js";
4
8
  import { buildLanguageDirective } from "./language-prompt.js";
5
9
  import { isSessionProvider, providerCliInstalled } from "./session-provider.js";
@@ -306,7 +310,7 @@ async function callClaudeText(request, cwd, language, opts) {
306
310
  const stdout = await runCliText("claude", args, request.prompt, {
307
311
  cwd,
308
312
  timeoutMs: cliTextTimeoutMs(CLAUDE_MESSAGE_TIMEOUT_MS, opts),
309
- inheritEnv: opts.inheritEnv,
313
+ inheritEnv: opts.inheritEnv, signal: opts.signal,
310
314
  });
311
315
  const text = stdout.trim();
312
316
  if (!text)
@@ -538,6 +542,8 @@ function buildGrokTextArgs(request, opts, allowTools = false) {
538
542
  args.push(...systemPromptArgsFrom("grok", request.system));
539
543
  if (allowTools)
540
544
  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");
541
547
  return args;
542
548
  }
543
549
  /**
@@ -568,6 +574,8 @@ function buildQoderTextArgs(request, opts, allowTools = false) {
568
574
  args.push(...systemPromptArgsFrom("qoder", request.system));
569
575
  if (allowTools)
570
576
  args.push("--permission-mode", "bypass_permissions");
577
+ else
578
+ args.push("--tools", "", "--strict-mcp-config");
571
579
  return args;
572
580
  }
573
581
  /** provider 自己的系统提示开关 + 规则文本;没有开关的 provider 返回空。 */
@@ -583,35 +591,48 @@ function contentWithSystemPrompt(provider, request) {
583
591
  return systemPromptFlag(provider) ? request.prompt : composeSystemFallback(request.system, request.prompt);
584
592
  }
585
593
  function runCliText(command, args, prompt, opts) {
594
+ opts.signal?.throwIfAborted();
586
595
  return new Promise((resolve, reject) => {
596
+ const baseEnv = buildChildEnv(opts.inheritEnv !== false);
587
597
  const child = spawn(command, args, {
588
598
  cwd: opts.cwd,
589
- env: buildChildEnv(opts.inheritEnv !== false),
599
+ env: opts.env ? opts.env(baseEnv) : baseEnv,
590
600
  stdio: ["pipe", "pipe", "pipe"],
591
601
  });
592
602
  let stdout = "";
593
603
  let stderr = "";
594
604
  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); };
595
614
  const timeout = setTimeout(() => {
596
615
  settled = true;
616
+ opts.signal?.removeEventListener("abort", cancelled);
597
617
  child.kill("SIGTERM");
598
618
  reject(new QuickCommitError(`${command} 调用超时。`, "CLAUDE_TIMEOUT"));
599
619
  }, opts.timeoutMs);
620
+ opts.signal?.addEventListener("abort", cancelled, { once: true });
600
621
  child.stdout?.on("data", (chunk) => { stdout += chunk.toString(); });
601
622
  child.stderr?.on("data", (chunk) => { stderr += chunk.toString(); });
602
623
  child.on("error", (error) => {
603
624
  if (settled)
604
625
  return;
605
626
  settled = true;
606
- clearTimeout(timeout);
627
+ cleanup();
607
628
  const code = error.code === "ENOENT" ? "CLAUDE_CLI_MISSING" : "CLAUDE_CLI_FAILED";
608
629
  reject(new QuickCommitError(error.code === "ENOENT" ? `未找到 ${command} CLI。` : `${command} CLI 失败:${error.message}`, code));
609
630
  });
610
631
  child.on("close", (code) => {
632
+ cleanup();
611
633
  if (settled)
612
634
  return;
613
635
  settled = true;
614
- clearTimeout(timeout);
615
636
  if (code === 0) {
616
637
  resolve(stdout);
617
638
  return;
@@ -632,7 +653,7 @@ function runCliText(command, args, prompt, opts) {
632
653
  if (settled || !expectsStdin)
633
654
  return;
634
655
  settled = true;
635
- clearTimeout(timeout);
656
+ cleanup();
636
657
  child.kill("SIGTERM");
637
658
  reject(new QuickCommitError(`${command} CLI stdin failed: ${error.code ?? "write error"}`, "CLAUDE_CLI_FAILED"));
638
659
  });
@@ -654,7 +675,7 @@ async function callCodexText(request, cwd, opts) {
654
675
  const stdout = await runCliText("codex", args, contentWithSystemPrompt("codex", request), {
655
676
  cwd,
656
677
  timeoutMs: cliTextTimeoutMs(CODEX_MESSAGE_TIMEOUT_MS, opts),
657
- inheritEnv: opts.inheritEnv,
678
+ inheritEnv: opts.inheritEnv, signal: opts.signal,
658
679
  });
659
680
  const text = extractCodexText(stdout);
660
681
  if (!text) {
@@ -663,7 +684,8 @@ async function callCodexText(request, cwd, opts) {
663
684
  return text;
664
685
  }
665
686
  async function callOpenCodeText(request, cwd, opts) {
666
- const args = ["run", "--format", "json"];
687
+ const agent = `wand-text-${randomUUID()}`;
688
+ const args = ["run", "--format", "json", "--pure", "--agent", agent];
667
689
  const model = opts.model?.trim();
668
690
  if (model && model !== "default")
669
691
  args.push("--model", model);
@@ -673,7 +695,18 @@ async function callOpenCodeText(request, cwd, opts) {
673
695
  const stdout = await runCliText("opencode", args, contentWithSystemPrompt("opencode", request), {
674
696
  cwd,
675
697
  timeoutMs: cliTextTimeoutMs(CODEX_MESSAGE_TIMEOUT_MS, opts),
676
- inheritEnv: opts.inheritEnv,
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
+ },
677
710
  });
678
711
  const text = extractOpenCodeText(stdout);
679
712
  if (!text)
@@ -684,7 +717,7 @@ async function callGrokText(request, cwd, opts) {
684
717
  const stdout = await runCliText("grok", buildGrokTextArgs(request, opts), "", {
685
718
  cwd,
686
719
  timeoutMs: cliTextTimeoutMs(CODEX_MESSAGE_TIMEOUT_MS, opts),
687
- inheritEnv: opts.inheritEnv,
720
+ inheritEnv: opts.inheritEnv, signal: opts.signal,
688
721
  });
689
722
  const text = extractGrokText(stdout);
690
723
  if (!text)
@@ -695,7 +728,7 @@ async function callQoderText(request, cwd, opts) {
695
728
  const stdout = await runCliText("qodercli", buildQoderTextArgs(request, opts), "", {
696
729
  cwd,
697
730
  timeoutMs: cliTextTimeoutMs(CODEX_MESSAGE_TIMEOUT_MS, opts),
698
- inheritEnv: opts.inheritEnv,
731
+ inheritEnv: opts.inheritEnv, signal: opts.signal,
699
732
  });
700
733
  const text = extractQoderText(stdout);
701
734
  if (!text)
@@ -704,25 +737,42 @@ async function callQoderText(request, cwd, opts) {
704
737
  }
705
738
  async function callPiText(request, cwd, opts) {
706
739
  const stdout = await runCliText("pi", buildPiTextArgs(request, opts), "", {
707
- cwd, timeoutMs: cliTextTimeoutMs(CODEX_MESSAGE_TIMEOUT_MS, opts), inheritEnv: opts.inheritEnv,
740
+ cwd, timeoutMs: cliTextTimeoutMs(CODEX_MESSAGE_TIMEOUT_MS, opts), inheritEnv: opts.inheritEnv, signal: opts.signal,
708
741
  });
709
742
  const text = extractPiText(stdout);
710
743
  if (!text)
711
744
  throw new QuickCommitError("Pi 返回了空的 commit message。", "EMPTY_AI_MESSAGE");
712
745
  return text;
713
746
  }
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
+ }
714
756
  async function callGeminiText(request, cwd, opts) {
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;
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
+ }
724
774
  }
725
- async function callCliAiText(request, cwd, language, opts) {
775
+ export async function callCliAiText(request, cwd, language, opts) {
726
776
  const provider = defaultProvider(opts.provider);
727
777
  if (provider === "codex") {
728
778
  return callCodexText(request, cwd, opts);
@@ -774,28 +824,56 @@ function singleCandidate(opts) {
774
824
  * 只有一条候选时保持原行为(错误原样抛出,不摘要)。
775
825
  * 仅用于一次性文本生成;执行工具的任务不能使用这条无条件重试路径。
776
826
  */
777
- async function callCliCandidates(request, cwd, language, opts, parseOutput) {
827
+ async function callTextCandidates(request, cwd, language, opts, parseOutput) {
828
+ opts.signal?.throwIfAborted();
778
829
  const { resolveModelGroupModels } = await import("./model-groups.js");
830
+ const modelGroups = opts.modelGroups ?? opts.employeeText?.config.modelGroups;
831
+ const seen = new Set();
779
832
  const chain = (opts.cliCandidates?.length ? opts.cliCandidates : [singleCandidate(opts)])
780
- .flatMap((candidate) => resolveModelGroupModels(opts.modelGroups, candidate.provider, candidate.model, { preferDefault: !candidate.model || candidate.model === "default" })
781
- .map((model) => ({ ...candidate, model: model || undefined })));
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
+ });
782
842
  if (opts.employeeChannelOnly && !opts.cliCandidates?.length) {
783
- throw new QuickCommitError("系统员工没有可用的 CLI 候选,已跳过 Wand Agent。", "AI_FALLBACK_FAILED");
843
+ throw new QuickCommitError("系统员工没有可用的执行候选。", "AI_FALLBACK_FAILED");
784
844
  }
785
845
  const deadline = Date.now() + Math.max(1, opts.budgetMs ?? CLI_CHAIN_BUDGET_MS);
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));
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));
790
867
  const attempts = installed.length ? installed : chain;
791
868
  const errors = [];
792
869
  for (const candidate of attempts) {
870
+ opts.signal?.throwIfAborted();
793
871
  if (Date.now() >= deadline) {
794
872
  errors.push("已超过等待上限");
795
873
  break;
796
874
  }
797
875
  try {
798
- const text = await callCliAiText(request, cwd, language, { ...candidateOptions(opts, candidate), deadline });
876
+ const text = await call(candidate);
799
877
  const result = await parseOutput(text);
800
878
  if (candidate !== attempts[0]) {
801
879
  console.info(`[SystemAi] 候选 ${chain.indexOf(candidate) + 1}/${chain.length} (${candidate.provider}) 成功`);
@@ -803,16 +881,17 @@ async function callCliCandidates(request, cwd, language, opts, parseOutput) {
803
881
  return result;
804
882
  }
805
883
  catch (error) {
884
+ opts.signal?.throwIfAborted();
806
885
  errors.push(`${candidate.provider}: ${getGitErrorMessage(error)}`);
807
886
  // 不记录提示词、路径、CLI 原始响应或可能带鉴权信息的错误正文。
808
887
  const code = error instanceof QuickCommitError ? error.code : "INVALID_AI_RESULT";
809
888
  console.warn(`[SystemAi] 候选 ${chain.indexOf(candidate) + 1}/${chain.length} (${candidate.provider}) 失败: ${code}`);
810
889
  }
811
890
  }
812
- throw new QuickCommitError(`所有 CLI 候选均失败:${errors.join(";")}`, "AI_FALLBACK_FAILED");
891
+ throw new QuickCommitError(`所有执行候选均失败:${errors.join(";")}`, "AI_FALLBACK_FAILED");
813
892
  }
814
893
  export async function callConfiguredAiText(request, cwd, language, opts, parseOutput = (raw) => raw) {
815
- return callCliCandidates(withOpsPersona(request, opts.opsPersona), cwd, language, opts, parseOutput);
894
+ return callTextCandidates(withOpsPersona(request, opts.opsPersona), cwd, language, opts, parseOutput);
816
895
  }
817
896
  /** Read the unstaged + staged tree without touching the index. */
818
897
  async function collectWorkingTreeDiff(cwd) {
@@ -3,6 +3,7 @@ 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";
6
7
  import type { LocalModelKind, LocalModelsStatus } from "./local-model-types.js";
7
8
  export interface LocalModelSetupDeps {
8
9
  configDir: string;
@@ -17,6 +18,7 @@ export interface LocalModelSetupDeps {
17
18
  run?: typeof runModelSetup;
18
19
  checkRuntime?: (python: string, signal: AbortSignal) => Promise<boolean>;
19
20
  timeoutMs?: number;
21
+ speechSupport?: (runtimeAvailable: boolean) => Promise<SpeechSupport>;
20
22
  }
21
23
  /** Deployment jobs only; existing inference owners retain their protocols, rate limits and capabilities. */
22
24
  export declare class LocalModelSetupService {
@@ -28,14 +30,18 @@ export declare class LocalModelSetupService {
28
30
  private readonly jobs;
29
31
  private sequence;
30
32
  private disposed;
33
+ private speechEnableRevision;
34
+ private supportCache;
31
35
  constructor(deps: LocalModelSetupDeps);
32
36
  private supported;
33
37
  private managedModel;
34
38
  private layaModel;
35
39
  private python;
36
40
  status(): Promise<LocalModelsStatus>;
41
+ private speechSupport;
42
+ setSpeechEnabled(enabled: boolean): Promise<void>;
37
43
  private parse;
38
- start(kind: LocalModelKind, action: "download" | "initialize", raw: unknown): void;
44
+ start(kind: LocalModelKind, action: "download" | "initialize" | "activate", raw: unknown): void;
39
45
  private update;
40
46
  private work;
41
47
  private checkRuntime;