@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.
- package/dist/build-info.json +3 -3
- package/dist/config.js +4 -3
- package/dist/server-settings-routes.js +134 -15
- package/dist/session-ai-context.js +1 -1
- package/dist/system-ai.d.ts +2 -2
- package/dist/system-ai.js +32 -6
- package/dist/types.d.ts +2 -0
- package/dist/web-ui/content/scripts.js +211 -67
- package/dist/web-ui/embedded-assets.d.ts +1 -1
- package/dist/web-ui/embedded-assets.js +2 -2
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"commit": "
|
|
3
|
-
"builtAt": "2026-07-
|
|
4
|
-
"version": "4.
|
|
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 && (
|
|
465
|
-
throw new Error("启用系统 AI API
|
|
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
|
-
...
|
|
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
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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)
|
|
88
|
+
const directApi = mergeSystemAiConfigs(config.systemAi, discoverApis(commitContext.provider));
|
|
89
89
|
return {
|
|
90
90
|
...commitContext,
|
|
91
91
|
...(directApi ? { systemAi: directApi } : {}),
|
package/dist/system-ai.d.ts
CHANGED
|
@@ -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.
|
|
15
|
-
*
|
|
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
|
|
49
|
+
.map((item) => {
|
|
45
50
|
try {
|
|
46
|
-
const
|
|
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.
|
|
292
|
-
*
|
|
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
|
-
|
|
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
|
-
: {
|
|
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;
|