@co0ontty/wand 4.14.0 → 4.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
- "commit": "a6331de5ad360ff5cd861a4ffe8bc5dc37f56562",
3
- "builtAt": "2026-07-18T00:02:56.112Z",
4
- "version": "4.14.0",
2
+ "commit": "4cb2d575fe6ffe06c991374d39dfef79740befeb",
3
+ "builtAt": "2026-07-18T00:38:58.299Z",
4
+ "version": "4.15.0",
5
5
  "channel": "stable"
6
6
  }
package/dist/cli.js CHANGED
@@ -110,6 +110,11 @@ async function main() {
110
110
  systemAi: config.systemAi ? {
111
111
  ...config.systemAi,
112
112
  apiKey: config.systemAi.apiKey ? "<set>" : "",
113
+ fallbacks: config.systemAi.fallbacks?.map((profile) => ({
114
+ ...profile,
115
+ apiKey: profile.apiKey ? "<set>" : "",
116
+ fallbacks: undefined,
117
+ })),
113
118
  } : undefined,
114
119
  };
115
120
  process.stdout.write(`${JSON.stringify(display, null, 2)}\n`);
package/dist/config.js CHANGED
@@ -4,7 +4,7 @@ import { chmod, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promi
4
4
  import path from "node:path";
5
5
  import process from "node:process";
6
6
  import { isRunningAsRoot } from "./env-utils.js";
7
- import { normalizeSystemAiConfig } from "./system-ai.js";
7
+ import { normalizeSystemAiConfig, systemAiProfiles } from "./system-ai.js";
8
8
  function isThinkingEffort(value) {
9
9
  return value === "off"
10
10
  || value === "standard"
@@ -513,8 +513,7 @@ export function writePreferenceToStorage(config, storage, key, value, options =
513
513
  export function validateCommitAiConfig(config) {
514
514
  if (config.commitAiSource !== "api")
515
515
  return;
516
- const directApi = config.systemAi;
517
- if (!directApi?.baseUrl || !directApi.apiKey || !directApi.model) {
516
+ if (!systemAiProfiles(config.systemAi, true).length) {
518
517
  throw new Error("选择直连 API 生成 Commit 时,必须先填写 API 地址、API Key 和模型。");
519
518
  }
520
519
  }
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { spawn } from "node:child_process";
3
3
  import { ClaudeRunError, runClaudePrint } from "./claude-sdk-runner.js";
4
- import { callSystemAiText } from "./system-ai.js";
4
+ import { callSystemAiTextWithFallback } from "./system-ai.js";
5
5
  import { buildChildEnv } from "./env-utils.js";
6
6
  import { runGit as runGitBase, runGitAsync as runGitAsyncBase, runGitRaw as runGitRawBase, runGitRawAsync as runGitRawAsyncBase, getGitErrorMessage, } from "./git-utils.js";
7
7
  import { thinkingEffortToClaudeCliEffort, thinkingEffortToCodexReasoningEffort, thinkingEffortToOpenCodeVariant } from "./structured-provider-common.js";
@@ -615,7 +615,7 @@ async function callCliAiText(prompt, cwd, language, opts) {
615
615
  return callClaudeText(prompt, cwd, language, opts.model);
616
616
  }
617
617
  async function callDirectApiText(prompt, systemAi) {
618
- const text = await callSystemAiText(prompt, systemAi);
618
+ const text = await callSystemAiTextWithFallback(prompt, systemAi);
619
619
  if (!text.trim()) {
620
620
  throw new QuickCommitError("直连 API 返回了空结果。", "EMPTY_AI_MESSAGE");
621
621
  }
@@ -18,7 +18,7 @@ import { getProviderCommandSessionId, getProviderResumeCommandSessionId } from "
18
18
  import { normalizeThinkingEffort, thinkingEffortToClaudeCliEffort, thinkingEffortToClaudeSlashEffort, thinkingEffortToCodexReasoningEffort, thinkingEffortToOpenCodeVariant } from "./structured-provider-common.js";
19
19
  import { SessionTopicCoordinator } from "./session-topic.js";
20
20
  import { getErrorMessage } from "./error-utils.js";
21
- import { resolveCommitAiContext } from "./session-ai-context.js";
21
+ import { resolveSystemAiContext } from "./session-ai-context.js";
22
22
  import { resolveSessionCwd } from "./session-cwd.js";
23
23
  import { ProviderHistoryScanner, } from "./provider-history-scanner.js";
24
24
  function resolveProviderFromCommand(command) {
@@ -1720,7 +1720,7 @@ export class ProcessManager extends EventEmitter {
1720
1720
  input: prompt,
1721
1721
  cwd: record.cwd,
1722
1722
  language: this.config.language,
1723
- ai: resolveCommitAiContext(record, this.config),
1723
+ ai: resolveSystemAiContext(record, this.config),
1724
1724
  onGenerating: (generating) => {
1725
1725
  if (!this.disposed)
1726
1726
  this.setSessionTopicGenerating(id, generating);
@@ -1,5 +1,5 @@
1
1
  import { ClaudeRunError, runClaudePrint } from "./claude-sdk-runner.js";
2
- import { callSystemAiText } from "./system-ai.js";
2
+ import { callSystemAiTextWithFallback } from "./system-ai.js";
3
3
  const CLAUDE_TIMEOUT_MS = 60_000;
4
4
  const MAX_INPUT_LENGTH = 8000;
5
5
  export class PromptOptimizeError extends Error {
@@ -51,7 +51,18 @@ export async function optimizePrompt(rawText, language, cwd, systemAi) {
51
51
  throw new PromptOptimizeError(`输入过长(${text.length} 字符),请缩短到 ${MAX_INPUT_LENGTH} 以内。`, "INPUT_TOO_LONG");
52
52
  }
53
53
  const prompt = buildOptimizePrompt(text, language);
54
- const raw = systemAi?.enabled ? await callSystemAiText(prompt, systemAi) : await callClaudeText(prompt, cwd, language);
54
+ let raw;
55
+ if (systemAi?.enabled) {
56
+ try {
57
+ raw = await callSystemAiTextWithFallback(prompt, systemAi);
58
+ }
59
+ catch {
60
+ raw = await callClaudeText(prompt, cwd, language);
61
+ }
62
+ }
63
+ else {
64
+ raw = await callClaudeText(prompt, cwd, language);
65
+ }
55
66
  const cleaned = raw
56
67
  .replace(/^```[a-zA-Z]*\n?/, "")
57
68
  .replace(/\n?```$/, "")
@@ -6,17 +6,13 @@ import { asyncRoute } from "./express-async.js";
6
6
  import { getProviderDefaultModels, PREFERENCE_KEYS, saveConfig, validateCommitAiConfig, writePreferenceToStorage, } from "./config.js";
7
7
  import { getCachedModels, refreshModels } from "./models.js";
8
8
  import { DEPLOYMENT_CONFIG_KEYS } from "./runtime-config.js";
9
- import { discoverCliSystemAiConfig, normalizeSystemAiConfig } from "./system-ai.js";
9
+ import { discoverCliSystemAiConfigs, normalizeSystemAiConfig } from "./system-ai.js";
10
10
  function publicConfig(config) {
11
11
  const { password: _password, appSecret: _appSecret, ...safe } = config;
12
12
  const defaultModels = getProviderDefaultModels(config);
13
13
  return {
14
14
  ...safe,
15
- systemAi: safe.systemAi ? {
16
- ...safe.systemAi,
17
- apiKey: "",
18
- hasApiKey: Boolean(safe.systemAi.apiKey),
19
- } : undefined,
15
+ systemAi: publicSystemAi(safe.systemAi),
20
16
  defaultModel: defaultModels.claude,
21
17
  defaultCodexModel: defaultModels.codex,
22
18
  defaultOpenCodeModel: defaultModels.opencode,
@@ -25,6 +21,21 @@ function publicConfig(config) {
25
21
  defaultModels,
26
22
  };
27
23
  }
24
+ function publicSystemAi(systemAi) {
25
+ if (!systemAi)
26
+ return undefined;
27
+ return {
28
+ ...systemAi,
29
+ apiKey: "",
30
+ hasApiKey: Boolean(systemAi.apiKey),
31
+ fallbacks: systemAi.fallbacks?.map((profile) => ({
32
+ ...profile,
33
+ apiKey: "",
34
+ hasApiKey: Boolean(profile.apiKey),
35
+ fallbacks: undefined,
36
+ })),
37
+ };
38
+ }
28
39
  function publicDistributionInfo(distribution) {
29
40
  const androidApk = { ...distribution.androidApk };
30
41
  const macosDmg = { ...distribution.macosDmg };
@@ -117,18 +128,19 @@ export function registerSettingsRoutes(app, deps) {
117
128
  const source = body.source === "codex" || body.source === "opencode" || body.source === "claude"
118
129
  ? body.source
119
130
  : config.commitCli;
120
- const imported = discoverCliSystemAiConfig(source);
121
- if (!imported) {
131
+ const imported = discoverCliSystemAiConfigs(source);
132
+ if (!imported.length) {
122
133
  res.status(404).json({ error: "没有在已配置的 CLI 文件中找到可直连的 API 地址、密钥和模型。" });
123
134
  return;
124
135
  }
125
136
  const candidate = runtimeConfig.createCandidate();
126
137
  writePreferenceToStorage(candidate, storage, "systemAi", {
127
- ...imported,
138
+ ...imported[0],
128
139
  enabled: candidate.systemAi?.enabled === true,
140
+ fallbacks: imported.slice(1),
129
141
  });
130
142
  runtimeConfig.commit(candidate, new Set(["systemAi"]));
131
- res.json({ ok: true, systemAi: (publicConfig(candidate).systemAi) });
143
+ res.json({ ok: true, count: imported.length, systemAi: (publicConfig(candidate).systemAi) });
132
144
  });
133
145
  app.get("/api/app-connect-code", requireAdmin, (req, res) => {
134
146
  res.json(deps.resolveAppConnectCode(req));
@@ -199,7 +211,15 @@ export function registerSettingsRoutes(app, deps) {
199
211
  const apiKey = typeof body.systemAi.apiKey === "string" && body.systemAi.apiKey.trim()
200
212
  ? body.systemAi.apiKey.trim()
201
213
  : previous?.apiKey ?? "";
202
- stagePreference("systemAi", normalizeSystemAiConfig({ ...previous, ...body.systemAi, apiKey }, previous));
214
+ const submittedFallbacks = Array.isArray(body.systemAi.fallbacks)
215
+ ? body.systemAi.fallbacks.map((item, index) => {
216
+ const raw = item && typeof item === "object" && !Array.isArray(item) ? item : {};
217
+ const prior = previous?.fallbacks?.[index];
218
+ const fallbackApiKey = typeof raw.apiKey === "string" && raw.apiKey.trim() ? raw.apiKey.trim() : prior?.apiKey ?? "";
219
+ return { ...prior, ...raw, apiKey: fallbackApiKey, fallbacks: undefined };
220
+ })
221
+ : previous?.fallbacks;
222
+ stagePreference("systemAi", normalizeSystemAiConfig({ ...previous, ...body.systemAi, apiKey, fallbacks: submittedFallbacks }, previous));
203
223
  }
204
224
  for (const field of PREFERENCE_KEYS) {
205
225
  if (field === "systemAi")
@@ -16,5 +16,7 @@ export interface SessionAiContext {
16
16
  export declare function resolveSessionProvider(snapshot: Pick<SessionSnapshot, "provider" | "structuredState" | "runner" | "command">): SessionProvider;
17
17
  /** Build the provider-specific settings used by session-adjacent AI actions. */
18
18
  export declare function resolveSessionAiContext(snapshot: Pick<SessionSnapshot, "provider" | "structuredState" | "runner" | "command" | "selectedModel" | "thinkingEffort">, config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel" | "defaultGrokModel" | "defaultQoderModel" | "defaultThinkingEffort" | "inheritEnv">): SessionAiContext;
19
+ /** Build the source order for Wand-owned AI features such as titles. */
20
+ export declare function resolveSystemAiContext(snapshot: Parameters<typeof resolveSessionAiContext>[0], config: Parameters<typeof resolveSessionAiContext>[1] & Pick<WandConfig, "systemAi" | "commitCli" | "commitModel">): SessionAiContext;
19
21
  /** Build the AI context for quick-commit actions from their global preferences. */
20
22
  export declare function resolveCommitAiContext(snapshot: Pick<SessionSnapshot, "provider" | "structuredState" | "runner" | "command" | "selectedModel" | "thinkingEffort">, config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel" | "defaultGrokModel" | "defaultQoderModel" | "defaultThinkingEffort" | "inheritEnv" | "commitCli" | "commitModel" | "commitAiSource" | "systemAi">): SessionAiContext;
@@ -1,4 +1,5 @@
1
1
  import { getDefaultModelForProvider } from "./config.js";
2
+ import { systemAiProfiles } from "./system-ai.js";
2
3
  /**
3
4
  * Resolve the provider from every representation used by current and legacy
4
5
  * sessions. Older persisted sessions may not have the top-level provider, but
@@ -45,7 +46,7 @@ function normalizeModel(value) {
45
46
  return model && model !== "default" ? model : undefined;
46
47
  }
47
48
  function usableSystemAi(config) {
48
- if (!config.baseUrl.trim() || !config.apiKey.trim() || !config.model.trim())
49
+ if (!systemAiProfiles(config, true).length)
49
50
  return undefined;
50
51
  return { ...config, enabled: true };
51
52
  }
@@ -61,6 +62,17 @@ export function resolveSessionAiContext(snapshot, config) {
61
62
  inheritEnv: config.inheritEnv,
62
63
  };
63
64
  }
65
+ /** Build the source order for Wand-owned AI features such as titles. */
66
+ export function resolveSystemAiContext(snapshot, config) {
67
+ const sessionContext = resolveSessionAiContext(snapshot, config);
68
+ const directApi = config.systemAi ? usableSystemAi(config.systemAi) : undefined;
69
+ const cliContext = {
70
+ ...sessionContext,
71
+ provider: config.commitCli === "codex" || config.commitCli === "opencode" ? config.commitCli : "claude",
72
+ model: normalizeModel(config.commitModel),
73
+ };
74
+ return directApi && config.systemAi?.enabled ? { ...cliContext, systemAi: directApi } : cliContext;
75
+ }
64
76
  /** Build the AI context for quick-commit actions from their global preferences. */
65
77
  export function resolveCommitAiContext(snapshot, config) {
66
78
  const sessionContext = resolveSessionAiContext(snapshot, config);
@@ -7,7 +7,7 @@ import { getErrorMessage } from "./error-utils.js";
7
7
  import { resolveSdkClaudeBinary } from "./claude-sdk-runner.js";
8
8
  import { SessionTopicCoordinator } from "./session-topic.js";
9
9
  import { resolveSessionCwd } from "./session-cwd.js";
10
- import { resolveCommitAiContext } from "./session-ai-context.js";
10
+ import { resolveSystemAiContext } from "./session-ai-context.js";
11
11
  import { CodexRunner } from "./structured-codex-adapter.js";
12
12
  import { normalizeStructuredToolResultContent } from "./structured-content.js";
13
13
  import { buildAppendSystemPromptParts, buildClaudeSdkThinking, ClaudeCliRunner, derivePermissionPolicy, } from "./structured-claude-adapter.js";
@@ -501,7 +501,7 @@ export class StructuredSessionManager {
501
501
  input,
502
502
  cwd: session.cwd,
503
503
  language: this.config.language,
504
- ai: resolveCommitAiContext(session, this.config),
504
+ ai: resolveSystemAiContext(session, this.config),
505
505
  onGenerating: (generating) => {
506
506
  if (!this.disposed)
507
507
  this.setSessionTopicGenerating(id, generating);
@@ -4,6 +4,12 @@ export declare class SystemAiError extends Error {
4
4
  constructor(message: string, code: string);
5
5
  }
6
6
  export declare function normalizeSystemAiConfig(value: unknown, fallback?: SystemAiConfig): SystemAiConfig;
7
- /** Copy the first usable direct-API profile from the user's configured CLIs. */
7
+ /** Copy every usable direct-API profile from the user's configured CLIs. */
8
+ export declare function discoverCliSystemAiConfigs(preferred?: SessionProvider, home?: string): SystemAiConfig[];
9
+ /** Backward-compatible first-profile discovery. */
8
10
  export declare function discoverCliSystemAiConfig(preferred?: SessionProvider, home?: string): SystemAiConfig | null;
11
+ /** Return the configured API chain in call order, excluding incomplete entries. */
12
+ export declare function systemAiProfiles(config: SystemAiConfig | undefined, forceEnabled?: boolean): SystemAiConfig[];
9
13
  export declare function callSystemAiText(prompt: string, config: SystemAiConfig, timeoutMs?: number): Promise<string>;
14
+ /** Try every configured API in order. Empty responses are treated as unavailable. */
15
+ export declare function callSystemAiTextWithFallback(prompt: string, config: SystemAiConfig, timeoutMs?: number): Promise<string>;
package/dist/system-ai.js CHANGED
@@ -28,7 +28,7 @@ export function normalizeSystemAiConfig(value, fallback) {
28
28
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
29
29
  throw new Error("系统 AI API 地址必须使用 http 或 https。");
30
30
  }
31
- return {
31
+ const normalized = {
32
32
  enabled: raw.enabled === true,
33
33
  protocol,
34
34
  baseUrl,
@@ -37,6 +37,25 @@ export function normalizeSystemAiConfig(value, fallback) {
37
37
  authHeader: raw.authHeader === "x-api-key" ? "x-api-key" : "bearer",
38
38
  source: raw.source === "claude" || raw.source === "codex" || raw.source === "opencode" ? raw.source : "custom",
39
39
  };
40
+ if (Array.isArray(raw.fallbacks)) {
41
+ normalized.fallbacks = raw.fallbacks
42
+ .map((item, index) => {
43
+ try {
44
+ const itemFallback = fallback?.fallbacks?.[index];
45
+ const profile = normalizeSystemAiConfig(item, itemFallback);
46
+ delete profile.fallbacks;
47
+ return profile;
48
+ }
49
+ catch {
50
+ return null;
51
+ }
52
+ })
53
+ .filter((item) => item !== null);
54
+ }
55
+ else if (fallback?.fallbacks?.length) {
56
+ normalized.fallbacks = fallback.fallbacks.map((item) => ({ ...item, fallbacks: undefined }));
57
+ }
58
+ return normalized;
40
59
  }
41
60
  function readJson(filePath) {
42
61
  if (!existsSync(filePath))
@@ -68,14 +87,30 @@ function discoverOpenCode(home) {
68
87
  const selectedModel = typeof config?.model === "string" ? config.model : "";
69
88
  const providerId = selectedModel.split("/", 1)[0] ?? "";
70
89
  const providers = config?.provider && typeof config.provider === "object" ? config.provider : {};
71
- const provider = providers[providerId] && typeof providers[providerId] === "object" ? providers[providerId] : null;
72
- const options = provider?.options && typeof provider.options === "object" ? provider.options : {};
73
- const apiKey = typeof options.apiKey === "string" ? options.apiKey : "";
74
- const baseUrl = typeof options.baseURL === "string" ? options.baseURL : typeof options.baseUrl === "string" ? options.baseUrl : "";
75
- if (!apiKey || !baseUrl || !selectedModel)
76
- return null;
77
- const model = selectedModel.includes("/") ? selectedModel.slice(selectedModel.indexOf("/") + 1) : selectedModel;
78
- return normalizeSystemAiConfig({ enabled: true, protocol: "openai", baseUrl, apiKey, model, authHeader: "bearer", source: "opencode" });
90
+ const orderedProviders = [providerId, ...Object.keys(providers).filter((id) => id !== providerId)];
91
+ const found = [];
92
+ for (const id of orderedProviders) {
93
+ const provider = providers[id] && typeof providers[id] === "object" ? providers[id] : null;
94
+ const options = provider?.options && typeof provider.options === "object" ? provider.options : {};
95
+ const apiKey = typeof options.apiKey === "string" ? options.apiKey : "";
96
+ const baseUrl = typeof options.baseURL === "string" ? options.baseURL : typeof options.baseUrl === "string" ? options.baseUrl : "";
97
+ const models = provider?.models && typeof provider.models === "object" ? provider.models : {};
98
+ const configuredModel = id === providerId && selectedModel
99
+ ? (selectedModel.includes("/") ? selectedModel.slice(selectedModel.indexOf("/") + 1) : selectedModel)
100
+ : typeof provider?.model === "string" ? provider.model : Object.keys(models)[0] ?? "";
101
+ if (!apiKey || !baseUrl || !configuredModel)
102
+ continue;
103
+ found.push(normalizeSystemAiConfig({
104
+ enabled: true,
105
+ protocol: "openai",
106
+ baseUrl,
107
+ apiKey,
108
+ model: configuredModel,
109
+ authHeader: "bearer",
110
+ source: "opencode",
111
+ }));
112
+ }
113
+ return found;
79
114
  }
80
115
  function discoverCodex(home) {
81
116
  const auth = readJson(path.join(home, ".codex", "auth.json"));
@@ -94,18 +129,49 @@ function discoverCodex(home) {
94
129
  return null;
95
130
  return normalizeSystemAiConfig({ enabled: true, protocol: "openai", baseUrl, apiKey, model, authHeader: "bearer", source: "codex" });
96
131
  }
97
- /** Copy the first usable direct-API profile from the user's configured CLIs. */
98
- export function discoverCliSystemAiConfig(preferred, home = os.homedir()) {
99
- const discoverers = { claude: discoverClaude, codex: discoverCodex, opencode: discoverOpenCode };
132
+ /** Copy every usable direct-API profile from the user's configured CLIs. */
133
+ export function discoverCliSystemAiConfigs(preferred, home = os.homedir()) {
134
+ const discoverers = {
135
+ claude: (dir) => [discoverClaude(dir)].filter((item) => item !== null),
136
+ codex: (dir) => [discoverCodex(dir)].filter((item) => item !== null),
137
+ opencode: discoverOpenCode,
138
+ };
100
139
  const order = [preferred ?? "claude", "claude", "opencode", "codex"];
140
+ const found = [];
141
+ const seen = new Set();
101
142
  for (const provider of [...new Set(order)]) {
102
143
  if (provider === "grok" || provider === "qoder")
103
144
  continue;
104
- const found = discoverers[provider](home);
105
- if (found)
106
- return found;
145
+ for (const profile of discoverers[provider](home)) {
146
+ const key = [profile.protocol, profile.baseUrl, profile.apiKey, profile.model].join("\0");
147
+ if (seen.has(key))
148
+ continue;
149
+ seen.add(key);
150
+ found.push(profile);
151
+ }
107
152
  }
108
- return null;
153
+ return found;
154
+ }
155
+ /** Backward-compatible first-profile discovery. */
156
+ export function discoverCliSystemAiConfig(preferred, home = os.homedir()) {
157
+ return discoverCliSystemAiConfigs(preferred, home)[0] ?? null;
158
+ }
159
+ /** Return the configured API chain in call order, excluding incomplete entries. */
160
+ export function systemAiProfiles(config, forceEnabled = false) {
161
+ if (!config || (!forceEnabled && !config.enabled))
162
+ return [];
163
+ const candidates = [config, ...(config.fallbacks ?? [])];
164
+ const seen = new Set();
165
+ return candidates.flatMap((candidate) => {
166
+ const normalized = normalizeSystemAiConfig({ ...candidate, enabled: true, fallbacks: undefined });
167
+ if (!normalized.baseUrl || !normalized.apiKey || !normalized.model)
168
+ return [];
169
+ const key = [normalized.protocol, normalized.baseUrl, normalized.apiKey, normalized.model].join("\0");
170
+ if (seen.has(key))
171
+ return [];
172
+ seen.add(key);
173
+ return [normalized];
174
+ });
109
175
  }
110
176
  function endpoint(baseUrl, protocol) {
111
177
  const url = new URL(baseUrl);
@@ -166,3 +232,23 @@ export async function callSystemAiText(prompt, config, timeoutMs = SYSTEM_AI_TIM
166
232
  throw new SystemAiError("系统 AI API 返回了无法解析的响应。", "SYSTEM_AI_INVALID_RESPONSE");
167
233
  }
168
234
  }
235
+ /** Try every configured API in order. Empty responses are treated as unavailable. */
236
+ export async function callSystemAiTextWithFallback(prompt, config, timeoutMs = SYSTEM_AI_TIMEOUT_MS) {
237
+ const profiles = systemAiProfiles(config, true);
238
+ if (!profiles.length) {
239
+ throw new SystemAiError("系统 AI API 配置不完整。", "SYSTEM_AI_CONFIG_INVALID");
240
+ }
241
+ const errors = [];
242
+ for (const profile of profiles) {
243
+ try {
244
+ const text = await callSystemAiText(prompt, profile, timeoutMs);
245
+ if (text.trim())
246
+ return text;
247
+ errors.push(`${profile.source ?? "custom"}: 返回空结果`);
248
+ }
249
+ catch (error) {
250
+ errors.push(`${profile.source ?? "custom"}: ${error instanceof Error ? error.message : String(error)}`);
251
+ }
252
+ }
253
+ throw new SystemAiError(`所有系统 AI API 均不可用:${errors.join(";")}`, "SYSTEM_AI_ALL_FAILED");
254
+ }
package/dist/types.d.ts CHANGED
@@ -146,6 +146,11 @@ export interface SystemAiConfig {
146
146
  authHeader?: SystemAiAuthHeader;
147
147
  /** 自动导入时记录来源,仅用于设置页说明。 */
148
148
  source?: "claude" | "codex" | "opencode" | "custom";
149
+ /**
150
+ * 其余可直连 API,按数组顺序依次尝试。保留顶层字段作为首选项,
151
+ * 以兼容已有配置与手工编辑入口。
152
+ */
153
+ fallbacks?: SystemAiConfig[];
149
154
  }
150
155
  export type ClaudeModelSource = "builtin" | "configured" | "verified-cache" | "models-api";
151
156
  export type ClaudeModelAvailability = "default" | "candidate" | "verified" | "stale";