web-access-cli 0.5.0 → 0.7.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.
Files changed (50) hide show
  1. package/README.md +104 -207
  2. package/README_CN.md +106 -209
  3. package/THIRD_PARTY_NOTICES.md +0 -13
  4. package/dist/config/config.js +10 -2
  5. package/dist/config/config.js.map +1 -1
  6. package/dist/core/router.js +4 -2
  7. package/dist/core/router.js.map +1 -1
  8. package/dist/core/schema.js +1 -1
  9. package/dist/core/types.js +1 -0
  10. package/dist/core/types.js.map +1 -1
  11. package/dist/gateway/admin.js +670 -0
  12. package/dist/gateway/admin.js.map +1 -0
  13. package/dist/gateway/panel.js +2405 -0
  14. package/dist/gateway/panel.js.map +1 -0
  15. package/dist/gateway/records.js +464 -0
  16. package/dist/gateway/records.js.map +1 -0
  17. package/dist/gateway/schema.js +115 -0
  18. package/dist/gateway/schema.js.map +1 -0
  19. package/dist/gateway/server.js +663 -0
  20. package/dist/gateway/server.js.map +1 -0
  21. package/dist/gateway/service.js +607 -0
  22. package/dist/gateway/service.js.map +1 -0
  23. package/dist/providers/anysearch.js +5 -3
  24. package/dist/providers/anysearch.js.map +1 -1
  25. package/dist/providers/deepseek.js +173 -106
  26. package/dist/providers/deepseek.js.map +1 -1
  27. package/dist/providers/registry.js +2 -0
  28. package/dist/providers/registry.js.map +1 -1
  29. package/dist/providers/search.js +4 -2
  30. package/dist/providers/search.js.map +1 -1
  31. package/dist/providers/xai.js +16 -8
  32. package/dist/providers/xai.js.map +1 -1
  33. package/dist/providers/xcrawl.js +1 -1
  34. package/dist/providers/xcrawl.js.map +1 -1
  35. package/dist/providers/yuanbao.js +214 -0
  36. package/dist/providers/yuanbao.js.map +1 -0
  37. package/dist/server.js +127 -0
  38. package/dist/server.js.map +1 -0
  39. package/dist/transport/http.js +8 -0
  40. package/dist/transport/http.js.map +1 -1
  41. package/dist/version.js +1 -1
  42. package/package.json +4 -2
  43. package/schemas/config.schema.json +4 -0
  44. package/schemas/gatewayError.schema.json +33 -0
  45. package/schemas/gatewayFetchRequest.schema.json +13 -0
  46. package/schemas/gatewayFetchResponse.schema.json +16 -0
  47. package/schemas/gatewaySearchRequest.schema.json +37 -0
  48. package/schemas/gatewaySearchResponse.schema.json +81 -0
  49. package/schemas/output.schema.json +52 -0
  50. package/schemas/searchRequest.schema.json +1 -1
@@ -0,0 +1,670 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { lstat, mkdir, readFile, realpath, stat, writeFile, } from "node:fs/promises";
3
+ import { dirname } from "node:path";
4
+ import { applyEdits, modify } from "jsonc-parser";
5
+ import writeFileAtomic from "write-file-atomic";
6
+ import { capabilitySupports, loadConfig, loadConfigContents, } from "../config/config.js";
7
+ import { CONFIG_SCHEMA_URL } from "../config/file.js";
8
+ import { executeDoctor } from "../core/diagnostics.js";
9
+ import { PROVIDER_TYPES, SEARCH_FILTER_MODES, } from "../core/types.js";
10
+ import { getAdapter } from "../providers/registry.js";
11
+ import { VERSION } from "../version.js";
12
+ import { GatewayValidationError } from "./schema.js";
13
+ import { checkGatewayReadiness, } from "./service.js";
14
+ const BUILTIN_PROVIDER_IDS = new Set(PROVIDER_TYPES);
15
+ function emptyProviderStats() {
16
+ return {
17
+ search: {
18
+ attempts: 0,
19
+ successes: 0,
20
+ failures: 0,
21
+ skipped: 0,
22
+ totalDurationMs: 0,
23
+ avgDurationMs: 0,
24
+ errorCodes: {},
25
+ },
26
+ extract: {
27
+ attempts: 0,
28
+ successes: 0,
29
+ failures: 0,
30
+ skipped: 0,
31
+ totalDurationMs: 0,
32
+ avgDurationMs: 0,
33
+ errorCodes: {},
34
+ },
35
+ };
36
+ }
37
+ function serializeLoadedConfig(loaded) {
38
+ return `${JSON.stringify({
39
+ $schema: CONFIG_SCHEMA_URL,
40
+ providers: loaded.app.providers,
41
+ search: loaded.app.search,
42
+ extract: loaded.app.extract,
43
+ }, null, 2)}\n`;
44
+ }
45
+ function isPlainObject(value) {
46
+ return typeof value === "object" && value !== null && !Array.isArray(value);
47
+ }
48
+ function parseStringArray(value, label) {
49
+ if (!Array.isArray(value)) {
50
+ throw new GatewayValidationError(`${label} 必须是字符串数组。`);
51
+ }
52
+ return value.map((item, idx) => {
53
+ if (typeof item !== "string" || !item.trim()) {
54
+ throw new GatewayValidationError(`${label}[${idx}] 必须是非空字符串。`);
55
+ }
56
+ return item.trim().toLowerCase();
57
+ });
58
+ }
59
+ function parseOptionalInt(value, label, min = 1, max) {
60
+ if (value === undefined)
61
+ return undefined;
62
+ if (typeof value !== "number" || !Number.isInteger(value) || value < min) {
63
+ throw new GatewayValidationError(`${label} 必须是大于等于 ${min} 的整数。`);
64
+ }
65
+ if (max !== undefined && value > max) {
66
+ throw new GatewayValidationError(`${label} 不能超过 ${max}。`);
67
+ }
68
+ return value;
69
+ }
70
+ function parseNullableString(value, label) {
71
+ if (value === undefined)
72
+ return undefined;
73
+ if (value === null)
74
+ return null;
75
+ if (typeof value !== "string") {
76
+ throw new GatewayValidationError(`${label} 必须是字符串或 null。`);
77
+ }
78
+ const trimmed = value.trim();
79
+ return trimmed.length > 0 ? trimmed : null;
80
+ }
81
+ export function parseGatewayConfigUpdateRequest(body) {
82
+ if (!isPlainObject(body)) {
83
+ throw new GatewayValidationError("配置更新请求体必须是 JSON 对象。");
84
+ }
85
+ const allowedTop = new Set([
86
+ "search",
87
+ "extract",
88
+ "providers",
89
+ "removeProviders",
90
+ "allowUnready",
91
+ ]);
92
+ for (const key of Object.keys(body)) {
93
+ if (!allowedTop.has(key)) {
94
+ throw new GatewayValidationError(`未知配置更新字段: ${key}。`);
95
+ }
96
+ }
97
+ const result = {};
98
+ if (body.search !== undefined) {
99
+ if (!isPlainObject(body.search)) {
100
+ throw new GatewayValidationError("search 必须是对象。");
101
+ }
102
+ const s = body.search;
103
+ const allowedSearch = new Set([
104
+ "providers",
105
+ "providers_fallback",
106
+ "limit",
107
+ "timeoutMs",
108
+ "attemptTimeoutMs",
109
+ "maxResponseBytes",
110
+ ]);
111
+ for (const key of Object.keys(s)) {
112
+ if (!allowedSearch.has(key)) {
113
+ throw new GatewayValidationError(`search 包含未知字段: ${key}。`);
114
+ }
115
+ }
116
+ result.search = {
117
+ ...(s.providers !== undefined
118
+ ? { providers: parseStringArray(s.providers, "search.providers") }
119
+ : {}),
120
+ ...(s.providers_fallback !== undefined
121
+ ? {
122
+ providers_fallback: parseStringArray(s.providers_fallback, "search.providers_fallback"),
123
+ }
124
+ : {}),
125
+ ...(s.limit !== undefined
126
+ ? { limit: parseOptionalInt(s.limit, "search.limit", 1, 20) }
127
+ : {}),
128
+ ...(s.timeoutMs !== undefined
129
+ ? { timeoutMs: parseOptionalInt(s.timeoutMs, "search.timeoutMs", 1) }
130
+ : {}),
131
+ ...(s.attemptTimeoutMs !== undefined
132
+ ? {
133
+ attemptTimeoutMs: parseOptionalInt(s.attemptTimeoutMs, "search.attemptTimeoutMs", 1),
134
+ }
135
+ : {}),
136
+ ...(s.maxResponseBytes !== undefined
137
+ ? {
138
+ maxResponseBytes: parseOptionalInt(s.maxResponseBytes, "search.maxResponseBytes", 1024),
139
+ }
140
+ : {}),
141
+ };
142
+ }
143
+ if (body.extract !== undefined) {
144
+ if (!isPlainObject(body.extract)) {
145
+ throw new GatewayValidationError("extract 必须是对象。");
146
+ }
147
+ const e = body.extract;
148
+ const allowedExtract = new Set([
149
+ "providers",
150
+ "providers_fallback",
151
+ "timeoutMs",
152
+ "attemptTimeoutMs",
153
+ "minContentCharacters",
154
+ "maxResponseBytes",
155
+ ]);
156
+ for (const key of Object.keys(e)) {
157
+ if (!allowedExtract.has(key)) {
158
+ throw new GatewayValidationError(`extract 包含未知字段: ${key}。`);
159
+ }
160
+ }
161
+ result.extract = {
162
+ ...(e.providers !== undefined
163
+ ? { providers: parseStringArray(e.providers, "extract.providers") }
164
+ : {}),
165
+ ...(e.providers_fallback !== undefined
166
+ ? {
167
+ providers_fallback: parseStringArray(e.providers_fallback, "extract.providers_fallback"),
168
+ }
169
+ : {}),
170
+ ...(e.timeoutMs !== undefined
171
+ ? { timeoutMs: parseOptionalInt(e.timeoutMs, "extract.timeoutMs", 1) }
172
+ : {}),
173
+ ...(e.attemptTimeoutMs !== undefined
174
+ ? {
175
+ attemptTimeoutMs: parseOptionalInt(e.attemptTimeoutMs, "extract.attemptTimeoutMs", 1),
176
+ }
177
+ : {}),
178
+ ...(e.minContentCharacters !== undefined
179
+ ? {
180
+ minContentCharacters: parseOptionalInt(e.minContentCharacters, "extract.minContentCharacters", 1),
181
+ }
182
+ : {}),
183
+ ...(e.maxResponseBytes !== undefined
184
+ ? {
185
+ maxResponseBytes: parseOptionalInt(e.maxResponseBytes, "extract.maxResponseBytes", 1024),
186
+ }
187
+ : {}),
188
+ };
189
+ }
190
+ if (body.providers !== undefined) {
191
+ if (!Array.isArray(body.providers)) {
192
+ throw new GatewayValidationError("providers 必须是数组。");
193
+ }
194
+ const allowedProviderKeys = new Set([
195
+ "id",
196
+ "type",
197
+ "apiKeyAction",
198
+ "apiKey",
199
+ "apiKeyEnv",
200
+ "baseUrl",
201
+ "baseUrlEnv",
202
+ "authJson",
203
+ "authJsonEnv",
204
+ "model",
205
+ "modelEnv",
206
+ "searchFilterMode",
207
+ ]);
208
+ result.providers = body.providers.map((raw, idx) => {
209
+ const label = `providers[${idx}]`;
210
+ if (!isPlainObject(raw)) {
211
+ throw new GatewayValidationError(`${label} 必须是对象。`);
212
+ }
213
+ for (const key of Object.keys(raw)) {
214
+ if (!allowedProviderKeys.has(key)) {
215
+ throw new GatewayValidationError(`${label} 包含未知字段: ${key}。`);
216
+ }
217
+ }
218
+ if (typeof raw.id !== "string" || !raw.id.trim()) {
219
+ throw new GatewayValidationError(`${label}.id 必须是非空字符串。`);
220
+ }
221
+ if (typeof raw.type !== "string" || !raw.type.trim()) {
222
+ throw new GatewayValidationError(`${label}.type 必须是非空字符串。`);
223
+ }
224
+ const type = raw.type.trim().toLowerCase();
225
+ if (!PROVIDER_TYPES.includes(type)) {
226
+ throw new GatewayValidationError(`${label}.type 不受支持: ${type}。`);
227
+ }
228
+ let apiKeyAction = "keep";
229
+ if (raw.apiKeyAction !== undefined) {
230
+ if (raw.apiKeyAction !== "keep" &&
231
+ raw.apiKeyAction !== "set" &&
232
+ raw.apiKeyAction !== "clear") {
233
+ throw new GatewayValidationError(`${label}.apiKeyAction 必须是 keep、set 或 clear。`);
234
+ }
235
+ apiKeyAction = raw.apiKeyAction;
236
+ }
237
+ let apiKey;
238
+ if (apiKeyAction === "set") {
239
+ if (typeof raw.apiKey !== "string" || !raw.apiKey.trim()) {
240
+ throw new GatewayValidationError(`${label}.apiKey 在 apiKeyAction=set 时必须提供非空字符串。`);
241
+ }
242
+ apiKey = raw.apiKey.trim();
243
+ }
244
+ const searchFilterModeRaw = parseNullableString(raw.searchFilterMode, `${label}.searchFilterMode`);
245
+ if (typeof searchFilterModeRaw === "string" &&
246
+ !SEARCH_FILTER_MODES.includes(searchFilterModeRaw)) {
247
+ throw new GatewayValidationError(`${label}.searchFilterMode 必须是 strict 或 best_effort。`);
248
+ }
249
+ return {
250
+ id: raw.id.trim().toLowerCase(),
251
+ type: type,
252
+ apiKeyAction,
253
+ ...(apiKey ? { apiKey } : {}),
254
+ apiKeyEnv: parseNullableString(raw.apiKeyEnv, `${label}.apiKeyEnv`),
255
+ baseUrl: parseNullableString(raw.baseUrl, `${label}.baseUrl`),
256
+ baseUrlEnv: parseNullableString(raw.baseUrlEnv, `${label}.baseUrlEnv`),
257
+ authJson: parseNullableString(raw.authJson, `${label}.authJson`),
258
+ authJsonEnv: parseNullableString(raw.authJsonEnv, `${label}.authJsonEnv`),
259
+ model: parseNullableString(raw.model, `${label}.model`),
260
+ modelEnv: parseNullableString(raw.modelEnv, `${label}.modelEnv`),
261
+ searchFilterMode: searchFilterModeRaw,
262
+ };
263
+ });
264
+ }
265
+ if (body.removeProviders !== undefined) {
266
+ result.removeProviders = parseStringArray(body.removeProviders, "removeProviders");
267
+ }
268
+ if (body.allowUnready !== undefined) {
269
+ if (typeof body.allowUnready !== "boolean") {
270
+ throw new GatewayValidationError("allowUnready 必须是布尔值。");
271
+ }
272
+ result.allowUnready = body.allowUnready;
273
+ }
274
+ return result;
275
+ }
276
+ async function resolveTargetFile(path) {
277
+ try {
278
+ const info = await lstat(path);
279
+ if (!info.isSymbolicLink())
280
+ return path;
281
+ const target = await realpath(path);
282
+ const targetInfo = await stat(target);
283
+ if (!targetInfo.isFile()) {
284
+ throw new Error(`配置文件符号链接目标不是普通文件: ${path}`);
285
+ }
286
+ return target;
287
+ }
288
+ catch (error) {
289
+ if (error?.code === "ENOENT") {
290
+ return path;
291
+ }
292
+ throw error;
293
+ }
294
+ }
295
+ async function persistConfigText(configPath, contents) {
296
+ const target = await resolveTargetFile(configPath);
297
+ try {
298
+ await mkdir(dirname(target), { recursive: true });
299
+ }
300
+ catch {
301
+ // Parent directory may be on a read-only rootfs in container mode.
302
+ }
303
+ try {
304
+ await writeFileAtomic(target, contents, { encoding: "utf8", fsync: true });
305
+ return { persisted: true };
306
+ }
307
+ catch (atomicError) {
308
+ const code = atomicError?.code;
309
+ if (code !== "EROFS" &&
310
+ code !== "EBUSY" &&
311
+ code !== "EPERM" &&
312
+ code !== "EACCES" &&
313
+ code !== "EXDEV" &&
314
+ code !== "ENOENT") {
315
+ throw atomicError;
316
+ }
317
+ try {
318
+ await writeFile(target, contents, "utf8");
319
+ return { persisted: true };
320
+ }
321
+ catch (directError) {
322
+ const directCode = directError?.code;
323
+ if (directCode === "EROFS" ||
324
+ directCode === "EACCES" ||
325
+ directCode === "EPERM" ||
326
+ directCode === "ENOENT") {
327
+ return {
328
+ persisted: false,
329
+ persistWarning: "当前配置文件为只读挂载,修改已在内存立即生效,容器重启后将恢复文件原配置。",
330
+ };
331
+ }
332
+ throw directError;
333
+ }
334
+ }
335
+ }
336
+ function applyPatchToConfigText(baseText, currentLoaded, patch) {
337
+ let text = baseText;
338
+ const formattingOptions = {
339
+ insertSpaces: true,
340
+ tabSize: 2,
341
+ eol: text.includes("\r\n") ? "\r\n" : "\n",
342
+ };
343
+ const setField = (path, value) => {
344
+ const edits = modify(text, path, value, { formattingOptions });
345
+ text = applyEdits(text, edits);
346
+ };
347
+ if (patch.search) {
348
+ for (const [key, value] of Object.entries(patch.search)) {
349
+ if (value !== undefined) {
350
+ setField(["search", key], value);
351
+ }
352
+ }
353
+ }
354
+ if (patch.extract) {
355
+ for (const [key, value] of Object.entries(patch.extract)) {
356
+ if (value !== undefined) {
357
+ setField(["extract", key], value);
358
+ }
359
+ }
360
+ }
361
+ if ((patch.providers && patch.providers.length > 0) ||
362
+ (patch.removeProviders && patch.removeProviders.length > 0)) {
363
+ let parsedRoot = {};
364
+ try {
365
+ parsedRoot = JSON.parse(text);
366
+ }
367
+ catch {
368
+ parsedRoot = {};
369
+ }
370
+ const rawProviders = Array.isArray(parsedRoot.providers)
371
+ ? parsedRoot.providers
372
+ : [];
373
+ const byId = new Map();
374
+ for (const entry of currentLoaded.app.providers) {
375
+ // Include existing non-default or configured entries
376
+ const hasCustom = !BUILTIN_PROVIDER_IDS.has(entry.id) ||
377
+ entry.apiKey !== undefined ||
378
+ entry.apiKeyEnv !== undefined ||
379
+ entry.baseUrl !== undefined ||
380
+ entry.baseUrlEnv !== undefined ||
381
+ entry.authJson !== undefined ||
382
+ entry.authJsonEnv !== undefined ||
383
+ entry.model !== undefined ||
384
+ entry.modelEnv !== undefined ||
385
+ entry.headers !== undefined ||
386
+ ((entry.type === "anysearch" || entry.type === "xcrawl") &&
387
+ entry.searchFilterMode !== undefined &&
388
+ entry.searchFilterMode !== "strict");
389
+ if (hasCustom) {
390
+ byId.set(entry.id, { ...entry });
391
+ }
392
+ }
393
+ for (const entry of rawProviders) {
394
+ if (entry && typeof entry.id === "string") {
395
+ const id = entry.id.toLowerCase();
396
+ byId.set(id, { ...(byId.get(id) ?? {}), ...entry, id });
397
+ }
398
+ }
399
+ for (const removeId of patch.removeProviders ?? []) {
400
+ byId.delete(removeId.toLowerCase());
401
+ }
402
+ for (const item of patch.providers ?? []) {
403
+ const existing = byId.get(item.id) ??
404
+ currentLoaded.app.providers.find((candidate) => candidate.id === item.id);
405
+ const next = {
406
+ ...(existing ? { ...existing } : {}),
407
+ id: item.id,
408
+ type: item.type,
409
+ };
410
+ if (item.apiKeyAction === "set" && item.apiKey) {
411
+ next.apiKey = item.apiKey;
412
+ }
413
+ else if (item.apiKeyAction === "clear") {
414
+ delete next.apiKey;
415
+ }
416
+ const optionalFields = [
417
+ "apiKeyEnv",
418
+ "baseUrl",
419
+ "baseUrlEnv",
420
+ "authJson",
421
+ "authJsonEnv",
422
+ "model",
423
+ "modelEnv",
424
+ "searchFilterMode",
425
+ ];
426
+ for (const field of optionalFields) {
427
+ const val = item[field];
428
+ if (val === null) {
429
+ delete next[field];
430
+ }
431
+ else if (val !== undefined) {
432
+ next[field] = val;
433
+ }
434
+ }
435
+ byId.set(item.id, next);
436
+ }
437
+ setField(["providers"], Array.from(byId.values()));
438
+ }
439
+ return text;
440
+ }
441
+ export function createGatewayAdminController(options) {
442
+ const { service, stats, retention } = options;
443
+ const startedAt = options.startedAt ?? Date.now();
444
+ const env = options.env ?? process.env;
445
+ let configPersisted = true;
446
+ let persistWarning;
447
+ let lastConfigText;
448
+ let writeQueue = Promise.resolve();
449
+ function readCurrentConfigText(loaded) {
450
+ if (lastConfigText !== undefined && !configPersisted) {
451
+ return lastConfigText;
452
+ }
453
+ if (existsSync(loaded.path)) {
454
+ try {
455
+ return readFileSync(loaded.path, "utf8");
456
+ }
457
+ catch {
458
+ // Fall back to serialized current config
459
+ }
460
+ }
461
+ return serializeLoadedConfig(loaded);
462
+ }
463
+ function buildOverview() {
464
+ const loaded = service.getLoaded();
465
+ const readiness = service.readiness();
466
+ const statsSnapshot = stats.snapshot();
467
+ const rawById = new Map(loaded.app.providers.map((item) => [item.id, item]));
468
+ const searchPrimary = loaded.app.search.providers;
469
+ const searchFallback = loaded.app.search.providers_fallback;
470
+ const extractPrimary = loaded.app.extract.providers;
471
+ const extractFallback = loaded.app.extract.providers_fallback;
472
+ const providers = loaded.instances.map((instance) => {
473
+ const raw = rawById.get(instance.id);
474
+ const searchAdapter = getAdapter(instance.type, "search");
475
+ const extractAdapter = getAdapter(instance.type, "extract");
476
+ const searchPrimaryIdx = searchPrimary.indexOf(instance.id);
477
+ const searchFallbackIdx = searchFallback.indexOf(instance.id);
478
+ const extractPrimaryIdx = extractPrimary.indexOf(instance.id);
479
+ const extractFallbackIdx = extractFallback.indexOf(instance.id);
480
+ const searchRole = searchPrimaryIdx !== -1
481
+ ? "primary"
482
+ : searchFallbackIdx !== -1
483
+ ? "fallback"
484
+ : "disabled";
485
+ const extractRole = extractPrimaryIdx !== -1
486
+ ? "primary"
487
+ : extractFallbackIdx !== -1
488
+ ? "fallback"
489
+ : "disabled";
490
+ const capabilities = ["search", "extract"].filter((cap) => capabilitySupports(instance.type, cap));
491
+ return {
492
+ id: instance.id,
493
+ type: instance.type,
494
+ isBuiltin: BUILTIN_PROVIDER_IDS.has(instance.id),
495
+ capabilities,
496
+ routeRole: {
497
+ search: searchRole,
498
+ searchIndex: searchPrimaryIdx !== -1
499
+ ? searchPrimaryIdx
500
+ : searchFallbackIdx !== -1
501
+ ? searchFallbackIdx
502
+ : null,
503
+ extract: extractRole,
504
+ extractIndex: extractPrimaryIdx !== -1
505
+ ? extractPrimaryIdx
506
+ : extractFallbackIdx !== -1
507
+ ? extractFallbackIdx
508
+ : null,
509
+ },
510
+ configured: {
511
+ credential: instance.credentialSource !== "missing",
512
+ baseUrl: instance.baseUrl !== null,
513
+ adapter: Boolean(searchAdapter || extractAdapter),
514
+ searchReady: Boolean(searchAdapter?.isConfigured(instance)),
515
+ extractReady: Boolean(extractAdapter?.isConfigured(instance)),
516
+ },
517
+ credentialSource: instance.credentialSource,
518
+ baseUrlSource: instance.baseUrlSource,
519
+ authJsonSource: instance.authJsonSource,
520
+ modelSource: instance.modelSource,
521
+ config: {
522
+ hasConfigApiKey: Boolean(raw?.apiKey),
523
+ ...(raw?.apiKeyEnv ? { apiKeyEnv: raw.apiKeyEnv } : {}),
524
+ ...(raw?.baseUrl ? { configBaseUrl: raw.baseUrl } : {}),
525
+ effectiveBaseUrl: instance.baseUrl,
526
+ ...(raw?.baseUrlEnv ? { baseUrlEnv: raw.baseUrlEnv } : {}),
527
+ ...(raw?.authJson ? { authJson: raw.authJson } : {}),
528
+ ...(raw?.authJsonEnv ? { authJsonEnv: raw.authJsonEnv } : {}),
529
+ ...(raw?.model ? { model: raw.model } : {}),
530
+ ...(raw?.modelEnv ? { modelEnv: raw.modelEnv } : {}),
531
+ ...(instance.searchFilterMode
532
+ ? { searchFilterMode: instance.searchFilterMode }
533
+ : {}),
534
+ headerNames: Object.keys(raw?.headers ?? {}),
535
+ },
536
+ stats: statsSnapshot.providers[instance.id] ?? emptyProviderStats(),
537
+ };
538
+ });
539
+ const doctorEnvelope = executeDoctor(loaded);
540
+ const doctorDetails = (doctorEnvelope.ok ? doctorEnvelope.data : doctorEnvelope.error.details);
541
+ const providerTypes = PROVIDER_TYPES.map((type) => ({
542
+ type,
543
+ capabilities: ["search", "extract"].filter((cap) => capabilitySupports(type, cap)),
544
+ supportsFilterMode: type === "anysearch" || type === "xcrawl",
545
+ isXai: type === "xai_x_search" || type === "xai_web_search",
546
+ }));
547
+ const now = Date.now();
548
+ return {
549
+ generatedAt: now,
550
+ gateway: {
551
+ version: VERSION,
552
+ startedAt,
553
+ uptimeMs: Math.max(0, now - startedAt),
554
+ readiness,
555
+ configPath: loaded.path,
556
+ configExists: loaded.exists || existsSync(loaded.path),
557
+ configPersisted,
558
+ ...(persistWarning ? { persistWarning } : {}),
559
+ retention,
560
+ },
561
+ routes: {
562
+ search: {
563
+ providers: [...loaded.app.search.providers],
564
+ providers_fallback: [...loaded.app.search.providers_fallback],
565
+ limit: loaded.app.search.limit,
566
+ timeoutMs: loaded.app.search.timeoutMs,
567
+ attemptTimeoutMs: loaded.app.search.attemptTimeoutMs,
568
+ maxResponseBytes: loaded.app.search.maxResponseBytes,
569
+ },
570
+ extract: {
571
+ providers: [...loaded.app.extract.providers],
572
+ providers_fallback: [...loaded.app.extract.providers_fallback],
573
+ timeoutMs: loaded.app.extract.timeoutMs,
574
+ attemptTimeoutMs: loaded.app.extract.attemptTimeoutMs,
575
+ maxResponseBytes: loaded.app.extract.maxResponseBytes,
576
+ minContentCharacters: loaded.app.extract.minContentCharacters,
577
+ },
578
+ },
579
+ providers,
580
+ providerTypes,
581
+ doctor: {
582
+ ok: doctorEnvelope.ok,
583
+ ...(!doctorEnvelope.ok
584
+ ? { message: doctorEnvelope.error.message }
585
+ : {}),
586
+ routes: doctorDetails.routes,
587
+ providers: doctorDetails.providers,
588
+ },
589
+ stats: statsSnapshot,
590
+ };
591
+ }
592
+ function runExclusive(task) {
593
+ const next = writeQueue.then(task, task);
594
+ writeQueue = next.then(() => undefined, () => undefined);
595
+ return next;
596
+ }
597
+ return {
598
+ getOverview() {
599
+ return buildOverview();
600
+ },
601
+ updateConfig(patch) {
602
+ return runExclusive(async () => {
603
+ const currentLoaded = service.getLoaded();
604
+ const baseText = readCurrentConfigText(currentLoaded);
605
+ const nextText = applyPatchToConfigText(baseText, currentLoaded, patch);
606
+ let nextLoaded;
607
+ try {
608
+ nextLoaded = loadConfigContents(currentLoaded.path, nextText, env);
609
+ }
610
+ catch (error) {
611
+ const message = error instanceof Error ? error.message : "配置文件校验失败。";
612
+ throw new GatewayValidationError(message);
613
+ }
614
+ const readiness = checkGatewayReadiness(nextLoaded);
615
+ if (!readiness.ready && !patch.allowUnready) {
616
+ throw new GatewayValidationError(`更新后的配置会导致网关未就绪(${readiness.reason}),请确保 search 与 extract 路由中各有至少一个已配置凭据的 Provider。`);
617
+ }
618
+ const writeResult = await persistConfigText(currentLoaded.path, nextText);
619
+ configPersisted = writeResult.persisted;
620
+ persistWarning = writeResult.persistWarning;
621
+ lastConfigText = nextText;
622
+ service.setLoaded({
623
+ ...nextLoaded,
624
+ exists: writeResult.persisted || currentLoaded.exists,
625
+ });
626
+ return {
627
+ updated: true,
628
+ persisted: writeResult.persisted,
629
+ ...(writeResult.persistWarning
630
+ ? { persistWarning: writeResult.persistWarning }
631
+ : {}),
632
+ overview: buildOverview(),
633
+ };
634
+ });
635
+ },
636
+ reloadConfig() {
637
+ return runExclusive(async () => {
638
+ const currentLoaded = service.getLoaded();
639
+ let nextLoaded;
640
+ try {
641
+ if (existsSync(currentLoaded.path)) {
642
+ const contents = await readFile(currentLoaded.path, "utf8");
643
+ nextLoaded = loadConfigContents(currentLoaded.path, contents, env);
644
+ lastConfigText = contents;
645
+ }
646
+ else {
647
+ nextLoaded = loadConfig(currentLoaded.path, env);
648
+ lastConfigText = undefined;
649
+ }
650
+ }
651
+ catch (error) {
652
+ const message = error instanceof Error ? error.message : "重载配置文件失败。";
653
+ throw new GatewayValidationError(message);
654
+ }
655
+ const readiness = checkGatewayReadiness(nextLoaded);
656
+ if (!readiness.ready) {
657
+ throw new GatewayValidationError(`磁盘配置未就绪(${readiness.reason}),已保留当前运行配置。`);
658
+ }
659
+ configPersisted = true;
660
+ persistWarning = undefined;
661
+ service.setLoaded(nextLoaded);
662
+ return {
663
+ reloaded: true,
664
+ overview: buildOverview(),
665
+ };
666
+ });
667
+ },
668
+ };
669
+ }
670
+ //# sourceMappingURL=admin.js.map