@klarkxy/dsh-web-search-manager 0.1.11 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -1,174 +1,54 @@
1
- function __createDshNodeShims() {
2
- class EventEmitter {
3
- constructor() {
4
- this._e = Object.create(null)
5
- }
6
- on(type, fn) {
7
- (this._e[type] ||= []).push(fn)
8
- return this
9
- }
10
- addListener(type, fn) {
11
- return this.on(type, fn)
12
- }
13
- once(type, fn) {
14
- const wrap = (...args) => {
15
- this.off(type, wrap)
16
- fn(...args)
17
- }
18
- return this.on(type, wrap)
19
- }
20
- off(type, fn) {
21
- const list = this._e[type]
22
- if (list) this._e[type] = list.filter((item) => item !== fn)
23
- return this
24
- }
25
- removeListener(type, fn) {
26
- return this.off(type, fn)
27
- }
28
- emit(type, ...args) {
29
- const list = this._e[type]
30
- if (!list) return false
31
- for (const fn of list.slice()) fn(...args)
32
- return true
33
- }
34
- removeAllListeners(type) {
35
- if (type) delete this._e[type]
36
- else this._e = Object.create(null)
37
- return this
38
- }
39
- listeners(type) {
40
- return (this._e[type] || []).slice()
41
- }
42
- setMaxListeners() {
43
- return this
44
- }
45
- }
46
-
47
- class Buffer extends Uint8Array {
48
- static from(value, encodingOrOffset, length) {
49
- if (typeof value === 'string') return new Buffer(new TextEncoder().encode(value))
50
- if (value instanceof ArrayBuffer) return new Buffer(new Uint8Array(value, encodingOrOffset, length))
51
- if (ArrayBuffer.isView(value)) return new Buffer(new Uint8Array(value.buffer, value.byteOffset, value.byteLength))
52
- if (Array.isArray(value) || value && typeof value.length === 'number') return new Buffer(Uint8Array.from(value))
53
- return new Buffer(0)
54
- }
55
- static alloc(size, fill) {
56
- const buf = new Buffer(size)
57
- if (fill !== undefined && fill !== 0) {
58
- if (typeof fill === 'string') {
59
- const bytes = new TextEncoder().encode(fill)
60
- for (let i = 0; i < size; i += 1) buf[i] = bytes[i % bytes.length]
61
- } else buf.fill(fill)
62
- }
63
- return buf
64
- }
65
- static allocUnsafe(size) {
66
- return new Buffer(size)
67
- }
68
- static allocUnsafeSlow(size) {
69
- return new Buffer(size)
70
- }
71
- static concat(list, totalLength) {
72
- const length = totalLength ?? list.reduce((sum, item) => sum + item.length, 0)
73
- const out = new Buffer(length)
74
- let offset = 0
75
- for (const item of list) {
76
- out.set(item, offset)
77
- offset += item.length
78
- }
79
- return out
80
- }
81
- static isBuffer(value) {
82
- return value instanceof Buffer
83
- }
84
- static byteLength(value) {
85
- if (typeof value === 'string') return new TextEncoder().encode(value).length
86
- return value?.byteLength ?? value?.length ?? 0
87
- }
88
- static isEncoding(encoding) {
89
- return /^(utf8|utf-8|ascii|latin1|binary|hex|base64|base64url)$/i.test(String(encoding || ''))
90
- }
91
- toString(encoding) {
92
- if (encoding === 'hex') return [...this].map((byte) => byte.toString(16).padStart(2, '0')).join('')
93
- if (encoding === 'base64') {
94
- let binary = ''
95
- this.forEach((byte) => {
96
- binary += String.fromCharCode(byte)
97
- })
98
- return btoa(binary)
99
- }
100
- return new TextDecoder().decode(this)
101
- }
102
- slice(start, end) {
103
- return Buffer.from(super.subarray(start, end))
104
- }
105
- copy(target, targetStart = 0, start = 0, end = this.length) {
106
- const sliced = this.subarray(start, end)
107
- target.set(sliced, targetStart)
108
- return sliced.length
109
- }
110
- write(string, offset = 0) {
111
- const bytes = new TextEncoder().encode(string)
112
- const written = Math.min(bytes.length, this.length - offset)
113
- this.set(bytes.subarray(0, written), offset)
114
- return written
115
- }
116
- }
117
-
118
- function inherits(ctor, superCtor) {
119
- ctor.super_ = superCtor
120
- ctor.prototype = Object.create(superCtor.prototype, {
121
- constructor: { value: ctor, writable: true, configurable: true },
122
- })
123
- }
124
-
125
- if (!globalThis.Buffer) globalThis.Buffer = Buffer
126
-
127
- return {
128
- buffer: { Buffer, SlowBuffer: Buffer, INSPECT_MAX_BYTES: 50, kMaxLength: 0x7fffffff },
129
- stream: {
130
- Stream: EventEmitter,
131
- Readable: EventEmitter,
132
- Writable: EventEmitter,
133
- Duplex: EventEmitter,
134
- Transform: EventEmitter,
135
- PassThrough: EventEmitter,
136
- },
137
- events: { EventEmitter },
138
- util: {
139
- deprecate(fn) {
140
- return fn
141
- },
142
- inherits,
143
- inspect() {
144
- return ''
145
- },
146
- format(value) {
147
- return String(value)
148
- },
149
- types: { isBuffer: Buffer.isBuffer },
150
- },
151
- }
152
- }
153
-
154
1
  window.__ModuleLoader__.load({
155
2
  id: "@klarkxy/dsh-web-search-manager",
156
3
  factory: (dshRequire) => {
157
- var __nodeShims;
158
- var process = globalThis.process || { env: {}, nextTick: function (fn) { var args = [].slice.call(arguments, 1); queueMicrotask(function () { fn.apply(null, args); }); } };
159
- var require = function (id) {
160
- if (id === 'buffer' || id === 'stream' || id === 'util' || id === 'events') {
161
- if (!__nodeShims) __nodeShims = __createDshNodeShims();
162
- return __nodeShims[id];
163
- }
164
- return dshRequire(id);
165
- };
4
+ var require = dshRequire;
166
5
  var module = { exports: {} };
167
6
  var exports = module.exports;
168
7
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
169
8
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
170
9
  let react = require("react");
171
10
  let react_jsx_runtime = require("react/jsx-runtime");
11
+ //#region src/client-error.ts
12
+ /** Local adapter diagnostics only; third-party descriptions remain verbatim. */
13
+ const localErrors = {
14
+ "网络搜索设置格式无效。": "Invalid web search settings format.",
15
+ "未知操作。": "Unknown operation.",
16
+ "网络设置操作失败,请刷新后重试。": "The network settings operation failed. Refresh and try again.",
17
+ "查询须为 1 至 4000 字符。": "The query must contain 1 to 4,000 characters.",
18
+ "网络搜索插件已停止。": "The web search plugin has stopped.",
19
+ "请在设置 → 网络搜索中配置凭据并启用。": "Configure credentials and enable web search in Settings → Web search.",
20
+ "网络访问设置已改变,请重新发起请求。": "Network access settings changed. Start the request again.",
21
+ "网络请求已取消。": "The network request was cancelled.",
22
+ "网络请求已取消或超时。": "The network request was cancelled or timed out.",
23
+ "设置已被其他页面修改,请刷新后重试。": "Settings changed in another page. Refresh and try again.",
24
+ "供应商不支持自定义地址。": "The provider does not support a custom address.",
25
+ "供应商地址须为无凭据、无查询参数的 HTTPS 地址。": "The provider address must use HTTPS without credentials or query parameters.",
26
+ "需选择已安装的供应商并填写有效凭据。": "Choose an installed provider and enter valid credentials.",
27
+ "保存失败,本次运行已暂停网络访问以防意外联网。": "Saving failed. Network access is paused for this run to prevent unintended requests.",
28
+ "供应商未提供错误描述。": "The provider did not supply an error description.",
29
+ "供应商请求失败。": "Provider request failed.",
30
+ "搜索凭据已删除或不可用,请重新配置。": "Search credentials were deleted or are unavailable. Configure them again.",
31
+ "供应商配置不可用。": "Provider configuration is unavailable.",
32
+ "查询无效。": "Invalid query.",
33
+ "结果数须为 1 至 20。": "The result count must be from 1 to 20.",
34
+ "搜索已取消。": "Search cancelled.",
35
+ "Tavily 搜索已取消。": "Tavily search cancelled.",
36
+ "响应格式无效。": "Invalid response shape",
37
+ "响应缺失。": "Missing response",
38
+ "响应过大。": "Response too large",
39
+ "地址无效。": "Invalid endpoint"
40
+ };
41
+ function localErrorText(message, locale) {
42
+ const prefix = message.match(/^(\[(?:search|fetch):[^\]]+\] )(.*)$/s);
43
+ if (prefix) return prefix[1] + localErrorText(prefix[2], locale);
44
+ if (locale === "zh") return Object.entries(localErrors).find(([, english]) => english === message)?.[0] ?? message;
45
+ if (localErrors[message]) return localErrors[message];
46
+ const http = message.match(/^供应商请求失败(HTTP (\d{3}))。$/);
47
+ if (http) return `Provider request failed (HTTP ${http[1]}).`;
48
+ const config = message.match(/^(brave|bocha|firecrawl|serper|Tavily) 配置不可用。$/);
49
+ if (config) return `${config[1]} configuration is unavailable.`;
50
+ return message;
51
+ }
172
52
  Object.values({
173
53
  /** Surfaces, from the page up to the topmost nested card. */
174
54
  surface: [
@@ -466,7 +346,7 @@ ${scope} {
466
346
  border: 0.5px solid var(--dsw-alias-border-l3);
467
347
  border-radius: var(--dsw-radius-md);
468
348
  background: var(--dsw-alias-bg-layer-1);
469
- font-family: var(--ds-font-family-code, ui-monospace, 'Cascadia Mono', Consolas, monospace);
349
+ font-family: var(--ds-font-family-code);
470
350
  font-size: 12px;
471
351
  line-height: 20px;
472
352
  color: var(--dsw-alias-label-secondary);
@@ -542,6 +422,49 @@ ${scope} {
542
422
  `.replace(/^& (\.dsh-ui-[a-z0-9-]+)(?![+>~])/gm, (_match, className) => `&${className}, & ${className}`);
543
423
  }
544
424
  //#endregion
425
+ //#region ../dsh-plugin-kit/src/client-utils.ts
426
+ /** Fixed pause for failing background reads; manual work is never replayed. */
427
+ function createPluginReadGate(now = Date.now) {
428
+ let pausedUntil = 0;
429
+ let epoch = 0;
430
+ return {
431
+ canRead: () => now() >= pausedUntil,
432
+ reset: () => {
433
+ epoch++;
434
+ pausedUntil = 0;
435
+ },
436
+ async run(read) {
437
+ const current = epoch;
438
+ try {
439
+ const value = await read();
440
+ if (current === epoch) pausedUntil = 0;
441
+ return value;
442
+ } catch (error) {
443
+ const message = error instanceof Error ? error.message : "";
444
+ if (current === epoch && (/(?:^|\s)transport failure for .+: HTTP (?:404|405|503)\b/.test(message) || error instanceof TypeError && /^(?:Failed to fetch|fetch failed|NetworkError when attempting to fetch resource\.?)$/i.test(message))) pausedUntil = now() + 3e4;
445
+ throw error;
446
+ }
447
+ }
448
+ };
449
+ }
450
+ //#endregion
451
+ //#region src/contracts.ts
452
+ const WEB_SEARCH_RPC_CHANNEL = "/web-search-manager";
453
+ function pickActiveSearch(order, configured) {
454
+ return order.find(configured) ?? "";
455
+ }
456
+ function safeConfigurationUrl(value) {
457
+ if (typeof value !== "string" || !value || /[\u0000-\u0020\\]/.test(value)) return;
458
+ try {
459
+ const url = new URL(value, "https://dsh.invalid");
460
+ if (url.username || url.password) return;
461
+ if (value.startsWith("/") && !value.startsWith("//") || value.startsWith("#")) return url.origin === "https://dsh.invalid" ? value : void 0;
462
+ return /^https:\/\//i.test(value) && url.protocol === "https:" ? value : void 0;
463
+ } catch {
464
+ return;
465
+ }
466
+ }
467
+ //#endregion
545
468
  //#region src/provider-info.ts
546
469
  const PROVIDER_PRICING_CHECKED = "2026-09-21";
547
470
  const providerInfo = {
@@ -586,22 +509,94 @@ const providerInfo = {
586
509
  pricingUrl: "https://docs.tavily.com/documentation/api-credits"
587
510
  }
588
511
  };
589
- function searchProviderInfo(provider) {
590
- const known = Object.hasOwn(providerInfo, provider.id) ? providerInfo[provider.id] : void 0;
591
- return {
592
- description: known?.description ?? provider.description,
593
- pricing: provider.pricing ?? known?.pricing ?? (provider.billing === "none" ? "该后端不收取搜索费用。" : provider.billing === "model-and-tools" ? "按模型用量与工具调用计费;额度与单价见供应商说明。" : "按 API 调用计费;额度与单价见供应商说明。"),
594
- pricingUrl: provider.pricingUrl ?? known?.pricingUrl ?? provider.signupUrl
595
- };
512
+ const englishInfo = {
513
+ ddg: {
514
+ description: "Search public DuckDuckGo pages without registration or an API key.",
515
+ pricing: "Free to use; no search API fees."
516
+ },
517
+ bocha: {
518
+ description: "Search Chinese web content, returning titles, links and summaries.",
519
+ pricing: "Charged per request: listed price ¥0.036/request; trial credits depend on official offers."
520
+ },
521
+ brave: {
522
+ description: "Search web pages, news and more with Brave’s independent index.",
523
+ pricing: "$5 in monthly credits (about 1,000 requests); then $5/1,000 requests. A payment card is required to activate."
524
+ },
525
+ "deepseek-official": {
526
+ description: "Call built-in search through a DeepSeek model using the model API key.",
527
+ pricing: "Default Flash token pricing per million: input ¥1–2 (cache hits ¥0.02–0.04), output ¥4–8, varying by peak/off-peak hours. Platform terms govern promotional balance and tool fees."
528
+ },
529
+ exa: {
530
+ description: "Web search for AI with relevant pages and content snippets.",
531
+ pricing: "$20 on signup, then $10 monthly credits; $7/1,000 requests (up to 10 results each), plus $1/1,000 additional results beyond 10."
532
+ },
533
+ firecrawl: {
534
+ description: "Search the web and extract page content.",
535
+ pricing: "1,000 free credits/month; 2 credits per 10 search results. Hobby includes 5,000 credits/month at $16/month billed annually; extra credits cost $5/1,000."
536
+ },
537
+ serper: {
538
+ description: "Get Google web search results.",
539
+ pricing: "2,500 one-time signup requests; $50/50,000 requests ($1/1,000). Paid credits expire after 6 months."
540
+ },
541
+ tavily: {
542
+ description: "Web search for AI, currently using basic search.",
543
+ pricing: "1,000 free credits/month; basic search uses 1 credit/request. Pay as you go: $0.008/credit ($8/1,000 requests)."
544
+ }
545
+ };
546
+ const bundledDescriptions = {
547
+ ddg: ["无需注册。默认后端。", "No registration required. Default backend."],
548
+ "deepseek-official": ["使用已配置的 DeepSeek API Key,可能产生额外搜索费用。", "Uses the configured DeepSeek API key; additional search fees may apply."],
549
+ exa: ["独立 Search API。", "Independent Search API."],
550
+ brave: ["Brave Search API。", "Brave Search API."],
551
+ bocha: ["中文网页搜索 API。", "Chinese web search API."],
552
+ serper: ["Google 结果的 Search API。", "Search API for Google results."],
553
+ firecrawl: ["网页搜索与提取 API。", "Web search and extraction API."],
554
+ tavily: ["Search API;固定 basic 检索,不自动升级搜索深度。", "Search API using basic search; search depth is never upgraded automatically."]
555
+ };
556
+ /** Translate only manager-owned labels; externally supplied metadata stays verbatim. */
557
+ function providerDisplayLabel(provider, locale) {
558
+ if (!bundledCatalogId(provider)) return provider.label;
559
+ if (provider.label === "DeepSeek 搜索") return locale === "zh" ? provider.label : "DeepSeek search";
560
+ if (provider.label === "博查") return locale === "zh" ? provider.label : "Bocha";
561
+ if (provider.label === "网页读取") return locale === "zh" ? provider.label : "Read web pages";
562
+ return provider.label;
596
563
  }
597
- //#endregion
598
- //#region src/contracts.ts
599
- const WEB_SEARCH_RPC_CHANNEL = "/web-search-manager";
600
- function resolveSearchOrder(knownIds, settings, _keyed) {
601
- return [...new Set(settings.searchOrder.filter((id) => knownIds.includes(id)))];
564
+ function bundledCatalogId(provider) {
565
+ if (!(typeof provider.configurationOwned === "boolean" ? provider.configurationOwned : !provider.configurationOwner || provider.configurationOwner === "@klarkxy/dsh-web-search-manager")) return;
566
+ return provider.id === "deepseek-managed" ? "deepseek-official" : provider.id;
602
567
  }
603
- function pickActiveSearch(order, configured) {
604
- return order.find(configured) ?? "";
568
+ function missingPricing(locale) {
569
+ return locale === "zh" ? "未提供费用信息" : "No pricing information provided";
570
+ }
571
+ function usesBundledPricing(provider) {
572
+ if (provider.pricing?.trim()) return false;
573
+ const catalogId = bundledCatalogId(provider);
574
+ return Boolean(catalogId && Object.hasOwn(providerInfo, catalogId));
575
+ }
576
+ function searchProviderInfo(provider, locale = "zh") {
577
+ const catalogId = bundledCatalogId(provider);
578
+ const base = catalogId && Object.hasOwn(providerInfo, catalogId) ? providerInfo[catalogId] : void 0;
579
+ const known = base && locale === "en" ? {
580
+ ...base,
581
+ ...englishInfo[catalogId]
582
+ } : base;
583
+ const ownedCopy = catalogId ? bundledDescriptions[catalogId] : void 0;
584
+ const description = ownedCopy && provider.description === ownedCopy[0] ? ownedCopy[locale === "zh" ? 0 : 1] : base && provider.description === base.description ? known.description : provider.description.trim() ? provider.description : known?.description ?? provider.description;
585
+ if (provider.pricing?.trim()) return {
586
+ description,
587
+ pricing: provider.pricing,
588
+ pricingUrl: provider.pricingUrl ?? known?.pricingUrl ?? provider.signupUrl
589
+ };
590
+ if (known) return {
591
+ description,
592
+ pricing: known.pricing,
593
+ pricingUrl: provider.pricingUrl ?? known.pricingUrl ?? provider.signupUrl
594
+ };
595
+ return {
596
+ description,
597
+ pricing: missingPricing(locale),
598
+ pricingUrl: provider.pricingUrl ?? provider.signupUrl
599
+ };
605
600
  }
606
601
  //#endregion
607
602
  //#region src/client.tsx
@@ -621,32 +616,129 @@ function editable(settings) {
621
616
  const { revision: _revision, ...rest } = settings;
622
617
  return rest;
623
618
  }
619
+ const STATUS_REFRESH_MS = 3e3;
620
+ function isPageHidden(hidden) {
621
+ if (hidden) return true;
622
+ return typeof document !== "undefined" && document.visibilityState === "hidden";
623
+ }
624
+ function canStartStatusRead(input) {
625
+ if (!input.mounted) return false;
626
+ if (input.source === "poll") return !input.hidden && !input.busy && !input.reordering && !input.reading;
627
+ return true;
628
+ }
629
+ function isExternalConfigurationUrl(url) {
630
+ return /^https:\/\//i.test(url);
631
+ }
632
+ function shouldApplyPolledStatus(input) {
633
+ if (input.hidden || input.busy || input.reordering) return false;
634
+ if (input.requestId !== input.latestRequestId) return false;
635
+ if (input.currentRevision !== void 0 && input.incomingRevision < input.currentRevision) return false;
636
+ return true;
637
+ }
638
+ function bindStatusRefresh(options) {
639
+ const intervalMs = options.intervalMs ?? 3e3;
640
+ let inFlight = false;
641
+ let stopped = false;
642
+ const run = async () => {
643
+ if (stopped || inFlight || options.isHidden()) return;
644
+ inFlight = true;
645
+ try {
646
+ await options.tick();
647
+ } finally {
648
+ inFlight = false;
649
+ }
650
+ };
651
+ const timer = (options.setIntervalFn ?? setInterval)(() => {
652
+ run();
653
+ }, intervalMs);
654
+ const onVisible = () => {
655
+ if (!options.isHidden()) run();
656
+ };
657
+ const add = options.addListener ?? ((type, listener) => {
658
+ if (typeof document !== "undefined") document.addEventListener(type, listener);
659
+ });
660
+ const remove = options.removeListener ?? ((type, listener) => {
661
+ if (typeof document !== "undefined") document.removeEventListener(type, listener);
662
+ });
663
+ add("visibilitychange", onVisible);
664
+ return () => {
665
+ stopped = true;
666
+ (options.clearIntervalFn ?? clearInterval)(timer);
667
+ remove("visibilitychange", onVisible);
668
+ };
669
+ }
624
670
  function isProviderOn(status, provider) {
625
- const enabled = provider.kind === "search" ? status.searchActive : status.fetchActive;
626
- const selected = status.settings[provider.kind === "search" ? "searchProvider" : "fetchProvider"];
627
- return enabled && selected === provider.id;
671
+ if (provider.kind === "fetch") return status.fetchActive && status.settings.fetchProvider === provider.id && provider.configured;
672
+ return status.searchActive && selectedSearchBackend(status)?.id === provider.id;
628
673
  }
629
- function searchBackends(status) {
630
- const all = status.providers.filter((provider) => provider.kind === "search");
631
- return resolveSearchOrder(all.map((provider) => provider.id), status.settings, (id) => Boolean(all.find((provider) => provider.id === id)?.credentialRef)).flatMap((id) => all.find((provider) => provider.id === id) ?? []);
674
+ function dormantSearchPlaceholder(id) {
675
+ return {
676
+ id,
677
+ label: id,
678
+ description: "",
679
+ billing: "unknown",
680
+ kind: "search",
681
+ configured: false,
682
+ calls: 0,
683
+ failures: 0,
684
+ configurationOwned: false
685
+ };
686
+ }
687
+ function isRegisteredSearch(status, id) {
688
+ return status.providers.some((provider) => provider.kind === "search" && provider.id === id);
632
689
  }
633
690
  function catalogSearchBackends(status) {
634
- const enabled = searchBackends(status);
635
- const rest = status.providers.filter((provider) => provider.kind === "search" && !enabled.some((row) => row.id === provider.id));
636
- return [...enabled, ...rest];
691
+ const byId = new Map(status.providers.filter((provider) => provider.kind === "search").map((provider) => [provider.id, provider]));
692
+ const rows = [];
693
+ const seen = /* @__PURE__ */ new Set();
694
+ for (const id of status.settings.searchOrder) {
695
+ if (!id || seen.has(id)) continue;
696
+ seen.add(id);
697
+ rows.push(byId.get(id) ?? dormantSearchPlaceholder(id));
698
+ }
699
+ for (const provider of status.providers) {
700
+ if (provider.kind !== "search" || seen.has(provider.id)) continue;
701
+ rows.push(provider);
702
+ }
703
+ return rows;
704
+ }
705
+ function searchBackends(status) {
706
+ const participating = new Set(status.settings.searchOrder);
707
+ return catalogSearchBackends(status).filter((provider) => participating.has(provider.id));
637
708
  }
638
709
  function selectedSearchBackend(status) {
639
- const backends = searchBackends(status);
710
+ const backends = searchBackends(status).filter((provider) => isRegisteredSearch(status, provider.id));
640
711
  const id = pickActiveSearch(backends.map((provider) => provider.id), (id) => Boolean(backends.find((provider) => provider.id === id)?.configured));
641
712
  return backends.find((provider) => provider.id === id);
642
713
  }
643
- function ExternalLink({ url, label, children }) {
714
+ function droppedDormantIds(settings, order, providers) {
715
+ const known = new Set(providers.filter((provider) => provider.kind === "search").map((provider) => provider.id));
716
+ return [...new Set(settings.searchOrder.filter((id) => id && !known.has(id) && !order.includes(id)))];
717
+ }
718
+ function configurationAccess(provider) {
719
+ if (ownsProviderConfiguration(provider)) return { kind: "owned" };
720
+ const url = safeConfigurationUrl(provider.configurationUrl);
721
+ if (url) return {
722
+ kind: "link",
723
+ url
724
+ };
725
+ if (provider.configurationOwner?.trim()) return {
726
+ kind: "owner",
727
+ name: provider.configurationOwner
728
+ };
729
+ return { kind: "unknown" };
730
+ }
731
+ function AppLink({ url, label, children }) {
732
+ const external = isExternalConfigurationUrl(url);
644
733
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
645
734
  href: url,
646
- target: "_blank",
647
- rel: "noreferrer noopener",
735
+ ...external ? {
736
+ target: "_blank",
737
+ rel: "noreferrer noopener"
738
+ } : {},
648
739
  "aria-label": label,
649
740
  onClick: (event) => {
741
+ if (!external) return;
650
742
  const bridge = globalThis.dshWindow;
651
743
  if (bridge?.openExternal) {
652
744
  event.preventDefault();
@@ -662,9 +754,13 @@ function canEnableSearch(order, providers, keys, writable) {
662
754
  if (!provider) return false;
663
755
  if (provider.configured) return true;
664
756
  const typed = (keys[provider.id] ?? "").trim();
665
- return Boolean(provider.credentialRef && typed && writable[provider.credentialRef] !== false);
757
+ return Boolean(ownsProviderConfiguration(provider) && provider.credentialRef && typed && writable[provider.credentialRef] !== false);
666
758
  });
667
759
  }
760
+ function ownsProviderConfiguration(provider) {
761
+ if (typeof provider.configurationOwned === "boolean") return provider.configurationOwned;
762
+ return !provider.configurationOwner || provider.configurationOwner === "@klarkxy/dsh-web-search-manager";
763
+ }
668
764
  function nextSearchEnabled(currentlyEnabled, nextOrder, providers, keys, writable) {
669
765
  return currentlyEnabled && canEnableSearch(nextOrder, providers, keys, writable);
670
766
  }
@@ -716,11 +812,14 @@ const copy = {
716
812
  dragPrefix: "拖动排序 ",
717
813
  dragHint: "拖动排序,或用上下方向键",
718
814
  inUse: "当前使用",
815
+ available: "可用",
816
+ unavailable: "暂不可用",
817
+ dormant: "未加载,保留选择",
719
818
  signup: "去注册",
720
819
  pricing: "费用说明",
721
820
  website: "官网",
821
+ openConfig: "打开配置",
722
822
  needsKey: "待配置 Key",
723
- unavailable: "暂不可用",
724
823
  sharedKey: "与其他设置共用凭据",
725
824
  keyConfigured: "已配置,留空不替换",
726
825
  keyPlaceholder: "输入 API Key",
@@ -734,7 +833,11 @@ const copy = {
734
833
  maxResults: "每条查询结果上限",
735
834
  maxQueries: "每次调用查询上限",
736
835
  timeoutMs: "请求超时(毫秒)",
737
- maxFetchChars: "正文字符上限"
836
+ maxFetchChars: "正文字符上限",
837
+ fetchTitle: "网页读取",
838
+ fetchProvider: "读取服务",
839
+ ownerSettings: "配置入口:",
840
+ unknownOwner: "请在服务所属插件中配置"
738
841
  },
739
842
  en: {
740
843
  tabsLabel: "Web search settings",
@@ -756,11 +859,14 @@ const copy = {
756
859
  dragPrefix: "Reorder ",
757
860
  dragHint: "Drag to reorder, or use arrow keys",
758
861
  inUse: "In use",
862
+ available: "Available",
863
+ unavailable: "Unavailable",
864
+ dormant: "Not loaded; selection kept",
759
865
  signup: "Sign up",
760
866
  pricing: "Pricing",
761
867
  website: "Website",
868
+ openConfig: "Open configuration",
762
869
  needsKey: "Key required",
763
- unavailable: "Unavailable",
764
870
  sharedKey: "Shares a credential with other settings",
765
871
  keyConfigured: "Configured; empty keeps it",
766
872
  keyPlaceholder: "Enter API key",
@@ -774,14 +880,38 @@ const copy = {
774
880
  maxResults: "Max results/query",
775
881
  maxQueries: "Max queries/call",
776
882
  timeoutMs: "Request timeout (ms)",
777
- maxFetchChars: "Max text chars"
883
+ maxFetchChars: "Max text chars",
884
+ fetchTitle: "Read web pages",
885
+ fetchProvider: "Fetch service",
886
+ ownerSettings: "Configure in: ",
887
+ unknownOwner: "Configure this service in its plugin"
778
888
  }
779
889
  };
890
+ var UiError = class extends Error {
891
+ key;
892
+ constructor(key) {
893
+ super(key);
894
+ this.key = key;
895
+ }
896
+ };
897
+ function uiMessage(message, locale, providers = []) {
898
+ if (typeof message === "string") return localErrorText(message, locale);
899
+ if ("key" in message) return copy[locale][message.key];
900
+ if ("sources" in message) return testOkText(locale, message.sources);
901
+ if ("moved" in message) {
902
+ const provider = providers.find((item) => item.id === message.moved);
903
+ return movedText(locale, provider ? providerDisplayLabel(provider, locale) : message.moved, message.position);
904
+ }
905
+ return message.invalid.map((key) => {
906
+ const [, min, max] = LIMITS.find((item) => item[0] === key);
907
+ return limitInvalidText(locale, copy[locale][key], min, max);
908
+ }).join(" ");
909
+ }
780
910
  function movedText(locale, label, position) {
781
911
  return locale === "zh" ? `${label} 已移至第 ${position} 位` : `${label} moved to position ${position}`;
782
912
  }
783
913
  function pricingCheckedText(locale, date) {
784
- return locale === "zh" ? `价格核对于 ${date},额度与费用以供应商账户为准。` : `Prices checked on ${date}; your provider account is authoritative.`;
914
+ return locale === "zh" ? `内置适配器价格核对于 ${date},额度与费用以供应商账户为准。` : `Bundled adapter prices checked on ${date}; your provider account is authoritative.`;
785
915
  }
786
916
  function testOkText(locale, sources) {
787
917
  return locale === "zh" ? `连接正常,返回 ${sources} 条来源。` : `Connection OK: ${sources} sources returned.`;
@@ -822,13 +952,25 @@ function parseLimit(raw, min, max) {
822
952
  * Settings written by an immediate action (a switch or a reorder). It keeps the
823
953
  * saved limits and never includes typed-but-unsaved keys or limit drafts.
824
954
  */
825
- function immediateUpdate(settings, on, order) {
955
+ function immediateUpdate(settings, on, order, providers, remove = []) {
956
+ let searchOrder = order;
957
+ if (providers) {
958
+ const known = new Set(providers.filter((provider) => provider.kind === "search").map((provider) => provider.id));
959
+ const removed = new Set(remove);
960
+ const following = order.filter((id) => !removed.has(id));
961
+ if (following.some((id) => !known.has(id))) searchOrder = following;
962
+ else {
963
+ searchOrder = settings.searchOrder.flatMap((id) => {
964
+ if (removed.has(id)) return [];
965
+ return known.has(id) ? following.splice(0, 1) : [id];
966
+ });
967
+ searchOrder.push(...following);
968
+ }
969
+ }
826
970
  return {
827
971
  ...editable(settings),
828
- searchOrder: order,
829
- searchEnabled: on,
830
- fetchProvider: "http",
831
- fetchEnabled: on
972
+ searchOrder,
973
+ searchEnabled: on
832
974
  };
833
975
  }
834
976
  function limitTexts(settings) {
@@ -846,7 +988,7 @@ function useHostLocale(client) {
846
988
  }
847
989
  /** Keyboard reorders are batched: persist once the arrows stop for this long. */
848
990
  const REORDER_DEBOUNCE_MS = 500;
849
- function NetworkSearchSettings({ client }) {
991
+ function NetworkSearchSettings({ client, hidden }) {
850
992
  const locale = useHostLocale(client);
851
993
  const text = copy[locale];
852
994
  const tabsId = (0, react.useId)();
@@ -870,6 +1012,17 @@ function NetworkSearchSettings({ client }) {
870
1012
  const timerRef = (0, react.useRef)(void 0);
871
1013
  const handleRefs = (0, react.useRef)(/* @__PURE__ */ new Map());
872
1014
  const refocusRef = (0, react.useRef)(null);
1015
+ const busyRef = (0, react.useRef)(false);
1016
+ const hiddenRef = (0, react.useRef)(hidden);
1017
+ hiddenRef.current = hidden;
1018
+ const requestSeq = (0, react.useRef)(0);
1019
+ const mountedRef = (0, react.useRef)(true);
1020
+ const readingRef = (0, react.useRef)(false);
1021
+ const readingRequest = (0, react.useRef)(0);
1022
+ const statusRead = (0, react.useRef)(Promise.resolve());
1023
+ const readGateRef = (0, react.useRef)(void 0);
1024
+ if (!readGateRef.current) readGateRef.current = createPluginReadGate();
1025
+ const readGate = readGateRef.current;
873
1026
  async function call(endpoint, payload = {}) {
874
1027
  return unwrap(await client.connection.rpc.call(WEB_SEARCH_RPC_CHANNEL, endpoint, payload));
875
1028
  }
@@ -885,30 +1038,109 @@ function NetworkSearchSettings({ client }) {
885
1038
  });
886
1039
  if (resetDraft) setInvalid({});
887
1040
  }
888
- async function load(resetDraft = false) {
889
- const next = await call("status");
890
- adopt(next, resetDraft);
891
- setLoadError("");
892
- const refs = next.providers.flatMap((provider) => provider.credentialRef ? [provider.credentialRef] : []);
893
- if (refs.length) {
894
- const credentials = unwrap(await client.remote.credentials.describe(refs));
895
- setWritable(Object.fromEntries(Object.entries(credentials).map(([ref, value]) => [ref, value.writable])));
1041
+ function pollBlocked() {
1042
+ return !canStartStatusRead({
1043
+ source: "poll",
1044
+ mounted: mountedRef.current,
1045
+ hidden: isPageHidden(hiddenRef.current),
1046
+ busy: busyRef.current,
1047
+ reordering: pendingRef.current !== null,
1048
+ reading: readingRef.current
1049
+ });
1050
+ }
1051
+ async function describeOwned(next, mine) {
1052
+ const refs = next.providers.flatMap((provider) => ownsProviderConfiguration(provider) && provider.credentialRef ? [provider.credentialRef] : []);
1053
+ if (!refs.length) return;
1054
+ const credentials = unwrap(await readGate.run(() => client.remote.credentials.describe(refs)));
1055
+ if (!mountedRef.current || mine !== requestSeq.current) return;
1056
+ setWritable(Object.fromEntries(Object.entries(credentials).map(([ref, value]) => [ref, value.writable])));
1057
+ }
1058
+ async function load(resetDraft = false, source = "user") {
1059
+ if (source === "poll" && !readGate.canRead()) return;
1060
+ if (source === "user") {
1061
+ readGate.reset();
1062
+ requestSeq.current += 1;
1063
+ }
1064
+ while (readingRef.current) {
1065
+ if (source === "poll" || !mountedRef.current) return;
1066
+ await statusRead.current;
1067
+ }
1068
+ if (!mountedRef.current) return;
1069
+ if (source === "poll" && pollBlocked()) return;
1070
+ const mine = ++requestSeq.current;
1071
+ readingRef.current = true;
1072
+ readingRequest.current = mine;
1073
+ let release = () => {};
1074
+ statusRead.current = new Promise((resolve) => {
1075
+ release = resolve;
1076
+ });
1077
+ try {
1078
+ const next = unwrap(await readGate.run(() => client.connection.rpc.call(WEB_SEARCH_RPC_CHANNEL, "status", {})));
1079
+ if (!mountedRef.current || mine !== requestSeq.current) return next;
1080
+ if (source === "poll" && !shouldApplyPolledStatus({
1081
+ requestId: mine,
1082
+ latestRequestId: requestSeq.current,
1083
+ busy: busyRef.current,
1084
+ reordering: pendingRef.current !== null,
1085
+ hidden: isPageHidden(hiddenRef.current),
1086
+ currentRevision: statusRef.current?.settings.revision,
1087
+ incomingRevision: next.settings.revision
1088
+ })) return next;
1089
+ adopt(next, resetDraft);
1090
+ setLoadError("");
1091
+ await describeOwned(next, mine);
1092
+ return next;
1093
+ } catch (cause) {
1094
+ if (!mountedRef.current || mine !== requestSeq.current) return;
1095
+ setLoadError(cause instanceof Error && cause.message ? cause.message : { key: "failed" });
1096
+ if (source === "poll") return;
1097
+ throw cause;
1098
+ } finally {
1099
+ if (readingRequest.current === mine) readingRef.current = false;
1100
+ release();
896
1101
  }
897
- return next;
898
1102
  }
899
1103
  (0, react.useEffect)(() => {
900
- load().catch((cause) => setLoadError(cause instanceof Error && cause.message ? cause.message : text.failed));
1104
+ mountedRef.current = true;
1105
+ readGate.reset();
1106
+ load().catch((cause) => setLoadError(cause instanceof Error && cause.message ? cause.message : { key: "failed" }));
1107
+ const bind = () => bindStatusRefresh({
1108
+ isHidden: () => isPageHidden(hiddenRef.current),
1109
+ tick: async () => {
1110
+ await load(false, "poll").catch(() => {});
1111
+ }
1112
+ });
1113
+ let stop = bind();
1114
+ const stopConnection = client.connection.generation?.subscribe(() => {
1115
+ readGate.reset();
1116
+ requestSeq.current += 1;
1117
+ readingRef.current = false;
1118
+ statusRead.current = Promise.resolve();
1119
+ stop();
1120
+ stop = bind();
1121
+ load(false, "poll");
1122
+ });
1123
+ return () => {
1124
+ mountedRef.current = false;
1125
+ readGate.reset();
1126
+ requestSeq.current += 1;
1127
+ stop();
1128
+ stopConnection?.();
1129
+ if (timerRef.current === void 0) return;
1130
+ clearTimeout(timerRef.current);
1131
+ const current = statusRef.current;
1132
+ const next = pendingRef.current;
1133
+ if (current && next) call("update", {
1134
+ settings: immediateUpdate(current.settings, current.settings.searchEnabled, next, current.providers),
1135
+ expectedRevision: current.settings.revision
1136
+ }).catch(() => {});
1137
+ };
901
1138
  }, [client]);
902
- (0, react.useEffect)(() => () => {
903
- if (timerRef.current === void 0) return;
904
- clearTimeout(timerRef.current);
905
- const current = statusRef.current;
906
- const next = pendingRef.current;
907
- if (current && next) call("update", {
908
- settings: immediateUpdate(current.settings, current.settings.searchEnabled, next),
909
- expectedRevision: current.settings.revision
910
- }).catch(() => {});
911
- }, []);
1139
+ (0, react.useEffect)(() => {
1140
+ if (hidden) return;
1141
+ if (!statusRef.current) return;
1142
+ load(false, "poll").catch(() => {});
1143
+ }, [hidden]);
912
1144
  (0, react.useLayoutEffect)(() => {
913
1145
  const id = refocusRef.current;
914
1146
  if (!id) return;
@@ -917,20 +1149,24 @@ function NetworkSearchSettings({ client }) {
917
1149
  if (handle && document.activeElement !== handle) handle.focus();
918
1150
  });
919
1151
  async function action(run) {
1152
+ readGate.reset();
1153
+ requestSeq.current += 1;
1154
+ busyRef.current = true;
920
1155
  setBusy(true);
921
1156
  setNote("");
922
1157
  setError("");
923
1158
  try {
924
1159
  await run();
925
1160
  } catch (cause) {
926
- setError(cause instanceof Error && cause.message ? cause.message : text.failed);
1161
+ setError(cause instanceof UiError ? { key: cause.key } : cause instanceof Error && cause.message ? cause.message : { key: "failed" });
927
1162
  await load().catch(() => {});
928
1163
  } finally {
1164
+ busyRef.current = false;
929
1165
  setBusy(false);
930
1166
  }
931
1167
  }
932
1168
  /** Switches and reorders: settings only, applied at once. */
933
- function runImmediate(on, order) {
1169
+ function runImmediate(on, order, remove = []) {
934
1170
  clearTimeout(timerRef.current);
935
1171
  timerRef.current = void 0;
936
1172
  pendingRef.current = null;
@@ -938,7 +1174,7 @@ function NetworkSearchSettings({ client }) {
938
1174
  const current = statusRef.current;
939
1175
  if (!current) return;
940
1176
  const next = await call("update", {
941
- settings: immediateUpdate(current.settings, on, order),
1177
+ settings: immediateUpdate(current.settings, on, order, current.providers, remove),
942
1178
  expectedRevision: current.settings.revision
943
1179
  });
944
1180
  setStatus(next);
@@ -954,8 +1190,8 @@ function NetworkSearchSettings({ client }) {
954
1190
  if (provider.kind !== "search" || only && provider.id !== only.id) continue;
955
1191
  const snapshot = keys[provider.id] ?? "";
956
1192
  const typed = snapshot.trim();
957
- if (!provider.credentialRef || !typed) continue;
958
- if (writable[provider.credentialRef] === false) throw new Error(text.readOnly);
1193
+ if (!ownsProviderConfiguration(provider) || !provider.credentialRef || !typed) continue;
1194
+ if (writable[provider.credentialRef] === false) throw new UiError("readOnly");
959
1195
  unwrap(await client.remote.credentials.set(provider.credentialRef, typed));
960
1196
  setKeys((current) => current[provider.id] === snapshot ? {
961
1197
  ...current,
@@ -964,21 +1200,21 @@ function NetworkSearchSettings({ client }) {
964
1200
  }
965
1201
  }
966
1202
  async function removeKey(provider) {
967
- if (!provider.credentialRef || provider.credentialShared || !status) return;
1203
+ if (!ownsProviderConfiguration(provider) || !provider.credentialRef || provider.credentialShared || !status) return;
968
1204
  unwrap(await client.remote.credentials.unset(provider.credentialRef));
969
1205
  setKeys((current) => ({
970
1206
  ...current,
971
1207
  [provider.id]: ""
972
1208
  }));
973
1209
  await load();
974
- setNote(text.keyDeleted);
1210
+ setNote({ key: "keyDeleted" });
975
1211
  }
976
1212
  if (!status || !limitDraft) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
977
1213
  className: "web-search-settings ws-root dsh-ui-panel",
978
1214
  children: [loadError ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
979
1215
  role: "alert",
980
1216
  className: "dsh-ui-error",
981
- children: [text.loadFailed, loadError]
1217
+ children: [text.loadFailed, uiMessage(loadError, locale, status?.providers)]
982
1218
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
983
1219
  role: "status",
984
1220
  className: "dsh-ui-hint",
@@ -1017,7 +1253,10 @@ function NetworkSearchSettings({ client }) {
1017
1253
  pendingRef.current = next;
1018
1254
  setPendingOrder(next);
1019
1255
  if (keyboard) refocusRef.current = id;
1020
- setSortAnnouncement(movedText(locale, backends.find((provider) => provider.id === id)?.label ?? id, nextIndex + 1));
1256
+ setSortAnnouncement({
1257
+ moved: id,
1258
+ position: nextIndex + 1
1259
+ });
1021
1260
  clearTimeout(timerRef.current);
1022
1261
  timerRef.current = setTimeout(() => {
1023
1262
  timerRef.current = void 0;
@@ -1038,13 +1277,13 @@ function NetworkSearchSettings({ client }) {
1038
1277
  const value = parseLimit(limitDraft[key], min, max);
1039
1278
  if (value === void 0) {
1040
1279
  nextInvalid[key] = true;
1041
- messages.push(limitInvalidText(locale, text[key], min, max));
1280
+ messages.push(key);
1042
1281
  } else parsed[key] = value;
1043
1282
  }
1044
1283
  setInvalid(nextInvalid);
1045
1284
  if (messages.length) {
1046
1285
  setNote("");
1047
- setError(messages.join(" "));
1286
+ setError({ invalid: messages });
1048
1287
  return;
1049
1288
  }
1050
1289
  action(async () => {
@@ -1053,14 +1292,12 @@ function NetworkSearchSettings({ client }) {
1053
1292
  adopt(await call("update", {
1054
1293
  settings: {
1055
1294
  ...editable(current.settings),
1056
- ...parsed,
1057
- fetchProvider: "http",
1058
- fetchEnabled: current.settings.searchEnabled
1295
+ ...parsed
1059
1296
  },
1060
1297
  expectedRevision: current.settings.revision
1061
1298
  }), true);
1062
1299
  await load();
1063
- setNote(text.saved);
1300
+ setNote({ key: "saved" });
1064
1301
  });
1065
1302
  }
1066
1303
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
@@ -1087,30 +1324,46 @@ function NetworkSearchSettings({ client }) {
1087
1324
  className: "ws-sr-only",
1088
1325
  role: "status",
1089
1326
  "aria-live": "polite",
1090
- children: sortAnnouncement
1327
+ children: uiMessage(sortAnnouncement, locale, saved.providers)
1091
1328
  }),
1092
1329
  error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1093
1330
  role: "alert",
1094
1331
  className: "dsh-ui-error",
1095
- children: error
1332
+ children: uiMessage(error, locale, saved.providers)
1096
1333
  }),
1334
+ loadError ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1335
+ className: "dsh-ui-stack",
1336
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1337
+ role: "alert",
1338
+ className: "dsh-ui-error",
1339
+ children: [text.loadFailed, uiMessage(loadError, locale, status?.providers)]
1340
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1341
+ variant: "outline",
1342
+ size: "sm",
1343
+ disabled: busy,
1344
+ onClick: () => void action(async () => {
1345
+ await load();
1346
+ }),
1347
+ children: text.reconnect
1348
+ })]
1349
+ }) : null,
1097
1350
  note && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1098
1351
  role: "status",
1099
1352
  className: "dsh-ui-notice",
1100
- children: note
1353
+ children: uiMessage(note, locale, saved.providers)
1101
1354
  }),
1102
1355
  saved.storageFailed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1103
1356
  role: "alert",
1104
1357
  className: "dsh-ui-banner dsh-ui-banner--danger",
1105
1358
  children: text.storageFailed
1106
1359
  }),
1107
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1360
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1108
1361
  role: "tabpanel",
1109
1362
  id: tabsId + "-providers-panel",
1110
1363
  "aria-labelledby": tabsId + "-providers-tab",
1111
1364
  hidden: tab !== "providers",
1112
1365
  tabIndex: 0,
1113
- children: backends.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1366
+ children: [backends.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1114
1367
  className: "dsh-ui-empty",
1115
1368
  children: text.empty
1116
1369
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
@@ -1139,17 +1392,23 @@ function NetworkSearchSettings({ client }) {
1139
1392
  children: backends.map((provider) => {
1140
1393
  const participating = order.includes(provider.id);
1141
1394
  const rank = order.indexOf(provider.id);
1142
- const active = saved.searchActive && selected?.id === provider.id;
1143
- const info = searchProviderInfo(provider);
1144
- const needsKey = Boolean(provider.credentialRef);
1395
+ const dormant = !isRegisteredSearch(saved, provider.id);
1396
+ const active = !dormant && saved.searchActive && selected?.id === provider.id;
1397
+ const info = searchProviderInfo(provider, locale);
1398
+ const owned = ownsProviderConfiguration(provider);
1399
+ const access = configurationAccess(provider);
1400
+ const needsKey = owned && Boolean(provider.credentialRef);
1145
1401
  const shared = Boolean(provider.credentialShared);
1146
- const showKey = needsKey && participating && (!shared || !provider.configured);
1402
+ const showKey = !dormant && needsKey && participating && (!shared || !provider.configured);
1147
1403
  const canSort = participating && order.length > 1;
1148
1404
  const readOnly = writable[provider.credentialRef ?? ""] === false;
1149
1405
  const typedKey = (keys[provider.id] ?? "").trim();
1406
+ const availability = dormant ? "dormant" : provider.configured ? "available" : "unavailable";
1150
1407
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
1151
1408
  className: active ? "is-active" : void 0,
1152
1409
  "data-testid": `web-search-rank-${provider.id}`,
1410
+ "data-availability": availability,
1411
+ "data-in-use": active || void 0,
1153
1412
  "data-dragging": dragSource === provider.id || void 0,
1154
1413
  "data-drop": dropTarget === provider.id && dragSource ? order.indexOf(dragSource) < rank ? "after" : "before" : void 0,
1155
1414
  onDragOver: (event) => {
@@ -1217,7 +1476,7 @@ function NetworkSearchSettings({ client }) {
1217
1476
  }),
1218
1477
  draggable: !busy,
1219
1478
  "aria-disabled": busy,
1220
- "aria-label": text.dragPrefix + provider.label,
1479
+ "aria-label": text.dragPrefix + providerDisplayLabel(provider, locale),
1221
1480
  title: text.dragHint,
1222
1481
  onDragStart: (event) => {
1223
1482
  if (busy) {
@@ -1249,35 +1508,49 @@ function NetworkSearchSettings({ client }) {
1249
1508
  children: [
1250
1509
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
1251
1510
  className: "dsh-ui-heading",
1252
- children: provider.label
1511
+ children: providerDisplayLabel(provider, locale)
1512
+ }),
1513
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1514
+ className: "ws-provider-note",
1515
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: dormant ? "idle" : provider.configured ? "done" : "warning" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: dormant ? text.dormant : provider.configured ? text.available : text.unavailable })]
1253
1516
  }),
1254
1517
  active && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
1255
1518
  tone: "success",
1256
1519
  children: text.inUse
1257
1520
  }),
1258
- provider.signupUrl && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExternalLink, {
1521
+ provider.signupUrl && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppLink, {
1259
1522
  url: provider.signupUrl,
1260
- label: provider.label + " " + text.signup,
1523
+ label: providerDisplayLabel(provider, locale) + " " + text.signup,
1261
1524
  children: text.signup
1262
1525
  })
1263
1526
  ]
1264
1527
  }),
1265
1528
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1266
1529
  className: "web-search-provider-info",
1267
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: info.description }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [info.pricing, info.pricingUrl && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [" ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExternalLink, {
1530
+ children: [info.description ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: info.description }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [info.pricing, info.pricingUrl && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [" ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppLink, {
1268
1531
  url: info.pricingUrl,
1269
- label: provider.label + " " + text.pricing,
1532
+ label: providerDisplayLabel(provider, locale) + " " + text.pricing,
1270
1533
  children: provider.id === "ddg" ? text.website : text.pricing
1271
1534
  })] })] })]
1272
1535
  }),
1273
- participating && !provider.configured ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1274
- className: "ws-provider-note",
1275
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: "warning" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: needsKey ? text.needsKey : text.unavailable })]
1536
+ access.kind === "link" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1537
+ className: "dsh-ui-hint",
1538
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppLink, {
1539
+ url: access.url,
1540
+ label: text.openConfig,
1541
+ children: text.openConfig
1542
+ })
1543
+ }) : access.kind === "owner" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1544
+ className: "dsh-ui-hint",
1545
+ children: [text.ownerSettings, access.name]
1546
+ }) : access.kind === "unknown" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1547
+ className: "dsh-ui-hint",
1548
+ children: text.unknownOwner
1276
1549
  }) : null,
1277
- shared && participating && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", {
1550
+ shared && participating && owned ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", {
1278
1551
  className: "dsh-ui-meta",
1279
- children: provider.credentialHint ?? text.sharedKey
1280
- }),
1552
+ children: provider.credentialHint === "与模型设置共用 Key" ? locale === "zh" ? provider.credentialHint : "Shares a key with model settings" : provider.credentialHint ?? text.sharedKey
1553
+ }) : null,
1281
1554
  showKey ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1282
1555
  className: "dsh-ui-field",
1283
1556
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -1306,7 +1579,7 @@ function NetworkSearchSettings({ client }) {
1306
1579
  onClick: () => void action(async () => {
1307
1580
  await saveKeys(provider);
1308
1581
  await load();
1309
- setNote(text.keySaved);
1582
+ setNote({ key: "keySaved" });
1310
1583
  }),
1311
1584
  children: text.saveKey
1312
1585
  }) : null, needsKey && !shared && provider.configured ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfirmButton, {
@@ -1322,11 +1595,11 @@ function NetworkSearchSettings({ client }) {
1322
1595
  className: "web-search-rank-move",
1323
1596
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
1324
1597
  checked: participating,
1325
- label: provider.label,
1598
+ label: providerDisplayLabel(provider, locale),
1326
1599
  disabled: busy,
1327
1600
  onChange: (next) => {
1328
1601
  const following = next ? [...order, provider.id] : order.filter((id) => id !== provider.id);
1329
- runImmediate(nextSearchEnabled(saved.settings.searchEnabled, following, saved.providers, {}, writable), following);
1602
+ runImmediate(nextSearchEnabled(saved.settings.searchEnabled, following, saved.providers, {}, writable), following, droppedDormantIds(saved.settings, following, saved.providers));
1330
1603
  }
1331
1604
  })
1332
1605
  })
@@ -1334,10 +1607,10 @@ function NetworkSearchSettings({ client }) {
1334
1607
  }, provider.id);
1335
1608
  })
1336
1609
  }),
1337
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1610
+ backends.some(usesBundledPricing) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1338
1611
  className: "dsh-ui-hint",
1339
1612
  children: pricingCheckedText(locale, PROVIDER_PRICING_CHECKED)
1340
- }),
1613
+ }) : null,
1341
1614
  on ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1342
1615
  className: "dsh-ui-actions",
1343
1616
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
@@ -1348,13 +1621,110 @@ function NetworkSearchSettings({ client }) {
1348
1621
  await saveKeys();
1349
1622
  const result = await call("test");
1350
1623
  await load();
1351
- setNote(testOkText(locale, result.sources));
1624
+ setNote({ sources: result.sources });
1352
1625
  }),
1353
1626
  children: text.test
1354
1627
  })
1355
1628
  }) : null
1356
1629
  ]
1357
- })
1630
+ }), (saved.providers.some((provider) => provider.kind === "fetch") || saved.settings.fetchProvider) && (() => {
1631
+ const fetchId = saved.settings.fetchProvider;
1632
+ const fetchSelected = saved.providers.find((provider) => provider.kind === "fetch" && provider.id === fetchId);
1633
+ const fetchDormant = Boolean(fetchId) && !fetchSelected;
1634
+ const fetchActive = Boolean(fetchSelected && saved.fetchActive && fetchSelected.configured);
1635
+ const fetchAccess = fetchSelected ? configurationAccess(fetchSelected) : fetchDormant ? { kind: "unknown" } : void 0;
1636
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
1637
+ className: "dsh-ui-card dsh-ui-card--flat",
1638
+ "data-testid": "web-search-fetch",
1639
+ "data-availability": fetchDormant ? "dormant" : fetchSelected?.configured ? "available" : "unavailable",
1640
+ "data-in-use": fetchActive || void 0,
1641
+ children: [
1642
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
1643
+ className: "ws-card-head",
1644
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1645
+ className: "dsh-ui-title",
1646
+ children: text.fetchTitle
1647
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1648
+ className: "ws-provider-note",
1649
+ children: [
1650
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: fetchDormant ? "idle" : fetchSelected?.configured ? "done" : "warning" }),
1651
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: fetchDormant ? text.dormant : fetchSelected?.configured ? text.available : text.unavailable }),
1652
+ fetchActive ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
1653
+ tone: "success",
1654
+ children: text.inUse
1655
+ }) : null
1656
+ ]
1657
+ })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
1658
+ checked: saved.settings.fetchEnabled,
1659
+ label: text.fetchTitle,
1660
+ disabled: busy || reordering,
1661
+ onChange: (next) => void action(async () => {
1662
+ const current = statusRef.current;
1663
+ adopt(await call("update", {
1664
+ settings: {
1665
+ ...editable(current.settings),
1666
+ fetchEnabled: next
1667
+ },
1668
+ expectedRevision: current.settings.revision
1669
+ }));
1670
+ await load();
1671
+ })
1672
+ })]
1673
+ }),
1674
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1675
+ className: "dsh-ui-field",
1676
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1677
+ className: "dsh-ui-label",
1678
+ children: text.fetchProvider
1679
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1680
+ className: "dsh-ui-select",
1681
+ "aria-label": text.fetchProvider,
1682
+ value: fetchId,
1683
+ disabled: busy || reordering,
1684
+ onChange: (event) => {
1685
+ const id = event.target.value;
1686
+ action(async () => {
1687
+ const current = statusRef.current;
1688
+ adopt(await call("update", {
1689
+ settings: {
1690
+ ...editable(current.settings),
1691
+ fetchProvider: id
1692
+ },
1693
+ expectedRevision: current.settings.revision
1694
+ }));
1695
+ await load();
1696
+ });
1697
+ },
1698
+ children: [fetchDormant && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
1699
+ value: fetchId,
1700
+ children: [
1701
+ fetchId,
1702
+ " · ",
1703
+ text.dormant
1704
+ ]
1705
+ }), saved.providers.filter((provider) => provider.kind === "fetch").map((provider) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
1706
+ value: provider.id,
1707
+ children: [providerDisplayLabel(provider, locale), provider.configured ? "" : " · " + text.unavailable]
1708
+ }, provider.id))]
1709
+ })]
1710
+ }),
1711
+ fetchAccess?.kind === "link" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1712
+ className: "dsh-ui-hint",
1713
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppLink, {
1714
+ url: fetchAccess.url,
1715
+ label: text.openConfig,
1716
+ children: text.openConfig
1717
+ })
1718
+ }) : fetchAccess?.kind === "owner" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1719
+ className: "dsh-ui-hint",
1720
+ children: [text.ownerSettings, fetchAccess.name]
1721
+ }) : fetchAccess?.kind === "unknown" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1722
+ className: "dsh-ui-hint",
1723
+ children: text.unknownOwner
1724
+ }) : null
1725
+ ]
1726
+ });
1727
+ })()]
1358
1728
  }),
1359
1729
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1360
1730
  role: "tabpanel",
@@ -1496,21 +1866,35 @@ function apply(ctx) {
1496
1866
  ctx.effect(() => client.slots.inject("plugins.bundle.config", () => client.slots.register({
1497
1867
  name: "plugins.bundle.config",
1498
1868
  key: "@klarkxy/dsh-web-search-manager"
1499
- }, () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NetworkSearchSettings, { client }))), "web-search-manager.settings");
1869
+ }, (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NetworkSearchSettings, {
1870
+ client,
1871
+ hidden: props?.hidden
1872
+ }))), "web-search-manager.settings");
1500
1873
  }
1501
1874
  //#endregion
1502
1875
  exports.NetworkSearchSettings = NetworkSearchSettings;
1876
+ exports.STATUS_REFRESH_MS = STATUS_REFRESH_MS;
1503
1877
  exports.apply = apply;
1878
+ exports.bindStatusRefresh = bindStatusRefresh;
1504
1879
  exports.canEnableSearch = canEnableSearch;
1880
+ exports.canStartStatusRead = canStartStatusRead;
1505
1881
  exports.catalogSearchBackends = catalogSearchBackends;
1882
+ exports.configurationAccess = configurationAccess;
1883
+ exports.dormantSearchPlaceholder = dormantSearchPlaceholder;
1884
+ exports.droppedDormantIds = droppedDormantIds;
1506
1885
  exports.immediateUpdate = immediateUpdate;
1507
1886
  exports.inject = inject;
1887
+ exports.isExternalConfigurationUrl = isExternalConfigurationUrl;
1888
+ exports.isPageHidden = isPageHidden;
1508
1889
  exports.isProviderOn = isProviderOn;
1890
+ exports.isRegisteredSearch = isRegisteredSearch;
1509
1891
  exports.name = name;
1510
1892
  exports.nextSearchEnabled = nextSearchEnabled;
1893
+ exports.ownsProviderConfiguration = ownsProviderConfiguration;
1511
1894
  exports.parseLimit = parseLimit;
1512
1895
  exports.searchBackends = searchBackends;
1513
1896
  exports.selectedSearchBackend = selectedSearchBackend;
1897
+ exports.shouldApplyPolledStatus = shouldApplyPolledStatus;
1514
1898
 
1515
1899
  //# sourceMappingURL=client.inner.cjs.map
1516
1900
  return module.exports;