@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.
- package/dist/agent-dispatch.d.ts +5 -0
- package/dist/agent-dispatch.js +8 -0
- package/dist/ai-team-employee-binding.d.ts +1 -0
- package/dist/ai-team-employee-binding.js +1 -1
- package/dist/ai-team-runner.d.ts +1 -1
- package/dist/ai-team-runner.js +13 -5
- package/dist/ai-team-types.d.ts +2 -2
- package/dist/ai-team-types.js +3 -2
- package/dist/build-info.json +4 -4
- package/dist/commit-task-archive.d.ts +3 -0
- package/dist/commit-task-archive.js +15 -0
- package/dist/config.js +1 -0
- package/dist/daemon-maintenance-targets.js +1 -1
- package/dist/decision-expert-employee.d.ts +1 -1
- package/dist/decision-expert-employee.js +3 -3
- package/dist/decision-expert-service.d.ts +6 -5
- package/dist/decision-expert-service.js +75 -49
- package/dist/decision-types.d.ts +2 -1
- package/dist/default-employee.d.ts +5 -1
- package/dist/default-employee.js +17 -5
- package/dist/employee-text.d.ts +28 -0
- package/dist/employee-text.js +124 -0
- package/dist/fixed-employee-avatar.d.ts +19 -0
- package/dist/fixed-employee-avatar.js +13 -0
- package/dist/git-quick-commit.d.ts +13 -4
- package/dist/git-quick-commit.js +112 -33
- package/dist/local-model-setup.d.ts +7 -1
- package/dist/local-model-setup.js +54 -7
- package/dist/local-model-types.d.ts +2 -1
- package/dist/plush-avatar.d.ts +100 -0
- package/dist/plush-avatar.js +96 -0
- package/dist/process-manager.d.ts +4 -1
- package/dist/process-manager.js +9 -7
- package/dist/server-employee-routes.d.ts +6 -2
- package/dist/server-employee-routes.js +55 -53
- package/dist/server-file-routes.js +61 -25
- package/dist/server-local-model-routes.js +12 -0
- package/dist/server-session-routes.d.ts +2 -1
- package/dist/server-session-routes.js +13 -10
- package/dist/server-speech-routes.d.ts +2 -0
- package/dist/server-speech-routes.js +7 -1
- package/dist/server-task-routes.d.ts +3 -0
- package/dist/server-task-routes.js +19 -10
- package/dist/server.js +25 -8
- package/dist/session-ai-context.d.ts +6 -4
- package/dist/session-ai-context.js +10 -9
- package/dist/silicon-employee-dispatch.d.ts +4 -6
- package/dist/silicon-employee-dispatch.js +9 -6
- package/dist/speech-polisher-employee.d.ts +6 -0
- package/dist/speech-polisher-employee.js +23 -0
- package/dist/speech-polisher-identity.d.ts +4 -0
- package/dist/speech-polisher-identity.js +4 -0
- package/dist/speech-polisher-service.d.ts +31 -0
- package/dist/speech-polisher-service.js +88 -0
- package/dist/speech-service.d.ts +3 -1
- package/dist/speech-service.js +15 -4
- package/dist/speech-support.d.ts +6 -0
- package/dist/speech-support.js +26 -0
- package/dist/speech-types.d.ts +5 -0
- package/dist/storage.d.ts +13 -4
- package/dist/storage.js +84 -32
- package/dist/structured-session-manager.d.ts +3 -2
- package/dist/structured-session-manager.js +11 -7
- package/dist/system-employee.d.ts +6 -4
- package/dist/system-employee.js +13 -10
- package/dist/types.d.ts +3 -1
- package/dist/user-memory.d.ts +2 -0
- package/dist/user-memory.js +1 -1
- package/dist/wand-task-sync.js +13 -2
- package/dist/web-ui/content/ai-teams.js +13 -13
- package/dist/web-ui/content/plush-avatar.js +4220 -0
- package/dist/web-ui/content/scripts.js +572 -209
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/content/tailwind.css +1 -1
- package/dist/web-ui/embedded-assets.d.ts +2 -0
- package/dist/web-ui/embedded-assets.js +5 -3
- package/dist/web-ui/index.js +3 -3
- package/dist/web-ui/provider-identity.d.ts +2 -0
- package/dist/web-ui/provider-identity.js +7 -1
- package/dist/web-ui/scripts.d.ts +7 -0
- package/dist/web-ui/scripts.js +30 -5
- 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
|
-
/**
|
|
30
|
+
/** 员工候选链(按顺序);未设置时只用 provider/model 这一次调用。 */
|
|
30
31
|
cliCandidates?: import("./types.js").AiCliCandidate[];
|
|
31
|
-
/**
|
|
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
|
|
54
|
-
*
|
|
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>;
|
package/dist/git-quick-commit.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
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
|
|
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(
|
|
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("
|
|
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
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
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
|
|
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(
|
|
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
|
|
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;
|