@co0ontty/wand 4.24.0 → 4.25.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": "090ceecd30c3ef681295a17b38285bb22d37ce54",
3
- "builtAt": "2026-07-25T01:03:54.326Z",
4
- "version": "4.24.0",
2
+ "commit": "c37c92c6eb275aa3575fc63ff7ec93f650ddf71e",
3
+ "builtAt": "2026-07-25T10:01:47.790Z",
4
+ "version": "4.25.0",
5
5
  "channel": "stable"
6
6
  }
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"
@@ -79,6 +79,7 @@ export const defaultConfig = () => ({
79
79
  commitModel: "",
80
80
  commitAiSource: "cli",
81
81
  systemAi: {
82
+ id: crypto.randomUUID(),
82
83
  enabled: false,
83
84
  protocol: "openai",
84
85
  baseUrl: "",
@@ -461,8 +462,8 @@ export function writePreferenceToStorage(config, storage, key, value, options =
461
462
  throw new Error("systemAi 必须是对象。");
462
463
  }
463
464
  const normalized = normalizeSystemAiConfig(value, config.systemAi ?? defaultConfig().systemAi);
464
- if (normalized.enabled && (!normalized.baseUrl || !normalized.apiKey || !normalized.model)) {
465
- throw new Error("启用系统 AI API 时,地址、API Key 和模型不能为空。");
465
+ if (normalized.enabled && !systemAiProfiles(normalized, true).length) {
466
+ throw new Error("启用系统 AI API 时,至少需要一条地址、API Key 和模型完整的路由。");
466
467
  }
467
468
  if (!options.deferCommitAiValidation) {
468
469
  validateCommitAiConfig({ ...config, systemAi: normalized });
@@ -5,7 +5,15 @@ import { getErrorMessage } from "./error-utils.js";
5
5
  import { asyncRoute } from "./express-async.js";
6
6
  import { getProviderDefaultModels, PREFERENCE_KEYS, saveConfig, validateCommitAiConfig, writePreferenceToStorage, } from "./config.js";
7
7
  import { DEPLOYMENT_CONFIG_KEYS } from "./runtime-config.js";
8
- import { discoverCliSystemAiConfigs, normalizeSystemAiConfig } from "./system-ai.js";
8
+ import { callSystemAiText, discoverCliSystemAiConfigs, mergeSystemAiConfigs, normalizeSystemAiConfig, } from "./system-ai.js";
9
+ function systemAiRouteIdentity(profile) {
10
+ return [
11
+ profile.protocol,
12
+ profile.baseUrl,
13
+ profile.model,
14
+ profile.authHeader ?? "bearer",
15
+ ].join("\0");
16
+ }
9
17
  function publicConfig(config) {
10
18
  const { password: _password, appSecret: _appSecret, ...safe } = config;
11
19
  const defaultModels = getProviderDefaultModels(config);
@@ -129,14 +137,92 @@ export function registerSettingsRoutes(app, deps) {
129
137
  return;
130
138
  }
131
139
  const candidate = runtimeConfig.createCandidate();
140
+ const remainingImported = [...imported];
141
+ const existingRoutes = candidate.systemAi
142
+ ? [candidate.systemAi, ...(candidate.systemAi.fallbacks ?? [])]
143
+ .map((profile) => ({ ...profile, fallbacks: undefined }))
144
+ : [];
145
+ const refreshedExisting = existingRoutes.map((profile) => {
146
+ const importedIndex = remainingImported.findIndex((item) => systemAiRouteIdentity(item) === systemAiRouteIdentity(profile));
147
+ if (importedIndex < 0)
148
+ return profile;
149
+ const [matchingImport] = remainingImported.splice(importedIndex, 1);
150
+ return {
151
+ ...profile,
152
+ apiKey: matchingImport.apiKey,
153
+ source: matchingImport.source,
154
+ };
155
+ });
156
+ const merged = mergeSystemAiConfigs(refreshedExisting, remainingImported);
157
+ if (!merged) {
158
+ res.status(404).json({ error: "没有找到可用的系统 AI API 配置。" });
159
+ return;
160
+ }
132
161
  writePreferenceToStorage(candidate, storage, "systemAi", {
133
- ...imported[0],
162
+ ...merged,
134
163
  enabled: candidate.systemAi?.enabled === true,
135
- fallbacks: imported.slice(1),
136
164
  });
137
165
  runtimeConfig.commit(candidate, new Set(["systemAi"]));
138
166
  res.json({ ok: true, count: imported.length, systemAi: (publicConfig(candidate).systemAi) });
139
167
  });
168
+ app.post("/api/settings/system-ai/test", requireAdmin, asyncRoute(async (req, res) => {
169
+ const raw = req.body?.route;
170
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
171
+ res.status(400).json({ error: "请提供要测试的系统 AI 线路。" });
172
+ return;
173
+ }
174
+ const submitted = raw;
175
+ const desired = runtimeConfig.desiredSnapshot().systemAi;
176
+ const storedRoutes = desired
177
+ ? [desired, ...(desired.fallbacks ?? [])].map((profile) => ({ ...profile, fallbacks: undefined }))
178
+ : [];
179
+ const submittedId = typeof submitted.id === "string" ? submitted.id.trim() : "";
180
+ const stored = submittedId
181
+ ? storedRoutes.find((profile) => profile.id === submittedId)
182
+ : undefined;
183
+ const apiKey = submitted.clearApiKey === true
184
+ ? ""
185
+ : typeof submitted.apiKey === "string" && submitted.apiKey.trim()
186
+ ? submitted.apiKey.trim()
187
+ : stored?.apiKey ?? "";
188
+ let route;
189
+ try {
190
+ route = normalizeSystemAiConfig({
191
+ ...stored,
192
+ ...submitted,
193
+ enabled: true,
194
+ apiKey,
195
+ fallbacks: undefined,
196
+ }, stored);
197
+ if (!route.baseUrl || !route.apiKey || !route.model) {
198
+ throw new Error("测试线路需要完整的 API 地址、API Key 和模型。");
199
+ }
200
+ }
201
+ catch (error) {
202
+ res.status(400).json({ error: getErrorMessage(error, "测试线路配置无效。") });
203
+ return;
204
+ }
205
+ const startedAt = Date.now();
206
+ try {
207
+ const text = await callSystemAiText("这是 Wand 系统 API 线路验收。请只回复 WAND_API_OK。", route, 30_000);
208
+ if (!text.trim())
209
+ throw new Error("系统 AI API 返回了空结果。");
210
+ res.json({
211
+ ok: true,
212
+ source: route.source ?? "custom",
213
+ requestedModel: route.model,
214
+ reasoningEffort: route.protocol === "openai" ? "low" : "disabled",
215
+ latencyMs: Date.now() - startedAt,
216
+ });
217
+ }
218
+ catch (error) {
219
+ res.status(502).json({
220
+ error: getErrorMessage(error, "系统 AI API 测试失败。"),
221
+ requestedModel: route.model,
222
+ latencyMs: Date.now() - startedAt,
223
+ });
224
+ }
225
+ }));
140
226
  app.get("/api/app-connect-code", requireAdmin, (req, res) => {
141
227
  res.json(deps.resolveAppConnectCode(req));
142
228
  });
@@ -203,18 +289,51 @@ export function registerSettingsRoutes(app, deps) {
203
289
  throw new Error("systemAi 必须是对象。");
204
290
  }
205
291
  const previous = candidateConfig.systemAi;
206
- const apiKey = typeof body.systemAi.apiKey === "string" && body.systemAi.apiKey.trim()
207
- ? body.systemAi.apiKey.trim()
208
- : previous?.apiKey ?? "";
209
- const submittedFallbacks = Array.isArray(body.systemAi.fallbacks)
210
- ? body.systemAi.fallbacks.map((item, index) => {
211
- const raw = item && typeof item === "object" && !Array.isArray(item) ? item : {};
212
- const prior = previous?.fallbacks?.[index];
213
- const fallbackApiKey = typeof raw.apiKey === "string" && raw.apiKey.trim() ? raw.apiKey.trim() : prior?.apiKey ?? "";
214
- return { ...prior, ...raw, apiKey: fallbackApiKey, fallbacks: undefined };
215
- })
216
- : previous?.fallbacks;
217
- stagePreference("systemAi", normalizeSystemAiConfig({ ...previous, ...body.systemAi, apiKey, fallbacks: submittedFallbacks }, previous));
292
+ const systemAiEnabled = body.systemAi.enabled === true;
293
+ const previousRoutes = previous ? [previous, ...(previous.fallbacks ?? [])] : [];
294
+ const previousById = new Map(previousRoutes.flatMap((profile) => profile.id ? [[profile.id, profile]] : []));
295
+ const rawFallbacks = Array.isArray(body.systemAi.fallbacks) ? body.systemAi.fallbacks : [];
296
+ const rawRoutes = [body.systemAi, ...rawFallbacks].map((item) => item && typeof item === "object" && !Array.isArray(item)
297
+ ? item
298
+ : {});
299
+ const submittedIds = new Set();
300
+ const normalizedRoutes = rawRoutes.map((raw, index) => {
301
+ const submittedId = typeof raw.id === "string" ? raw.id.trim() : "";
302
+ if (submittedId) {
303
+ if (submittedIds.has(submittedId))
304
+ throw new Error("系统 AI 路由 ID 不能重复。");
305
+ submittedIds.add(submittedId);
306
+ }
307
+ // Older web clients did not submit route IDs. For those clients only,
308
+ // match a unique route by its non-secret identity; never fall back to
309
+ // the array index because routes may have been reordered.
310
+ const identityMatches = submittedId ? [] : previousRoutes.filter((profile) => profile.protocol === (raw.protocol === "anthropic" ? "anthropic" : "openai")
311
+ && profile.baseUrl === (typeof raw.baseUrl === "string" ? raw.baseUrl.trim().replace(/\/+$/, "") : "")
312
+ && profile.model === (typeof raw.model === "string" ? raw.model.trim() : "")
313
+ && (profile.authHeader ?? "bearer") === (raw.authHeader === "x-api-key" ? "x-api-key" : "bearer"));
314
+ const prior = (submittedId ? previousById.get(submittedId) : undefined)
315
+ ?? (identityMatches.length === 1 ? identityMatches[0] : undefined);
316
+ const apiKey = raw.clearApiKey === true
317
+ ? ""
318
+ : typeof raw.apiKey === "string" && raw.apiKey.trim()
319
+ ? raw.apiKey.trim()
320
+ : prior?.apiKey ?? "";
321
+ return normalizeSystemAiConfig({
322
+ ...prior,
323
+ ...raw,
324
+ enabled: index === 0 ? systemAiEnabled : true,
325
+ apiKey,
326
+ fallbacks: undefined,
327
+ }, prior);
328
+ });
329
+ const [primary, ...fallbacks] = normalizedRoutes;
330
+ if (!primary)
331
+ throw new Error("系统 AI 至少需要一个路由占位。");
332
+ stagePreference("systemAi", {
333
+ ...primary,
334
+ enabled: systemAiEnabled,
335
+ ...(fallbacks.length ? { fallbacks } : { fallbacks: [] }),
336
+ });
218
337
  }
219
338
  for (const field of PREFERENCE_KEYS) {
220
339
  if (field === "systemAi")
@@ -85,7 +85,7 @@ export function resolveCommitAiContext(snapshot, config, discoverApis = discover
85
85
  };
86
86
  if (config.commitAiSource !== "api")
87
87
  return commitContext;
88
- const directApi = mergeSystemAiConfigs(discoverApis(commitContext.provider), config.systemAi);
88
+ const directApi = mergeSystemAiConfigs(config.systemAi, discoverApis(commitContext.provider));
89
89
  return {
90
90
  ...commitContext,
91
91
  ...(directApi ? { systemAi: directApi } : {}),
@@ -11,8 +11,8 @@ export declare function discoverCliSystemAiConfig(preferred?: SessionProvider, h
11
11
  /** Return the configured API chain in call order, excluding incomplete entries. */
12
12
  export declare function systemAiProfiles(config: SystemAiConfig | undefined, forceEnabled?: boolean): SystemAiConfig[];
13
13
  /**
14
- * Flatten and combine direct-API groups in priority order. Dynamically discovered
15
- * profiles can be passed first, followed by stored/legacy settings.
14
+ * Flatten and combine direct-API groups in caller-defined priority order.
15
+ * Later groups are appended after earlier groups and duplicate routes are skipped.
16
16
  */
17
17
  export declare function mergeSystemAiConfigs(...groups: Array<SystemAiConfig | SystemAiConfig[] | undefined>): SystemAiConfig | undefined;
18
18
  export declare function callSystemAiText(prompt: string, config: SystemAiConfig, timeoutMs?: number): Promise<string>;
package/dist/system-ai.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
+ import { randomUUID } from "node:crypto";
2
3
  import os from "node:os";
3
4
  import path from "node:path";
4
5
  const SYSTEM_AI_TIMEOUT_MS = 60_000;
6
+ const SYSTEM_AI_ROUTE_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
5
7
  export class SystemAiError extends Error {
6
8
  code;
7
9
  constructor(message, code) {
@@ -29,6 +31,9 @@ export function normalizeSystemAiConfig(value, fallback) {
29
31
  throw new Error("系统 AI API 地址必须使用 http 或 https。");
30
32
  }
31
33
  const normalized = {
34
+ id: typeof raw.id === "string" && SYSTEM_AI_ROUTE_ID_PATTERN.test(raw.id.trim())
35
+ ? raw.id.trim()
36
+ : fallback?.id ?? randomUUID(),
32
37
  enabled: raw.enabled === true,
33
38
  protocol,
34
39
  baseUrl,
@@ -41,9 +46,15 @@ export function normalizeSystemAiConfig(value, fallback) {
41
46
  };
42
47
  if (Array.isArray(raw.fallbacks)) {
43
48
  normalized.fallbacks = raw.fallbacks
44
- .map((item, index) => {
49
+ .map((item) => {
45
50
  try {
46
- const itemFallback = fallback?.fallbacks?.[index];
51
+ const itemId = item && typeof item === "object" && !Array.isArray(item)
52
+ && typeof item.id === "string"
53
+ ? item.id.trim()
54
+ : "";
55
+ const itemFallback = itemId
56
+ ? fallback?.fallbacks?.find((profile) => profile.id === itemId)
57
+ : undefined;
47
58
  const profile = normalizeSystemAiConfig(item, itemFallback);
48
59
  delete profile.fallbacks;
49
60
  return profile;
@@ -70,6 +81,10 @@ function tryNormalizeSystemAiConfig(value) {
70
81
  }
71
82
  }
72
83
  function systemAiProfileKey(profile) {
84
+ // Credential is intentionally part of runtime dedupe: if a stored key is
85
+ // stale but a tool config has already rotated it, the discovered route must
86
+ // remain available later in the same fallback chain. Import handles that
87
+ // case separately by refreshing the stored route in place.
73
88
  return [
74
89
  profile.protocol,
75
90
  profile.baseUrl,
@@ -288,8 +303,8 @@ export function systemAiProfiles(config, forceEnabled = false) {
288
303
  });
289
304
  }
290
305
  /**
291
- * Flatten and combine direct-API groups in priority order. Dynamically discovered
292
- * profiles can be passed first, followed by stored/legacy settings.
306
+ * Flatten and combine direct-API groups in caller-defined priority order.
307
+ * Later groups are appended after earlier groups and duplicate routes are skipped.
293
308
  */
294
309
  export function mergeSystemAiConfigs(...groups) {
295
310
  const profiles = [];
@@ -343,11 +358,22 @@ export async function callSystemAiText(prompt, config, timeoutMs = SYSTEM_AI_TIM
343
358
  headers.authorization = `Bearer ${normalized.apiKey}`;
344
359
  }
345
360
  else {
346
- headers.authorization = `Bearer ${normalized.apiKey}`;
361
+ if (normalized.authHeader === "x-api-key")
362
+ headers["x-api-key"] = normalized.apiKey;
363
+ else
364
+ headers.authorization = `Bearer ${normalized.apiKey}`;
347
365
  }
348
366
  const body = normalized.protocol === "anthropic"
349
367
  ? { model: normalized.model, max_tokens: 2048, messages: [{ role: "user", content: prompt }] }
350
- : { model: normalized.model, messages: [{ role: "user", content: prompt }], stream: false };
368
+ : {
369
+ model: normalized.model,
370
+ // Quick system tasks should not silently inherit a model's expensive
371
+ // default reasoning level. Settings probes a route with this same
372
+ // payload, so an incompatible endpoint fails visibly before it is used.
373
+ reasoning_effort: "low",
374
+ messages: [{ role: "user", content: prompt }],
375
+ stream: false,
376
+ };
351
377
  let response;
352
378
  try {
353
379
  response = await fetch(endpoint(normalized.baseUrl, normalized.protocol), {
package/dist/types.d.ts CHANGED
@@ -138,6 +138,8 @@ export interface WandConfig {
138
138
  export type SystemAiProtocol = "openai" | "anthropic";
139
139
  export type SystemAiAuthHeader = "bearer" | "x-api-key";
140
140
  export interface SystemAiConfig {
141
+ /** 设置页路由的稳定标识,用于重排后安全地关联已保存密钥。 */
142
+ id?: string;
141
143
  enabled: boolean;
142
144
  protocol: SystemAiProtocol;
143
145
  baseUrl: string;