@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/README.md +34 -17
- package/docs/README.zh-CN.md +34 -17
- package/lib/client.inner.cjs +942 -299
- package/lib/client.inner.cjs.map +1 -1
- package/lib/client.js +942 -299
- package/lib/index.js +158 -29
- package/lib/index.js.map +1 -1
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +13 -6
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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: "
|
|
652
|
+
label: "网页读取",
|
|
535
653
|
billing: "none",
|
|
536
|
-
description: "
|
|
537
|
-
}, (options) =>
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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
|
-
|
|
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
|
-
|
|
893
|
-
|
|
894
|
-
|
|
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;
|