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,663 @@
1
+ import { createHash, randomBytes, randomUUID, timingSafeEqual, } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+ import { createServer, } from "node:http";
4
+ import { VERSION } from "../version.js";
5
+ import { createGatewayAdminController, parseGatewayConfigUpdateRequest, } from "./admin.js";
6
+ import { CONFIG_PATH, CONFIG_RELOAD_PATH, OVERVIEW_PATH, PANEL_PATH, RECORDS_PATH, renderPanelHtml, STATS_RESET_PATH, } from "./panel.js";
7
+ import { createGatewayRecordStore, createGatewayStatsTracker, GATEWAY_RECORD_FETCH_PREVIEW_CHARS, GATEWAY_RECORD_MAX_RESULTS, GATEWAY_RECORD_SEARCH_PREVIEW_CHARS, truncatePreview, } from "./records.js";
8
+ import { GatewayValidationError, parseGatewayFetchRequest, parseGatewaySearchRequest, } from "./schema.js";
9
+ import { createGatewayService, GatewayClientAbort, GatewayHttpError, } from "./service.js";
10
+ /**
11
+ * Minimum gateway key length. Deployments should still generate a 32-byte random
12
+ * value (64 hex characters); this floor only rejects obviously weak keys.
13
+ *
14
+ * The gateway deliberately imposes no local admission or body limits: it is a
15
+ * private single-user deployment, so concurrency, body size and body read time
16
+ * are bounded only by the upstream providers and the process itself.
17
+ */
18
+ export const GATEWAY_MIN_API_KEY_LENGTH = 12;
19
+ export function normalizeGatewayApiKey(raw) {
20
+ const value = raw.replace(/\r?\n$/, "");
21
+ if (!value.trim())
22
+ throw new Error("网关 API key 为空");
23
+ if (/\s/.test(value) || hasControlCharacters(value))
24
+ throw new Error("网关 API key 不能包含空白或控制字符");
25
+ if (value.length < GATEWAY_MIN_API_KEY_LENGTH)
26
+ throw new Error(`网关 API key 至少需要 ${GATEWAY_MIN_API_KEY_LENGTH} 个字符`);
27
+ return value;
28
+ }
29
+ /** Read the gateway bearer key from a file; never from argv or the environment. */
30
+ export function readGatewayApiKeyFile(path) {
31
+ return normalizeGatewayApiKey(readFileSync(path, "utf8"));
32
+ }
33
+ function digest(value) {
34
+ return createHash("sha256").update(value, "utf8").digest();
35
+ }
36
+ function hasControlCharacters(value) {
37
+ for (const character of value) {
38
+ const code = character.codePointAt(0) ?? 0;
39
+ if (code < 0x20 || code === 0x7f)
40
+ return true;
41
+ }
42
+ return false;
43
+ }
44
+ function sendJson(response, status, body, headers = {}) {
45
+ if (response.writableEnded || response.destroyed)
46
+ return;
47
+ const payload = JSON.stringify(body);
48
+ response.writeHead(status, {
49
+ "Content-Type": "application/json; charset=utf-8",
50
+ "Cache-Control": "no-store",
51
+ "Content-Length": Buffer.byteLength(payload, "utf8").toString(),
52
+ ...headers,
53
+ });
54
+ response.end(payload);
55
+ }
56
+ function sendError(response, error, headers = {}) {
57
+ sendJson(response, error.status, {
58
+ error: {
59
+ message: error.message,
60
+ type: error.type,
61
+ param: null,
62
+ code: error.code,
63
+ },
64
+ }, {
65
+ ...(error.retryAfterSeconds !== undefined
66
+ ? { "Retry-After": String(error.retryAfterSeconds) }
67
+ : {}),
68
+ ...headers,
69
+ });
70
+ }
71
+ function sendPanel(response, nonce, retention) {
72
+ if (response.writableEnded || response.destroyed)
73
+ return;
74
+ const body = renderPanelHtml({
75
+ nonce,
76
+ retention,
77
+ minKeyLength: GATEWAY_MIN_API_KEY_LENGTH,
78
+ version: VERSION,
79
+ });
80
+ response.writeHead(200, {
81
+ "Content-Type": "text/html; charset=utf-8",
82
+ "Cache-Control": "no-store",
83
+ "Content-Length": Buffer.byteLength(body, "utf8").toString(),
84
+ "Content-Security-Policy": [
85
+ "default-src 'none'",
86
+ `script-src 'nonce-${nonce}'`,
87
+ `style-src 'nonce-${nonce}'`,
88
+ "connect-src 'self'",
89
+ "img-src 'none'",
90
+ "base-uri 'none'",
91
+ "form-action 'none'",
92
+ "frame-ancestors 'none'",
93
+ ].join("; "),
94
+ "Referrer-Policy": "no-referrer",
95
+ "X-Content-Type-Options": "nosniff",
96
+ });
97
+ response.end(body);
98
+ }
99
+ const invalidRequest = (message) => new GatewayHttpError({
100
+ status: 400,
101
+ type: "invalid_request_error",
102
+ code: "invalid_request_error",
103
+ message,
104
+ });
105
+ const unauthorized = () => new GatewayHttpError({
106
+ status: 401,
107
+ type: "authentication_error",
108
+ code: "invalid_api_key",
109
+ message: "网关 API key 缺失或无效。",
110
+ });
111
+ const methodNotAllowed = (allowed) => new GatewayHttpError({
112
+ status: 405,
113
+ type: "invalid_request_error",
114
+ code: "method_not_allowed",
115
+ message: `该路径只接受 ${allowed} 请求。`,
116
+ });
117
+ const routeNotFound = () => new GatewayHttpError({
118
+ status: 404,
119
+ type: "invalid_request_error",
120
+ code: "not_found",
121
+ message: "接口不存在。",
122
+ });
123
+ class BodyAborted extends Error {
124
+ }
125
+ /**
126
+ * Read the JSON object request body. There is intentionally no byte cap and no
127
+ * read deadline: the body is bounded only by the client and by process memory.
128
+ */
129
+ function readJsonObject(request) {
130
+ return new Promise((resolve, reject) => {
131
+ const chunks = [];
132
+ let settled = false;
133
+ const finish = (error, value) => {
134
+ if (settled)
135
+ return;
136
+ settled = true;
137
+ request.off("data", onData);
138
+ request.off("end", onEnd);
139
+ request.off("error", onError);
140
+ request.off("aborted", onAborted);
141
+ if (error)
142
+ reject(error);
143
+ else
144
+ resolve(value);
145
+ };
146
+ const onData = (chunk) => {
147
+ chunks.push(chunk);
148
+ };
149
+ const onEnd = () => {
150
+ const text = Buffer.concat(chunks).toString("utf8");
151
+ if (!text.trim()) {
152
+ finish(invalidRequest("请求体不能为空。"));
153
+ return;
154
+ }
155
+ let parsed;
156
+ try {
157
+ parsed = JSON.parse(text);
158
+ }
159
+ catch {
160
+ finish(invalidRequest("请求体不是合法的 JSON。"));
161
+ return;
162
+ }
163
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
164
+ finish(invalidRequest("请求体必须是 JSON 对象。"));
165
+ return;
166
+ }
167
+ finish(undefined, parsed);
168
+ };
169
+ const onError = () => finish(new BodyAborted());
170
+ const onAborted = () => finish(new BodyAborted());
171
+ request.on("data", onData);
172
+ request.on("end", onEnd);
173
+ request.on("error", onError);
174
+ request.on("aborted", onAborted);
175
+ });
176
+ }
177
+ function contentTypeIsJson(header) {
178
+ if (!header)
179
+ return false;
180
+ const [mediaType, ...parameters] = header.split(";");
181
+ if (mediaType?.trim().toLowerCase() !== "application/json")
182
+ return false;
183
+ for (const parameter of parameters) {
184
+ const name = parameter.split("=", 1)[0]?.trim().toLowerCase();
185
+ if (name !== "charset")
186
+ continue;
187
+ const value = parameter
188
+ .slice(parameter.indexOf("=") + 1)
189
+ .trim()
190
+ .replace(/^"|"$/g, "")
191
+ .toLowerCase();
192
+ if (value !== "utf-8" && value !== "utf8")
193
+ return false;
194
+ }
195
+ return true;
196
+ }
197
+ /**
198
+ * Exactly one `Authorization: Bearer <key>` value is required. Duplicate header
199
+ * lines are visible through `headersDistinct` and are rejected without touching
200
+ * providers; `request.headers.authorization` silently keeps the first value.
201
+ */
202
+ function authorizationKey(request) {
203
+ const distinct = request.headersDistinct.authorization;
204
+ if (distinct?.length !== 1)
205
+ return "";
206
+ const header = distinct[0];
207
+ if (typeof header !== "string")
208
+ return "";
209
+ if (!header.startsWith("Bearer "))
210
+ return "";
211
+ const value = header.slice("Bearer ".length);
212
+ if (!value || value !== value.trim())
213
+ return "";
214
+ if (hasControlCharacters(value))
215
+ return "";
216
+ return value;
217
+ }
218
+ export function createGatewayServer(options) {
219
+ const service = createGatewayService({
220
+ loaded: options.loaded,
221
+ transport: options.transport,
222
+ now: options.now,
223
+ limits: options.limits,
224
+ });
225
+ const expected = digest(options.apiKey);
226
+ const records = createGatewayRecordStore(options.recordLimit);
227
+ const stats = createGatewayStatsTracker();
228
+ const admin = createGatewayAdminController({
229
+ service,
230
+ stats,
231
+ retention: records.retention,
232
+ });
233
+ const log = options.log ??
234
+ ((event) => process.stderr.write(`${JSON.stringify(event)}\n`));
235
+ let shuttingDown = false;
236
+ const active = new Set();
237
+ const authenticate = (request) => {
238
+ const providedKey = authorizationKey(request);
239
+ return (providedKey.length > 0 && timingSafeEqual(digest(providedKey), expected));
240
+ };
241
+ const handle = async (request, response, route) => {
242
+ const started = performance.now();
243
+ const rawRequestId = request.headers["x-request-id"];
244
+ const requestId = typeof rawRequestId === "string" &&
245
+ /^[A-Za-z0-9._-]{1,64}$/.test(rawRequestId)
246
+ ? rawRequestId
247
+ : randomUUID();
248
+ let status = 500;
249
+ let code;
250
+ let authenticated = false;
251
+ let clientAborted = false;
252
+ const summary = {};
253
+ const clientAbort = new AbortController();
254
+ response.once("close", () => {
255
+ if (!response.writableEnded)
256
+ clientAbort.abort();
257
+ });
258
+ try {
259
+ if (shuttingDown)
260
+ throw new GatewayHttpError({
261
+ status: 503,
262
+ type: "server_error",
263
+ code: "shutting_down",
264
+ message: "网关正在停止。",
265
+ });
266
+ if (!authenticate(request))
267
+ throw unauthorized();
268
+ authenticated = true;
269
+ if (!contentTypeIsJson(request.headers["content-type"]))
270
+ throw invalidRequest("Content-Type 必须是 application/json。");
271
+ if (request.headers["content-encoding"] !== undefined)
272
+ throw invalidRequest("不支持压缩请求体。");
273
+ const raw = await readJsonObject(request);
274
+ const parsed = route === "search"
275
+ ? parseGatewaySearchRequest(raw)
276
+ : parseGatewayFetchRequest(raw);
277
+ if (route === "search") {
278
+ const search = parsed;
279
+ summary.query = search.query;
280
+ summary.count = search.count;
281
+ summary.needContent = search.need_content ?? false;
282
+ }
283
+ else {
284
+ summary.url = parsed.url;
285
+ }
286
+ const observer = {
287
+ onSuccess: (info) => {
288
+ summary.provider = info.provider;
289
+ if (info.trace.attempts.length > 0) {
290
+ summary.attempts = info.trace.attempts;
291
+ }
292
+ if (info.trace.contentBackfill) {
293
+ summary.contentBackfill = info.trace.contentBackfill;
294
+ }
295
+ },
296
+ };
297
+ const body = route === "search"
298
+ ? await service.search(parsed, clientAbort.signal, observer)
299
+ : await service.fetch(parsed, clientAbort.signal, observer);
300
+ if (clientAbort.signal.aborted || response.writableEnded) {
301
+ clientAborted = true;
302
+ return;
303
+ }
304
+ if (route === "search") {
305
+ const searchBody = body;
306
+ summary.resultCount = searchBody.results.length;
307
+ summary.results = searchBody.results
308
+ .slice(0, GATEWAY_RECORD_MAX_RESULTS)
309
+ .map((result, idx) => {
310
+ const preview = truncatePreview(result.content, GATEWAY_RECORD_SEARCH_PREVIEW_CHARS);
311
+ return {
312
+ rank: idx + 1,
313
+ title: result.title,
314
+ url: result.url,
315
+ ...(result.snippet ? { snippet: result.snippet } : {}),
316
+ ...(preview
317
+ ? {
318
+ contentPreview: preview.preview,
319
+ contentChars: preview.chars,
320
+ contentTruncated: preview.truncated,
321
+ }
322
+ : {}),
323
+ };
324
+ });
325
+ }
326
+ else {
327
+ const fetchBody = body;
328
+ summary.title = fetchBody.title;
329
+ const preview = truncatePreview(fetchBody.content, GATEWAY_RECORD_FETCH_PREVIEW_CHARS);
330
+ if (preview !== undefined) {
331
+ summary.contentPreview = preview.preview;
332
+ summary.contentChars = preview.chars;
333
+ summary.contentTruncated = preview.truncated;
334
+ }
335
+ }
336
+ status = 200;
337
+ sendJson(response, 200, body);
338
+ }
339
+ catch (error) {
340
+ if (error instanceof GatewayClientAbort || clientAbort.signal.aborted) {
341
+ clientAborted = true;
342
+ return;
343
+ }
344
+ const mapped = error instanceof GatewayValidationError
345
+ ? invalidRequest(error.message)
346
+ : error instanceof BodyAborted
347
+ ? invalidRequest("请求读取中断。")
348
+ : error instanceof GatewayHttpError
349
+ ? error
350
+ : new GatewayHttpError({
351
+ status: 500,
352
+ type: "server_error",
353
+ code: "internal_error",
354
+ message: "网关内部错误。",
355
+ });
356
+ status = mapped.status;
357
+ code = mapped.code;
358
+ summary.provider = summary.provider ?? mapped.provider;
359
+ if (mapped.trace) {
360
+ if (!summary.attempts && mapped.trace.attempts.length > 0) {
361
+ summary.attempts = mapped.trace.attempts;
362
+ }
363
+ if (!summary.contentBackfill && mapped.trace.contentBackfill) {
364
+ summary.contentBackfill = mapped.trace.contentBackfill;
365
+ }
366
+ }
367
+ // A body read failure leaves the socket unusable for a follow-up request.
368
+ const closeConnection = error instanceof BodyAborted;
369
+ sendError(response, mapped, closeConnection ? { Connection: "close" } : {});
370
+ if (closeConnection)
371
+ request.destroy();
372
+ }
373
+ finally {
374
+ const durationMs = Math.max(0, Math.round(performance.now() - started));
375
+ log({
376
+ method: request.method ?? "",
377
+ route,
378
+ status,
379
+ durationMs,
380
+ requestId,
381
+ ...(code ? { code } : {}),
382
+ });
383
+ // The panel is an observability surface, not another log sink: only
384
+ // authenticated tool requests are retained, so unauthenticated probes
385
+ // cannot flush the ring buffer and failed keys stay in stderr logs.
386
+ if (authenticated && !clientAborted) {
387
+ const now = Date.now();
388
+ const record = {
389
+ id: requestId,
390
+ at: now,
391
+ route,
392
+ status,
393
+ durationMs,
394
+ ...(code ? { code } : {}),
395
+ ...summary,
396
+ };
397
+ records.add(record);
398
+ stats.recordRequest({
399
+ at: now,
400
+ route,
401
+ ok: status >= 200 && status < 300,
402
+ attempts: record.attempts,
403
+ contentBackfill: record.contentBackfill,
404
+ });
405
+ }
406
+ }
407
+ };
408
+ const handleAdminConfigUpdate = async (request, response) => {
409
+ try {
410
+ if (shuttingDown) {
411
+ throw new GatewayHttpError({
412
+ status: 503,
413
+ type: "server_error",
414
+ code: "shutting_down",
415
+ message: "网关正在停止。",
416
+ });
417
+ }
418
+ if (!authenticate(request))
419
+ throw unauthorized();
420
+ if (!contentTypeIsJson(request.headers["content-type"])) {
421
+ throw invalidRequest("Content-Type 必须是 application/json。");
422
+ }
423
+ if (request.headers["content-encoding"] !== undefined) {
424
+ throw invalidRequest("不支持压缩请求体。");
425
+ }
426
+ const raw = await readJsonObject(request);
427
+ const patch = parseGatewayConfigUpdateRequest(raw);
428
+ const result = await admin.updateConfig(patch);
429
+ sendJson(response, 200, result);
430
+ }
431
+ catch (error) {
432
+ const mapped = error instanceof GatewayValidationError
433
+ ? invalidRequest(error.message)
434
+ : error instanceof BodyAborted
435
+ ? invalidRequest("请求读取中断。")
436
+ : error instanceof GatewayHttpError
437
+ ? error
438
+ : new GatewayHttpError({
439
+ status: 500,
440
+ type: "server_error",
441
+ code: "internal_error",
442
+ message: "网关内部错误。",
443
+ });
444
+ const closeConnection = error instanceof BodyAborted;
445
+ sendError(response, mapped, closeConnection ? { Connection: "close" } : {});
446
+ if (closeConnection)
447
+ request.destroy();
448
+ }
449
+ };
450
+ const handleAdminConfigReload = async (request, response) => {
451
+ try {
452
+ if (shuttingDown) {
453
+ throw new GatewayHttpError({
454
+ status: 503,
455
+ type: "server_error",
456
+ code: "shutting_down",
457
+ message: "网关正在停止。",
458
+ });
459
+ }
460
+ if (!authenticate(request))
461
+ throw unauthorized();
462
+ const result = await admin.reloadConfig();
463
+ sendJson(response, 200, result);
464
+ }
465
+ catch (error) {
466
+ const mapped = error instanceof GatewayValidationError
467
+ ? invalidRequest(error.message)
468
+ : error instanceof GatewayHttpError
469
+ ? error
470
+ : new GatewayHttpError({
471
+ status: 500,
472
+ type: "server_error",
473
+ code: "internal_error",
474
+ message: "网关内部错误。",
475
+ });
476
+ sendError(response, mapped);
477
+ }
478
+ };
479
+ const server = createServer((request, response) => {
480
+ active.add(response);
481
+ response.once("close", () => active.delete(response));
482
+ const url = request.url ?? "/";
483
+ const path = url.split("?", 1)[0] ?? "/";
484
+ if (path === "/healthz" || path === "/readyz") {
485
+ if (request.method !== "GET") {
486
+ sendError(response, methodNotAllowed("GET"), { Allow: "GET" });
487
+ return;
488
+ }
489
+ if (path === "/healthz") {
490
+ sendJson(response, 200, { status: "ok" });
491
+ return;
492
+ }
493
+ if (shuttingDown) {
494
+ sendJson(response, 503, { status: "shutting_down" });
495
+ return;
496
+ }
497
+ const readiness = service.readiness();
498
+ sendJson(response, readiness.ready ? 200 : 503, readiness.ready
499
+ ? { status: "ready" }
500
+ : { status: "not_ready", reason: readiness.reason });
501
+ return;
502
+ }
503
+ if (path === "/" || path === PANEL_PATH) {
504
+ if (request.method !== "GET") {
505
+ sendError(response, methodNotAllowed("GET"), { Allow: "GET" });
506
+ return;
507
+ }
508
+ sendPanel(response, randomBytes(16).toString("base64url"), records.retention);
509
+ return;
510
+ }
511
+ if (path === OVERVIEW_PATH) {
512
+ if (request.method !== "GET") {
513
+ sendError(response, methodNotAllowed("GET"), { Allow: "GET" });
514
+ return;
515
+ }
516
+ if (!authenticate(request)) {
517
+ sendError(response, unauthorized());
518
+ return;
519
+ }
520
+ sendJson(response, 200, admin.getOverview());
521
+ return;
522
+ }
523
+ if (path === RECORDS_PATH) {
524
+ if (request.method !== "GET" && request.method !== "DELETE") {
525
+ sendError(response, methodNotAllowed("GET, DELETE"), {
526
+ Allow: "GET, DELETE",
527
+ });
528
+ return;
529
+ }
530
+ if (!authenticate(request)) {
531
+ sendError(response, unauthorized());
532
+ return;
533
+ }
534
+ if (request.method === "DELETE") {
535
+ const cleared = records.clear();
536
+ sendJson(response, 200, {
537
+ cleared,
538
+ retention: records.retention,
539
+ generatedAt: Date.now(),
540
+ });
541
+ return;
542
+ }
543
+ const queryIndex = url.indexOf("?");
544
+ const params = new URLSearchParams(queryIndex === -1 ? "" : url.slice(queryIndex + 1));
545
+ const rawLimit = params.get("limit");
546
+ let limit = records.retention;
547
+ if (rawLimit !== null) {
548
+ if (!/^[1-9]\d{0,3}$/.test(rawLimit) ||
549
+ Number(rawLimit) > records.retention) {
550
+ sendError(response, invalidRequest(`limit 必须是 1–${records.retention} 的整数。`));
551
+ return;
552
+ }
553
+ limit = Number(rawLimit);
554
+ }
555
+ const rawRoute = params.get("route");
556
+ if (rawRoute !== null &&
557
+ rawRoute !== "" &&
558
+ rawRoute !== "search" &&
559
+ rawRoute !== "fetch") {
560
+ sendError(response, invalidRequest("route 必须是 search 或 fetch。"));
561
+ return;
562
+ }
563
+ const rawStatus = params.get("status");
564
+ if (rawStatus !== null &&
565
+ rawStatus !== "" &&
566
+ rawStatus !== "ok" &&
567
+ rawStatus !== "error") {
568
+ sendError(response, invalidRequest("status 必须是 ok 或 error。"));
569
+ return;
570
+ }
571
+ const rawProvider = params.get("provider") ?? undefined;
572
+ const rawQuery = params.get("q") ?? undefined;
573
+ const list = records.list({
574
+ limit,
575
+ ...(rawRoute === "search" || rawRoute === "fetch"
576
+ ? { route: rawRoute }
577
+ : {}),
578
+ ...(rawStatus === "ok" || rawStatus === "error"
579
+ ? { status: rawStatus }
580
+ : {}),
581
+ ...(rawProvider ? { provider: rawProvider } : {}),
582
+ ...(rawQuery ? { q: rawQuery } : {}),
583
+ });
584
+ sendJson(response, 200, {
585
+ records: list,
586
+ count: list.length,
587
+ retention: records.retention,
588
+ generatedAt: Date.now(),
589
+ });
590
+ return;
591
+ }
592
+ if (path === STATS_RESET_PATH) {
593
+ if (request.method !== "POST") {
594
+ sendError(response, methodNotAllowed("POST"), { Allow: "POST" });
595
+ return;
596
+ }
597
+ if (!authenticate(request)) {
598
+ sendError(response, unauthorized());
599
+ return;
600
+ }
601
+ stats.reset();
602
+ sendJson(response, 200, {
603
+ reset: true,
604
+ stats: stats.snapshot(),
605
+ generatedAt: Date.now(),
606
+ });
607
+ return;
608
+ }
609
+ if (path === CONFIG_PATH) {
610
+ if (request.method !== "PUT") {
611
+ sendError(response, methodNotAllowed("PUT"), { Allow: "PUT" });
612
+ return;
613
+ }
614
+ void handleAdminConfigUpdate(request, response);
615
+ return;
616
+ }
617
+ if (path === CONFIG_RELOAD_PATH) {
618
+ if (request.method !== "POST") {
619
+ sendError(response, methodNotAllowed("POST"), { Allow: "POST" });
620
+ return;
621
+ }
622
+ void handleAdminConfigReload(request, response);
623
+ return;
624
+ }
625
+ if (path === "/v1/tools/search" || path === "/v1/tools/fetch") {
626
+ if (request.method !== "POST") {
627
+ sendError(response, methodNotAllowed("POST"), { Allow: "POST" });
628
+ return;
629
+ }
630
+ void handle(request, response, path === "/v1/tools/search" ? "search" : "fetch");
631
+ return;
632
+ }
633
+ sendError(response, routeNotFound());
634
+ });
635
+ return {
636
+ server,
637
+ service,
638
+ admin,
639
+ async shutdown(timeoutMs) {
640
+ shuttingDown = true;
641
+ const drained = new Promise((resolveDrained) => {
642
+ const poll = () => {
643
+ if (active.size === 0)
644
+ resolveDrained();
645
+ else
646
+ setTimeout(poll, 20).unref();
647
+ };
648
+ poll();
649
+ });
650
+ let timer;
651
+ const expired = new Promise((resolveExpired) => {
652
+ timer = setTimeout(resolveExpired, Math.max(0, timeoutMs));
653
+ });
654
+ await Promise.race([drained, expired]);
655
+ if (timer)
656
+ clearTimeout(timer);
657
+ for (const response of active)
658
+ response.destroy();
659
+ await new Promise((resolveClosed) => server.close(() => resolveClosed()));
660
+ },
661
+ };
662
+ }
663
+ //# sourceMappingURL=server.js.map