@klarkxy/dsh-web-search-manager 0.1.8 → 0.1.10

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/lib/index.js CHANGED
@@ -4,8 +4,129 @@ import { WebError } from "@deepseek-ai/dsh-web";
4
4
  import { DEEPSEEK_DEFAULT_API_VERSION, DEEPSEEK_DEFAULT_BASE_URL, DEEPSEEK_DEFAULT_MAX_TOKENS, DEEPSEEK_DEFAULT_MAX_USES, DEEPSEEK_DEFAULT_MODEL, DeepSeekSearchProvider } from "@deepseek-ai/dsh-web-search-deepseek";
5
5
  import { ExaSearchProvider } from "@deepseek-ai/dsh-web-search-exa";
6
6
  import { HttpFetchProvider } from "@deepseek-ai/dsh-web-fetch-http";
7
+ import { isIP } from "node:net";
7
8
  import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
8
9
  import { z } from "zod";
10
+ //#region src/jina-fetch.ts
11
+ /** Do not send obvious local addresses, credentials, or nested Reader URLs to a third party. */
12
+ function readerUrl(value) {
13
+ try {
14
+ const url = new URL(value);
15
+ const host = url.hostname.replace(/^\[|\]$/g, "").replace(/\.$/, "").toLowerCase();
16
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || isIP(host) || !host.includes(".") || host === "r.jina.ai" || /\.(?:localhost|local|internal|lan|home|test|invalid)$/.test(host)) return void 0;
17
+ return `https://r.jina.ai/${url.href}`;
18
+ } catch {
19
+ return;
20
+ }
21
+ }
22
+ function object(value) {
23
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
24
+ }
25
+ /** Accept Reader's text envelope or negotiated JSON, but not error/empty responses. */
26
+ function readerContent(result) {
27
+ if (result.statusCode < 200 || result.statusCode >= 300 || result.body.kind !== "text") return void 0;
28
+ const content = result.body.content;
29
+ if (content.trimStart().startsWith("{")) try {
30
+ const envelope = object(JSON.parse(content));
31
+ if (!envelope || envelope.code !== void 0 && envelope.code !== 200) return void 0;
32
+ const page = object(envelope.data) ?? envelope;
33
+ return typeof page.content === "string" && page.content.trim() && !page.warning ? page.content : void 0;
34
+ } catch {
35
+ return;
36
+ }
37
+ const marker = /^Markdown Content:[\t ]*\r?$/m.exec(content);
38
+ if (!marker || /^Warning:/im.test(content.slice(0, marker.index))) return void 0;
39
+ return content.slice(marker.index + marker[0].length).trim() ? content : void 0;
40
+ }
41
+ /** Bound the Reader stage even if a provider fails to honour its cooperative signal. */
42
+ async function readWithTimeout(read, timeoutMs, parent) {
43
+ parent?.throwIfAborted();
44
+ const controller = new AbortController();
45
+ const signal = parent ? AbortSignal.any([parent, controller.signal]) : controller.signal;
46
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
47
+ let off = () => {};
48
+ const aborted = new Promise((_, reject) => {
49
+ const onAbort = () => reject(signal.reason);
50
+ signal.addEventListener("abort", onAbort, { once: true });
51
+ off = () => signal.removeEventListener("abort", onAbort);
52
+ });
53
+ try {
54
+ return await Promise.race([Promise.resolve().then(() => {
55
+ signal.throwIfAborted();
56
+ return read(signal);
57
+ }), aborted]);
58
+ } finally {
59
+ clearTimeout(timer);
60
+ off();
61
+ }
62
+ }
63
+ /** One managed fetch route: anonymous Jina Reader first, existing safe HTTP transport second. */
64
+ var JinaFirstFetchProvider = class {
65
+ id = "http";
66
+ options;
67
+ constructor(options) {
68
+ if (!Number.isInteger(options.readerTimeoutMs) || options.readerTimeoutMs < 1 || options.readerTimeoutMs > 2147483647) throw new Error("Invalid Reader timeout");
69
+ this.options = options;
70
+ }
71
+ available() {
72
+ return this.options.direct.available();
73
+ }
74
+ async fetch(request, signal) {
75
+ signal?.throwIfAborted();
76
+ const url = readerUrl(request.url);
77
+ if (url && this.options.reader.available()) try {
78
+ const result = await readWithTimeout((combined) => this.options.reader.fetch({ url }, combined), this.options.readerTimeoutMs, signal);
79
+ signal?.throwIfAborted();
80
+ const content = readerContent(result);
81
+ if (content !== void 0) return {
82
+ ...result,
83
+ url: request.url,
84
+ body: {
85
+ kind: "text",
86
+ content
87
+ }
88
+ };
89
+ } catch {
90
+ signal?.throwIfAborted();
91
+ }
92
+ signal?.throwIfAborted();
93
+ return this.options.direct.fetch(request, signal);
94
+ }
95
+ };
96
+ //#endregion
97
+ //#region src/provider-error.ts
98
+ /** Preserve diagnostics, removing known credential values and common authentication fields only. */
99
+ function redactDescription(text, secrets = []) {
100
+ let result = text;
101
+ for (const secret of secrets.filter(Boolean).sort((a, b) => b.length - a.length)) for (const value of /* @__PURE__ */ new Set([secret, encodeURIComponent(secret)])) result = result.split(value).join("[REDACTED]");
102
+ result = result.replace(/(https?:\/\/)[^\s/@]+@/gi, "$1[REDACTED]@").replace(/\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+/gi, "$1 [REDACTED]").replace(/([?&](?:api[_-]?key|access[_-]?token|refresh[_-]?token|token|password|secret|key)=)[^&#\s]*/gi, "$1[REDACTED]").replace(/(["']?(?:api[_-]?key|access[_-]?token|refresh[_-]?token|password|secret|authorization|cookie|set-cookie)["']?\s*[:=]\s*)(?:"[^"]*"|'[^']*'|[^\s,;}]+)/gi, "$1[REDACTED]");
103
+ return result.slice(0, 1e3);
104
+ }
105
+ var HttpStatusError = class extends WebError {
106
+ status;
107
+ constructor(status) {
108
+ const safe = Number.isInteger(status) && status >= 100 && status <= 599 ? status : 0;
109
+ super(safe ? `供应商请求失败(HTTP ${safe})。` : "供应商请求失败。", "WEB_PROVIDER_ERROR");
110
+ this.status = safe;
111
+ }
112
+ };
113
+ /** Keep upstream code and descriptions, not the error object, stack, response or headers. */
114
+ function providerError(error, secrets = []) {
115
+ const seen = /* @__PURE__ */ new Set();
116
+ const descriptions = [];
117
+ let code;
118
+ let current = error;
119
+ for (let depth = 0; depth < 3 && current && typeof current === "object" && !seen.has(current); depth++) {
120
+ seen.add(current);
121
+ const item = current;
122
+ if (!code && typeof item.code === "string" && item.code) code = item.code;
123
+ const description = typeof item.description === "string" && item.description.trim() ? item.description : item.message;
124
+ if (typeof description === "string" && description.trim() && !descriptions.includes(description)) descriptions.push(description);
125
+ current = item.cause;
126
+ }
127
+ if (typeof error === "string" && error.trim()) descriptions.push(error);
128
+ return new WebError(redactDescription(descriptions.join(";") || "供应商未提供错误描述。", secrets), redactDescription(code || "WEB_PROVIDER_ERROR", secrets));
129
+ }
9
130
  const SEARCH_ENGINE_PAGE = "https://lite.duckduckgo.com/lite/";
10
131
  const MAX_RESPONSE_BYTES$2 = 2e6;
11
132
  const USER_AGENT = "Mozilla/5.0 (compatible; dsh-editor-search/1.0)";
@@ -117,7 +238,7 @@ var SearchEngineProvider = class {
117
238
  });
118
239
  if (!response.ok) {
119
240
  await response.body?.cancel().catch(() => {});
120
- throw new WebError(`搜索页请求失败(HTTP ${response.status})。`, "WEB_PROVIDER_ERROR");
241
+ throw new HttpStatusError(response.status);
121
242
  }
122
243
  const sources = parseSearchEngineHtml(await readText(response, signal), response.url || "https://lite.duckduckgo.com/lite/");
123
244
  return {
@@ -126,8 +247,7 @@ var SearchEngineProvider = class {
126
247
  };
127
248
  } catch (error) {
128
249
  if (signal?.aborted) throw new WebError("搜索已取消。", "WEB_ABORTED");
129
- if (error instanceof WebError) throw error;
130
- throw new WebError("无法读取公开搜索页。", "WEB_PROVIDER_ERROR");
250
+ throw providerError(error);
131
251
  }
132
252
  }
133
253
  };
@@ -228,7 +348,7 @@ function createRestSearch(spec) {
228
348
  });
229
349
  if (!response.ok) {
230
350
  await response.body?.cancel().catch(() => {});
231
- throw new WebError(`搜索请求失败(HTTP ${response.status})。`, "WEB_PROVIDER_ERROR");
351
+ throw new HttpStatusError(response.status);
232
352
  }
233
353
  const sources = spec.parse(await readJSON$1(response, signal));
234
354
  return {
@@ -237,8 +357,7 @@ function createRestSearch(spec) {
237
357
  };
238
358
  } catch (error) {
239
359
  if (signal?.aborted) throw new WebError("搜索已取消。", "WEB_ABORTED");
240
- if (error instanceof WebError) throw error;
241
- throw new WebError("无法完成搜索请求。", "WEB_PROVIDER_ERROR");
360
+ throw providerError(error, [this.options.apiKey]);
242
361
  }
243
362
  }
244
363
  };
@@ -395,7 +514,7 @@ var TavilySearchProvider = class {
395
514
  });
396
515
  if (!response.ok) {
397
516
  await response.body?.cancel().catch(() => {});
398
- throw new WebError(`Tavily 请求失败(HTTP ${response.status})。`, "WEB_PROVIDER_ERROR");
517
+ throw new HttpStatusError(response.status);
399
518
  }
400
519
  const payload = record(await readJSON(response, signal));
401
520
  if (!Array.isArray(payload?.results)) throw new Error("Invalid response shape");
@@ -423,8 +542,7 @@ var TavilySearchProvider = class {
423
542
  };
424
543
  } catch (error) {
425
544
  if (signal?.aborted) throw new WebError("Tavily 搜索已取消。", "WEB_ABORTED");
426
- if (error instanceof WebError) throw error;
427
- throw new WebError("Tavily 返回无效响应或网络连接失败。", "WEB_PROVIDER_ERROR");
545
+ throw providerError(error, [this.options.apiKey]);
428
546
  }
429
547
  }
430
548
  };
@@ -440,7 +558,7 @@ function registerBuiltins(manager) {
440
558
  const offDeepSeek = manager.registerSearchProvider({
441
559
  id: "deepseek-official",
442
560
  label: "DeepSeek 搜索",
443
- description: "使用已配置的 DeepSeek API Key。可能产生额外搜索费用。",
561
+ description: "使用已配置的 DeepSeek API Key,可能产生额外搜索费用。",
444
562
  defaultBaseURL: DEEPSEEK_DEFAULT_BASE_URL,
445
563
  credentialRef: "DEEPSEEK_API_KEY",
446
564
  credentialShared: true,
@@ -531,16 +649,28 @@ function registerBuiltins(manager) {
531
649
  }));
532
650
  const offHttp = manager.registerFetchProvider({
533
651
  id: "http",
534
- label: "HTTP 读取",
652
+ label: "网页读取",
535
653
  billing: "none",
536
- description: "本机直连公开网页,无需 Key,不产生搜索费用。"
537
- }, (options) => new HttpFetchProvider({
538
- maxResponseBytes: 5e6,
539
- maxBodyChars: options.maxFetchChars,
540
- timeoutMs: options.timeoutMs,
541
- maxRedirects: 5,
542
- userAgent: "dsh-editor/managed-web"
543
- }));
654
+ description: "优先匿名 Jina Reader 读取,失败后本机 HTTP 直连;无需 Key。"
655
+ }, (options) => {
656
+ const limits = {
657
+ maxResponseBytes: 5e6,
658
+ maxBodyChars: options.maxFetchChars,
659
+ timeoutMs: options.timeoutMs,
660
+ maxRedirects: 5,
661
+ userAgent: "dsh-editor/managed-web"
662
+ };
663
+ const readerTimeoutMs = Math.min(15e3, Math.floor(options.timeoutMs / 2));
664
+ return new JinaFirstFetchProvider({
665
+ reader: new HttpFetchProvider({
666
+ ...limits,
667
+ maxBodyChars: limits.maxResponseBytes,
668
+ timeoutMs: readerTimeoutMs
669
+ }),
670
+ direct: new HttpFetchProvider(limits),
671
+ readerTimeoutMs
672
+ });
673
+ });
544
674
  return () => {
545
675
  offHttp();
546
676
  offTavily();
@@ -859,16 +989,17 @@ var WebSearchManager = class {
859
989
  }));
860
990
  }
861
991
  async execute(entry, signal, run) {
862
- if (!this.selected(entry)) fail("WEB_DISABLED", "请在设置 → 网络搜索中配置凭据并主动启用。");
992
+ if (!this.selected(entry)) fail("WEB_DISABLED", "请在设置 → 网络搜索中配置凭据并启用。");
863
993
  const controller = new AbortController();
864
994
  const timeout = setTimeout(() => controller.abort(), this.settings.timeoutMs);
865
995
  const combined = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
866
996
  const epoch = this.epoch;
867
997
  entry.active.add(controller);
868
998
  let started = false;
999
+ let apiKey;
869
1000
  try {
870
1001
  const ref = entry.descriptor.credentialRef;
871
- const apiKey = ref ? await abortable(() => this.options.resolveCredential(ref), combined) : void 0;
1002
+ apiKey = ref ? await abortable(() => this.options.resolveCredential(ref), combined) : void 0;
872
1003
  if (ref && !apiKey?.trim()) {
873
1004
  entry.configured = false;
874
1005
  this.abort(entry);
@@ -889,11 +1020,9 @@ var WebSearchManager = class {
889
1020
  return result;
890
1021
  } catch (error) {
891
1022
  if (started) entry.failures += 1;
892
- const code = error instanceof WebError ? error.code : void 0;
893
- if (code === "WEB_CREDENTIAL_MISSING") throw new WebError("搜索凭据不可用,请重新配置。", "WEB_CREDENTIAL_MISSING");
894
- if (code === "WEB_DISABLED") throw new WebError("网络搜索未启用或设置已改变。", "WEB_DISABLED");
895
- if (combined.aborted || code === "WEB_ABORTED") throw new WebError("网络请求已取消或超时。", "WEB_ABORTED");
896
- throw new WebError("网络供应商请求失败,请检查凭据、额度和连接。未切换到其他供应商。", "WEB_PROVIDER_ERROR");
1023
+ if (combined.aborted && !(error instanceof WebError && error.code === "WEB_CREDENTIAL_MISSING")) throw new WebError("网络请求已取消或超时。", "WEB_ABORTED");
1024
+ const safe = providerError(error, apiKey ? [apiKey] : []);
1025
+ throw new WebError(`[${entry.kind}:${entry.descriptor.id}] ${safe.message}`, safe.code);
897
1026
  } finally {
898
1027
  clearTimeout(timeout);
899
1028
  entry.active.delete(controller);
@@ -923,7 +1052,7 @@ var WebSearchManager = class {
923
1052
  await this.refresh();
924
1053
  proposed.searchOrder = this.searchOrder(proposed);
925
1054
  proposed.searchProvider = this.activeSearchId(proposed);
926
- if (proposed.searchEnabled && !proposed.searchProvider) fail("WEB_CREDENTIAL_MISSING", "请先选择已安装的供应商并填写有效凭据。");
1055
+ if (proposed.searchEnabled && !proposed.searchProvider) fail("WEB_CREDENTIAL_MISSING", "需选择已安装的供应商并填写有效凭据。");
927
1056
  if (proposed.searchEnabled && this.entries.get(providerKey("fetch", "http"))) {
928
1057
  proposed.fetchEnabled = true;
929
1058
  proposed.fetchProvider = "http";
@@ -932,7 +1061,7 @@ var WebSearchManager = class {
932
1061
  if (proposed.fetchEnabled) {
933
1062
  const entry = this.entries.get(providerKey("fetch", proposed.fetchProvider));
934
1063
  if (!entry) proposed.fetchEnabled = false;
935
- else if (!entry.configured) fail("WEB_CREDENTIAL_MISSING", "请先选择已安装的供应商并填写有效凭据。");
1064
+ else if (!entry.configured) fail("WEB_CREDENTIAL_MISSING", "需选择已安装的供应商并填写有效凭据。");
936
1065
  }
937
1066
  this.suspended = true;
938
1067
  this.epoch += 1;
@@ -943,7 +1072,7 @@ var WebSearchManager = class {
943
1072
  } catch {
944
1073
  this.storageFailed = true;
945
1074
  this.notify();
946
- fail("WEB_CONFIG_SAVE_FAILED", "设置保存失败。为避免意外联网,本次运行已暂停网络访问。");
1075
+ fail("WEB_CONFIG_SAVE_FAILED", "保存失败,本次运行已暂停网络访问以防意外联网。");
947
1076
  }
948
1077
  this.settings = proposed;
949
1078
  this.storageFailed = false;