@klarkxy/dsh-web-search-manager 0.2.4 → 0.3.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.
@@ -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
@@ -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;AA0BjC,MAAa,uBAAuB,CAAC,KAAK;AAE1C,MAAa,yBAAsC;CACjD,UAAU;CAAG,eAAe;CAAM,cAAc;CAChD,gBAAgB;CAAO,eAAe;CAAQ,aAAa,CAAC,GAAG,oBAAoB;CACnF,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;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"}
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'>, expectedRevision: number): Promise<WebStatus>;
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
- /** Bound the Reader stage even if a provider fails to honour its cooperative signal. */
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
- return await Promise.race([Promise.resolve().then(() => {
57
- signal.throwIfAborted();
58
- return read(signal);
59
- }), aborted]);
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. 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.
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
- 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");
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
- 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
- }
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.settings.fetchProvider === entry.descriptor.id && this.entries.get(providerKey("fetch", entry.descriptor.id)) === entry;
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: structuredClone(this.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 if (this.settings.fetchProvider === legacy) this.settings.fetchProvider = provider.id;
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
- const result = await abortable(() => run(options, combined), combined);
1125
- if (epoch !== this.epoch || !this.selected(entry)) fail("WEB_ABORTED", "网络请求已取消。");
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
- ...structuredClone(next),
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
- if (proposed.fetchEnabled) {
1169
- const entry = this.entries.get(providerKey("fetch", proposed.fetchProvider));
1170
- if ((!this.settings.fetchEnabled || proposed.fetchProvider !== this.settings.fetchProvider) && (!entry || !this.ready(entry))) fail("WEB_CREDENTIAL_MISSING", "需选择已安装的供应商并填写有效凭据。");
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({ searchOrder: settingsSchema.shape.searchOrder.optional() }).transform((settings) => ({
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
- ctx.effect(() => registerBuiltins(ctx.web, {
1489
+ unregisterBuiltins = registerBuiltins(ctx.web, {
1265
1490
  settings: () => manager.status().settings,
1266
1491
  resolveCredential: async (ref) => (await host.credentials.resolve(credentialRef(ref)))?.value
1267
- }), "web-search-manager.providers");
1492
+ });
1268
1493
  ctx.effect(() => registerHostRpc$1(host, WEB_SEARCH_RPC_CHANNEL, async (endpoint, payload, signal) => {
1269
1494
  try {
1270
1495
  signal.throwIfAborted();