@klarkxy/dsh-web-search-manager 0.1.11 → 0.2.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.
- package/README.md +28 -10
- package/cordis.patch.yml +1 -8
- package/docs/README.zh-CN.md +28 -10
- package/lib/client.inner.cjs +507 -83
- package/lib/client.inner.cjs.map +1 -1
- package/lib/client.js +508 -245
- package/lib/contracts.d.ts +19 -2
- package/lib/contracts.js +13 -1
- package/lib/contracts.js.map +1 -1
- package/lib/index.d.ts +18 -3
- package/lib/index.js +488 -331
- package/lib/index.js.map +1 -1
- package/package.json +23 -22
package/lib/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
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
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
7
|
import { isIP } from "node:net";
|
|
8
|
+
import { registerHostRpc } from "@klarkxy/dsh-plugin-kit/host-rpc";
|
|
8
9
|
import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
|
|
9
10
|
import { z } from "zod";
|
|
10
11
|
//#region src/jina-fetch.ts
|
|
@@ -547,225 +548,331 @@ var TavilySearchProvider = class {
|
|
|
547
548
|
}
|
|
548
549
|
};
|
|
549
550
|
//#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
|
|
551
|
+
//#region src/native-providers.ts
|
|
552
|
+
function validate(descriptor) {
|
|
553
|
+
if (!/^[a-z][a-z0-9-]{0,63}$/.test(descriptor.id) || !descriptor.label.trim()) throw new WebError("供应商标识或名称无效。", "WEB_INVALID_PROVIDER");
|
|
554
|
+
if (descriptor.credentialRef && !/^[A-Z][A-Z0-9_]{1,127}$/.test(descriptor.credentialRef)) throw new WebError("供应商凭据引用无效。", "WEB_INVALID_PROVIDER");
|
|
555
|
+
if (descriptor.defaultBaseURL) validateBaseURL(descriptor.defaultBaseURL);
|
|
556
|
+
for (const [value, label] of [[descriptor.signupUrl, "注册地址"], [descriptor.pricingUrl, "费用说明地址"]]) {
|
|
557
|
+
if (!value) continue;
|
|
558
|
+
try {
|
|
559
|
+
const url = new URL(value);
|
|
560
|
+
if (url.protocol !== "https:" || url.username || url.password) throw new Error();
|
|
561
|
+
} catch {
|
|
562
|
+
throw new WebError(`${label}须为 HTTPS。`, "WEB_INVALID_PROVIDER");
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
async function configuredCall(kind, descriptor, configuration, signal, run) {
|
|
567
|
+
let apiKey;
|
|
568
|
+
try {
|
|
569
|
+
signal?.throwIfAborted();
|
|
570
|
+
apiKey = descriptor.credentialRef ? await configuration.resolveCredential(descriptor.credentialRef) : void 0;
|
|
571
|
+
signal?.throwIfAborted();
|
|
572
|
+
if (descriptor.credentialRef && !apiKey?.trim()) throw new WebError("搜索凭据已删除或不可用,请重新配置。", "WEB_CREDENTIAL_MISSING");
|
|
573
|
+
const settings = configuration.settings();
|
|
574
|
+
return await run({
|
|
575
|
+
apiKey,
|
|
576
|
+
baseURL: settings.endpoints[providerKey(kind, descriptor.id)] ?? descriptor.defaultBaseURL,
|
|
577
|
+
timeoutMs: settings.timeoutMs,
|
|
578
|
+
maxFetchChars: settings.maxFetchChars
|
|
672
579
|
});
|
|
580
|
+
} catch (error) {
|
|
581
|
+
throw providerError(error, apiKey ? [apiKey] : []);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
/** Bundle-owned adapters register through the same native ctx.web entry as any plugin. */
|
|
585
|
+
function registerNativeSearch(web, configuration, descriptor, factory) {
|
|
586
|
+
validate(descriptor);
|
|
587
|
+
return web.registerSearchProvider({
|
|
588
|
+
id: descriptor.id,
|
|
589
|
+
dshWebManagement: Object.freeze({
|
|
590
|
+
...descriptor,
|
|
591
|
+
configurationOwner: WEB_MANAGER_OWNER
|
|
592
|
+
}),
|
|
593
|
+
available: () => true,
|
|
594
|
+
search: (request, signal) => configuredCall("search", descriptor, configuration, signal, (options) => {
|
|
595
|
+
const provider = factory(options);
|
|
596
|
+
if (!provider.available()) throw new WebError("供应商配置不可用。", "WEB_PROVIDER_ERROR");
|
|
597
|
+
return provider.search(request, signal);
|
|
598
|
+
})
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
function registerNativeFetch(web, configuration, descriptor, factory) {
|
|
602
|
+
validate(descriptor);
|
|
603
|
+
return web.registerFetchProvider({
|
|
604
|
+
id: descriptor.id,
|
|
605
|
+
dshWebManagement: Object.freeze({
|
|
606
|
+
...descriptor,
|
|
607
|
+
configurationOwner: WEB_MANAGER_OWNER
|
|
608
|
+
}),
|
|
609
|
+
available: () => true,
|
|
610
|
+
fetch: (request, signal) => configuredCall("fetch", descriptor, configuration, signal, (options) => {
|
|
611
|
+
const provider = factory(options);
|
|
612
|
+
if (!provider.available()) throw new WebError("供应商配置不可用。", "WEB_PROVIDER_ERROR");
|
|
613
|
+
return provider.fetch(request, signal);
|
|
614
|
+
})
|
|
673
615
|
});
|
|
674
|
-
return () => {
|
|
675
|
-
offHttp();
|
|
676
|
-
offTavily();
|
|
677
|
-
offFirecrawl();
|
|
678
|
-
offSerper();
|
|
679
|
-
offBocha();
|
|
680
|
-
offBrave();
|
|
681
|
-
offExa();
|
|
682
|
-
offDeepSeek();
|
|
683
|
-
offEngine();
|
|
684
|
-
};
|
|
685
616
|
}
|
|
686
617
|
//#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();
|
|
618
|
+
//#region src/web-registry.ts
|
|
619
|
+
const owner = Symbol.for("@klarkxy/dsh-web-search-manager/registry-owner");
|
|
620
|
+
/** The only private-host boundary. Verified against DSH 0.2.0-rc.2.
|
|
621
|
+
* Keep the actual Maps: Cordis effects capture them for automatic unregistration.
|
|
622
|
+
* Replace this bridge when the host exposes enumeration/selection publicly.
|
|
623
|
+
*/
|
|
624
|
+
function runtimeOf(web) {
|
|
625
|
+
const runtime = web;
|
|
626
|
+
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");
|
|
627
|
+
return runtime;
|
|
628
|
+
}
|
|
629
|
+
function hasWebProvider(web, kind, id) {
|
|
630
|
+
return runtimeOf(web)[kind === "search" ? "searchProviders" : "fetchProviders"].has(id);
|
|
631
|
+
}
|
|
632
|
+
function observeWebRegistry(web, hooks) {
|
|
633
|
+
const runtime = runtimeOf(web);
|
|
634
|
+
const maps = [runtime.searchProviders, runtime.fetchProviders];
|
|
635
|
+
if (maps.some((map) => Object.hasOwn(map, owner))) throw new WebError("ctx.web 已由另一个搜索管理器管理。", "WEB_REGISTRY_MANAGED");
|
|
636
|
+
const undo = [];
|
|
637
|
+
try {
|
|
638
|
+
for (const [kind, map] of [["search", maps[0]], ["fetch", maps[1]]]) {
|
|
639
|
+
Object.defineProperty(map, owner, {
|
|
640
|
+
value: true,
|
|
641
|
+
configurable: true
|
|
719
642
|
});
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
643
|
+
undo.push(() => {
|
|
644
|
+
delete map[owner];
|
|
645
|
+
});
|
|
646
|
+
const originals = /* @__PURE__ */ new Map();
|
|
647
|
+
const set = map.set, remove = map.delete, clear = map.clear;
|
|
648
|
+
const wrap = (id, provider) => {
|
|
649
|
+
const wrapped = hooks.add(kind, provider);
|
|
650
|
+
originals.set(id, {
|
|
651
|
+
original: provider,
|
|
652
|
+
wrapped
|
|
653
|
+
});
|
|
654
|
+
return wrapped;
|
|
655
|
+
};
|
|
656
|
+
undo.push(() => {
|
|
657
|
+
for (const [id, row] of originals) if (map.get(id) === row.wrapped) set.call(map, id, row.original);
|
|
658
|
+
});
|
|
659
|
+
for (const [id, provider] of map) set.call(map, id, wrap(id, provider));
|
|
660
|
+
const replacements = {
|
|
661
|
+
set(id, provider) {
|
|
662
|
+
const wrapped = wrap(id, provider);
|
|
663
|
+
set.call(map, id, wrapped);
|
|
664
|
+
return map;
|
|
665
|
+
},
|
|
666
|
+
delete(id) {
|
|
667
|
+
const removed = remove.call(map, id);
|
|
668
|
+
if (removed) {
|
|
669
|
+
originals.delete(id);
|
|
670
|
+
hooks.remove(kind, id);
|
|
731
671
|
}
|
|
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");
|
|
672
|
+
return removed;
|
|
673
|
+
},
|
|
674
|
+
clear() {
|
|
675
|
+
const ids = [...map.keys()];
|
|
676
|
+
clear.call(map);
|
|
677
|
+
originals.clear();
|
|
678
|
+
for (const id of ids) hooks.remove(kind, id);
|
|
765
679
|
}
|
|
680
|
+
};
|
|
681
|
+
for (const key of [
|
|
682
|
+
"set",
|
|
683
|
+
"delete",
|
|
684
|
+
"clear"
|
|
685
|
+
]) {
|
|
686
|
+
const before = Object.getOwnPropertyDescriptor(map, key);
|
|
687
|
+
Object.defineProperty(map, key, {
|
|
688
|
+
value: replacements[key],
|
|
689
|
+
configurable: true,
|
|
690
|
+
writable: true
|
|
691
|
+
});
|
|
692
|
+
undo.push(() => {
|
|
693
|
+
if (map[key] !== replacements[key]) return;
|
|
694
|
+
if (before) Object.defineProperty(map, key, before);
|
|
695
|
+
else delete map[key];
|
|
696
|
+
});
|
|
766
697
|
}
|
|
767
698
|
}
|
|
768
|
-
|
|
699
|
+
for (const key of ["searchProviderId", "fetchProviderId"]) {
|
|
700
|
+
const before = Object.getOwnPropertyDescriptor(runtime, key);
|
|
701
|
+
Object.defineProperty(runtime, key, {
|
|
702
|
+
...before,
|
|
703
|
+
value: void 0
|
|
704
|
+
});
|
|
705
|
+
undo.push(() => {
|
|
706
|
+
if (runtime[key] === void 0) Object.defineProperty(runtime, key, before);
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
} catch (error) {
|
|
710
|
+
for (const restore of undo.reverse()) restore();
|
|
711
|
+
throw new WebError("无法接管 ctx.web 注册表,已恢复原状。", "WEB_REGISTRY_UNSUPPORTED", { cause: error });
|
|
712
|
+
}
|
|
713
|
+
let stopped = false;
|
|
714
|
+
return () => {
|
|
715
|
+
if (stopped) return;
|
|
716
|
+
stopped = true;
|
|
717
|
+
for (const restore of undo.reverse()) restore();
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
//#endregion
|
|
721
|
+
//#region src/builtins.ts
|
|
722
|
+
function registerBuiltins(web, configuration) {
|
|
723
|
+
const registered = [];
|
|
724
|
+
const registerSearch = (descriptor, factory) => {
|
|
725
|
+
if (descriptor.legacyId && hasWebProvider(web, "search", descriptor.legacyId)) return () => {};
|
|
726
|
+
if (hasWebProvider(web, "search", descriptor.id)) return () => {};
|
|
727
|
+
const off = registerNativeSearch(web, configuration, descriptor, factory);
|
|
728
|
+
registered.push(off);
|
|
729
|
+
return off;
|
|
730
|
+
};
|
|
731
|
+
const registerFetch = (descriptor, factory) => {
|
|
732
|
+
if (descriptor.legacyId && hasWebProvider(web, "fetch", descriptor.legacyId)) return () => {};
|
|
733
|
+
if (hasWebProvider(web, "fetch", descriptor.id)) return () => {};
|
|
734
|
+
const off = registerNativeFetch(web, configuration, descriptor, factory);
|
|
735
|
+
registered.push(off);
|
|
736
|
+
return off;
|
|
737
|
+
};
|
|
738
|
+
try {
|
|
739
|
+
registerSearch({
|
|
740
|
+
id: "ddg",
|
|
741
|
+
label: "DuckDuckGo",
|
|
742
|
+
billing: "none",
|
|
743
|
+
description: "无需注册。默认后端。"
|
|
744
|
+
}, () => new SearchEngineProvider());
|
|
745
|
+
registerSearch({
|
|
746
|
+
id: "deepseek-managed",
|
|
747
|
+
legacyId: "deepseek-official",
|
|
748
|
+
label: "DeepSeek 搜索",
|
|
749
|
+
description: "使用已配置的 DeepSeek API Key,可能产生额外搜索费用。",
|
|
750
|
+
defaultBaseURL: DEEPSEEK_DEFAULT_BASE_URL,
|
|
751
|
+
credentialRef: "DEEPSEEK_API_KEY",
|
|
752
|
+
credentialShared: true,
|
|
753
|
+
credentialHint: "与模型设置共用 Key",
|
|
754
|
+
billing: "model-and-tools",
|
|
755
|
+
signupUrl: "https://platform.deepseek.com/api_keys"
|
|
756
|
+
}, (options) => new DeepSeekSearchProvider(() => ({
|
|
757
|
+
apiKey: options.apiKey,
|
|
758
|
+
baseURL: options.baseURL ?? DEEPSEEK_DEFAULT_BASE_URL,
|
|
759
|
+
model: DEEPSEEK_DEFAULT_MODEL,
|
|
760
|
+
apiVersion: DEEPSEEK_DEFAULT_API_VERSION,
|
|
761
|
+
maxTokens: DEEPSEEK_DEFAULT_MAX_TOKENS,
|
|
762
|
+
maxUses: DEEPSEEK_DEFAULT_MAX_USES
|
|
763
|
+
})));
|
|
764
|
+
registerSearch({
|
|
765
|
+
id: "exa",
|
|
766
|
+
label: "Exa",
|
|
767
|
+
description: "独立 Search API。",
|
|
768
|
+
defaultBaseURL: "https://api.exa.ai",
|
|
769
|
+
credentialRef: "DSH_EDITOR_WEB_EXA_API_KEY",
|
|
770
|
+
billing: "request",
|
|
771
|
+
signupUrl: "https://dashboard.exa.ai/api-keys"
|
|
772
|
+
}, (options) => new ExaSearchProvider({
|
|
773
|
+
apiKey: options.apiKey ?? "",
|
|
774
|
+
baseURL: options.baseURL ?? "https://api.exa.ai",
|
|
775
|
+
searchType: "auto",
|
|
776
|
+
highlightsPerResult: 1
|
|
777
|
+
}));
|
|
778
|
+
registerSearch({
|
|
779
|
+
id: "brave",
|
|
780
|
+
label: "Brave",
|
|
781
|
+
description: "Brave Search API。",
|
|
782
|
+
defaultBaseURL: "https://api.search.brave.com",
|
|
783
|
+
credentialRef: "DSH_EDITOR_WEB_BRAVE_API_KEY",
|
|
784
|
+
billing: "request",
|
|
785
|
+
signupUrl: "https://api.search.brave.com"
|
|
786
|
+
}, (options) => new BraveSearchProvider({
|
|
787
|
+
apiKey: options.apiKey ?? "",
|
|
788
|
+
baseURL: options.baseURL
|
|
789
|
+
}));
|
|
790
|
+
registerSearch({
|
|
791
|
+
id: "bocha",
|
|
792
|
+
label: "博查",
|
|
793
|
+
description: "中文网页搜索 API。",
|
|
794
|
+
defaultBaseURL: "https://api.bochaai.com",
|
|
795
|
+
credentialRef: "DSH_EDITOR_WEB_BOCHA_API_KEY",
|
|
796
|
+
billing: "request",
|
|
797
|
+
signupUrl: "https://open.bochaai.com"
|
|
798
|
+
}, (options) => new BochaSearchProvider({
|
|
799
|
+
apiKey: options.apiKey ?? "",
|
|
800
|
+
baseURL: options.baseURL
|
|
801
|
+
}));
|
|
802
|
+
registerSearch({
|
|
803
|
+
id: "serper",
|
|
804
|
+
label: "Serper",
|
|
805
|
+
description: "Google 结果的 Search API。",
|
|
806
|
+
defaultBaseURL: "https://google.serper.dev",
|
|
807
|
+
credentialRef: "DSH_EDITOR_WEB_SERPER_API_KEY",
|
|
808
|
+
billing: "request",
|
|
809
|
+
signupUrl: "https://serper.dev"
|
|
810
|
+
}, (options) => new SerperSearchProvider({
|
|
811
|
+
apiKey: options.apiKey ?? "",
|
|
812
|
+
baseURL: options.baseURL
|
|
813
|
+
}));
|
|
814
|
+
registerSearch({
|
|
815
|
+
id: "firecrawl",
|
|
816
|
+
label: "Firecrawl",
|
|
817
|
+
description: "网页搜索与提取 API。",
|
|
818
|
+
defaultBaseURL: "https://api.firecrawl.dev",
|
|
819
|
+
credentialRef: "DSH_EDITOR_WEB_FIRECRAWL_API_KEY",
|
|
820
|
+
billing: "request",
|
|
821
|
+
signupUrl: "https://www.firecrawl.dev"
|
|
822
|
+
}, (options) => new FirecrawlSearchProvider({
|
|
823
|
+
apiKey: options.apiKey ?? "",
|
|
824
|
+
baseURL: options.baseURL
|
|
825
|
+
}));
|
|
826
|
+
registerSearch({
|
|
827
|
+
id: "tavily",
|
|
828
|
+
label: "Tavily",
|
|
829
|
+
defaultBaseURL: "https://api.tavily.com",
|
|
830
|
+
description: "Search API;固定 basic 检索,不自动升级搜索深度。",
|
|
831
|
+
credentialRef: "DSH_EDITOR_WEB_TAVILY_API_KEY",
|
|
832
|
+
billing: "request",
|
|
833
|
+
signupUrl: "https://app.tavily.com"
|
|
834
|
+
}, (options) => new TavilySearchProvider({
|
|
835
|
+
apiKey: options.apiKey ?? "",
|
|
836
|
+
baseURL: options.baseURL
|
|
837
|
+
}));
|
|
838
|
+
registerFetch({
|
|
839
|
+
id: "http-managed",
|
|
840
|
+
legacyId: "http",
|
|
841
|
+
label: "网页读取",
|
|
842
|
+
billing: "none",
|
|
843
|
+
description: "优先匿名 Jina Reader 读取,失败后本机 HTTP 直连;无需 Key。"
|
|
844
|
+
}, (options) => {
|
|
845
|
+
const limits = {
|
|
846
|
+
maxResponseBytes: 5e6,
|
|
847
|
+
maxBodyChars: options.maxFetchChars,
|
|
848
|
+
timeoutMs: options.timeoutMs,
|
|
849
|
+
maxRedirects: 5,
|
|
850
|
+
userAgent: "dsh-editor/managed-web"
|
|
851
|
+
};
|
|
852
|
+
const readerTimeoutMs = Math.min(15e3, Math.floor(options.timeoutMs / 2));
|
|
853
|
+
return new JinaFirstFetchProvider({
|
|
854
|
+
reader: new HttpFetchProvider({
|
|
855
|
+
...limits,
|
|
856
|
+
maxBodyChars: limits.maxResponseBytes,
|
|
857
|
+
timeoutMs: readerTimeoutMs
|
|
858
|
+
}),
|
|
859
|
+
direct: new HttpFetchProvider(limits),
|
|
860
|
+
readerTimeoutMs
|
|
861
|
+
});
|
|
862
|
+
});
|
|
863
|
+
} catch (error) {
|
|
864
|
+
for (const off of registered.reverse()) off();
|
|
865
|
+
throw error;
|
|
866
|
+
}
|
|
867
|
+
return () => {
|
|
868
|
+
for (const off of registered.splice(0).reverse()) off();
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
//#endregion
|
|
872
|
+
//#region src/host-rpc.ts
|
|
873
|
+
/** Keep the historical transport error text while sharing admission and request handling. */
|
|
874
|
+
function registerHostRpc$1(ctx, channel, call) {
|
|
875
|
+
return registerHostRpc(ctx, channel, call, { errorBody: (status) => status === 500 ? "network settings request failed" : void 0 });
|
|
769
876
|
}
|
|
770
877
|
//#endregion
|
|
771
878
|
//#region src/manager.ts
|
|
@@ -805,6 +912,9 @@ var WebSearchManager = class {
|
|
|
805
912
|
suspended = false;
|
|
806
913
|
disposed = false;
|
|
807
914
|
storageFailed = false;
|
|
915
|
+
refreshRevision = 0;
|
|
916
|
+
detach;
|
|
917
|
+
legacyRegistrations = /* @__PURE__ */ new Set();
|
|
808
918
|
constructor(options) {
|
|
809
919
|
this.options = options;
|
|
810
920
|
const initial = structuredClone(options.initial ?? defaultSettings());
|
|
@@ -813,6 +923,18 @@ var WebSearchManager = class {
|
|
|
813
923
|
...initial,
|
|
814
924
|
searchOrder: migrateSearchOrder(initial)
|
|
815
925
|
};
|
|
926
|
+
this.detach = observeWebRegistry(options.web, {
|
|
927
|
+
add: (kind, provider) => this.discover(kind, provider),
|
|
928
|
+
remove: (kind, id) => {
|
|
929
|
+
const key = providerKey(kind, id);
|
|
930
|
+
const entry = this.entries.get(key);
|
|
931
|
+
if (entry) {
|
|
932
|
+
this.abort(entry);
|
|
933
|
+
this.entries.delete(key);
|
|
934
|
+
this.notify();
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
});
|
|
816
938
|
}
|
|
817
939
|
subscribe(listener) {
|
|
818
940
|
this.listeners.add(listener);
|
|
@@ -832,7 +954,15 @@ var WebSearchManager = class {
|
|
|
832
954
|
return Boolean(this.entries.get(providerKey("search", id))?.descriptor.credentialRef);
|
|
833
955
|
}
|
|
834
956
|
configured(id) {
|
|
835
|
-
|
|
957
|
+
const entry = this.entries.get(providerKey("search", id));
|
|
958
|
+
return Boolean(entry && this.ready(entry));
|
|
959
|
+
}
|
|
960
|
+
ready(entry) {
|
|
961
|
+
try {
|
|
962
|
+
return entry.configured && entry.provider.available();
|
|
963
|
+
} catch {
|
|
964
|
+
return false;
|
|
965
|
+
}
|
|
836
966
|
}
|
|
837
967
|
searchOrder(settings = this.settings) {
|
|
838
968
|
return resolveSearchOrder(this.knownSearchIds(), settings, (id) => this.keyed(id));
|
|
@@ -841,7 +971,8 @@ var WebSearchManager = class {
|
|
|
841
971
|
return pickActiveSearch(this.searchOrder(settings), (id) => this.configured(id));
|
|
842
972
|
}
|
|
843
973
|
selected(entry) {
|
|
844
|
-
if (this.disposed || this.suspended || !entry
|
|
974
|
+
if (this.disposed || this.suspended || !this.ready(entry)) return false;
|
|
975
|
+
if (this.entries.get(providerKey(entry.kind, entry.descriptor.id)) !== entry) return false;
|
|
845
976
|
if (entry.kind === "fetch") return this.settings.fetchEnabled && this.settings.fetchProvider === entry.descriptor.id && this.entries.get(providerKey("fetch", entry.descriptor.id)) === entry;
|
|
846
977
|
return this.settings.searchEnabled && this.activeSearchId() === entry.descriptor.id;
|
|
847
978
|
}
|
|
@@ -855,8 +986,9 @@ var WebSearchManager = class {
|
|
|
855
986
|
providers: entries.map((entry) => ({
|
|
856
987
|
...entry.descriptor,
|
|
857
988
|
kind: entry.kind,
|
|
858
|
-
configured: entry
|
|
859
|
-
|
|
989
|
+
configured: this.ready(entry),
|
|
990
|
+
configurationOwned: entry.descriptor.configurationOwner === WEB_MANAGER_OWNER,
|
|
991
|
+
baseURL: entry.descriptor.configurationOwner === "@klarkxy/dsh-web-search-manager" ? this.settings.endpoints[providerKey(entry.kind, entry.descriptor.id)] ?? entry.descriptor.defaultBaseURL : void 0,
|
|
860
992
|
calls: entry.calls,
|
|
861
993
|
failures: entry.failures
|
|
862
994
|
})).sort((a, b) => providerKey(a.kind, a.id).localeCompare(providerKey(b.kind, b.id)))
|
|
@@ -864,18 +996,20 @@ var WebSearchManager = class {
|
|
|
864
996
|
}
|
|
865
997
|
async refresh() {
|
|
866
998
|
const epoch = this.epoch;
|
|
999
|
+
const revision = ++this.refreshRevision;
|
|
867
1000
|
await Promise.all([...this.entries.entries()].map(async ([key, entry]) => {
|
|
868
|
-
let configured =
|
|
869
|
-
if (entry.descriptor.credentialRef) try {
|
|
1001
|
+
let configured = true;
|
|
1002
|
+
if (entry.descriptor.configurationOwner === "@klarkxy/dsh-web-search-manager" && entry.descriptor.credentialRef) try {
|
|
870
1003
|
configured = Boolean((await this.options.resolveCredential(entry.descriptor.credentialRef))?.trim());
|
|
871
1004
|
} catch {
|
|
872
1005
|
configured = false;
|
|
873
1006
|
}
|
|
874
|
-
if (this.disposed || epoch !== this.epoch || this.entries.get(key) !== entry) return;
|
|
1007
|
+
if (this.disposed || epoch !== this.epoch || revision !== this.refreshRevision || this.entries.get(key) !== entry) return;
|
|
875
1008
|
if (entry.configured !== configured) {
|
|
876
1009
|
entry.configured = configured;
|
|
877
1010
|
if (!configured) this.abort(entry);
|
|
878
1011
|
}
|
|
1012
|
+
if (!this.ready(entry)) this.abort(entry);
|
|
879
1013
|
}));
|
|
880
1014
|
this.notify();
|
|
881
1015
|
return this.status();
|
|
@@ -883,99 +1017,97 @@ var WebSearchManager = class {
|
|
|
883
1017
|
abort(entry) {
|
|
884
1018
|
for (const target of entry ? [entry] : this.entries.values()) for (const controller of target.active) controller.abort();
|
|
885
1019
|
}
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
const
|
|
889
|
-
if (
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1020
|
+
discover(kind, provider) {
|
|
1021
|
+
const key = providerKey(kind, provider.id);
|
|
1022
|
+
const old = this.entries.get(key);
|
|
1023
|
+
if (old) this.abort(old);
|
|
1024
|
+
const metadata = provider.dshWebManagement;
|
|
1025
|
+
const descriptor = {
|
|
1026
|
+
id: provider.id,
|
|
1027
|
+
label: typeof metadata?.label === "string" && metadata.label ? metadata.label : provider.id,
|
|
1028
|
+
description: typeof metadata?.description === "string" ? metadata.description : "",
|
|
1029
|
+
billing: [
|
|
1030
|
+
"none",
|
|
1031
|
+
"request",
|
|
1032
|
+
"model-and-tools"
|
|
1033
|
+
].includes(metadata?.billing ?? "") ? metadata.billing : "unknown",
|
|
1034
|
+
configurationOwner: typeof metadata?.configurationOwner === "string" && metadata.configurationOwner ? metadata.configurationOwner : void 0,
|
|
1035
|
+
configurationUrl: safeConfigurationUrl(metadata?.configurationUrl)
|
|
1036
|
+
};
|
|
1037
|
+
for (const field of [
|
|
1038
|
+
"credentialHint",
|
|
1039
|
+
"pricing",
|
|
1040
|
+
"defaultBaseURL"
|
|
1041
|
+
]) if (typeof metadata?.[field] === "string") descriptor[field] = metadata[field];
|
|
1042
|
+
if (typeof metadata?.credentialRef === "string" && /^[A-Z][A-Z0-9_]{1,127}$/.test(metadata.credentialRef)) descriptor.credentialRef = metadata.credentialRef;
|
|
1043
|
+
if (metadata?.credentialShared === true) descriptor.credentialShared = true;
|
|
1044
|
+
for (const field of ["signupUrl", "pricingUrl"]) try {
|
|
1045
|
+
const url = new URL(metadata?.[field] ?? "");
|
|
1046
|
+
if (url.protocol === "https:" && !url.username && !url.password) descriptor[field] = metadata[field];
|
|
1047
|
+
} catch {}
|
|
902
1048
|
const entry = {
|
|
903
|
-
descriptor: Object.freeze(
|
|
1049
|
+
descriptor: Object.freeze(descriptor),
|
|
904
1050
|
kind,
|
|
905
|
-
|
|
1051
|
+
provider,
|
|
1052
|
+
configured: descriptor.configurationOwner !== "@klarkxy/dsh-web-search-manager" || !descriptor.credentialRef,
|
|
906
1053
|
calls: 0,
|
|
907
1054
|
failures: 0,
|
|
908
|
-
unregister() {},
|
|
909
1055
|
active: /* @__PURE__ */ new Set()
|
|
910
1056
|
};
|
|
911
1057
|
this.entries.set(key, entry);
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
1058
|
+
const legacy = metadata?.legacyId;
|
|
1059
|
+
const expectedLegacy = kind === "search" && provider.id === "deepseek-managed" ? "deepseek-official" : kind === "fetch" && provider.id === "http-managed" ? "http" : void 0;
|
|
1060
|
+
if (descriptor.configurationOwner === "@klarkxy/dsh-web-search-manager" && legacy === expectedLegacy && legacy && !this.entries.has(providerKey(kind, legacy))) {
|
|
1061
|
+
if (kind === "search") {
|
|
1062
|
+
this.settings.searchOrder = this.settings.searchOrder.map((id) => id === legacy ? provider.id : id);
|
|
1063
|
+
if (this.settings.searchProvider === legacy) this.settings.searchProvider = provider.id;
|
|
1064
|
+
} else if (this.settings.fetchProvider === legacy) this.settings.fetchProvider = provider.id;
|
|
1065
|
+
const legacyKey = providerKey(kind, legacy);
|
|
1066
|
+
if (this.settings.endpoints[legacyKey] && !this.settings.endpoints[key]) this.settings.endpoints[key] = this.settings.endpoints[legacyKey];
|
|
921
1067
|
}
|
|
922
1068
|
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,
|
|
1069
|
+
if (kind === "search") {
|
|
1070
|
+
const search = provider;
|
|
1071
|
+
return {
|
|
1072
|
+
id: provider.id,
|
|
1073
|
+
available: () => this.selected(entry),
|
|
1074
|
+
search: (request, signal) => {
|
|
1075
|
+
if (typeof request.query !== "string" || !request.query.trim() || request.query.length > 4e3) fail("WEB_INVALID_REQUEST", "查询须为 1 至 4000 字符。");
|
|
1076
|
+
const count = Math.min(bounded(request.maxResults ?? this.settings.maxResults, 1, 100, "结果数"), this.settings.maxResults);
|
|
1077
|
+
return this.execute(entry, signal, async (_options, combined) => {
|
|
1078
|
+
const result = await search.search({
|
|
1079
|
+
...request,
|
|
1080
|
+
query: request.query.trim(),
|
|
1081
|
+
maxResults: count
|
|
1082
|
+
}, combined);
|
|
1083
|
+
const sources = result.sources.slice(0, count).filter((source) => {
|
|
1084
|
+
try {
|
|
1085
|
+
const url = new URL(source.url);
|
|
1086
|
+
return ["http:", "https:"].includes(url.protocol) && !url.username && !url.password;
|
|
1087
|
+
} catch {
|
|
1088
|
+
return false;
|
|
1089
|
+
}
|
|
1090
|
+
}).map((source) => ({
|
|
1091
|
+
...source,
|
|
1092
|
+
title: source.title?.slice(0, 1e3),
|
|
1093
|
+
snippet: source.snippet?.slice(0, 8e3)
|
|
1094
|
+
}));
|
|
1095
|
+
return {
|
|
1096
|
+
...result,
|
|
1097
|
+
content: result.content?.slice(0, 4e4),
|
|
1098
|
+
sources,
|
|
1099
|
+
truncated: result.truncated || result.sources.length > count
|
|
1100
|
+
};
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
const fetch = provider;
|
|
1106
|
+
return {
|
|
1107
|
+
id: provider.id,
|
|
974
1108
|
available: () => this.selected(entry),
|
|
975
1109
|
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);
|
|
1110
|
+
const result = await fetch.fetch(request, combined);
|
|
979
1111
|
const content = result.body.content.slice(0, options.maxFetchChars);
|
|
980
1112
|
return {
|
|
981
1113
|
...result,
|
|
@@ -986,7 +1118,31 @@ var WebSearchManager = class {
|
|
|
986
1118
|
truncated: result.truncated || content.length !== result.body.content.length
|
|
987
1119
|
};
|
|
988
1120
|
})
|
|
989
|
-
}
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
/** @deprecated Provider plugins should register directly with ctx.web. */
|
|
1124
|
+
registerSearchProvider(descriptor, factory) {
|
|
1125
|
+
return this.legacyRegistration(() => registerNativeSearch(this.options.web, this.providerConfiguration(), descriptor, factory));
|
|
1126
|
+
}
|
|
1127
|
+
/** @deprecated Provider plugins should register directly with ctx.web. */
|
|
1128
|
+
registerFetchProvider(descriptor, factory) {
|
|
1129
|
+
return this.legacyRegistration(() => registerNativeFetch(this.options.web, this.providerConfiguration(), descriptor, factory));
|
|
1130
|
+
}
|
|
1131
|
+
providerConfiguration() {
|
|
1132
|
+
return {
|
|
1133
|
+
settings: () => structuredClone(this.settings),
|
|
1134
|
+
resolveCredential: (ref) => this.options.resolveCredential(ref)
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
legacyRegistration(register) {
|
|
1138
|
+
if (this.disposed) fail("WEB_DISABLED", "网络搜索插件已停止。");
|
|
1139
|
+
const unregister = register();
|
|
1140
|
+
const off = () => {
|
|
1141
|
+
this.legacyRegistrations.delete(off);
|
|
1142
|
+
unregister();
|
|
1143
|
+
};
|
|
1144
|
+
this.legacyRegistrations.add(off);
|
|
1145
|
+
return off;
|
|
990
1146
|
}
|
|
991
1147
|
async execute(entry, signal, run) {
|
|
992
1148
|
if (!this.selected(entry)) fail("WEB_DISABLED", "请在设置 → 网络搜索中配置凭据并启用。");
|
|
@@ -996,19 +1152,9 @@ var WebSearchManager = class {
|
|
|
996
1152
|
const epoch = this.epoch;
|
|
997
1153
|
entry.active.add(controller);
|
|
998
1154
|
let started = false;
|
|
999
|
-
let apiKey;
|
|
1000
1155
|
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
1156
|
if (epoch !== this.epoch || !this.selected(entry)) fail("WEB_DISABLED", "网络访问设置已改变,请重新发起请求。");
|
|
1010
1157
|
const options = {
|
|
1011
|
-
apiKey,
|
|
1012
1158
|
baseURL: this.settings.endpoints[providerKey(entry.kind, entry.descriptor.id)] ?? entry.descriptor.defaultBaseURL,
|
|
1013
1159
|
timeoutMs: this.settings.timeoutMs,
|
|
1014
1160
|
maxFetchChars: this.settings.maxFetchChars
|
|
@@ -1020,8 +1166,13 @@ var WebSearchManager = class {
|
|
|
1020
1166
|
return result;
|
|
1021
1167
|
} catch (error) {
|
|
1022
1168
|
if (started) entry.failures += 1;
|
|
1169
|
+
if (error instanceof WebError && error.code === "WEB_CREDENTIAL_MISSING") {
|
|
1170
|
+
entry.configured = false;
|
|
1171
|
+
this.abort(entry);
|
|
1172
|
+
this.notify();
|
|
1173
|
+
}
|
|
1023
1174
|
if (combined.aborted && !(error instanceof WebError && error.code === "WEB_CREDENTIAL_MISSING")) throw new WebError("网络请求已取消或超时。", "WEB_ABORTED");
|
|
1024
|
-
const safe = providerError(error
|
|
1175
|
+
const safe = providerError(error);
|
|
1025
1176
|
throw new WebError(`[${entry.kind}:${entry.descriptor.id}] ${safe.message}`, safe.code);
|
|
1026
1177
|
} finally {
|
|
1027
1178
|
clearTimeout(timeout);
|
|
@@ -1042,7 +1193,7 @@ var WebSearchManager = class {
|
|
|
1042
1193
|
bounded(proposed.maxFetchChars, 1e3, 2e5, "正文长度");
|
|
1043
1194
|
for (const [key, value] of Object.entries(proposed.endpoints)) {
|
|
1044
1195
|
const entry = this.entries.get(key);
|
|
1045
|
-
if (entry && !entry.descriptor.defaultBaseURL) fail("WEB_INVALID_CONFIG", "供应商不支持自定义地址。");
|
|
1196
|
+
if (entry && (entry.descriptor.configurationOwner !== "@klarkxy/dsh-web-search-manager" || !entry.descriptor.defaultBaseURL) && value !== this.settings.endpoints[key]) fail("WEB_INVALID_CONFIG", "供应商不支持自定义地址。");
|
|
1046
1197
|
try {
|
|
1047
1198
|
proposed.endpoints[key] = validateBaseURL(value);
|
|
1048
1199
|
} catch {
|
|
@@ -1050,18 +1201,13 @@ var WebSearchManager = class {
|
|
|
1050
1201
|
}
|
|
1051
1202
|
}
|
|
1052
1203
|
await this.refresh();
|
|
1053
|
-
proposed.searchOrder =
|
|
1204
|
+
proposed.searchOrder = [...new Set(proposed.searchOrder)];
|
|
1054
1205
|
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;
|
|
1206
|
+
const activatingSearch = !this.settings.searchEnabled || JSON.stringify(proposed.searchOrder) !== JSON.stringify(this.settings.searchOrder);
|
|
1207
|
+
if (proposed.searchEnabled && !proposed.searchProvider && activatingSearch) fail("WEB_CREDENTIAL_MISSING", "需选择已安装的供应商并填写有效凭据。");
|
|
1061
1208
|
if (proposed.fetchEnabled) {
|
|
1062
1209
|
const entry = this.entries.get(providerKey("fetch", proposed.fetchProvider));
|
|
1063
|
-
if (!
|
|
1064
|
-
else if (!entry.configured) fail("WEB_CREDENTIAL_MISSING", "需选择已安装的供应商并填写有效凭据。");
|
|
1210
|
+
if ((!this.settings.fetchEnabled || proposed.fetchProvider !== this.settings.fetchProvider) && (!entry || !this.ready(entry))) fail("WEB_CREDENTIAL_MISSING", "需选择已安装的供应商并填写有效凭据。");
|
|
1065
1211
|
}
|
|
1066
1212
|
this.suspended = true;
|
|
1067
1213
|
this.epoch += 1;
|
|
@@ -1088,10 +1234,11 @@ var WebSearchManager = class {
|
|
|
1088
1234
|
this.epoch += 1;
|
|
1089
1235
|
this.abort();
|
|
1090
1236
|
this.notify();
|
|
1091
|
-
for (const
|
|
1237
|
+
for (const off of [...this.legacyRegistrations]) off();
|
|
1238
|
+
await this.pending;
|
|
1239
|
+
this.detach();
|
|
1092
1240
|
this.entries.clear();
|
|
1093
1241
|
this.listeners.clear();
|
|
1094
|
-
await this.pending;
|
|
1095
1242
|
}
|
|
1096
1243
|
};
|
|
1097
1244
|
//#endregion
|
|
@@ -1135,20 +1282,30 @@ const inject = [
|
|
|
1135
1282
|
async function apply(ctx) {
|
|
1136
1283
|
const host = ctx;
|
|
1137
1284
|
const domain = await ctx.storageDomain.open(webSettingsDomain);
|
|
1285
|
+
let managed;
|
|
1286
|
+
ctx.effect(() => async () => {
|
|
1287
|
+
await managed?.dispose();
|
|
1288
|
+
await domain.close();
|
|
1289
|
+
}, "web-search-manager.dispose");
|
|
1138
1290
|
const table = domain.table("settings");
|
|
1139
|
-
const manager = new WebSearchManager({
|
|
1291
|
+
const manager = managed = new WebSearchManager({
|
|
1140
1292
|
web: ctx.web,
|
|
1141
1293
|
initial: table.get("global"),
|
|
1142
1294
|
resolveCredential: async (ref) => (await host.credentials.resolve(credentialRef(ref)))?.value,
|
|
1143
1295
|
save: (settings) => table.put("global", settings)
|
|
1144
1296
|
});
|
|
1145
|
-
ctx.effect(() => async () => {
|
|
1146
|
-
await manager.dispose();
|
|
1147
|
-
await domain.close();
|
|
1148
|
-
}, "web-search-manager.dispose");
|
|
1149
1297
|
ctx.provide("webSearchManager", manager);
|
|
1150
|
-
ctx.
|
|
1151
|
-
|
|
1298
|
+
ctx.on("credentials/reference-updated", () => {
|
|
1299
|
+
manager.refresh();
|
|
1300
|
+
});
|
|
1301
|
+
ctx.on("web/provider-availability-updated", () => {
|
|
1302
|
+
manager.refresh();
|
|
1303
|
+
});
|
|
1304
|
+
ctx.effect(() => registerBuiltins(ctx.web, {
|
|
1305
|
+
settings: () => manager.status().settings,
|
|
1306
|
+
resolveCredential: async (ref) => (await host.credentials.resolve(credentialRef(ref)))?.value
|
|
1307
|
+
}), "web-search-manager.providers");
|
|
1308
|
+
ctx.effect(() => registerHostRpc$1(host, WEB_SEARCH_RPC_CHANNEL, async (endpoint, payload, signal) => {
|
|
1152
1309
|
try {
|
|
1153
1310
|
signal.throwIfAborted();
|
|
1154
1311
|
if (endpoint === "status") return {
|