@klarkxy/dsh-web-search-manager 0.1.11 → 0.2.1
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 +31 -10
- package/cordis.patch.yml +1 -8
- package/docs/README.zh-CN.md +31 -10
- package/lib/client.inner.cjs +649 -104
- package/lib/client.inner.cjs.map +1 -1
- package/lib/client.js +650 -266
- package/lib/contracts.d.ts +15 -20
- package/lib/contracts.js +13 -1
- package/lib/contracts.js.map +1 -1
- package/lib/index.d.ts +19 -3
- package/lib/index.js +506 -389
- package/lib/index.js.map +1 -1
- package/package.json +23 -22
package/lib/index.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { WEB_SEARCH_RPC_CHANNEL, defaultSettings, migrateSearchOrder, pickActiveSearch, providerKey, resolveSearchOrder, validateBaseURL } from "./contracts.js";
|
|
1
|
+
import { WEB_MANAGER_OWNER, WEB_SEARCH_RPC_CHANNEL, defaultSettings, migrateSearchOrder, pickActiveSearch, providerKey, resolveSearchOrder, safeConfigurationUrl, validateBaseURL } from "./contracts.js";
|
|
2
2
|
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
3
3
|
import { WebError } from "@deepseek-ai/dsh-web";
|
|
4
|
+
import { WEB_PROVIDER_AVAILABILITY_EVENT } from "@klarkxy/dsh-plugin-kit/contracts";
|
|
4
5
|
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
6
|
import { ExaSearchProvider } from "@deepseek-ai/dsh-web-search-exa";
|
|
6
7
|
import { HttpFetchProvider } from "@deepseek-ai/dsh-web-fetch-http";
|
|
7
8
|
import { isIP } from "node:net";
|
|
9
|
+
import { registerHostRpc } from "@klarkxy/dsh-plugin-kit/host-rpc";
|
|
8
10
|
import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
|
|
9
11
|
import { z } from "zod";
|
|
10
12
|
//#region src/jina-fetch.ts
|
|
@@ -128,7 +130,7 @@ function providerError(error, secrets = []) {
|
|
|
128
130
|
return new WebError(redactDescription(descriptions.join(";") || "供应商未提供错误描述。", secrets), redactDescription(code || "WEB_PROVIDER_ERROR", secrets));
|
|
129
131
|
}
|
|
130
132
|
const SEARCH_ENGINE_PAGE = "https://lite.duckduckgo.com/lite/";
|
|
131
|
-
const MAX_RESPONSE_BYTES$
|
|
133
|
+
const MAX_RESPONSE_BYTES$1 = 2e6;
|
|
132
134
|
const USER_AGENT = "Mozilla/5.0 (compatible; dsh-editor-search/1.0)";
|
|
133
135
|
function searchEngineUrl(query, page = SEARCH_ENGINE_PAGE) {
|
|
134
136
|
const url = new URL(page);
|
|
@@ -195,7 +197,7 @@ async function readText(response, signal) {
|
|
|
195
197
|
const { value, done } = await reader.read();
|
|
196
198
|
if (done) break;
|
|
197
199
|
size += value.length;
|
|
198
|
-
if (size > MAX_RESPONSE_BYTES$
|
|
200
|
+
if (size > MAX_RESPONSE_BYTES$1) throw new Error("Response too large");
|
|
199
201
|
chunks.push(value);
|
|
200
202
|
}
|
|
201
203
|
} finally {
|
|
@@ -251,10 +253,10 @@ var SearchEngineProvider = class {
|
|
|
251
253
|
}
|
|
252
254
|
}
|
|
253
255
|
};
|
|
254
|
-
function record
|
|
256
|
+
function record(value) {
|
|
255
257
|
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
256
258
|
}
|
|
257
|
-
async function readJSON
|
|
259
|
+
async function readJSON(response, signal) {
|
|
258
260
|
const reader = response.body?.getReader();
|
|
259
261
|
if (!reader) throw new Error("Missing response");
|
|
260
262
|
let size = 0;
|
|
@@ -286,7 +288,7 @@ function httpsEndpoint(base, path = "") {
|
|
|
286
288
|
return `${url.href.replace(/\/+$/, "")}${path}`;
|
|
287
289
|
}
|
|
288
290
|
function sourceFrom(raw, urlKey, titleKey, snippetKey) {
|
|
289
|
-
const row = record
|
|
291
|
+
const row = record(raw);
|
|
290
292
|
if (typeof row?.[urlKey] !== "string") return;
|
|
291
293
|
let url;
|
|
292
294
|
try {
|
|
@@ -350,7 +352,7 @@ function createRestSearch(spec) {
|
|
|
350
352
|
await response.body?.cancel().catch(() => {});
|
|
351
353
|
throw new HttpStatusError(response.status);
|
|
352
354
|
}
|
|
353
|
-
const sources = spec.parse(await readJSON
|
|
355
|
+
const sources = spec.parse(await readJSON(response, signal));
|
|
354
356
|
return {
|
|
355
357
|
sources: sources.slice(0, count),
|
|
356
358
|
truncated: sources.length > count
|
|
@@ -373,7 +375,7 @@ const BraveSearchProvider = createRestSearch({
|
|
|
373
375
|
count: String(count)
|
|
374
376
|
}),
|
|
375
377
|
parse(payload) {
|
|
376
|
-
const results = record
|
|
378
|
+
const results = record(record(payload)?.web)?.results;
|
|
377
379
|
return Array.isArray(results) ? results.flatMap((row) => sourceFrom(row, "url", "title", "description") ?? []) : [];
|
|
378
380
|
}
|
|
379
381
|
});
|
|
@@ -393,7 +395,7 @@ const BochaSearchProvider = createRestSearch({
|
|
|
393
395
|
summary: false
|
|
394
396
|
}),
|
|
395
397
|
parse(payload) {
|
|
396
|
-
const pages = record
|
|
398
|
+
const pages = record(record(record(payload)?.data)?.webPages)?.value ?? record(record(payload)?.webPages)?.value;
|
|
397
399
|
return Array.isArray(pages) ? pages.flatMap((row) => sourceFrom(row, "url", "name", "snippet") ?? []) : [];
|
|
398
400
|
}
|
|
399
401
|
});
|
|
@@ -411,7 +413,7 @@ const SerperSearchProvider = createRestSearch({
|
|
|
411
413
|
num: count
|
|
412
414
|
}),
|
|
413
415
|
parse(payload) {
|
|
414
|
-
const organic = record
|
|
416
|
+
const organic = record(payload)?.organic;
|
|
415
417
|
return Array.isArray(organic) ? organic.flatMap((row) => sourceFrom(row, "link", "title", "snippet") ?? []) : [];
|
|
416
418
|
}
|
|
417
419
|
});
|
|
@@ -429,47 +431,12 @@ const FirecrawlSearchProvider = createRestSearch({
|
|
|
429
431
|
limit: count
|
|
430
432
|
}),
|
|
431
433
|
parse(payload) {
|
|
432
|
-
const data = record
|
|
434
|
+
const data = record(payload)?.data;
|
|
433
435
|
return Array.isArray(data) ? data.flatMap((row) => sourceFrom(row, "url", "title", "description") ?? []) : [];
|
|
434
436
|
}
|
|
435
437
|
});
|
|
436
438
|
//#endregion
|
|
437
439
|
//#region src/tavily.ts
|
|
438
|
-
const MAX_RESPONSE_BYTES = 2e6;
|
|
439
|
-
function endpoint(base) {
|
|
440
|
-
const url = new URL(base);
|
|
441
|
-
if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash) throw new Error("Invalid endpoint");
|
|
442
|
-
return `${url.href.replace(/\/+$/, "")}/search`;
|
|
443
|
-
}
|
|
444
|
-
function record(value) {
|
|
445
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
446
|
-
}
|
|
447
|
-
async function readJSON(response, signal) {
|
|
448
|
-
const reader = response.body?.getReader();
|
|
449
|
-
if (!reader) throw new Error("Missing response");
|
|
450
|
-
let size = 0;
|
|
451
|
-
const chunks = [];
|
|
452
|
-
try {
|
|
453
|
-
while (true) {
|
|
454
|
-
signal?.throwIfAborted();
|
|
455
|
-
const { value, done } = await reader.read();
|
|
456
|
-
if (done) break;
|
|
457
|
-
size += value.length;
|
|
458
|
-
if (size > MAX_RESPONSE_BYTES) throw new Error("Response too large");
|
|
459
|
-
chunks.push(value);
|
|
460
|
-
}
|
|
461
|
-
const bytes = new Uint8Array(size);
|
|
462
|
-
let offset = 0;
|
|
463
|
-
for (const chunk of chunks) {
|
|
464
|
-
bytes.set(chunk, offset);
|
|
465
|
-
offset += chunk.length;
|
|
466
|
-
}
|
|
467
|
-
return JSON.parse(new TextDecoder().decode(bytes));
|
|
468
|
-
} finally {
|
|
469
|
-
await reader.cancel().catch(() => {});
|
|
470
|
-
reader.releaseLock();
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
440
|
/** Direct REST adapter. No auxiliary LLM, retries, anonymous fallback or auto-depth. */
|
|
474
441
|
var TavilySearchProvider = class {
|
|
475
442
|
options;
|
|
@@ -479,7 +446,7 @@ var TavilySearchProvider = class {
|
|
|
479
446
|
}
|
|
480
447
|
available() {
|
|
481
448
|
try {
|
|
482
|
-
|
|
449
|
+
httpsEndpoint(this.options.baseURL ?? "https://api.tavily.com", "/search");
|
|
483
450
|
return Boolean(this.options.apiKey.trim());
|
|
484
451
|
} catch {
|
|
485
452
|
return false;
|
|
@@ -492,7 +459,7 @@ var TavilySearchProvider = class {
|
|
|
492
459
|
if (!Number.isInteger(count) || count < 1 || count > 20) throw new WebError("结果数须为 1 至 20。", "WEB_INVALID_REQUEST");
|
|
493
460
|
try {
|
|
494
461
|
signal?.throwIfAborted();
|
|
495
|
-
const response = await (this.options.fetch ?? fetch)(
|
|
462
|
+
const response = await (this.options.fetch ?? fetch)(httpsEndpoint(this.options.baseURL ?? "https://api.tavily.com", "/search"), {
|
|
496
463
|
method: "POST",
|
|
497
464
|
redirect: "error",
|
|
498
465
|
signal,
|
|
@@ -521,18 +488,12 @@ var TavilySearchProvider = class {
|
|
|
521
488
|
const sources = [];
|
|
522
489
|
for (const raw of payload.results) {
|
|
523
490
|
const row = record(raw);
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
try {
|
|
527
|
-
url = new URL(row.url);
|
|
528
|
-
} catch {
|
|
529
|
-
continue;
|
|
530
|
-
}
|
|
531
|
-
if (!["https:", "http:"].includes(url.protocol) || url.username || url.password) continue;
|
|
491
|
+
const source = sourceFrom(row, "url", "title", "content");
|
|
492
|
+
if (!source || !row) continue;
|
|
532
493
|
sources.push({
|
|
533
|
-
|
|
534
|
-
...
|
|
535
|
-
...
|
|
494
|
+
...source,
|
|
495
|
+
...row.title === "" ? { title: "" } : {},
|
|
496
|
+
...row.content === "" ? { snippet: "" } : {},
|
|
536
497
|
...typeof row.published_date === "string" && Number.isFinite(Date.parse(row.published_date)) ? { publishedAt: new Date(row.published_date).toISOString() } : {}
|
|
537
498
|
});
|
|
538
499
|
}
|
|
@@ -547,225 +508,331 @@ var TavilySearchProvider = class {
|
|
|
547
508
|
}
|
|
548
509
|
};
|
|
549
510
|
//#endregion
|
|
550
|
-
//#region src/
|
|
551
|
-
function
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
apiKey:
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
label: "Exa",
|
|
579
|
-
description: "独立 Search API。",
|
|
580
|
-
defaultBaseURL: "https://api.exa.ai",
|
|
581
|
-
credentialRef: "DSH_EDITOR_WEB_EXA_API_KEY",
|
|
582
|
-
billing: "request",
|
|
583
|
-
signupUrl: "https://dashboard.exa.ai/api-keys"
|
|
584
|
-
}, (options) => new ExaSearchProvider({
|
|
585
|
-
apiKey: options.apiKey ?? "",
|
|
586
|
-
baseURL: options.baseURL ?? "https://api.exa.ai",
|
|
587
|
-
searchType: "auto",
|
|
588
|
-
highlightsPerResult: 1
|
|
589
|
-
}));
|
|
590
|
-
const offBrave = manager.registerSearchProvider({
|
|
591
|
-
id: "brave",
|
|
592
|
-
label: "Brave",
|
|
593
|
-
description: "Brave Search API。",
|
|
594
|
-
defaultBaseURL: "https://api.search.brave.com",
|
|
595
|
-
credentialRef: "DSH_EDITOR_WEB_BRAVE_API_KEY",
|
|
596
|
-
billing: "request",
|
|
597
|
-
signupUrl: "https://api.search.brave.com"
|
|
598
|
-
}, (options) => new BraveSearchProvider({
|
|
599
|
-
apiKey: options.apiKey ?? "",
|
|
600
|
-
baseURL: options.baseURL
|
|
601
|
-
}));
|
|
602
|
-
const offBocha = manager.registerSearchProvider({
|
|
603
|
-
id: "bocha",
|
|
604
|
-
label: "博查",
|
|
605
|
-
description: "中文网页搜索 API。",
|
|
606
|
-
defaultBaseURL: "https://api.bochaai.com",
|
|
607
|
-
credentialRef: "DSH_EDITOR_WEB_BOCHA_API_KEY",
|
|
608
|
-
billing: "request",
|
|
609
|
-
signupUrl: "https://open.bochaai.com"
|
|
610
|
-
}, (options) => new BochaSearchProvider({
|
|
611
|
-
apiKey: options.apiKey ?? "",
|
|
612
|
-
baseURL: options.baseURL
|
|
613
|
-
}));
|
|
614
|
-
const offSerper = manager.registerSearchProvider({
|
|
615
|
-
id: "serper",
|
|
616
|
-
label: "Serper",
|
|
617
|
-
description: "Google 结果的 Search API。",
|
|
618
|
-
defaultBaseURL: "https://google.serper.dev",
|
|
619
|
-
credentialRef: "DSH_EDITOR_WEB_SERPER_API_KEY",
|
|
620
|
-
billing: "request",
|
|
621
|
-
signupUrl: "https://serper.dev"
|
|
622
|
-
}, (options) => new SerperSearchProvider({
|
|
623
|
-
apiKey: options.apiKey ?? "",
|
|
624
|
-
baseURL: options.baseURL
|
|
625
|
-
}));
|
|
626
|
-
const offFirecrawl = manager.registerSearchProvider({
|
|
627
|
-
id: "firecrawl",
|
|
628
|
-
label: "Firecrawl",
|
|
629
|
-
description: "网页搜索与提取 API。",
|
|
630
|
-
defaultBaseURL: "https://api.firecrawl.dev",
|
|
631
|
-
credentialRef: "DSH_EDITOR_WEB_FIRECRAWL_API_KEY",
|
|
632
|
-
billing: "request",
|
|
633
|
-
signupUrl: "https://www.firecrawl.dev"
|
|
634
|
-
}, (options) => new FirecrawlSearchProvider({
|
|
635
|
-
apiKey: options.apiKey ?? "",
|
|
636
|
-
baseURL: options.baseURL
|
|
637
|
-
}));
|
|
638
|
-
const offTavily = manager.registerSearchProvider({
|
|
639
|
-
id: "tavily",
|
|
640
|
-
label: "Tavily",
|
|
641
|
-
defaultBaseURL: "https://api.tavily.com",
|
|
642
|
-
description: "Search API;固定 basic 检索,不自动升级搜索深度。",
|
|
643
|
-
credentialRef: "DSH_EDITOR_WEB_TAVILY_API_KEY",
|
|
644
|
-
billing: "request",
|
|
645
|
-
signupUrl: "https://app.tavily.com"
|
|
646
|
-
}, (options) => new TavilySearchProvider({
|
|
647
|
-
apiKey: options.apiKey ?? "",
|
|
648
|
-
baseURL: options.baseURL
|
|
649
|
-
}));
|
|
650
|
-
const offHttp = manager.registerFetchProvider({
|
|
651
|
-
id: "http",
|
|
652
|
-
label: "网页读取",
|
|
653
|
-
billing: "none",
|
|
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
|
|
511
|
+
//#region src/native-providers.ts
|
|
512
|
+
function validate(descriptor) {
|
|
513
|
+
if (!/^[a-z][a-z0-9-]{0,63}$/.test(descriptor.id) || !descriptor.label.trim()) throw new WebError("供应商标识或名称无效。", "WEB_INVALID_PROVIDER");
|
|
514
|
+
if (descriptor.credentialRef && !/^[A-Z][A-Z0-9_]{1,127}$/.test(descriptor.credentialRef)) throw new WebError("供应商凭据引用无效。", "WEB_INVALID_PROVIDER");
|
|
515
|
+
if (descriptor.defaultBaseURL) validateBaseURL(descriptor.defaultBaseURL);
|
|
516
|
+
for (const [value, label] of [[descriptor.signupUrl, "注册地址"], [descriptor.pricingUrl, "费用说明地址"]]) {
|
|
517
|
+
if (!value) continue;
|
|
518
|
+
try {
|
|
519
|
+
const url = new URL(value);
|
|
520
|
+
if (url.protocol !== "https:" || url.username || url.password) throw new Error();
|
|
521
|
+
} catch {
|
|
522
|
+
throw new WebError(`${label}须为 HTTPS。`, "WEB_INVALID_PROVIDER");
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
async function configuredCall(kind, descriptor, configuration, signal, run) {
|
|
527
|
+
let apiKey;
|
|
528
|
+
try {
|
|
529
|
+
signal?.throwIfAborted();
|
|
530
|
+
apiKey = descriptor.credentialRef ? await configuration.resolveCredential(descriptor.credentialRef) : void 0;
|
|
531
|
+
signal?.throwIfAborted();
|
|
532
|
+
if (descriptor.credentialRef && !apiKey?.trim()) throw new WebError("搜索凭据已删除或不可用,请重新配置。", "WEB_CREDENTIAL_MISSING");
|
|
533
|
+
const settings = configuration.settings();
|
|
534
|
+
return await run({
|
|
535
|
+
apiKey,
|
|
536
|
+
baseURL: settings.endpoints[providerKey(kind, descriptor.id)] ?? descriptor.defaultBaseURL,
|
|
537
|
+
timeoutMs: settings.timeoutMs,
|
|
538
|
+
maxFetchChars: settings.maxFetchChars
|
|
672
539
|
});
|
|
540
|
+
} catch (error) {
|
|
541
|
+
throw providerError(error, apiKey ? [apiKey] : []);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
/** Bundle-owned adapters register through the same native ctx.web entry as any plugin. */
|
|
545
|
+
function registerNativeSearch(web, configuration, descriptor, factory) {
|
|
546
|
+
validate(descriptor);
|
|
547
|
+
return web.registerSearchProvider({
|
|
548
|
+
id: descriptor.id,
|
|
549
|
+
dshWebManagement: Object.freeze({
|
|
550
|
+
...descriptor,
|
|
551
|
+
configurationOwner: WEB_MANAGER_OWNER
|
|
552
|
+
}),
|
|
553
|
+
available: () => true,
|
|
554
|
+
search: (request, signal) => configuredCall("search", descriptor, configuration, signal, (options) => {
|
|
555
|
+
const provider = factory(options);
|
|
556
|
+
if (!provider.available()) throw new WebError("供应商配置不可用。", "WEB_PROVIDER_ERROR");
|
|
557
|
+
return provider.search(request, signal);
|
|
558
|
+
})
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
function registerNativeFetch(web, configuration, descriptor, factory) {
|
|
562
|
+
validate(descriptor);
|
|
563
|
+
return web.registerFetchProvider({
|
|
564
|
+
id: descriptor.id,
|
|
565
|
+
dshWebManagement: Object.freeze({
|
|
566
|
+
...descriptor,
|
|
567
|
+
configurationOwner: WEB_MANAGER_OWNER
|
|
568
|
+
}),
|
|
569
|
+
available: () => true,
|
|
570
|
+
fetch: (request, signal) => configuredCall("fetch", descriptor, configuration, signal, (options) => {
|
|
571
|
+
const provider = factory(options);
|
|
572
|
+
if (!provider.available()) throw new WebError("供应商配置不可用。", "WEB_PROVIDER_ERROR");
|
|
573
|
+
return provider.fetch(request, signal);
|
|
574
|
+
})
|
|
673
575
|
});
|
|
674
|
-
return () => {
|
|
675
|
-
offHttp();
|
|
676
|
-
offTavily();
|
|
677
|
-
offFirecrawl();
|
|
678
|
-
offSerper();
|
|
679
|
-
offBocha();
|
|
680
|
-
offBrave();
|
|
681
|
-
offExa();
|
|
682
|
-
offDeepSeek();
|
|
683
|
-
offEngine();
|
|
684
|
-
};
|
|
685
576
|
}
|
|
686
577
|
//#endregion
|
|
687
|
-
//#region src/
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
if (String(req.headers["content-type"] ?? "").split(";")[0]?.trim() !== "application/json") {
|
|
712
|
-
res.writeHead(415);
|
|
713
|
-
res.end();
|
|
714
|
-
return;
|
|
715
|
-
}
|
|
716
|
-
const controller = new AbortController();
|
|
717
|
-
res.on("close", () => {
|
|
718
|
-
if (!res.writableEnded) controller.abort();
|
|
578
|
+
//#region src/web-registry.ts
|
|
579
|
+
const owner = Symbol.for("@klarkxy/dsh-web-search-manager/registry-owner");
|
|
580
|
+
/** The only private-host boundary. Verified against DSH 0.2.0-rc.2.
|
|
581
|
+
* Keep the actual Maps: Cordis effects capture them for automatic unregistration.
|
|
582
|
+
* Replace this bridge when the host exposes enumeration/selection publicly.
|
|
583
|
+
*/
|
|
584
|
+
function runtimeOf(web) {
|
|
585
|
+
const runtime = web;
|
|
586
|
+
if (!(runtime.searchProviders instanceof Map) || !(runtime.fetchProviders instanceof Map) || !Object.hasOwn(runtime, "searchProviderId") || !Object.hasOwn(runtime, "fetchProviderId")) throw new WebError("当前 ctx.web 不支持注册表管理,请使用兼容的 DSH 版本。", "WEB_REGISTRY_UNSUPPORTED");
|
|
587
|
+
return runtime;
|
|
588
|
+
}
|
|
589
|
+
function hasWebProvider(web, kind, id) {
|
|
590
|
+
return runtimeOf(web)[kind === "search" ? "searchProviders" : "fetchProviders"].has(id);
|
|
591
|
+
}
|
|
592
|
+
function observeWebRegistry(web, hooks) {
|
|
593
|
+
const runtime = runtimeOf(web);
|
|
594
|
+
const maps = [runtime.searchProviders, runtime.fetchProviders];
|
|
595
|
+
if (maps.some((map) => Object.hasOwn(map, owner))) throw new WebError("ctx.web 已由另一个搜索管理器管理。", "WEB_REGISTRY_MANAGED");
|
|
596
|
+
const undo = [];
|
|
597
|
+
try {
|
|
598
|
+
for (const [kind, map] of [["search", maps[0]], ["fetch", maps[1]]]) {
|
|
599
|
+
Object.defineProperty(map, owner, {
|
|
600
|
+
value: true,
|
|
601
|
+
configurable: true
|
|
719
602
|
});
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
603
|
+
undo.push(() => {
|
|
604
|
+
delete map[owner];
|
|
605
|
+
});
|
|
606
|
+
const originals = /* @__PURE__ */ new Map();
|
|
607
|
+
const set = map.set, remove = map.delete, clear = map.clear;
|
|
608
|
+
const wrap = (id, provider) => {
|
|
609
|
+
const wrapped = hooks.add(kind, provider);
|
|
610
|
+
originals.set(id, {
|
|
611
|
+
original: provider,
|
|
612
|
+
wrapped
|
|
613
|
+
});
|
|
614
|
+
return wrapped;
|
|
615
|
+
};
|
|
616
|
+
undo.push(() => {
|
|
617
|
+
for (const [id, row] of originals) if (map.get(id) === row.wrapped) set.call(map, id, row.original);
|
|
618
|
+
});
|
|
619
|
+
for (const [id, provider] of map) set.call(map, id, wrap(id, provider));
|
|
620
|
+
const replacements = {
|
|
621
|
+
set(id, provider) {
|
|
622
|
+
const wrapped = wrap(id, provider);
|
|
623
|
+
set.call(map, id, wrapped);
|
|
624
|
+
return map;
|
|
625
|
+
},
|
|
626
|
+
delete(id) {
|
|
627
|
+
const removed = remove.call(map, id);
|
|
628
|
+
if (removed) {
|
|
629
|
+
originals.delete(id);
|
|
630
|
+
hooks.remove(kind, id);
|
|
731
631
|
}
|
|
732
|
-
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
} catch {
|
|
740
|
-
res.writeHead(400);
|
|
741
|
-
res.end();
|
|
742
|
-
return;
|
|
743
|
-
}
|
|
744
|
-
if (request.type !== "client-request" || request.method !== endpoint || typeof request.rpcId !== "string") {
|
|
745
|
-
res.writeHead(400);
|
|
746
|
-
res.end();
|
|
747
|
-
return;
|
|
748
|
-
}
|
|
749
|
-
const result = await call(endpoint, request.payload, controller.signal);
|
|
750
|
-
if (!res.destroyed) {
|
|
751
|
-
res.writeHead(200, {
|
|
752
|
-
"content-type": "application/json",
|
|
753
|
-
"cache-control": "no-store"
|
|
754
|
-
});
|
|
755
|
-
res.end(JSON.stringify({
|
|
756
|
-
type: "server-response",
|
|
757
|
-
rpcId: request.rpcId,
|
|
758
|
-
result
|
|
759
|
-
}));
|
|
760
|
-
}
|
|
761
|
-
} catch {
|
|
762
|
-
if (!res.destroyed) {
|
|
763
|
-
res.writeHead(500);
|
|
764
|
-
res.end("network settings request failed");
|
|
632
|
+
return removed;
|
|
633
|
+
},
|
|
634
|
+
clear() {
|
|
635
|
+
const ids = [...map.keys()];
|
|
636
|
+
clear.call(map);
|
|
637
|
+
originals.clear();
|
|
638
|
+
for (const id of ids) hooks.remove(kind, id);
|
|
765
639
|
}
|
|
640
|
+
};
|
|
641
|
+
for (const key of [
|
|
642
|
+
"set",
|
|
643
|
+
"delete",
|
|
644
|
+
"clear"
|
|
645
|
+
]) {
|
|
646
|
+
const before = Object.getOwnPropertyDescriptor(map, key);
|
|
647
|
+
Object.defineProperty(map, key, {
|
|
648
|
+
value: replacements[key],
|
|
649
|
+
configurable: true,
|
|
650
|
+
writable: true
|
|
651
|
+
});
|
|
652
|
+
undo.push(() => {
|
|
653
|
+
if (map[key] !== replacements[key]) return;
|
|
654
|
+
if (before) Object.defineProperty(map, key, before);
|
|
655
|
+
else delete map[key];
|
|
656
|
+
});
|
|
766
657
|
}
|
|
767
658
|
}
|
|
768
|
-
|
|
659
|
+
for (const key of ["searchProviderId", "fetchProviderId"]) {
|
|
660
|
+
const before = Object.getOwnPropertyDescriptor(runtime, key);
|
|
661
|
+
Object.defineProperty(runtime, key, {
|
|
662
|
+
...before,
|
|
663
|
+
value: void 0
|
|
664
|
+
});
|
|
665
|
+
undo.push(() => {
|
|
666
|
+
if (runtime[key] === void 0) Object.defineProperty(runtime, key, before);
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
} catch (error) {
|
|
670
|
+
for (const restore of undo.reverse()) restore();
|
|
671
|
+
throw new WebError("无法接管 ctx.web 注册表,已恢复原状。", "WEB_REGISTRY_UNSUPPORTED", { cause: error });
|
|
672
|
+
}
|
|
673
|
+
let stopped = false;
|
|
674
|
+
return () => {
|
|
675
|
+
if (stopped) return;
|
|
676
|
+
stopped = true;
|
|
677
|
+
for (const restore of undo.reverse()) restore();
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
//#endregion
|
|
681
|
+
//#region src/builtins.ts
|
|
682
|
+
function registerBuiltins(web, configuration) {
|
|
683
|
+
const registered = [];
|
|
684
|
+
const registerSearch = (descriptor, factory) => {
|
|
685
|
+
if (descriptor.legacyId && hasWebProvider(web, "search", descriptor.legacyId)) return () => {};
|
|
686
|
+
if (hasWebProvider(web, "search", descriptor.id)) return () => {};
|
|
687
|
+
const off = registerNativeSearch(web, configuration, descriptor, factory);
|
|
688
|
+
registered.push(off);
|
|
689
|
+
return off;
|
|
690
|
+
};
|
|
691
|
+
const registerFetch = (descriptor, factory) => {
|
|
692
|
+
if (descriptor.legacyId && hasWebProvider(web, "fetch", descriptor.legacyId)) return () => {};
|
|
693
|
+
if (hasWebProvider(web, "fetch", descriptor.id)) return () => {};
|
|
694
|
+
const off = registerNativeFetch(web, configuration, descriptor, factory);
|
|
695
|
+
registered.push(off);
|
|
696
|
+
return off;
|
|
697
|
+
};
|
|
698
|
+
try {
|
|
699
|
+
registerSearch({
|
|
700
|
+
id: "ddg",
|
|
701
|
+
label: "DuckDuckGo",
|
|
702
|
+
billing: "none",
|
|
703
|
+
description: "无需注册。默认后端。"
|
|
704
|
+
}, () => new SearchEngineProvider());
|
|
705
|
+
registerSearch({
|
|
706
|
+
id: "deepseek-managed",
|
|
707
|
+
legacyId: "deepseek-official",
|
|
708
|
+
label: "DeepSeek 搜索",
|
|
709
|
+
description: "使用已配置的 DeepSeek API Key,可能产生额外搜索费用。",
|
|
710
|
+
defaultBaseURL: DEEPSEEK_DEFAULT_BASE_URL,
|
|
711
|
+
credentialRef: "DEEPSEEK_API_KEY",
|
|
712
|
+
credentialShared: true,
|
|
713
|
+
credentialHint: "与模型设置共用 Key",
|
|
714
|
+
billing: "model-and-tools",
|
|
715
|
+
signupUrl: "https://platform.deepseek.com/api_keys"
|
|
716
|
+
}, (options) => new DeepSeekSearchProvider(() => ({
|
|
717
|
+
apiKey: options.apiKey,
|
|
718
|
+
baseURL: options.baseURL ?? DEEPSEEK_DEFAULT_BASE_URL,
|
|
719
|
+
model: DEEPSEEK_DEFAULT_MODEL,
|
|
720
|
+
apiVersion: DEEPSEEK_DEFAULT_API_VERSION,
|
|
721
|
+
maxTokens: DEEPSEEK_DEFAULT_MAX_TOKENS,
|
|
722
|
+
maxUses: DEEPSEEK_DEFAULT_MAX_USES
|
|
723
|
+
})));
|
|
724
|
+
registerSearch({
|
|
725
|
+
id: "exa",
|
|
726
|
+
label: "Exa",
|
|
727
|
+
description: "独立 Search API。",
|
|
728
|
+
defaultBaseURL: "https://api.exa.ai",
|
|
729
|
+
credentialRef: "DSH_EDITOR_WEB_EXA_API_KEY",
|
|
730
|
+
billing: "request",
|
|
731
|
+
signupUrl: "https://dashboard.exa.ai/api-keys"
|
|
732
|
+
}, (options) => new ExaSearchProvider({
|
|
733
|
+
apiKey: options.apiKey ?? "",
|
|
734
|
+
baseURL: options.baseURL ?? "https://api.exa.ai",
|
|
735
|
+
searchType: "auto",
|
|
736
|
+
highlightsPerResult: 1
|
|
737
|
+
}));
|
|
738
|
+
registerSearch({
|
|
739
|
+
id: "brave",
|
|
740
|
+
label: "Brave",
|
|
741
|
+
description: "Brave Search API。",
|
|
742
|
+
defaultBaseURL: "https://api.search.brave.com",
|
|
743
|
+
credentialRef: "DSH_EDITOR_WEB_BRAVE_API_KEY",
|
|
744
|
+
billing: "request",
|
|
745
|
+
signupUrl: "https://api.search.brave.com"
|
|
746
|
+
}, (options) => new BraveSearchProvider({
|
|
747
|
+
apiKey: options.apiKey ?? "",
|
|
748
|
+
baseURL: options.baseURL
|
|
749
|
+
}));
|
|
750
|
+
registerSearch({
|
|
751
|
+
id: "bocha",
|
|
752
|
+
label: "博查",
|
|
753
|
+
description: "中文网页搜索 API。",
|
|
754
|
+
defaultBaseURL: "https://api.bochaai.com",
|
|
755
|
+
credentialRef: "DSH_EDITOR_WEB_BOCHA_API_KEY",
|
|
756
|
+
billing: "request",
|
|
757
|
+
signupUrl: "https://open.bochaai.com"
|
|
758
|
+
}, (options) => new BochaSearchProvider({
|
|
759
|
+
apiKey: options.apiKey ?? "",
|
|
760
|
+
baseURL: options.baseURL
|
|
761
|
+
}));
|
|
762
|
+
registerSearch({
|
|
763
|
+
id: "serper",
|
|
764
|
+
label: "Serper",
|
|
765
|
+
description: "Google 结果的 Search API。",
|
|
766
|
+
defaultBaseURL: "https://google.serper.dev",
|
|
767
|
+
credentialRef: "DSH_EDITOR_WEB_SERPER_API_KEY",
|
|
768
|
+
billing: "request",
|
|
769
|
+
signupUrl: "https://serper.dev"
|
|
770
|
+
}, (options) => new SerperSearchProvider({
|
|
771
|
+
apiKey: options.apiKey ?? "",
|
|
772
|
+
baseURL: options.baseURL
|
|
773
|
+
}));
|
|
774
|
+
registerSearch({
|
|
775
|
+
id: "firecrawl",
|
|
776
|
+
label: "Firecrawl",
|
|
777
|
+
description: "网页搜索与提取 API。",
|
|
778
|
+
defaultBaseURL: "https://api.firecrawl.dev",
|
|
779
|
+
credentialRef: "DSH_EDITOR_WEB_FIRECRAWL_API_KEY",
|
|
780
|
+
billing: "request",
|
|
781
|
+
signupUrl: "https://www.firecrawl.dev"
|
|
782
|
+
}, (options) => new FirecrawlSearchProvider({
|
|
783
|
+
apiKey: options.apiKey ?? "",
|
|
784
|
+
baseURL: options.baseURL
|
|
785
|
+
}));
|
|
786
|
+
registerSearch({
|
|
787
|
+
id: "tavily",
|
|
788
|
+
label: "Tavily",
|
|
789
|
+
defaultBaseURL: "https://api.tavily.com",
|
|
790
|
+
description: "Search API;固定 basic 检索,不自动升级搜索深度。",
|
|
791
|
+
credentialRef: "DSH_EDITOR_WEB_TAVILY_API_KEY",
|
|
792
|
+
billing: "request",
|
|
793
|
+
signupUrl: "https://app.tavily.com"
|
|
794
|
+
}, (options) => new TavilySearchProvider({
|
|
795
|
+
apiKey: options.apiKey ?? "",
|
|
796
|
+
baseURL: options.baseURL
|
|
797
|
+
}));
|
|
798
|
+
registerFetch({
|
|
799
|
+
id: "http-managed",
|
|
800
|
+
legacyId: "http",
|
|
801
|
+
label: "网页读取",
|
|
802
|
+
billing: "none",
|
|
803
|
+
description: "优先匿名 Jina Reader 读取,失败后本机 HTTP 直连;无需 Key。"
|
|
804
|
+
}, (options) => {
|
|
805
|
+
const limits = {
|
|
806
|
+
maxResponseBytes: 5e6,
|
|
807
|
+
maxBodyChars: options.maxFetchChars,
|
|
808
|
+
timeoutMs: options.timeoutMs,
|
|
809
|
+
maxRedirects: 5,
|
|
810
|
+
userAgent: "dsh-editor/managed-web"
|
|
811
|
+
};
|
|
812
|
+
const readerTimeoutMs = Math.min(15e3, Math.floor(options.timeoutMs / 2));
|
|
813
|
+
return new JinaFirstFetchProvider({
|
|
814
|
+
reader: new HttpFetchProvider({
|
|
815
|
+
...limits,
|
|
816
|
+
maxBodyChars: limits.maxResponseBytes,
|
|
817
|
+
timeoutMs: readerTimeoutMs
|
|
818
|
+
}),
|
|
819
|
+
direct: new HttpFetchProvider(limits),
|
|
820
|
+
readerTimeoutMs
|
|
821
|
+
});
|
|
822
|
+
});
|
|
823
|
+
} catch (error) {
|
|
824
|
+
for (const off of registered.reverse()) off();
|
|
825
|
+
throw error;
|
|
826
|
+
}
|
|
827
|
+
return () => {
|
|
828
|
+
for (const off of registered.splice(0).reverse()) off();
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
//#endregion
|
|
832
|
+
//#region src/host-rpc.ts
|
|
833
|
+
/** Keep the historical transport error text while sharing admission and request handling. */
|
|
834
|
+
function registerHostRpc$1(ctx, channel, call) {
|
|
835
|
+
return registerHostRpc(ctx, channel, call, { errorBody: (status) => status === 500 ? "network settings request failed" : void 0 });
|
|
769
836
|
}
|
|
770
837
|
//#endregion
|
|
771
838
|
//#region src/manager.ts
|
|
@@ -805,6 +872,9 @@ var WebSearchManager = class {
|
|
|
805
872
|
suspended = false;
|
|
806
873
|
disposed = false;
|
|
807
874
|
storageFailed = false;
|
|
875
|
+
refreshRevision = 0;
|
|
876
|
+
detach;
|
|
877
|
+
legacyRegistrations = /* @__PURE__ */ new Set();
|
|
808
878
|
constructor(options) {
|
|
809
879
|
this.options = options;
|
|
810
880
|
const initial = structuredClone(options.initial ?? defaultSettings());
|
|
@@ -813,6 +883,18 @@ var WebSearchManager = class {
|
|
|
813
883
|
...initial,
|
|
814
884
|
searchOrder: migrateSearchOrder(initial)
|
|
815
885
|
};
|
|
886
|
+
this.detach = observeWebRegistry(options.web, {
|
|
887
|
+
add: (kind, provider) => this.discover(kind, provider),
|
|
888
|
+
remove: (kind, id) => {
|
|
889
|
+
const key = providerKey(kind, id);
|
|
890
|
+
const entry = this.entries.get(key);
|
|
891
|
+
if (entry) {
|
|
892
|
+
this.abort(entry);
|
|
893
|
+
this.entries.delete(key);
|
|
894
|
+
this.notify();
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
});
|
|
816
898
|
}
|
|
817
899
|
subscribe(listener) {
|
|
818
900
|
this.listeners.add(listener);
|
|
@@ -832,7 +914,15 @@ var WebSearchManager = class {
|
|
|
832
914
|
return Boolean(this.entries.get(providerKey("search", id))?.descriptor.credentialRef);
|
|
833
915
|
}
|
|
834
916
|
configured(id) {
|
|
835
|
-
|
|
917
|
+
const entry = this.entries.get(providerKey("search", id));
|
|
918
|
+
return Boolean(entry && this.ready(entry));
|
|
919
|
+
}
|
|
920
|
+
ready(entry) {
|
|
921
|
+
try {
|
|
922
|
+
return entry.configured && entry.provider.available();
|
|
923
|
+
} catch {
|
|
924
|
+
return false;
|
|
925
|
+
}
|
|
836
926
|
}
|
|
837
927
|
searchOrder(settings = this.settings) {
|
|
838
928
|
return resolveSearchOrder(this.knownSearchIds(), settings, (id) => this.keyed(id));
|
|
@@ -841,7 +931,8 @@ var WebSearchManager = class {
|
|
|
841
931
|
return pickActiveSearch(this.searchOrder(settings), (id) => this.configured(id));
|
|
842
932
|
}
|
|
843
933
|
selected(entry) {
|
|
844
|
-
if (this.disposed || this.suspended || !entry
|
|
934
|
+
if (this.disposed || this.suspended || !this.ready(entry)) return false;
|
|
935
|
+
if (this.entries.get(providerKey(entry.kind, entry.descriptor.id)) !== entry) return false;
|
|
845
936
|
if (entry.kind === "fetch") return this.settings.fetchEnabled && this.settings.fetchProvider === entry.descriptor.id && this.entries.get(providerKey("fetch", entry.descriptor.id)) === entry;
|
|
846
937
|
return this.settings.searchEnabled && this.activeSearchId() === entry.descriptor.id;
|
|
847
938
|
}
|
|
@@ -855,8 +946,9 @@ var WebSearchManager = class {
|
|
|
855
946
|
providers: entries.map((entry) => ({
|
|
856
947
|
...entry.descriptor,
|
|
857
948
|
kind: entry.kind,
|
|
858
|
-
configured: entry
|
|
859
|
-
|
|
949
|
+
configured: this.ready(entry),
|
|
950
|
+
configurationOwned: entry.descriptor.configurationOwner === WEB_MANAGER_OWNER,
|
|
951
|
+
baseURL: entry.descriptor.configurationOwner === "@klarkxy/dsh-web-search-manager" ? this.settings.endpoints[providerKey(entry.kind, entry.descriptor.id)] ?? entry.descriptor.defaultBaseURL : void 0,
|
|
860
952
|
calls: entry.calls,
|
|
861
953
|
failures: entry.failures
|
|
862
954
|
})).sort((a, b) => providerKey(a.kind, a.id).localeCompare(providerKey(b.kind, b.id)))
|
|
@@ -864,18 +956,20 @@ var WebSearchManager = class {
|
|
|
864
956
|
}
|
|
865
957
|
async refresh() {
|
|
866
958
|
const epoch = this.epoch;
|
|
959
|
+
const revision = ++this.refreshRevision;
|
|
867
960
|
await Promise.all([...this.entries.entries()].map(async ([key, entry]) => {
|
|
868
|
-
let configured =
|
|
869
|
-
if (entry.descriptor.credentialRef) try {
|
|
961
|
+
let configured = true;
|
|
962
|
+
if (entry.descriptor.configurationOwner === "@klarkxy/dsh-web-search-manager" && entry.descriptor.credentialRef) try {
|
|
870
963
|
configured = Boolean((await this.options.resolveCredential(entry.descriptor.credentialRef))?.trim());
|
|
871
964
|
} catch {
|
|
872
965
|
configured = false;
|
|
873
966
|
}
|
|
874
|
-
if (this.disposed || epoch !== this.epoch || this.entries.get(key) !== entry) return;
|
|
967
|
+
if (this.disposed || epoch !== this.epoch || revision !== this.refreshRevision || this.entries.get(key) !== entry) return;
|
|
875
968
|
if (entry.configured !== configured) {
|
|
876
969
|
entry.configured = configured;
|
|
877
970
|
if (!configured) this.abort(entry);
|
|
878
971
|
}
|
|
972
|
+
if (!this.ready(entry)) this.abort(entry);
|
|
879
973
|
}));
|
|
880
974
|
this.notify();
|
|
881
975
|
return this.status();
|
|
@@ -883,99 +977,97 @@ var WebSearchManager = class {
|
|
|
883
977
|
abort(entry) {
|
|
884
978
|
for (const target of entry ? [entry] : this.entries.values()) for (const controller of target.active) controller.abort();
|
|
885
979
|
}
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
const
|
|
889
|
-
if (
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
980
|
+
discover(kind, provider) {
|
|
981
|
+
const key = providerKey(kind, provider.id);
|
|
982
|
+
const old = this.entries.get(key);
|
|
983
|
+
if (old) this.abort(old);
|
|
984
|
+
const metadata = provider.dshWebManagement;
|
|
985
|
+
const descriptor = {
|
|
986
|
+
id: provider.id,
|
|
987
|
+
label: typeof metadata?.label === "string" && metadata.label ? metadata.label : provider.id,
|
|
988
|
+
description: typeof metadata?.description === "string" ? metadata.description : "",
|
|
989
|
+
billing: [
|
|
990
|
+
"none",
|
|
991
|
+
"request",
|
|
992
|
+
"model-and-tools"
|
|
993
|
+
].includes(metadata?.billing ?? "") ? metadata.billing : "unknown",
|
|
994
|
+
configurationOwner: typeof metadata?.configurationOwner === "string" && metadata.configurationOwner ? metadata.configurationOwner : void 0,
|
|
995
|
+
configurationUrl: safeConfigurationUrl(metadata?.configurationUrl)
|
|
996
|
+
};
|
|
997
|
+
for (const field of [
|
|
998
|
+
"credentialHint",
|
|
999
|
+
"pricing",
|
|
1000
|
+
"defaultBaseURL"
|
|
1001
|
+
]) if (typeof metadata?.[field] === "string") descriptor[field] = metadata[field];
|
|
1002
|
+
if (typeof metadata?.credentialRef === "string" && /^[A-Z][A-Z0-9_]{1,127}$/.test(metadata.credentialRef)) descriptor.credentialRef = metadata.credentialRef;
|
|
1003
|
+
if (metadata?.credentialShared === true) descriptor.credentialShared = true;
|
|
1004
|
+
for (const field of ["signupUrl", "pricingUrl"]) try {
|
|
1005
|
+
const url = new URL(metadata?.[field] ?? "");
|
|
1006
|
+
if (url.protocol === "https:" && !url.username && !url.password) descriptor[field] = metadata[field];
|
|
1007
|
+
} catch {}
|
|
902
1008
|
const entry = {
|
|
903
|
-
descriptor: Object.freeze(
|
|
1009
|
+
descriptor: Object.freeze(descriptor),
|
|
904
1010
|
kind,
|
|
905
|
-
|
|
1011
|
+
provider,
|
|
1012
|
+
configured: descriptor.configurationOwner !== "@klarkxy/dsh-web-search-manager" || !descriptor.credentialRef,
|
|
906
1013
|
calls: 0,
|
|
907
1014
|
failures: 0,
|
|
908
|
-
unregister() {},
|
|
909
1015
|
active: /* @__PURE__ */ new Set()
|
|
910
1016
|
};
|
|
911
1017
|
this.entries.set(key, entry);
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
1018
|
+
const legacy = metadata?.legacyId;
|
|
1019
|
+
const expectedLegacy = kind === "search" && provider.id === "deepseek-managed" ? "deepseek-official" : kind === "fetch" && provider.id === "http-managed" ? "http" : void 0;
|
|
1020
|
+
if (descriptor.configurationOwner === "@klarkxy/dsh-web-search-manager" && legacy === expectedLegacy && legacy && !this.entries.has(providerKey(kind, legacy))) {
|
|
1021
|
+
if (kind === "search") {
|
|
1022
|
+
this.settings.searchOrder = this.settings.searchOrder.map((id) => id === legacy ? provider.id : id);
|
|
1023
|
+
if (this.settings.searchProvider === legacy) this.settings.searchProvider = provider.id;
|
|
1024
|
+
} else if (this.settings.fetchProvider === legacy) this.settings.fetchProvider = provider.id;
|
|
1025
|
+
const legacyKey = providerKey(kind, legacy);
|
|
1026
|
+
if (this.settings.endpoints[legacyKey] && !this.settings.endpoints[key]) this.settings.endpoints[key] = this.settings.endpoints[legacyKey];
|
|
921
1027
|
}
|
|
922
1028
|
this.refresh();
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
}
|
|
955
|
-
})
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
content: result.content?.slice(0, 4e4),
|
|
963
|
-
sources,
|
|
964
|
-
truncated: result.truncated || result.sources.length > count
|
|
965
|
-
};
|
|
966
|
-
});
|
|
967
|
-
}
|
|
968
|
-
}));
|
|
969
|
-
}
|
|
970
|
-
registerFetchProvider(descriptor, factory) {
|
|
971
|
-
const entry = this.add("fetch", descriptor);
|
|
972
|
-
return this.registration(entry, () => this.options.web.registerFetchProvider({
|
|
973
|
-
id: descriptor.id,
|
|
1029
|
+
if (kind === "search") {
|
|
1030
|
+
const search = provider;
|
|
1031
|
+
return {
|
|
1032
|
+
id: provider.id,
|
|
1033
|
+
available: () => this.selected(entry),
|
|
1034
|
+
search: (request, signal) => {
|
|
1035
|
+
if (typeof request.query !== "string" || !request.query.trim() || request.query.length > 4e3) fail("WEB_INVALID_REQUEST", "查询须为 1 至 4000 字符。");
|
|
1036
|
+
const count = Math.min(bounded(request.maxResults ?? this.settings.maxResults, 1, 100, "结果数"), this.settings.maxResults);
|
|
1037
|
+
return this.execute(entry, signal, async (_options, combined) => {
|
|
1038
|
+
const result = await search.search({
|
|
1039
|
+
...request,
|
|
1040
|
+
query: request.query.trim(),
|
|
1041
|
+
maxResults: count
|
|
1042
|
+
}, combined);
|
|
1043
|
+
const sources = result.sources.slice(0, count).filter((source) => {
|
|
1044
|
+
try {
|
|
1045
|
+
const url = new URL(source.url);
|
|
1046
|
+
return ["http:", "https:"].includes(url.protocol) && !url.username && !url.password;
|
|
1047
|
+
} catch {
|
|
1048
|
+
return false;
|
|
1049
|
+
}
|
|
1050
|
+
}).map((source) => ({
|
|
1051
|
+
...source,
|
|
1052
|
+
title: source.title?.slice(0, 1e3),
|
|
1053
|
+
snippet: source.snippet?.slice(0, 8e3)
|
|
1054
|
+
}));
|
|
1055
|
+
return {
|
|
1056
|
+
...result,
|
|
1057
|
+
content: result.content?.slice(0, 4e4),
|
|
1058
|
+
sources,
|
|
1059
|
+
truncated: result.truncated || result.sources.length > count
|
|
1060
|
+
};
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
const fetch = provider;
|
|
1066
|
+
return {
|
|
1067
|
+
id: provider.id,
|
|
974
1068
|
available: () => this.selected(entry),
|
|
975
1069
|
fetch: (request, signal) => this.execute(entry, signal, async (options, combined) => {
|
|
976
|
-
const
|
|
977
|
-
if (!provider.available()) fail("WEB_PROVIDER_ERROR", "供应商配置不可用。");
|
|
978
|
-
const result = await provider.fetch(request, combined);
|
|
1070
|
+
const result = await fetch.fetch(request, combined);
|
|
979
1071
|
const content = result.body.content.slice(0, options.maxFetchChars);
|
|
980
1072
|
return {
|
|
981
1073
|
...result,
|
|
@@ -986,7 +1078,31 @@ var WebSearchManager = class {
|
|
|
986
1078
|
truncated: result.truncated || content.length !== result.body.content.length
|
|
987
1079
|
};
|
|
988
1080
|
})
|
|
989
|
-
}
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
/** @deprecated Provider plugins should register directly with ctx.web. */
|
|
1084
|
+
registerSearchProvider(descriptor, factory) {
|
|
1085
|
+
return this.legacyRegistration(() => registerNativeSearch(this.options.web, this.providerConfiguration(), descriptor, factory));
|
|
1086
|
+
}
|
|
1087
|
+
/** @deprecated Provider plugins should register directly with ctx.web. */
|
|
1088
|
+
registerFetchProvider(descriptor, factory) {
|
|
1089
|
+
return this.legacyRegistration(() => registerNativeFetch(this.options.web, this.providerConfiguration(), descriptor, factory));
|
|
1090
|
+
}
|
|
1091
|
+
providerConfiguration() {
|
|
1092
|
+
return {
|
|
1093
|
+
settings: () => structuredClone(this.settings),
|
|
1094
|
+
resolveCredential: (ref) => this.options.resolveCredential(ref)
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
legacyRegistration(register) {
|
|
1098
|
+
if (this.disposed) fail("WEB_DISABLED", "网络搜索插件已停止。");
|
|
1099
|
+
const unregister = register();
|
|
1100
|
+
const off = () => {
|
|
1101
|
+
this.legacyRegistrations.delete(off);
|
|
1102
|
+
unregister();
|
|
1103
|
+
};
|
|
1104
|
+
this.legacyRegistrations.add(off);
|
|
1105
|
+
return off;
|
|
990
1106
|
}
|
|
991
1107
|
async execute(entry, signal, run) {
|
|
992
1108
|
if (!this.selected(entry)) fail("WEB_DISABLED", "请在设置 → 网络搜索中配置凭据并启用。");
|
|
@@ -996,19 +1112,9 @@ var WebSearchManager = class {
|
|
|
996
1112
|
const epoch = this.epoch;
|
|
997
1113
|
entry.active.add(controller);
|
|
998
1114
|
let started = false;
|
|
999
|
-
let apiKey;
|
|
1000
1115
|
try {
|
|
1001
|
-
const ref = entry.descriptor.credentialRef;
|
|
1002
|
-
apiKey = ref ? await abortable(() => this.options.resolveCredential(ref), combined) : void 0;
|
|
1003
|
-
if (ref && !apiKey?.trim()) {
|
|
1004
|
-
entry.configured = false;
|
|
1005
|
-
this.abort(entry);
|
|
1006
|
-
this.notify();
|
|
1007
|
-
fail("WEB_CREDENTIAL_MISSING", "搜索凭据已删除或不可用,请重新配置。");
|
|
1008
|
-
}
|
|
1009
1116
|
if (epoch !== this.epoch || !this.selected(entry)) fail("WEB_DISABLED", "网络访问设置已改变,请重新发起请求。");
|
|
1010
1117
|
const options = {
|
|
1011
|
-
apiKey,
|
|
1012
1118
|
baseURL: this.settings.endpoints[providerKey(entry.kind, entry.descriptor.id)] ?? entry.descriptor.defaultBaseURL,
|
|
1013
1119
|
timeoutMs: this.settings.timeoutMs,
|
|
1014
1120
|
maxFetchChars: this.settings.maxFetchChars
|
|
@@ -1020,8 +1126,13 @@ var WebSearchManager = class {
|
|
|
1020
1126
|
return result;
|
|
1021
1127
|
} catch (error) {
|
|
1022
1128
|
if (started) entry.failures += 1;
|
|
1129
|
+
if (error instanceof WebError && error.code === "WEB_CREDENTIAL_MISSING") {
|
|
1130
|
+
entry.configured = false;
|
|
1131
|
+
this.abort(entry);
|
|
1132
|
+
this.notify();
|
|
1133
|
+
}
|
|
1023
1134
|
if (combined.aborted && !(error instanceof WebError && error.code === "WEB_CREDENTIAL_MISSING")) throw new WebError("网络请求已取消或超时。", "WEB_ABORTED");
|
|
1024
|
-
const safe = providerError(error
|
|
1135
|
+
const safe = providerError(error);
|
|
1025
1136
|
throw new WebError(`[${entry.kind}:${entry.descriptor.id}] ${safe.message}`, safe.code);
|
|
1026
1137
|
} finally {
|
|
1027
1138
|
clearTimeout(timeout);
|
|
@@ -1042,7 +1153,7 @@ var WebSearchManager = class {
|
|
|
1042
1153
|
bounded(proposed.maxFetchChars, 1e3, 2e5, "正文长度");
|
|
1043
1154
|
for (const [key, value] of Object.entries(proposed.endpoints)) {
|
|
1044
1155
|
const entry = this.entries.get(key);
|
|
1045
|
-
if (entry && !entry.descriptor.defaultBaseURL) fail("WEB_INVALID_CONFIG", "供应商不支持自定义地址。");
|
|
1156
|
+
if (entry && (entry.descriptor.configurationOwner !== "@klarkxy/dsh-web-search-manager" || !entry.descriptor.defaultBaseURL) && value !== this.settings.endpoints[key]) fail("WEB_INVALID_CONFIG", "供应商不支持自定义地址。");
|
|
1046
1157
|
try {
|
|
1047
1158
|
proposed.endpoints[key] = validateBaseURL(value);
|
|
1048
1159
|
} catch {
|
|
@@ -1050,18 +1161,13 @@ var WebSearchManager = class {
|
|
|
1050
1161
|
}
|
|
1051
1162
|
}
|
|
1052
1163
|
await this.refresh();
|
|
1053
|
-
proposed.searchOrder =
|
|
1164
|
+
proposed.searchOrder = [...new Set(proposed.searchOrder)];
|
|
1054
1165
|
proposed.searchProvider = this.activeSearchId(proposed);
|
|
1055
|
-
|
|
1056
|
-
if (proposed.searchEnabled &&
|
|
1057
|
-
proposed.fetchEnabled = true;
|
|
1058
|
-
proposed.fetchProvider = "http";
|
|
1059
|
-
}
|
|
1060
|
-
if (!proposed.searchEnabled) proposed.fetchEnabled = false;
|
|
1166
|
+
const activatingSearch = !this.settings.searchEnabled || JSON.stringify(proposed.searchOrder) !== JSON.stringify(this.settings.searchOrder);
|
|
1167
|
+
if (proposed.searchEnabled && !proposed.searchProvider && activatingSearch) fail("WEB_CREDENTIAL_MISSING", "需选择已安装的供应商并填写有效凭据。");
|
|
1061
1168
|
if (proposed.fetchEnabled) {
|
|
1062
1169
|
const entry = this.entries.get(providerKey("fetch", proposed.fetchProvider));
|
|
1063
|
-
if (!
|
|
1064
|
-
else if (!entry.configured) fail("WEB_CREDENTIAL_MISSING", "需选择已安装的供应商并填写有效凭据。");
|
|
1170
|
+
if ((!this.settings.fetchEnabled || proposed.fetchProvider !== this.settings.fetchProvider) && (!entry || !this.ready(entry))) fail("WEB_CREDENTIAL_MISSING", "需选择已安装的供应商并填写有效凭据。");
|
|
1065
1171
|
}
|
|
1066
1172
|
this.suspended = true;
|
|
1067
1173
|
this.epoch += 1;
|
|
@@ -1088,10 +1194,11 @@ var WebSearchManager = class {
|
|
|
1088
1194
|
this.epoch += 1;
|
|
1089
1195
|
this.abort();
|
|
1090
1196
|
this.notify();
|
|
1091
|
-
for (const
|
|
1197
|
+
for (const off of [...this.legacyRegistrations]) off();
|
|
1198
|
+
await this.pending;
|
|
1199
|
+
this.detach();
|
|
1092
1200
|
this.entries.clear();
|
|
1093
1201
|
this.listeners.clear();
|
|
1094
|
-
await this.pending;
|
|
1095
1202
|
}
|
|
1096
1203
|
};
|
|
1097
1204
|
//#endregion
|
|
@@ -1135,20 +1242,30 @@ const inject = [
|
|
|
1135
1242
|
async function apply(ctx) {
|
|
1136
1243
|
const host = ctx;
|
|
1137
1244
|
const domain = await ctx.storageDomain.open(webSettingsDomain);
|
|
1245
|
+
let managed;
|
|
1246
|
+
ctx.effect(() => async () => {
|
|
1247
|
+
await managed?.dispose();
|
|
1248
|
+
await domain.close();
|
|
1249
|
+
}, "web-search-manager.dispose");
|
|
1138
1250
|
const table = domain.table("settings");
|
|
1139
|
-
const manager = new WebSearchManager({
|
|
1251
|
+
const manager = managed = new WebSearchManager({
|
|
1140
1252
|
web: ctx.web,
|
|
1141
1253
|
initial: table.get("global"),
|
|
1142
1254
|
resolveCredential: async (ref) => (await host.credentials.resolve(credentialRef(ref)))?.value,
|
|
1143
1255
|
save: (settings) => table.put("global", settings)
|
|
1144
1256
|
});
|
|
1145
|
-
ctx.effect(() => async () => {
|
|
1146
|
-
await manager.dispose();
|
|
1147
|
-
await domain.close();
|
|
1148
|
-
}, "web-search-manager.dispose");
|
|
1149
1257
|
ctx.provide("webSearchManager", manager);
|
|
1150
|
-
ctx.
|
|
1151
|
-
|
|
1258
|
+
ctx.on("credentials/reference-updated", () => {
|
|
1259
|
+
manager.refresh();
|
|
1260
|
+
});
|
|
1261
|
+
ctx.on(WEB_PROVIDER_AVAILABILITY_EVENT, () => {
|
|
1262
|
+
manager.refresh();
|
|
1263
|
+
});
|
|
1264
|
+
ctx.effect(() => registerBuiltins(ctx.web, {
|
|
1265
|
+
settings: () => manager.status().settings,
|
|
1266
|
+
resolveCredential: async (ref) => (await host.credentials.resolve(credentialRef(ref)))?.value
|
|
1267
|
+
}), "web-search-manager.providers");
|
|
1268
|
+
ctx.effect(() => registerHostRpc$1(host, WEB_SEARCH_RPC_CHANNEL, async (endpoint, payload, signal) => {
|
|
1152
1269
|
try {
|
|
1153
1270
|
signal.throwIfAborted();
|
|
1154
1271
|
if (endpoint === "status") return {
|