@co0ontty/wand 4.89.0 → 4.89.1
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-runner.d.ts +1 -1
- package/dist/ai-team-runner.js +13 -5
- package/dist/ai-team-types.js +3 -2
- package/dist/build-info.json +3 -3
- 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/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/employee-text.d.ts +28 -0
- package/dist/employee-text.js +124 -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/process-manager.d.ts +4 -1
- package/dist/process-manager.js +5 -6
- package/dist/server-employee-routes.d.ts +2 -0
- package/dist/server-employee-routes.js +19 -45
- 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 +13 -10
- package/dist/server.js +20 -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 +14 -3
- 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 +2 -0
- package/dist/storage.js +18 -1
- 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 +8 -5
- package/dist/types.d.ts +3 -1
- package/dist/user-memory.d.ts +2 -0
- package/dist/user-memory.js +1 -1
- package/dist/web-ui/content/ai-teams.js +10 -10
- package/dist/web-ui/content/scripts.js +316 -204
- 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 +1 -0
- package/dist/web-ui/embedded-assets.js +4 -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 +2 -0
- package/dist/web-ui/scripts.js +5 -0
- package/package.json +1 -1
|
@@ -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;
|
|
@@ -7,6 +7,7 @@ import { LAYA_FILES, LAYA_REPOSITORY, layaFileUrl } from "./laya-model-manifest.
|
|
|
7
7
|
import { ModelFileStore } from "./model-file-store.js";
|
|
8
8
|
import { runModelSetup } from "./model-setup-process.js";
|
|
9
9
|
import { speechModel } from "./speech-models.js";
|
|
10
|
+
import { speechSupport } from "./speech-support.js";
|
|
10
11
|
import { systemEnvValue } from "./env-utils.js";
|
|
11
12
|
import { decisionHardware } from "./decision-hardware.js";
|
|
12
13
|
import { DECISION_EXPERT_ID } from "./decision-expert-identity.js";
|
|
@@ -20,6 +21,8 @@ export class LocalModelSetupService {
|
|
|
20
21
|
jobs = new Map();
|
|
21
22
|
sequence = 0;
|
|
22
23
|
disposed = false;
|
|
24
|
+
speechEnableRevision = 0;
|
|
25
|
+
supportCache = null;
|
|
23
26
|
constructor(deps) {
|
|
24
27
|
this.deps = deps;
|
|
25
28
|
this.files = new ModelFileStore(deps.fetch);
|
|
@@ -55,6 +58,7 @@ export class LocalModelSetupService {
|
|
|
55
58
|
const hardware = decisionHardware();
|
|
56
59
|
const operation = this.operations.get("laya") ?? null;
|
|
57
60
|
const selected = speech.models.find((value) => value.id === speech.settings.model);
|
|
61
|
+
const support = await this.speechSupport(speech.runtime.available);
|
|
58
62
|
return {
|
|
59
63
|
laya: { kind: "laya", label: "LAYA 本地决策", supported,
|
|
60
64
|
reason: !supported ? hardware.message
|
|
@@ -62,12 +66,38 @@ export class LocalModelSetupService {
|
|
|
62
66
|
enabled: decision.enabled, model: LAYA_REPOSITORY, modelSize: this.manifest.reduce((sum, file) => sum + file.size, 0),
|
|
63
67
|
downloaded: model.downloaded, runtimeAvailable: !!python, initialized: decision.state === "ready", busy: this.jobs.has("laya") || decision.queued > 0 || decision.state === "loading",
|
|
64
68
|
operation: operation ? { ...operation } : null, hardware, decisionEmployeeId: DECISION_EXPERT_ID },
|
|
65
|
-
speech: { kind: "speech", label: "服务端语音识别", supported:
|
|
69
|
+
speech: { kind: "speech", label: "服务端语音识别", supported: support.supported, supportReason: support.reason, reason: speech.reason,
|
|
66
70
|
enabled: speech.settings.enabled, model: speech.settings.model, modelSize: selected?.size ?? 0,
|
|
67
71
|
downloaded: selected?.downloaded ?? false, runtimeAvailable: speech.runtime.available, initialized: speech.initialized === true,
|
|
68
72
|
busy: speech.busy || this.jobs.has("speech"), operation: this.operations.has("speech") ? { ...this.operations.get("speech") } : null },
|
|
69
73
|
};
|
|
70
74
|
}
|
|
75
|
+
speechSupport(runtimeAvailable) {
|
|
76
|
+
if (!this.supportCache || this.supportCache.expires < Date.now() || this.supportCache.runtimeAvailable !== runtimeAvailable) {
|
|
77
|
+
this.supportCache = { expires: Date.now() + 30_000, runtimeAvailable, value: (this.deps.speechSupport ?? speechSupport)(runtimeAvailable) };
|
|
78
|
+
}
|
|
79
|
+
return this.supportCache.value;
|
|
80
|
+
}
|
|
81
|
+
async setSpeechEnabled(enabled) {
|
|
82
|
+
const revision = ++this.speechEnableRevision;
|
|
83
|
+
const settingsRevision = this.deps.speech.settingsRevision();
|
|
84
|
+
if (this.disposed)
|
|
85
|
+
throw new DecisionError("UNAVAILABLE", "模型管理已关闭。", 503);
|
|
86
|
+
if (!enabled) {
|
|
87
|
+
this.cancel("speech");
|
|
88
|
+
this.deps.speech.configure({ enabled: false });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const status = await this.deps.speech.status();
|
|
92
|
+
const support = await this.speechSupport(status.runtime.available);
|
|
93
|
+
if (revision !== this.speechEnableRevision || this.disposed || settingsRevision !== this.deps.speech.settingsRevision())
|
|
94
|
+
return;
|
|
95
|
+
if (!support.supported)
|
|
96
|
+
throw new DecisionError("UNSUPPORTED", support.reason || "本机不支持语音识别。", 409);
|
|
97
|
+
if (status.busy || status.download?.phase === "downloading")
|
|
98
|
+
throw new DecisionError("BUSY", "语音识别或模型下载正在进行,请稍后启用。", 409);
|
|
99
|
+
this.start("speech", "activate", {});
|
|
100
|
+
}
|
|
71
101
|
parse(kind, raw) {
|
|
72
102
|
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
73
103
|
throw new DecisionError("INVALID_REQUEST", "模型操作参数无效。");
|
|
@@ -90,6 +120,8 @@ export class LocalModelSetupService {
|
|
|
90
120
|
throw new DecisionError("UNAVAILABLE", "模型管理已关闭。", 503);
|
|
91
121
|
if (kind === "laya" && !this.supported())
|
|
92
122
|
throw new DecisionError("UNSUPPORTED", `LAYA-MLX 需要 Apple Silicon macOS 与 Metal;${decisionHardware().message}`, 409);
|
|
123
|
+
if (kind === "laya" && action === "activate")
|
|
124
|
+
throw new DecisionError("INVALID_REQUEST", "一键启用仅用于语音识别。");
|
|
93
125
|
const input = this.parse(kind, raw);
|
|
94
126
|
if (this.jobs.has(kind))
|
|
95
127
|
throw new DecisionError("BUSY", "该模型已有安装任务,请等待或取消。", 409);
|
|
@@ -119,6 +151,8 @@ export class LocalModelSetupService {
|
|
|
119
151
|
timer.unref();
|
|
120
152
|
const signal = abort.signal;
|
|
121
153
|
const phase = (value, message) => this.update(kind, id, { phase: value, message });
|
|
154
|
+
let activationMaintenance = false;
|
|
155
|
+
const settingsRevision = this.deps.speech.settingsRevision();
|
|
122
156
|
try {
|
|
123
157
|
if (kind === "laya") {
|
|
124
158
|
let model = await this.layaModel();
|
|
@@ -170,7 +204,7 @@ export class LocalModelSetupService {
|
|
|
170
204
|
}
|
|
171
205
|
else {
|
|
172
206
|
const selected = input.model ?? this.deps.speech.settings().model;
|
|
173
|
-
if (action === "download") {
|
|
207
|
+
if (action === "download" || action === "activate") {
|
|
174
208
|
phase("downloading", "下载并校验语音模型");
|
|
175
209
|
await this.deps.speech.startDownload(selected);
|
|
176
210
|
while (true) {
|
|
@@ -188,7 +222,12 @@ export class LocalModelSetupService {
|
|
|
188
222
|
});
|
|
189
223
|
}
|
|
190
224
|
}
|
|
191
|
-
|
|
225
|
+
if (action !== "download") {
|
|
226
|
+
if (action === "activate") {
|
|
227
|
+
signal.throwIfAborted();
|
|
228
|
+
this.deps.speech.acquireMaintenance();
|
|
229
|
+
activationMaintenance = true;
|
|
230
|
+
}
|
|
192
231
|
const status = await this.deps.speech.status();
|
|
193
232
|
signal.throwIfAborted();
|
|
194
233
|
const backend = input.backend === "auto" || !input.backend ? process.platform === "darwin" ? "metal" : "cpu" : input.backend;
|
|
@@ -207,22 +246,30 @@ export class LocalModelSetupService {
|
|
|
207
246
|
await rm(work, { recursive: true, force: true }).catch(() => { });
|
|
208
247
|
}
|
|
209
248
|
}
|
|
210
|
-
phase("initializing", "加载所选语音模型并用合成静音检查,未启用识别或重启服务");
|
|
211
|
-
await this.deps.speech.initializeModel(selected, signal);
|
|
249
|
+
phase("initializing", action === "activate" ? "正在检查语音模型,完成后自动启用。" : "加载所选语音模型并用合成静音检查,未启用识别或重启服务");
|
|
250
|
+
await this.deps.speech.initializeModel(selected, signal, action === "activate" ? "auto" : undefined);
|
|
251
|
+
if (action === "activate") {
|
|
252
|
+
signal.throwIfAborted();
|
|
253
|
+
if (settingsRevision !== this.deps.speech.settingsRevision())
|
|
254
|
+
throw new DecisionError("SETTINGS_CHANGED", "语音设置已由其他操作修改,本次启用已取消。");
|
|
255
|
+
this.deps.speech.configure({ enabled: true, acceleration: "auto" });
|
|
256
|
+
}
|
|
212
257
|
}
|
|
213
258
|
}
|
|
214
259
|
signal.throwIfAborted();
|
|
215
|
-
phase("completed", action === "download" ? "模型下载及完整性校验完成;可继续初始化" : "运行时和模型初始化检查完成;启用状态保持不变");
|
|
260
|
+
phase("completed", action === "activate" ? "服务端语音识别已启用。" : action === "download" ? "模型下载及完整性校验完成;可继续初始化" : "运行时和模型初始化检查完成;启用状态保持不变");
|
|
216
261
|
}
|
|
217
262
|
catch (error) {
|
|
218
263
|
const cancelled = signal.aborted;
|
|
219
264
|
const message = cancelled ? "安装任务已取消,完整文件保留,可重试" : error instanceof DecisionError || (error instanceof Error && /请先|校验|自定义|独立/.test(error.message))
|
|
220
265
|
? error.message : "安装或初始化失败,请检查网络、编译工具/Python版本、磁盘空间和平台能力,再重试。";
|
|
221
266
|
this.update(kind, id, { phase: cancelled ? "cancelled" : "failed", message, ...(cancelled ? {} : { error: message }) });
|
|
222
|
-
if (kind === "speech" && action === "download" && cancelled)
|
|
267
|
+
if (kind === "speech" && (action === "download" || action === "activate") && cancelled)
|
|
223
268
|
this.deps.speech.cancelDownload();
|
|
224
269
|
}
|
|
225
270
|
finally {
|
|
271
|
+
if (activationMaintenance)
|
|
272
|
+
this.deps.speech.releaseMaintenance();
|
|
226
273
|
clearTimeout(timer);
|
|
227
274
|
}
|
|
228
275
|
}
|
|
@@ -3,7 +3,7 @@ export type LocalModelKind = "laya" | "speech";
|
|
|
3
3
|
export type ModelSetupPhase = "downloading" | "verifying" | "runtime" | "initializing" | "completed" | "failed" | "cancelled";
|
|
4
4
|
export interface ModelSetupOperation {
|
|
5
5
|
id: number;
|
|
6
|
-
action: "download" | "initialize";
|
|
6
|
+
action: "download" | "initialize" | "activate";
|
|
7
7
|
phase: ModelSetupPhase;
|
|
8
8
|
message: string;
|
|
9
9
|
received: number;
|
|
@@ -14,6 +14,7 @@ export interface LocalModelStatus {
|
|
|
14
14
|
kind: LocalModelKind;
|
|
15
15
|
label: string;
|
|
16
16
|
supported: boolean;
|
|
17
|
+
supportReason?: string | null;
|
|
17
18
|
reason: string | null;
|
|
18
19
|
enabled: boolean;
|
|
19
20
|
model: string;
|
|
@@ -3,6 +3,7 @@ import { EventEmitter } from "node:events";
|
|
|
3
3
|
import { WandStorage } from "./storage.js";
|
|
4
4
|
import { ExecutionMode, ProcessEventHandler, SessionProvider, SessionSnapshot, SessionSource, WandConfig } from "./types.js";
|
|
5
5
|
import { type PermissionResolution } from "./claude-pty-bridge.js";
|
|
6
|
+
import type { EmployeeTextDeps } from "./employee-text.js";
|
|
6
7
|
import { type PtyTerminalSnapshot, type PtyHistoryPage } from "./pty-terminal-state.js";
|
|
7
8
|
import { samplePtyForegrounds } from "./pty-foreground.js";
|
|
8
9
|
import { type TerminalHost } from "./terminal-host.js";
|
|
@@ -34,6 +35,7 @@ export declare const PTY_TURN_IDLE_MS = 3000;
|
|
|
34
35
|
*/
|
|
35
36
|
export declare const PTY_FOREGROUND_SAMPLE_MS = 1000;
|
|
36
37
|
export interface ProcessManagerOptions {
|
|
38
|
+
free?: EmployeeTextDeps["free"];
|
|
37
39
|
/** Overrides the non-Claude PTY turn quiet window (tests use a short one). */
|
|
38
40
|
ptyTurnIdleMs?: number;
|
|
39
41
|
/** Overrides the foreground-process-group sampling interval (tests use a short one). */
|
|
@@ -55,6 +57,7 @@ export declare class ProcessManager extends EventEmitter {
|
|
|
55
57
|
private readonly ptyForegroundSampleMs;
|
|
56
58
|
/** Foreground probe; injectable so tests can drive it without a real process table. */
|
|
57
59
|
private readonly probePtyForegrounds;
|
|
60
|
+
private readonly free;
|
|
58
61
|
private readonly resolveDecisionEvaluate;
|
|
59
62
|
/** 24h archive scan timer */
|
|
60
63
|
private archiveTimer;
|
|
@@ -177,7 +180,7 @@ export declare class ProcessManager extends EventEmitter {
|
|
|
177
180
|
*/
|
|
178
181
|
setSessionModel(id: string, model: string | null): SessionSnapshot;
|
|
179
182
|
/**
|
|
180
|
-
*
|
|
183
|
+
* 归档只写标记,保留正在运行的终端、会话 ID 和 provider session id。
|
|
181
184
|
* 取消归档只清标记;重新拉起由恢复入口负责。
|
|
182
185
|
*/
|
|
183
186
|
setSessionArchived(id: string, archived: boolean): SessionSnapshot;
|
package/dist/process-manager.js
CHANGED
|
@@ -563,6 +563,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
563
563
|
ptyForegroundSampleMs;
|
|
564
564
|
/** Foreground probe; injectable so tests can drive it without a real process table. */
|
|
565
565
|
probePtyForegrounds;
|
|
566
|
+
free;
|
|
566
567
|
resolveDecisionEvaluate;
|
|
567
568
|
/** 24h archive scan timer */
|
|
568
569
|
archiveTimer = null;
|
|
@@ -595,6 +596,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
595
596
|
this.ptyTurnIdleMs = options.ptyTurnIdleMs ?? PTY_TURN_IDLE_MS;
|
|
596
597
|
this.ptyForegroundSampleMs = options.ptyForegroundSampleMs ?? PTY_FOREGROUND_SAMPLE_MS;
|
|
597
598
|
this.probePtyForegrounds = options.samplePtyForegrounds ?? samplePtyForegrounds;
|
|
599
|
+
this.free = options.free;
|
|
598
600
|
this.resolveDecisionEvaluate = options.resolveDecisionEvaluate ?? (() => undefined);
|
|
599
601
|
this.terminalHost = terminalHost ?? new InProcessTerminalHost();
|
|
600
602
|
this.logger = new SessionLogger(configDir || path.join(process.env.HOME || process.cwd(), ".wand"), config.shortcutLogMaxBytes);
|
|
@@ -1229,7 +1231,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
1229
1231
|
ai: resolveSystemAiContext({
|
|
1230
1232
|
provider: groupProvider, runner: undefined, command: providerCliCommand(groupProvider),
|
|
1231
1233
|
structuredState: undefined, selectedModel: null, thinkingEffort: opts?.thinkingEffort,
|
|
1232
|
-
}, this.config, this.storage.getSystemSiliconEmployee()),
|
|
1234
|
+
}, this.config, this.storage.getSystemSiliconEmployee(), this.free),
|
|
1233
1235
|
cwd: resolveSessionCwd(cwd, this.config.defaultCwd),
|
|
1234
1236
|
language: this.config.language,
|
|
1235
1237
|
});
|
|
@@ -1775,13 +1777,10 @@ export class ProcessManager extends EventEmitter {
|
|
|
1775
1777
|
return this.snapshot(record);
|
|
1776
1778
|
}
|
|
1777
1779
|
/**
|
|
1778
|
-
*
|
|
1780
|
+
* 归档只写标记,保留正在运行的终端、会话 ID 和 provider session id。
|
|
1779
1781
|
* 取消归档只清标记;重新拉起由恢复入口负责。
|
|
1780
1782
|
*/
|
|
1781
1783
|
setSessionArchived(id, archived) {
|
|
1782
|
-
if (archived && this.mustGet(id).status === "running") {
|
|
1783
|
-
this.stop(id);
|
|
1784
|
-
}
|
|
1785
1784
|
const record = this.mustGet(id);
|
|
1786
1785
|
record.archived = archived;
|
|
1787
1786
|
record.archivedAt = archived ? (record.archivedAt ?? new Date().toISOString()) : null;
|
|
@@ -2354,7 +2353,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
2354
2353
|
input: prompt,
|
|
2355
2354
|
cwd: record.cwd,
|
|
2356
2355
|
language: this.config.language,
|
|
2357
|
-
ai: resolveSystemAiContext(record, this.config, this.storage.getSystemSiliconEmployee()),
|
|
2356
|
+
ai: resolveSystemAiContext(record, this.config, this.storage.getSystemSiliconEmployee(), this.free),
|
|
2358
2357
|
readNativeTitle: () => readNativeSessionTitle(resolveSessionProvider(record), record.claudeSessionId, record.cwd),
|
|
2359
2358
|
hasNativeTitle,
|
|
2360
2359
|
onGenerating: (generating) => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Express } from "express";
|
|
2
2
|
import { type SiliconEmployee, type SiliconEmployeeDraft } from "./ai-team-types.js";
|
|
3
|
+
import type { OpenRouterFreeModelsService } from "./openrouter-free-models.js";
|
|
3
4
|
import type { WandStorage } from "./storage.js";
|
|
4
5
|
import type { WandTaskAgent } from "./task-types.js";
|
|
5
6
|
import type { WandConfig } from "./types.js";
|
|
@@ -17,6 +18,7 @@ export declare function registerSiliconEmployeeRoutes(app: Express, deps: {
|
|
|
17
18
|
storage: WandStorage;
|
|
18
19
|
notifyEmployeeChanged?: (employeeId: string) => void;
|
|
19
20
|
config?: WandConfig;
|
|
21
|
+
free?: Pick<OpenRouterFreeModelsService, "resolveForCall">;
|
|
20
22
|
/** 测试注入点:默认按系统 AI 配置真实调用模型。 */
|
|
21
23
|
generateDraft?: (request: EmployeeDraftRequest) => Promise<SiliconEmployeeDraft>;
|
|
22
24
|
/** 测试注入点:CLI 可用性探测。 */
|