@klarkxy/dsh-web-search-manager 0.2.5 → 0.3.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 +16 -10
- package/docs/README.zh-CN.md +14 -8
- package/lib/client.inner.cjs +513 -491
- package/lib/client.inner.cjs.map +1 -1
- package/lib/client.js +513 -491
- package/lib/contracts.d.ts +7 -1
- package/lib/contracts.js +6 -1
- package/lib/contracts.js.map +1 -1
- package/lib/index.d.ts +7 -2
- package/lib/index.js +287 -62
- package/lib/index.js.map +1 -1
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +15 -12
package/lib/contracts.d.ts
CHANGED
|
@@ -40,6 +40,7 @@ interface WebSettings {
|
|
|
40
40
|
searchProvider: string;
|
|
41
41
|
fetchProvider: string;
|
|
42
42
|
searchOrder: string[];
|
|
43
|
+
fetchOrder: string[];
|
|
43
44
|
maxResults: number;
|
|
44
45
|
maxQueries: number;
|
|
45
46
|
timeoutMs: number;
|
|
@@ -54,6 +55,11 @@ declare function migrateSearchOrder(settings: {
|
|
|
54
55
|
searchOrder?: string[];
|
|
55
56
|
searchProvider?: string;
|
|
56
57
|
}): string[];
|
|
58
|
+
/** Missing order is a legacy single selection; an empty selection never opts in. */
|
|
59
|
+
declare function migrateFetchOrder(settings: {
|
|
60
|
+
fetchOrder?: string[];
|
|
61
|
+
fetchProvider?: string;
|
|
62
|
+
}): string[];
|
|
57
63
|
declare function resolveSearchOrder(knownIds: readonly string[], settings: Pick<WebSettings, 'searchOrder' | 'searchProvider'>, _keyed: (id: string) => boolean): string[];
|
|
58
64
|
declare function pickActiveSearch(order: readonly string[], configured: (id: string) => boolean): string;
|
|
59
65
|
interface ProviderView extends ProviderDescriptor {
|
|
@@ -76,5 +82,5 @@ declare function providerKey(kind: ProviderKind, id: string): string;
|
|
|
76
82
|
declare function safeConfigurationUrl(value: unknown): string | undefined;
|
|
77
83
|
declare function validateBaseURL(value: string): string;
|
|
78
84
|
//#endregion
|
|
79
|
-
export { DEFAULT_SEARCH_ORDER, DiscoverableFetchProvider, DiscoverableSearchProvider, FetchProviderFactory, ProviderDescriptor, ProviderKind, ProviderOptions, ProviderView, RpcResult, SearchProviderFactory, WEB_MANAGER_OWNER, WEB_SEARCH_RPC_CHANNEL, WebSettings, WebStatus, defaultSearchOrder, defaultSettings, migrateSearchOrder, pickActiveSearch, providerKey, resolveSearchOrder, safeConfigurationUrl, validateBaseURL };
|
|
85
|
+
export { DEFAULT_SEARCH_ORDER, DiscoverableFetchProvider, DiscoverableSearchProvider, FetchProviderFactory, ProviderDescriptor, ProviderKind, ProviderOptions, ProviderView, RpcResult, SearchProviderFactory, WEB_MANAGER_OWNER, WEB_SEARCH_RPC_CHANNEL, WebSettings, WebStatus, defaultSearchOrder, defaultSettings, migrateFetchOrder, migrateSearchOrder, pickActiveSearch, providerKey, resolveSearchOrder, safeConfigurationUrl, validateBaseURL };
|
|
80
86
|
//# sourceMappingURL=contracts.d.ts.map
|
package/lib/contracts.js
CHANGED
|
@@ -9,6 +9,7 @@ const defaultSettings = () => ({
|
|
|
9
9
|
searchProvider: "ddg",
|
|
10
10
|
fetchProvider: "http",
|
|
11
11
|
searchOrder: [...DEFAULT_SEARCH_ORDER],
|
|
12
|
+
fetchOrder: ["http"],
|
|
12
13
|
maxResults: 5,
|
|
13
14
|
maxQueries: 1,
|
|
14
15
|
timeoutMs: 3e4,
|
|
@@ -22,6 +23,10 @@ function defaultSearchOrder(ids, keyed) {
|
|
|
22
23
|
function migrateSearchOrder(settings) {
|
|
23
24
|
return settings.searchOrder ?? (settings.searchProvider ? [settings.searchProvider] : [...DEFAULT_SEARCH_ORDER]);
|
|
24
25
|
}
|
|
26
|
+
/** Missing order is a legacy single selection; an empty selection never opts in. */
|
|
27
|
+
function migrateFetchOrder(settings) {
|
|
28
|
+
return settings.fetchOrder ?? (settings.fetchProvider ? [settings.fetchProvider] : []);
|
|
29
|
+
}
|
|
25
30
|
function resolveSearchOrder(knownIds, settings, _keyed) {
|
|
26
31
|
return [...new Set(settings.searchOrder.filter((id) => knownIds.includes(id)))];
|
|
27
32
|
}
|
|
@@ -48,6 +53,6 @@ function validateBaseURL(value) {
|
|
|
48
53
|
return url.href.replace(/\/+$/, "");
|
|
49
54
|
}
|
|
50
55
|
//#endregion
|
|
51
|
-
export { DEFAULT_SEARCH_ORDER, WEB_MANAGER_OWNER, WEB_SEARCH_RPC_CHANNEL, defaultSearchOrder, defaultSettings, migrateSearchOrder, pickActiveSearch, providerKey, resolveSearchOrder, safeConfigurationUrl, validateBaseURL };
|
|
56
|
+
export { DEFAULT_SEARCH_ORDER, WEB_MANAGER_OWNER, WEB_SEARCH_RPC_CHANNEL, defaultSearchOrder, defaultSettings, migrateFetchOrder, migrateSearchOrder, pickActiveSearch, providerKey, resolveSearchOrder, safeConfigurationUrl, validateBaseURL };
|
|
52
57
|
|
|
53
58
|
//# sourceMappingURL=contracts.js.map
|
package/lib/contracts.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contracts.js","names":[],"sources":["../src/contracts.ts"],"sourcesContent":["import type { WebFetchProvider, WebSearchProvider } from '@deepseek-ai/dsh-web'\nimport type { DshWebManagementDescriptor } from '@klarkxy/dsh-plugin-kit/contracts'\n\nexport const WEB_SEARCH_RPC_CHANNEL = '/web-search-manager'\nexport type RpcResult<T = unknown> = { ok: true; value: T } | {\n ok: false; error: { code: string; message: string; details: Record<string, unknown> }\n}\nexport type ProviderKind = 'search' | 'fetch'\n/** Management metadata only. Field contract lives in plugin-kit; execution stays with ctx.web. */\nexport interface ProviderDescriptor extends DshWebManagementDescriptor {}\nexport const WEB_MANAGER_OWNER = '@klarkxy/dsh-web-search-manager'\n/** Optional display metadata on a provider registered directly with ctx.web. */\nexport type DiscoverableSearchProvider = WebSearchProvider & { readonly dshWebManagement?: ProviderDescriptor }\nexport type DiscoverableFetchProvider = WebFetchProvider & { readonly dshWebManagement?: ProviderDescriptor }\nexport interface ProviderOptions {\n readonly apiKey?: string\n readonly baseURL?: string\n readonly timeoutMs: number\n readonly maxFetchChars: number\n}\nexport type SearchProviderFactory = (options: ProviderOptions) => WebSearchProvider\nexport type FetchProviderFactory = (options: ProviderOptions) => WebFetchProvider\n/** Never store credential values here. Keys are resolved by reference per call. */\nexport interface WebSettings {\n revision: number\n searchEnabled: boolean\n fetchEnabled: boolean\n searchProvider: string\n fetchProvider: string\n searchOrder: string[]\n maxResults: number\n maxQueries: number\n timeoutMs: number\n maxFetchChars: number\n endpoints: Record<string, string>\n}\nexport const DEFAULT_SEARCH_ORDER = ['ddg'] as const\n\nexport const defaultSettings = (): WebSettings => ({\n revision: 0, searchEnabled: true, fetchEnabled: true,\n searchProvider: 'ddg', fetchProvider: 'http', searchOrder: [...DEFAULT_SEARCH_ORDER],\n maxResults: 5, maxQueries: 1,\n timeoutMs: 30_000, maxFetchChars: 100_000, endpoints: {},\n})\n\n/** Keyed backends first, keyless last — same idea as OpenClaw auto-detect. */\nexport function defaultSearchOrder(ids: readonly string[], keyed: (id: string) => boolean): string[] {\n return [...ids.filter(keyed), ...ids.filter(id => !keyed(id))]\n}\n\nexport function migrateSearchOrder(settings: { searchOrder?: string[]; searchProvider?: string }): string[] {\n return settings.searchOrder ?? (settings.searchProvider ? [settings.searchProvider] : [...DEFAULT_SEARCH_ORDER])\n}\n\nexport function resolveSearchOrder(\n knownIds: readonly string[],\n settings: Pick<WebSettings, 'searchOrder' | 'searchProvider'>,\n _keyed: (id: string) => boolean,\n): string[] {\n return [...new Set(settings.searchOrder.filter(id => knownIds.includes(id)))]\n}\n\nexport function pickActiveSearch(order: readonly string[], configured: (id: string) => boolean): string {\n return order.find(configured) ?? ''\n}\nexport interface ProviderView extends ProviderDescriptor {\n kind: ProviderKind\n configured: boolean\n /** True only for configuration explicitly owned by this manager bundle. */\n configurationOwned?: boolean\n baseURL?: string\n calls: number\n failures: number\n}\nexport interface WebStatus {\n settings: WebSettings\n providers: ProviderView[]\n searchActive: boolean\n fetchActive: boolean\n storageFailed: boolean\n}\nexport function providerKey(kind: ProviderKind, id: string): string { return `${kind}:${id}` }\nexport function safeConfigurationUrl(value: unknown): string | undefined {\n if (typeof value !== 'string' || !value || /[\\u0000-\\u0020\\\\]/.test(value)) return\n try {\n const url = new URL(value, 'https://dsh.invalid')\n if (url.username || url.password) return\n if ((value.startsWith('/') && !value.startsWith('//')) || value.startsWith('#')) {\n return url.origin === 'https://dsh.invalid' ? value : undefined\n }\n return /^https:\\/\\//i.test(value) && url.protocol === 'https:' ? value : undefined\n } catch { return }\n}\nexport function validateBaseURL(value: string): string {\n const url = new URL(value)\n if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash) {\n throw new Error('供应商地址必须是 HTTPS,且不能包含用户名、密码、查询参数或片段。')\n }\n return url.href.replace(/\\/+$/, '')\n}\n"],"mappings":";AAGA,MAAa,yBAAyB;AAOtC,MAAa,oBAAoB;
|
|
1
|
+
{"version":3,"file":"contracts.js","names":[],"sources":["../src/contracts.ts"],"sourcesContent":["import type { WebFetchProvider, WebSearchProvider } from '@deepseek-ai/dsh-web'\nimport type { DshWebManagementDescriptor } from '@klarkxy/dsh-plugin-kit/contracts'\n\nexport const WEB_SEARCH_RPC_CHANNEL = '/web-search-manager'\nexport type RpcResult<T = unknown> = { ok: true; value: T } | {\n ok: false; error: { code: string; message: string; details: Record<string, unknown> }\n}\nexport type ProviderKind = 'search' | 'fetch'\n/** Management metadata only. Field contract lives in plugin-kit; execution stays with ctx.web. */\nexport interface ProviderDescriptor extends DshWebManagementDescriptor {}\nexport const WEB_MANAGER_OWNER = '@klarkxy/dsh-web-search-manager'\n/** Optional display metadata on a provider registered directly with ctx.web. */\nexport type DiscoverableSearchProvider = WebSearchProvider & { readonly dshWebManagement?: ProviderDescriptor }\nexport type DiscoverableFetchProvider = WebFetchProvider & { readonly dshWebManagement?: ProviderDescriptor }\nexport interface ProviderOptions {\n readonly apiKey?: string\n readonly baseURL?: string\n readonly timeoutMs: number\n readonly maxFetchChars: number\n}\nexport type SearchProviderFactory = (options: ProviderOptions) => WebSearchProvider\nexport type FetchProviderFactory = (options: ProviderOptions) => WebFetchProvider\n/** Never store credential values here. Keys are resolved by reference per call. */\nexport interface WebSettings {\n revision: number\n searchEnabled: boolean\n fetchEnabled: boolean\n searchProvider: string\n fetchProvider: string\n searchOrder: string[]\n fetchOrder: string[]\n maxResults: number\n maxQueries: number\n timeoutMs: number\n maxFetchChars: number\n endpoints: Record<string, string>\n}\nexport const DEFAULT_SEARCH_ORDER = ['ddg'] as const\n\nexport const defaultSettings = (): WebSettings => ({\n revision: 0, searchEnabled: true, fetchEnabled: true,\n searchProvider: 'ddg', fetchProvider: 'http', searchOrder: [...DEFAULT_SEARCH_ORDER], fetchOrder: ['http'],\n maxResults: 5, maxQueries: 1,\n timeoutMs: 30_000, maxFetchChars: 100_000, endpoints: {},\n})\n\n/** Keyed backends first, keyless last — same idea as OpenClaw auto-detect. */\nexport function defaultSearchOrder(ids: readonly string[], keyed: (id: string) => boolean): string[] {\n return [...ids.filter(keyed), ...ids.filter(id => !keyed(id))]\n}\n\nexport function migrateSearchOrder(settings: { searchOrder?: string[]; searchProvider?: string }): string[] {\n return settings.searchOrder ?? (settings.searchProvider ? [settings.searchProvider] : [...DEFAULT_SEARCH_ORDER])\n}\n\n/** Missing order is a legacy single selection; an empty selection never opts in. */\nexport function migrateFetchOrder(settings: { fetchOrder?: string[]; fetchProvider?: string }): string[] {\n return settings.fetchOrder ?? (settings.fetchProvider ? [settings.fetchProvider] : [])\n}\n\nexport function resolveSearchOrder(\n knownIds: readonly string[],\n settings: Pick<WebSettings, 'searchOrder' | 'searchProvider'>,\n _keyed: (id: string) => boolean,\n): string[] {\n return [...new Set(settings.searchOrder.filter(id => knownIds.includes(id)))]\n}\n\nexport function pickActiveSearch(order: readonly string[], configured: (id: string) => boolean): string {\n return order.find(configured) ?? ''\n}\nexport interface ProviderView extends ProviderDescriptor {\n kind: ProviderKind\n configured: boolean\n /** True only for configuration explicitly owned by this manager bundle. */\n configurationOwned?: boolean\n baseURL?: string\n calls: number\n failures: number\n}\nexport interface WebStatus {\n settings: WebSettings\n providers: ProviderView[]\n searchActive: boolean\n fetchActive: boolean\n storageFailed: boolean\n}\nexport function providerKey(kind: ProviderKind, id: string): string { return `${kind}:${id}` }\nexport function safeConfigurationUrl(value: unknown): string | undefined {\n if (typeof value !== 'string' || !value || /[\\u0000-\\u0020\\\\]/.test(value)) return\n try {\n const url = new URL(value, 'https://dsh.invalid')\n if (url.username || url.password) return\n if ((value.startsWith('/') && !value.startsWith('//')) || value.startsWith('#')) {\n return url.origin === 'https://dsh.invalid' ? value : undefined\n }\n return /^https:\\/\\//i.test(value) && url.protocol === 'https:' ? value : undefined\n } catch { return }\n}\nexport function validateBaseURL(value: string): string {\n const url = new URL(value)\n if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash) {\n throw new Error('供应商地址必须是 HTTPS,且不能包含用户名、密码、查询参数或片段。')\n }\n return url.href.replace(/\\/+$/, '')\n}\n"],"mappings":";AAGA,MAAa,yBAAyB;AAOtC,MAAa,oBAAoB;AA2BjC,MAAa,uBAAuB,CAAC,KAAK;AAE1C,MAAa,yBAAsC;CACjD,UAAU;CAAG,eAAe;CAAM,cAAc;CAChD,gBAAgB;CAAO,eAAe;CAAQ,aAAa,CAAC,GAAG,oBAAoB;CAAG,YAAY,CAAC,MAAM;CACzG,YAAY;CAAG,YAAY;CAC3B,WAAW;CAAQ,eAAe;CAAS,WAAW,CAAC;AACzD;;AAGA,SAAgB,mBAAmB,KAAwB,OAA0C;CACnG,OAAO,CAAC,GAAG,IAAI,OAAO,KAAK,GAAG,GAAG,IAAI,QAAO,OAAM,CAAC,MAAM,EAAE,CAAC,CAAC;AAC/D;AAEA,SAAgB,mBAAmB,UAAyE;CAC1G,OAAO,SAAS,gBAAgB,SAAS,iBAAiB,CAAC,SAAS,cAAc,IAAI,CAAC,GAAG,oBAAoB;AAChH;;AAGA,SAAgB,kBAAkB,UAAuE;CACvG,OAAO,SAAS,eAAe,SAAS,gBAAgB,CAAC,SAAS,aAAa,IAAI,CAAC;AACtF;AAEA,SAAgB,mBACd,UACA,UACA,QACU;CACV,OAAO,CAAC,GAAG,IAAI,IAAI,SAAS,YAAY,QAAO,OAAM,SAAS,SAAS,EAAE,CAAC,CAAC,CAAC;AAC9E;AAEA,SAAgB,iBAAiB,OAA0B,YAA6C;CACtG,OAAO,MAAM,KAAK,UAAU,KAAK;AACnC;AAiBA,SAAgB,YAAY,MAAoB,IAAoB;CAAE,OAAO,GAAG,KAAK,GAAG;AAAK;AAC7F,SAAgB,qBAAqB,OAAoC;CACvE,IAAI,OAAO,UAAU,YAAY,CAAC,SAAS,oBAAoB,KAAK,KAAK,GAAG;CAC5E,IAAI;EACF,MAAM,MAAM,IAAI,IAAI,OAAO,qBAAqB;EAChD,IAAI,IAAI,YAAY,IAAI,UAAU;EAClC,IAAK,MAAM,WAAW,GAAG,KAAK,CAAC,MAAM,WAAW,IAAI,KAAM,MAAM,WAAW,GAAG,GAC5E,OAAO,IAAI,WAAW,wBAAwB,QAAQ,KAAA;EAExD,OAAO,eAAe,KAAK,KAAK,KAAK,IAAI,aAAa,WAAW,QAAQ,KAAA;CAC3E,QAAQ;EAAE;CAAO;AACnB;AACA,SAAgB,gBAAgB,OAAuB;CACrD,MAAM,MAAM,IAAI,IAAI,KAAK;CACzB,IAAI,IAAI,aAAa,YAAY,IAAI,YAAY,IAAI,YAAY,IAAI,UAAU,IAAI,MACjF,MAAM,IAAI,MAAM,qCAAqC;CAEvD,OAAO,IAAI,KAAK,QAAQ,QAAQ,EAAE;AACpC"}
|
package/lib/index.d.ts
CHANGED
|
@@ -11,8 +11,9 @@ interface WebRegistry {
|
|
|
11
11
|
//#region src/manager.d.ts
|
|
12
12
|
interface ManagerOptions {
|
|
13
13
|
web: WebRegistry;
|
|
14
|
-
initial?: Omit<WebSettings, 'searchOrder'> & {
|
|
14
|
+
initial?: Omit<WebSettings, 'searchOrder' | 'fetchOrder'> & {
|
|
15
15
|
searchOrder?: string[];
|
|
16
|
+
fetchOrder?: string[];
|
|
16
17
|
};
|
|
17
18
|
resolveCredential(ref: string): Promise<string | undefined>;
|
|
18
19
|
save(settings: WebSettings): Promise<void>;
|
|
@@ -31,6 +32,7 @@ declare class WebSearchManager {
|
|
|
31
32
|
private refreshRevision;
|
|
32
33
|
private detach;
|
|
33
34
|
private legacyRegistrations;
|
|
35
|
+
private work;
|
|
34
36
|
constructor(options: ManagerOptions);
|
|
35
37
|
subscribe(listener: () => void): () => void;
|
|
36
38
|
private notify;
|
|
@@ -40,6 +42,7 @@ declare class WebSearchManager {
|
|
|
40
42
|
private ready;
|
|
41
43
|
private searchOrder;
|
|
42
44
|
private activeSearchId;
|
|
45
|
+
private activeFetchId;
|
|
43
46
|
private selected;
|
|
44
47
|
status(): WebStatus;
|
|
45
48
|
refresh(): Promise<WebStatus>;
|
|
@@ -52,7 +55,9 @@ declare class WebSearchManager {
|
|
|
52
55
|
private providerConfiguration;
|
|
53
56
|
private legacyRegistration;
|
|
54
57
|
private execute;
|
|
55
|
-
update(next: Omit<WebSettings, 'revision'
|
|
58
|
+
update(next: Omit<WebSettings, 'revision' | 'fetchOrder'> & {
|
|
59
|
+
fetchOrder?: string[];
|
|
60
|
+
}, expectedRevision: number): Promise<WebStatus>;
|
|
56
61
|
dispose(): Promise<void>;
|
|
57
62
|
}
|
|
58
63
|
//#endregion
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { WEB_MANAGER_OWNER, WEB_SEARCH_RPC_CHANNEL, defaultSettings, migrateSearchOrder, pickActiveSearch, providerKey, resolveSearchOrder, safeConfigurationUrl, validateBaseURL } from "./contracts.js";
|
|
1
|
+
import { WEB_MANAGER_OWNER, WEB_SEARCH_RPC_CHANNEL, defaultSettings, migrateFetchOrder, 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 { WEB_PROVIDER_AVAILABILITY_EVENT } from "@klarkxy/dsh-plugin-kit/contracts";
|
|
@@ -40,26 +40,19 @@ function readerContent(result) {
|
|
|
40
40
|
if (!marker || /^Warning:/im.test(content.slice(0, marker.index))) return void 0;
|
|
41
41
|
return content.slice(marker.index + marker[0].length).trim() ? content : void 0;
|
|
42
42
|
}
|
|
43
|
-
/**
|
|
43
|
+
/** Abort the Reader deadline and wait for its transport to release resources before fallback. */
|
|
44
44
|
async function readWithTimeout(read, timeoutMs, parent) {
|
|
45
45
|
parent?.throwIfAborted();
|
|
46
46
|
const controller = new AbortController();
|
|
47
47
|
const signal = parent ? AbortSignal.any([parent, controller.signal]) : controller.signal;
|
|
48
48
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
49
|
-
let off = () => {};
|
|
50
|
-
const aborted = new Promise((_, reject) => {
|
|
51
|
-
const onAbort = () => reject(signal.reason);
|
|
52
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
53
|
-
off = () => signal.removeEventListener("abort", onAbort);
|
|
54
|
-
});
|
|
55
49
|
try {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
50
|
+
signal.throwIfAborted();
|
|
51
|
+
const result = await read(signal);
|
|
52
|
+
signal.throwIfAborted();
|
|
53
|
+
return result;
|
|
60
54
|
} finally {
|
|
61
55
|
clearTimeout(timer);
|
|
62
|
-
off();
|
|
63
56
|
}
|
|
64
57
|
}
|
|
65
58
|
/** One managed fetch route: anonymous Jina Reader first, existing safe HTTP transport second. */
|
|
@@ -417,6 +410,57 @@ const SerperSearchProvider = createRestSearch({
|
|
|
417
410
|
return Array.isArray(organic) ? organic.flatMap((row) => sourceFrom(row, "link", "title", "snippet") ?? []) : [];
|
|
418
411
|
}
|
|
419
412
|
});
|
|
413
|
+
const PerplexitySearchProvider = createRestSearch({
|
|
414
|
+
id: "perplexity",
|
|
415
|
+
fallback: "https://api.perplexity.ai",
|
|
416
|
+
path: "/search",
|
|
417
|
+
method: "POST",
|
|
418
|
+
headers: (apiKey) => ({
|
|
419
|
+
authorization: `Bearer ${apiKey}`,
|
|
420
|
+
"content-type": "application/json"
|
|
421
|
+
}),
|
|
422
|
+
body: ({ query, count }) => ({
|
|
423
|
+
query,
|
|
424
|
+
max_results: count,
|
|
425
|
+
search_type: "web"
|
|
426
|
+
}),
|
|
427
|
+
parse(payload) {
|
|
428
|
+
const results = record(payload)?.results;
|
|
429
|
+
if (!Array.isArray(results)) throw new Error("Perplexity returned no results array.");
|
|
430
|
+
return results.flatMap((row) => sourceFrom(row, "url", "title", "snippet") ?? []);
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
const YouSearchProvider = createRestSearch({
|
|
434
|
+
id: "you",
|
|
435
|
+
fallback: "https://ydc-index.io",
|
|
436
|
+
path: "/v1/search",
|
|
437
|
+
method: "POST",
|
|
438
|
+
headers: (apiKey) => ({
|
|
439
|
+
"x-api-key": apiKey,
|
|
440
|
+
"content-type": "application/json"
|
|
441
|
+
}),
|
|
442
|
+
body: ({ query, count }) => ({
|
|
443
|
+
query,
|
|
444
|
+
count
|
|
445
|
+
}),
|
|
446
|
+
parse(payload) {
|
|
447
|
+
const results = record(record(payload)?.results);
|
|
448
|
+
if (!results || !Array.isArray(results.web) && !Array.isArray(results.news)) throw new Error("You.com returned no search results arrays.");
|
|
449
|
+
const rows = [...Array.isArray(results.web) ? results.web : [], ...Array.isArray(results.news) ? results.news : []];
|
|
450
|
+
const seen = /* @__PURE__ */ new Set();
|
|
451
|
+
return rows.flatMap((raw) => {
|
|
452
|
+
const row = record(raw);
|
|
453
|
+
const snippets = Array.isArray(row?.snippets) ? row.snippets.filter((value) => typeof value === "string").join("\n") : "";
|
|
454
|
+
const source = sourceFrom({
|
|
455
|
+
...row,
|
|
456
|
+
snippet: snippets || row?.description
|
|
457
|
+
}, "url", "title", "snippet");
|
|
458
|
+
if (!source || seen.has(source.url)) return [];
|
|
459
|
+
seen.add(source.url);
|
|
460
|
+
return [source];
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
});
|
|
420
464
|
const FirecrawlSearchProvider = createRestSearch({
|
|
421
465
|
id: "firecrawl",
|
|
422
466
|
fallback: "https://api.firecrawl.dev",
|
|
@@ -436,6 +480,76 @@ const FirecrawlSearchProvider = createRestSearch({
|
|
|
436
480
|
}
|
|
437
481
|
});
|
|
438
482
|
//#endregion
|
|
483
|
+
//#region src/firecrawl-fetch.ts
|
|
484
|
+
function pageURL(value) {
|
|
485
|
+
if (typeof value !== "string") throw new WebError("Firecrawl 未返回最终网页地址。", "WEB_PROVIDER_ERROR");
|
|
486
|
+
const url = new URL(value);
|
|
487
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) throw new WebError("网页地址无效。", "WEB_INVALID_URL");
|
|
488
|
+
return url.href;
|
|
489
|
+
}
|
|
490
|
+
/** https://docs.firecrawl.dev/api-reference/endpoint/scrape — API HTTP status is not the page status. */
|
|
491
|
+
var FirecrawlFetchProvider = class {
|
|
492
|
+
options;
|
|
493
|
+
id = "firecrawl";
|
|
494
|
+
constructor(options) {
|
|
495
|
+
this.options = options;
|
|
496
|
+
}
|
|
497
|
+
available() {
|
|
498
|
+
try {
|
|
499
|
+
httpsEndpoint(this.options.baseURL ?? "https://api.firecrawl.dev", "/v2/scrape");
|
|
500
|
+
return Boolean(this.options.apiKey.trim());
|
|
501
|
+
} catch {
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
async fetch(request, signal) {
|
|
506
|
+
if (!this.available()) throw new WebError("Firecrawl 配置不可用。", "WEB_CREDENTIAL_MISSING");
|
|
507
|
+
try {
|
|
508
|
+
signal?.throwIfAborted();
|
|
509
|
+
const url = pageURL(request.url);
|
|
510
|
+
const response = await (this.options.fetch ?? fetch)(httpsEndpoint(this.options.baseURL ?? "https://api.firecrawl.dev", "/v2/scrape"), {
|
|
511
|
+
method: "POST",
|
|
512
|
+
redirect: "error",
|
|
513
|
+
signal,
|
|
514
|
+
headers: {
|
|
515
|
+
accept: "application/json",
|
|
516
|
+
authorization: `Bearer ${this.options.apiKey}`,
|
|
517
|
+
"content-type": "application/json"
|
|
518
|
+
},
|
|
519
|
+
body: JSON.stringify({
|
|
520
|
+
url,
|
|
521
|
+
formats: ["markdown"],
|
|
522
|
+
onlyMainContent: true,
|
|
523
|
+
skipTlsVerification: false
|
|
524
|
+
})
|
|
525
|
+
});
|
|
526
|
+
if (!response.ok) {
|
|
527
|
+
await response.body?.cancel().catch(() => {});
|
|
528
|
+
throw new HttpStatusError(response.status);
|
|
529
|
+
}
|
|
530
|
+
const payload = record(await readJSON(response, signal));
|
|
531
|
+
signal?.throwIfAborted();
|
|
532
|
+
const data = record(payload?.data), metadata = record(data?.metadata);
|
|
533
|
+
const statusCode = metadata?.statusCode;
|
|
534
|
+
if (payload?.success !== true || typeof data?.markdown !== "string" || typeof statusCode !== "number" || !Number.isInteger(statusCode) || statusCode < 100 || statusCode > 599) throw new WebError("Firecrawl 未返回正文和真实网页状态码。", "WEB_PROVIDER_ERROR");
|
|
535
|
+
const finalURL = pageURL(metadata?.url);
|
|
536
|
+
const limit = this.options.maxFetchChars ?? 1e5;
|
|
537
|
+
return {
|
|
538
|
+
url: finalURL,
|
|
539
|
+
statusCode,
|
|
540
|
+
body: {
|
|
541
|
+
kind: "text",
|
|
542
|
+
content: data.markdown.slice(0, limit)
|
|
543
|
+
},
|
|
544
|
+
truncated: data.markdown.length > limit
|
|
545
|
+
};
|
|
546
|
+
} catch (error) {
|
|
547
|
+
if (signal?.aborted) throw new WebError("网页读取已取消。", "WEB_ABORTED");
|
|
548
|
+
throw providerError(error, [this.options.apiKey]);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
//#endregion
|
|
439
553
|
//#region src/tavily.ts
|
|
440
554
|
/** Direct REST adapter. No auxiliary LLM, retries, anonymous fallback or auto-depth. */
|
|
441
555
|
var TavilySearchProvider = class {
|
|
@@ -577,18 +691,81 @@ function registerNativeFetch(web, configuration, descriptor, factory) {
|
|
|
577
691
|
//#endregion
|
|
578
692
|
//#region src/web-registry.ts
|
|
579
693
|
const owner = Symbol.for("@klarkxy/dsh-web-search-manager/registry-owner");
|
|
580
|
-
/** The only private-host boundary.
|
|
581
|
-
*
|
|
582
|
-
*
|
|
694
|
+
/** The only private-host boundary. Public ctx.web has no selection API.
|
|
695
|
+
* Old hosts snapshot config/env into own searchProviderId/fetchProviderId.
|
|
696
|
+
* Current hosts read config.searchProvider.get() ?? searchProviderEnv on each call
|
|
697
|
+
* (fetch uses the matching pair). Keep the actual Maps: Cordis effects capture them.
|
|
583
698
|
*/
|
|
584
699
|
function runtimeOf(web) {
|
|
585
700
|
const runtime = web;
|
|
586
|
-
|
|
701
|
+
const maps = runtime.searchProviders instanceof Map && runtime.fetchProviders instanceof Map;
|
|
702
|
+
const legacy = Object.hasOwn(runtime, "searchProviderId") && Object.hasOwn(runtime, "fetchProviderId");
|
|
703
|
+
const current = Object.hasOwn(runtime, "searchProviderEnv") && Object.hasOwn(runtime, "fetchProviderEnv") && typeof runtime.config?.searchProvider?.get === "function" && typeof runtime.config?.fetchProvider?.get === "function";
|
|
704
|
+
if (!maps || !legacy && !current) throw new WebError("当前 ctx.web 不支持注册表管理,请使用兼容的 DSH 版本。", "WEB_REGISTRY_UNSUPPORTED");
|
|
587
705
|
return runtime;
|
|
588
706
|
}
|
|
589
707
|
function hasWebProvider(web, kind, id) {
|
|
590
708
|
return runtimeOf(web)[kind === "search" ? "searchProviders" : "fetchProviders"].has(id);
|
|
591
709
|
}
|
|
710
|
+
/** Hide one volatile selection field without replacing the object the settings UI reads.
|
|
711
|
+
* The host builds these with cosmokit's createVolatile and freezes them, so `get` is an own
|
|
712
|
+
* non-configurable data property that a get trap may not answer differently. Return a standalone
|
|
713
|
+
* facade instead of wrapping the cell: the call path reads one thing from it, config.<field>.get().
|
|
714
|
+
*/
|
|
715
|
+
function selectionView(field) {
|
|
716
|
+
if (!field || typeof field !== "object" && typeof field !== "function") return field;
|
|
717
|
+
return Object.freeze({ get: () => void 0 });
|
|
718
|
+
}
|
|
719
|
+
/** One call sees no host selection. Config, its volatile source, and env stay on the real runtime.
|
|
720
|
+
* Cordis reads own data properties from the service and ignores an Object.create overlay, so this
|
|
721
|
+
* has to be a get trap. Other reads go to the traced receiver and keep that call's context. Config
|
|
722
|
+
* is proxied over an empty target for the same reason as the cell: a host that froze it would make
|
|
723
|
+
* the two selection fields non-configurable own properties, which no get trap may replace.
|
|
724
|
+
*/
|
|
725
|
+
function unmanagedView(runtime, callReceiver) {
|
|
726
|
+
return new Proxy(runtime, { get(target, key, receiver) {
|
|
727
|
+
if (key === "searchProviderId" || key === "fetchProviderId" || key === "searchProviderEnv" || key === "fetchProviderEnv") return void 0;
|
|
728
|
+
if (key === "config") {
|
|
729
|
+
const config = Reflect.get(target, "config", target) ?? {};
|
|
730
|
+
return new Proxy({}, { get(_view, configKey) {
|
|
731
|
+
if (configKey === "searchProvider" || configKey === "fetchProvider") return selectionView(Reflect.get(config, configKey, config));
|
|
732
|
+
return Reflect.get(config, configKey, config);
|
|
733
|
+
} });
|
|
734
|
+
}
|
|
735
|
+
if (callReceiver !== target && callReceiver !== receiver) return Reflect.get(callReceiver, key, callReceiver);
|
|
736
|
+
return Reflect.get(target, key, target);
|
|
737
|
+
} });
|
|
738
|
+
}
|
|
739
|
+
function isThisRuntime(runtime, receiver) {
|
|
740
|
+
if (receiver === runtime) return true;
|
|
741
|
+
try {
|
|
742
|
+
return Reflect.get(receiver, "searchProviders", receiver) === runtime.searchProviders && Reflect.get(receiver, "fetchProviders", receiver) === runtime.fetchProviders;
|
|
743
|
+
} catch {
|
|
744
|
+
return false;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
function installCallView(runtime, key, undo) {
|
|
748
|
+
if (Object.hasOwn(runtime, key)) throw new TypeError(`ctx.web.${key} is already owned by this runtime`);
|
|
749
|
+
let prototype = Object.getPrototypeOf(runtime);
|
|
750
|
+
while (prototype && !Object.hasOwn(prototype, key)) prototype = Object.getPrototypeOf(prototype);
|
|
751
|
+
const inherited = prototype ? Object.getOwnPropertyDescriptor(prototype, key) : void 0;
|
|
752
|
+
if (!inherited || !("value" in inherited) || typeof inherited.value !== "function") throw new TypeError(`ctx.web.${key} is not a replaceable method`);
|
|
753
|
+
const original = inherited.value;
|
|
754
|
+
const wrapped = function(...args) {
|
|
755
|
+
const receiver = isThisRuntime(runtime, this) ? unmanagedView(runtime, this) : this;
|
|
756
|
+
return original.call(receiver, ...args);
|
|
757
|
+
};
|
|
758
|
+
Object.defineProperty(runtime, key, {
|
|
759
|
+
value: wrapped,
|
|
760
|
+
writable: true,
|
|
761
|
+
configurable: true,
|
|
762
|
+
enumerable: false
|
|
763
|
+
});
|
|
764
|
+
undo.push(() => {
|
|
765
|
+
if (Object.getOwnPropertyDescriptor(runtime, key)?.value !== wrapped) return;
|
|
766
|
+
delete runtime[key];
|
|
767
|
+
});
|
|
768
|
+
}
|
|
592
769
|
function observeWebRegistry(web, hooks) {
|
|
593
770
|
const runtime = runtimeOf(web);
|
|
594
771
|
const maps = [runtime.searchProviders, runtime.fetchProviders];
|
|
@@ -656,16 +833,8 @@ function observeWebRegistry(web, hooks) {
|
|
|
656
833
|
});
|
|
657
834
|
}
|
|
658
835
|
}
|
|
659
|
-
|
|
660
|
-
|
|
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
|
-
}
|
|
836
|
+
installCallView(runtime, "search", undo);
|
|
837
|
+
installCallView(runtime, "fetch", undo);
|
|
669
838
|
} catch (error) {
|
|
670
839
|
for (const restore of undo.reverse()) restore();
|
|
671
840
|
throw new WebError("无法接管 ctx.web 注册表,已恢复原状。", "WEB_REGISTRY_UNSUPPORTED", { cause: error });
|
|
@@ -778,6 +947,8 @@ function registerBuiltins(web, configuration) {
|
|
|
778
947
|
defaultBaseURL: "https://api.firecrawl.dev",
|
|
779
948
|
credentialRef: "DSH_EDITOR_WEB_FIRECRAWL_API_KEY",
|
|
780
949
|
billing: "request",
|
|
950
|
+
credentialShared: true,
|
|
951
|
+
credentialHint: "与 Firecrawl 网页读取共用 Key",
|
|
781
952
|
signupUrl: "https://www.firecrawl.dev"
|
|
782
953
|
}, (options) => new FirecrawlSearchProvider({
|
|
783
954
|
apiKey: options.apiKey ?? "",
|
|
@@ -795,6 +966,30 @@ function registerBuiltins(web, configuration) {
|
|
|
795
966
|
apiKey: options.apiKey ?? "",
|
|
796
967
|
baseURL: options.baseURL
|
|
797
968
|
}));
|
|
969
|
+
registerSearch({
|
|
970
|
+
id: "perplexity",
|
|
971
|
+
label: "Perplexity",
|
|
972
|
+
description: "Search API;返回网页来源,不生成回答。",
|
|
973
|
+
defaultBaseURL: "https://api.perplexity.ai",
|
|
974
|
+
credentialRef: "DSH_EDITOR_WEB_PERPLEXITY_API_KEY",
|
|
975
|
+
billing: "request",
|
|
976
|
+
signupUrl: "https://console.perplexity.ai"
|
|
977
|
+
}, (options) => new PerplexitySearchProvider({
|
|
978
|
+
apiKey: options.apiKey ?? "",
|
|
979
|
+
baseURL: options.baseURL
|
|
980
|
+
}));
|
|
981
|
+
registerSearch({
|
|
982
|
+
id: "you",
|
|
983
|
+
label: "You.com",
|
|
984
|
+
description: "网页与新闻搜索;不额外提取全文。",
|
|
985
|
+
defaultBaseURL: "https://ydc-index.io",
|
|
986
|
+
credentialRef: "DSH_EDITOR_WEB_YOU_API_KEY",
|
|
987
|
+
billing: "request",
|
|
988
|
+
signupUrl: "https://you.com/platform"
|
|
989
|
+
}, (options) => new YouSearchProvider({
|
|
990
|
+
apiKey: options.apiKey ?? "",
|
|
991
|
+
baseURL: options.baseURL
|
|
992
|
+
}));
|
|
798
993
|
registerFetch({
|
|
799
994
|
id: "http-managed",
|
|
800
995
|
legacyId: "http",
|
|
@@ -820,6 +1015,21 @@ function registerBuiltins(web, configuration) {
|
|
|
820
1015
|
readerTimeoutMs
|
|
821
1016
|
});
|
|
822
1017
|
});
|
|
1018
|
+
registerFetch({
|
|
1019
|
+
id: "firecrawl",
|
|
1020
|
+
label: "Firecrawl",
|
|
1021
|
+
description: "读取网页 Markdown;与 Firecrawl 搜索共用 Key。",
|
|
1022
|
+
defaultBaseURL: "https://api.firecrawl.dev",
|
|
1023
|
+
credentialRef: "DSH_EDITOR_WEB_FIRECRAWL_API_KEY",
|
|
1024
|
+
billing: "request",
|
|
1025
|
+
credentialShared: true,
|
|
1026
|
+
credentialHint: "与 Firecrawl 搜索共用 Key",
|
|
1027
|
+
signupUrl: "https://www.firecrawl.dev"
|
|
1028
|
+
}, (options) => new FirecrawlFetchProvider({
|
|
1029
|
+
apiKey: options.apiKey ?? "",
|
|
1030
|
+
baseURL: options.baseURL,
|
|
1031
|
+
maxFetchChars: options.maxFetchChars
|
|
1032
|
+
}));
|
|
823
1033
|
} catch (error) {
|
|
824
1034
|
for (const off of registered.reverse()) off();
|
|
825
1035
|
throw error;
|
|
@@ -843,24 +1053,6 @@ function bounded(value, min, max, label) {
|
|
|
843
1053
|
if (!Number.isInteger(value) || value < min || value > max) fail("WEB_INVALID_CONFIG", `${label}须为 ${min} 至 ${max} 的整数。`);
|
|
844
1054
|
return value;
|
|
845
1055
|
}
|
|
846
|
-
/** Enforce cancellation even when an extension ignores its cooperative signal. */
|
|
847
|
-
async function abortable(work, signal) {
|
|
848
|
-
signal.throwIfAborted();
|
|
849
|
-
let off = () => {};
|
|
850
|
-
const aborted = new Promise((_, reject) => {
|
|
851
|
-
const onAbort = () => reject(new WebError("网络请求已取消或超时。", "WEB_ABORTED"));
|
|
852
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
853
|
-
off = () => signal.removeEventListener("abort", onAbort);
|
|
854
|
-
});
|
|
855
|
-
try {
|
|
856
|
-
return await Promise.race([Promise.resolve().then(() => {
|
|
857
|
-
signal.throwIfAborted();
|
|
858
|
-
return work();
|
|
859
|
-
}), aborted]);
|
|
860
|
-
} finally {
|
|
861
|
-
off();
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
1056
|
/** Manages consent and provider lifecycle. Selection and execution stay in ctx.web. */
|
|
865
1057
|
var WebSearchManager = class {
|
|
866
1058
|
options;
|
|
@@ -875,13 +1067,15 @@ var WebSearchManager = class {
|
|
|
875
1067
|
refreshRevision = 0;
|
|
876
1068
|
detach;
|
|
877
1069
|
legacyRegistrations = /* @__PURE__ */ new Set();
|
|
1070
|
+
work = /* @__PURE__ */ new Map();
|
|
878
1071
|
constructor(options) {
|
|
879
1072
|
this.options = options;
|
|
880
1073
|
const initial = structuredClone(options.initial ?? defaultSettings());
|
|
881
1074
|
this.settings = {
|
|
882
1075
|
...defaultSettings(),
|
|
883
1076
|
...initial,
|
|
884
|
-
searchOrder: migrateSearchOrder(initial)
|
|
1077
|
+
searchOrder: migrateSearchOrder(initial),
|
|
1078
|
+
fetchOrder: migrateFetchOrder(initial)
|
|
885
1079
|
};
|
|
886
1080
|
this.detach = observeWebRegistry(options.web, {
|
|
887
1081
|
add: (kind, provider) => this.discover(kind, provider),
|
|
@@ -930,16 +1124,25 @@ var WebSearchManager = class {
|
|
|
930
1124
|
activeSearchId(settings = this.settings) {
|
|
931
1125
|
return pickActiveSearch(this.searchOrder(settings), (id) => this.configured(id));
|
|
932
1126
|
}
|
|
1127
|
+
activeFetchId(settings = this.settings) {
|
|
1128
|
+
return settings.fetchOrder.find((id) => {
|
|
1129
|
+
const entry = this.entries.get(providerKey("fetch", id));
|
|
1130
|
+
return entry !== void 0 && this.ready(entry);
|
|
1131
|
+
}) ?? "";
|
|
1132
|
+
}
|
|
933
1133
|
selected(entry) {
|
|
934
1134
|
if (this.disposed || this.suspended || !this.ready(entry)) return false;
|
|
935
1135
|
if (this.entries.get(providerKey(entry.kind, entry.descriptor.id)) !== entry) return false;
|
|
936
|
-
if (entry.kind === "fetch") return this.settings.fetchEnabled && this.
|
|
1136
|
+
if (entry.kind === "fetch") return this.settings.fetchEnabled && this.activeFetchId() === entry.descriptor.id;
|
|
937
1137
|
return this.settings.searchEnabled && this.activeSearchId() === entry.descriptor.id;
|
|
938
1138
|
}
|
|
939
1139
|
status() {
|
|
940
1140
|
const entries = [...this.entries.values()];
|
|
941
1141
|
return {
|
|
942
|
-
settings:
|
|
1142
|
+
settings: {
|
|
1143
|
+
...structuredClone(this.settings),
|
|
1144
|
+
fetchProvider: this.activeFetchId()
|
|
1145
|
+
},
|
|
943
1146
|
storageFailed: this.storageFailed,
|
|
944
1147
|
searchActive: entries.some((entry) => entry.kind === "search" && this.selected(entry)),
|
|
945
1148
|
fetchActive: entries.some((entry) => entry.kind === "fetch" && this.selected(entry)),
|
|
@@ -975,6 +1178,7 @@ var WebSearchManager = class {
|
|
|
975
1178
|
return this.status();
|
|
976
1179
|
}
|
|
977
1180
|
abort(entry) {
|
|
1181
|
+
if (!entry) for (const controller of this.work.values()) controller.abort();
|
|
978
1182
|
for (const target of entry ? [entry] : this.entries.values()) for (const controller of target.active) controller.abort();
|
|
979
1183
|
}
|
|
980
1184
|
discover(kind, provider) {
|
|
@@ -1021,7 +1225,10 @@ var WebSearchManager = class {
|
|
|
1021
1225
|
if (kind === "search") {
|
|
1022
1226
|
this.settings.searchOrder = this.settings.searchOrder.map((id) => id === legacy ? provider.id : id);
|
|
1023
1227
|
if (this.settings.searchProvider === legacy) this.settings.searchProvider = provider.id;
|
|
1024
|
-
} else
|
|
1228
|
+
} else {
|
|
1229
|
+
this.settings.fetchOrder = this.settings.fetchOrder.map((id) => id === legacy ? provider.id : id);
|
|
1230
|
+
if (this.settings.fetchProvider === legacy) this.settings.fetchProvider = provider.id;
|
|
1231
|
+
}
|
|
1025
1232
|
const legacyKey = providerKey(kind, legacy);
|
|
1026
1233
|
if (this.settings.endpoints[legacyKey] && !this.settings.endpoints[key]) this.settings.endpoints[key] = this.settings.endpoints[legacyKey];
|
|
1027
1234
|
}
|
|
@@ -1112,6 +1319,7 @@ var WebSearchManager = class {
|
|
|
1112
1319
|
const epoch = this.epoch;
|
|
1113
1320
|
entry.active.add(controller);
|
|
1114
1321
|
let started = false;
|
|
1322
|
+
let work;
|
|
1115
1323
|
try {
|
|
1116
1324
|
if (epoch !== this.epoch || !this.selected(entry)) fail("WEB_DISABLED", "网络访问设置已改变,请重新发起请求。");
|
|
1117
1325
|
const options = {
|
|
@@ -1121,8 +1329,13 @@ var WebSearchManager = class {
|
|
|
1121
1329
|
};
|
|
1122
1330
|
started = true;
|
|
1123
1331
|
entry.calls += 1;
|
|
1124
|
-
|
|
1125
|
-
|
|
1332
|
+
work = Promise.resolve().then(() => {
|
|
1333
|
+
combined.throwIfAborted();
|
|
1334
|
+
return run(options, combined);
|
|
1335
|
+
});
|
|
1336
|
+
this.work.set(work, controller);
|
|
1337
|
+
const result = await work;
|
|
1338
|
+
if (combined.aborted || epoch !== this.epoch || !this.selected(entry)) fail("WEB_ABORTED", "网络请求已取消。");
|
|
1126
1339
|
return result;
|
|
1127
1340
|
} catch (error) {
|
|
1128
1341
|
if (started) entry.failures += 1;
|
|
@@ -1137,14 +1350,18 @@ var WebSearchManager = class {
|
|
|
1137
1350
|
} finally {
|
|
1138
1351
|
clearTimeout(timeout);
|
|
1139
1352
|
entry.active.delete(controller);
|
|
1353
|
+
if (work) this.work.delete(work);
|
|
1140
1354
|
}
|
|
1141
1355
|
}
|
|
1142
1356
|
update(next, expectedRevision) {
|
|
1143
1357
|
const task = this.pending.then(async () => {
|
|
1144
1358
|
if (this.disposed) fail("WEB_DISABLED", "网络搜索插件已停止。");
|
|
1145
1359
|
if (expectedRevision !== this.settings.revision) fail("WEB_CONFIG_CONFLICT", "设置已被其他页面修改,请刷新后重试。");
|
|
1360
|
+
const input = structuredClone(next);
|
|
1361
|
+
const fetchOrder = input.fetchOrder ?? (input.fetchProvider === this.activeFetchId() ? [...this.settings.fetchOrder] : migrateFetchOrder(input));
|
|
1146
1362
|
const proposed = {
|
|
1147
|
-
...
|
|
1363
|
+
...input,
|
|
1364
|
+
fetchOrder,
|
|
1148
1365
|
revision: expectedRevision + 1
|
|
1149
1366
|
};
|
|
1150
1367
|
bounded(proposed.maxResults, 1, 20, "结果上限");
|
|
@@ -1165,10 +1382,10 @@ var WebSearchManager = class {
|
|
|
1165
1382
|
proposed.searchProvider = this.activeSearchId(proposed);
|
|
1166
1383
|
const activatingSearch = !this.settings.searchEnabled || JSON.stringify(proposed.searchOrder) !== JSON.stringify(this.settings.searchOrder);
|
|
1167
1384
|
if (proposed.searchEnabled && !proposed.searchProvider && activatingSearch) fail("WEB_CREDENTIAL_MISSING", "需选择已安装的供应商并填写有效凭据。");
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1385
|
+
proposed.fetchOrder = [...new Set(proposed.fetchOrder)];
|
|
1386
|
+
proposed.fetchProvider = this.activeFetchId(proposed);
|
|
1387
|
+
const activatingFetch = !this.settings.fetchEnabled || JSON.stringify(proposed.fetchOrder) !== JSON.stringify(this.settings.fetchOrder);
|
|
1388
|
+
if (proposed.fetchEnabled && !proposed.fetchProvider && activatingFetch) fail("WEB_CREDENTIAL_MISSING", "需选择已安装的供应商并填写有效凭据。");
|
|
1172
1389
|
this.suspended = true;
|
|
1173
1390
|
this.epoch += 1;
|
|
1174
1391
|
this.abort();
|
|
@@ -1194,8 +1411,9 @@ var WebSearchManager = class {
|
|
|
1194
1411
|
this.epoch += 1;
|
|
1195
1412
|
this.abort();
|
|
1196
1413
|
this.notify();
|
|
1197
|
-
for (const off of [...this.legacyRegistrations]) off();
|
|
1198
1414
|
await this.pending;
|
|
1415
|
+
await Promise.allSettled([...this.work.keys()]);
|
|
1416
|
+
for (const off of [...this.legacyRegistrations]) off();
|
|
1199
1417
|
this.detach();
|
|
1200
1418
|
this.entries.clear();
|
|
1201
1419
|
this.listeners.clear();
|
|
@@ -1210,6 +1428,7 @@ const settingsSchema = z.object({
|
|
|
1210
1428
|
searchProvider: z.string().max(64),
|
|
1211
1429
|
fetchProvider: z.string().max(64),
|
|
1212
1430
|
searchOrder: z.array(z.string().min(1).max(64)).max(32),
|
|
1431
|
+
fetchOrder: z.array(z.string().min(1).max(64)).max(32),
|
|
1213
1432
|
maxResults: z.number().int().min(1).max(20),
|
|
1214
1433
|
maxQueries: z.number().int().min(1).max(5),
|
|
1215
1434
|
timeoutMs: z.number().int().min(1e3).max(12e4),
|
|
@@ -1218,11 +1437,15 @@ const settingsSchema = z.object({
|
|
|
1218
1437
|
}).strict();
|
|
1219
1438
|
const updateSchema = z.object({
|
|
1220
1439
|
expectedRevision: z.number().int().nonnegative(),
|
|
1221
|
-
settings: settingsSchema.omit({ revision: true })
|
|
1440
|
+
settings: settingsSchema.omit({ revision: true }).extend({ fetchOrder: settingsSchema.shape.fetchOrder.optional() })
|
|
1222
1441
|
}).strict();
|
|
1223
|
-
const storedSettingsSchema = settingsSchema.extend({
|
|
1442
|
+
const storedSettingsSchema = settingsSchema.extend({
|
|
1443
|
+
searchOrder: settingsSchema.shape.searchOrder.optional(),
|
|
1444
|
+
fetchOrder: settingsSchema.shape.fetchOrder.optional()
|
|
1445
|
+
}).transform((settings) => ({
|
|
1224
1446
|
...settings,
|
|
1225
|
-
searchOrder: migrateSearchOrder(settings)
|
|
1447
|
+
searchOrder: migrateSearchOrder(settings),
|
|
1448
|
+
fetchOrder: migrateFetchOrder(settings)
|
|
1226
1449
|
}));
|
|
1227
1450
|
const webSettingsDomain = defineDomain({
|
|
1228
1451
|
name: "dsh_editor_web_search",
|
|
@@ -1243,8 +1466,10 @@ async function apply(ctx) {
|
|
|
1243
1466
|
const host = ctx;
|
|
1244
1467
|
const domain = await ctx.storageDomain.open(webSettingsDomain);
|
|
1245
1468
|
let managed;
|
|
1469
|
+
let unregisterBuiltins;
|
|
1246
1470
|
ctx.effect(() => async () => {
|
|
1247
1471
|
await managed?.dispose();
|
|
1472
|
+
unregisterBuiltins?.();
|
|
1248
1473
|
await domain.close();
|
|
1249
1474
|
}, "web-search-manager.dispose");
|
|
1250
1475
|
const table = domain.table("settings");
|
|
@@ -1261,10 +1486,10 @@ async function apply(ctx) {
|
|
|
1261
1486
|
ctx.on(WEB_PROVIDER_AVAILABILITY_EVENT, () => {
|
|
1262
1487
|
manager.refresh();
|
|
1263
1488
|
});
|
|
1264
|
-
|
|
1489
|
+
unregisterBuiltins = registerBuiltins(ctx.web, {
|
|
1265
1490
|
settings: () => manager.status().settings,
|
|
1266
1491
|
resolveCredential: async (ref) => (await host.credentials.resolve(credentialRef(ref)))?.value
|
|
1267
|
-
})
|
|
1492
|
+
});
|
|
1268
1493
|
ctx.effect(() => registerHostRpc$1(host, WEB_SEARCH_RPC_CHANNEL, async (endpoint, payload, signal) => {
|
|
1269
1494
|
try {
|
|
1270
1495
|
signal.throwIfAborted();
|