@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.
@@ -2,6 +2,47 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
3
3
  let react = require("react");
4
4
  let react_jsx_runtime = require("react/jsx-runtime");
5
+ //#region src/client-error.ts
6
+ /** Local adapter diagnostics only; third-party descriptions remain verbatim. */
7
+ const localErrors = {
8
+ "网络搜索设置格式无效。": "Invalid web search settings format.",
9
+ "未知操作。": "Unknown operation.",
10
+ "网络设置操作失败,请刷新后重试。": "The network settings operation failed. Refresh and try again.",
11
+ "查询须为 1 至 4000 字符。": "The query must contain 1 to 4,000 characters.",
12
+ "网络搜索插件已停止。": "The web search plugin has stopped.",
13
+ "请在设置 → 网络搜索中配置凭据并启用。": "Configure credentials and enable web search in Settings → Web search.",
14
+ "网络访问设置已改变,请重新发起请求。": "Network access settings changed. Start the request again.",
15
+ "网络请求已取消。": "The network request was cancelled.",
16
+ "网络请求已取消或超时。": "The network request was cancelled or timed out.",
17
+ "设置已被其他页面修改,请刷新后重试。": "Settings changed in another page. Refresh and try again.",
18
+ "供应商不支持自定义地址。": "The provider does not support a custom address.",
19
+ "供应商地址须为无凭据、无查询参数的 HTTPS 地址。": "The provider address must use HTTPS without credentials or query parameters.",
20
+ "需选择已安装的供应商并填写有效凭据。": "Choose an installed provider and enter valid credentials.",
21
+ "保存失败,本次运行已暂停网络访问以防意外联网。": "Saving failed. Network access is paused for this run to prevent unintended requests.",
22
+ "供应商未提供错误描述。": "The provider did not supply an error description.",
23
+ "供应商请求失败。": "Provider request failed.",
24
+ "搜索凭据已删除或不可用,请重新配置。": "Search credentials were deleted or are unavailable. Configure them again.",
25
+ "供应商配置不可用。": "Provider configuration is unavailable.",
26
+ "查询无效。": "Invalid query.",
27
+ "结果数须为 1 至 20。": "The result count must be from 1 to 20.",
28
+ "搜索已取消。": "Search cancelled.",
29
+ "Tavily 搜索已取消。": "Tavily search cancelled.",
30
+ "响应格式无效。": "Invalid response shape",
31
+ "响应缺失。": "Missing response",
32
+ "响应过大。": "Response too large",
33
+ "地址无效。": "Invalid endpoint"
34
+ };
35
+ function localErrorText(message, locale) {
36
+ const prefix = message.match(/^(\[(?:search|fetch):[^\]]+\] )(.*)$/s);
37
+ if (prefix) return prefix[1] + localErrorText(prefix[2], locale);
38
+ if (locale === "zh") return Object.entries(localErrors).find(([, english]) => english === message)?.[0] ?? message;
39
+ if (localErrors[message]) return localErrors[message];
40
+ const http = message.match(/^供应商请求失败(HTTP (\d{3}))。$/);
41
+ if (http) return `Provider request failed (HTTP ${http[1]}).`;
42
+ const config = message.match(/^(brave|bocha|firecrawl|serper|Tavily) 配置不可用。$/);
43
+ if (config) return `${config[1]} configuration is unavailable.`;
44
+ return message;
45
+ }
5
46
  Object.values({
6
47
  /** Surfaces, from the page up to the topmost nested card. */
7
48
  surface: [
@@ -299,7 +340,7 @@ ${scope} {
299
340
  border: 0.5px solid var(--dsw-alias-border-l3);
300
341
  border-radius: var(--dsw-radius-md);
301
342
  background: var(--dsw-alias-bg-layer-1);
302
- font-family: var(--ds-font-family-code, ui-monospace, 'Cascadia Mono', Consolas, monospace);
343
+ font-family: var(--ds-font-family-code);
303
344
  font-size: 12px;
304
345
  line-height: 20px;
305
346
  color: var(--dsw-alias-label-secondary);
@@ -375,6 +416,49 @@ ${scope} {
375
416
  `.replace(/^& (\.dsh-ui-[a-z0-9-]+)(?![+>~])/gm, (_match, className) => `&${className}, & ${className}`);
376
417
  }
377
418
  //#endregion
419
+ //#region ../dsh-plugin-kit/src/client-utils.ts
420
+ /** Fixed pause for failing background reads; manual work is never replayed. */
421
+ function createPluginReadGate(now = Date.now) {
422
+ let pausedUntil = 0;
423
+ let epoch = 0;
424
+ return {
425
+ canRead: () => now() >= pausedUntil,
426
+ reset: () => {
427
+ epoch++;
428
+ pausedUntil = 0;
429
+ },
430
+ async run(read) {
431
+ const current = epoch;
432
+ try {
433
+ const value = await read();
434
+ if (current === epoch) pausedUntil = 0;
435
+ return value;
436
+ } catch (error) {
437
+ const message = error instanceof Error ? error.message : "";
438
+ 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;
439
+ throw error;
440
+ }
441
+ }
442
+ };
443
+ }
444
+ //#endregion
445
+ //#region src/contracts.ts
446
+ const WEB_SEARCH_RPC_CHANNEL = "/web-search-manager";
447
+ function pickActiveSearch(order, configured) {
448
+ return order.find(configured) ?? "";
449
+ }
450
+ function safeConfigurationUrl(value) {
451
+ if (typeof value !== "string" || !value || /[\u0000-\u0020\\]/.test(value)) return;
452
+ try {
453
+ const url = new URL(value, "https://dsh.invalid");
454
+ if (url.username || url.password) return;
455
+ if (value.startsWith("/") && !value.startsWith("//") || value.startsWith("#")) return url.origin === "https://dsh.invalid" ? value : void 0;
456
+ return /^https:\/\//i.test(value) && url.protocol === "https:" ? value : void 0;
457
+ } catch {
458
+ return;
459
+ }
460
+ }
461
+ //#endregion
378
462
  //#region src/provider-info.ts
379
463
  const PROVIDER_PRICING_CHECKED = "2026-09-21";
380
464
  const providerInfo = {
@@ -419,22 +503,94 @@ const providerInfo = {
419
503
  pricingUrl: "https://docs.tavily.com/documentation/api-credits"
420
504
  }
421
505
  };
422
- function searchProviderInfo(provider) {
423
- const known = Object.hasOwn(providerInfo, provider.id) ? providerInfo[provider.id] : void 0;
424
- return {
425
- description: known?.description ?? provider.description,
426
- pricing: provider.pricing ?? known?.pricing ?? (provider.billing === "none" ? "该后端不收取搜索费用。" : provider.billing === "model-and-tools" ? "按模型用量与工具调用计费;额度与单价见供应商说明。" : "按 API 调用计费;额度与单价见供应商说明。"),
427
- pricingUrl: provider.pricingUrl ?? known?.pricingUrl ?? provider.signupUrl
428
- };
506
+ const englishInfo = {
507
+ ddg: {
508
+ description: "Search public DuckDuckGo pages without registration or an API key.",
509
+ pricing: "Free to use; no search API fees."
510
+ },
511
+ bocha: {
512
+ description: "Search Chinese web content, returning titles, links and summaries.",
513
+ pricing: "Charged per request: listed price ¥0.036/request; trial credits depend on official offers."
514
+ },
515
+ brave: {
516
+ description: "Search web pages, news and more with Brave’s independent index.",
517
+ pricing: "$5 in monthly credits (about 1,000 requests); then $5/1,000 requests. A payment card is required to activate."
518
+ },
519
+ "deepseek-official": {
520
+ description: "Call built-in search through a DeepSeek model using the model API key.",
521
+ 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."
522
+ },
523
+ exa: {
524
+ description: "Web search for AI with relevant pages and content snippets.",
525
+ 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."
526
+ },
527
+ firecrawl: {
528
+ description: "Search the web and extract page content.",
529
+ 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."
530
+ },
531
+ serper: {
532
+ description: "Get Google web search results.",
533
+ pricing: "2,500 one-time signup requests; $50/50,000 requests ($1/1,000). Paid credits expire after 6 months."
534
+ },
535
+ tavily: {
536
+ description: "Web search for AI, currently using basic search.",
537
+ pricing: "1,000 free credits/month; basic search uses 1 credit/request. Pay as you go: $0.008/credit ($8/1,000 requests)."
538
+ }
539
+ };
540
+ const bundledDescriptions = {
541
+ ddg: ["无需注册。默认后端。", "No registration required. Default backend."],
542
+ "deepseek-official": ["使用已配置的 DeepSeek API Key,可能产生额外搜索费用。", "Uses the configured DeepSeek API key; additional search fees may apply."],
543
+ exa: ["独立 Search API。", "Independent Search API."],
544
+ brave: ["Brave Search API。", "Brave Search API."],
545
+ bocha: ["中文网页搜索 API。", "Chinese web search API."],
546
+ serper: ["Google 结果的 Search API。", "Search API for Google results."],
547
+ firecrawl: ["网页搜索与提取 API。", "Web search and extraction API."],
548
+ tavily: ["Search API;固定 basic 检索,不自动升级搜索深度。", "Search API using basic search; search depth is never upgraded automatically."]
549
+ };
550
+ /** Translate only manager-owned labels; externally supplied metadata stays verbatim. */
551
+ function providerDisplayLabel(provider, locale) {
552
+ if (!bundledCatalogId(provider)) return provider.label;
553
+ if (provider.label === "DeepSeek 搜索") return locale === "zh" ? provider.label : "DeepSeek search";
554
+ if (provider.label === "博查") return locale === "zh" ? provider.label : "Bocha";
555
+ if (provider.label === "网页读取") return locale === "zh" ? provider.label : "Read web pages";
556
+ return provider.label;
429
557
  }
430
- //#endregion
431
- //#region src/contracts.ts
432
- const WEB_SEARCH_RPC_CHANNEL = "/web-search-manager";
433
- function resolveSearchOrder(knownIds, settings, _keyed) {
434
- return [...new Set(settings.searchOrder.filter((id) => knownIds.includes(id)))];
558
+ function bundledCatalogId(provider) {
559
+ if (!(typeof provider.configurationOwned === "boolean" ? provider.configurationOwned : !provider.configurationOwner || provider.configurationOwner === "@klarkxy/dsh-web-search-manager")) return;
560
+ return provider.id === "deepseek-managed" ? "deepseek-official" : provider.id;
435
561
  }
436
- function pickActiveSearch(order, configured) {
437
- return order.find(configured) ?? "";
562
+ function missingPricing(locale) {
563
+ return locale === "zh" ? "未提供费用信息" : "No pricing information provided";
564
+ }
565
+ function usesBundledPricing(provider) {
566
+ if (provider.pricing?.trim()) return false;
567
+ const catalogId = bundledCatalogId(provider);
568
+ return Boolean(catalogId && Object.hasOwn(providerInfo, catalogId));
569
+ }
570
+ function searchProviderInfo(provider, locale = "zh") {
571
+ const catalogId = bundledCatalogId(provider);
572
+ const base = catalogId && Object.hasOwn(providerInfo, catalogId) ? providerInfo[catalogId] : void 0;
573
+ const known = base && locale === "en" ? {
574
+ ...base,
575
+ ...englishInfo[catalogId]
576
+ } : base;
577
+ const ownedCopy = catalogId ? bundledDescriptions[catalogId] : void 0;
578
+ 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;
579
+ if (provider.pricing?.trim()) return {
580
+ description,
581
+ pricing: provider.pricing,
582
+ pricingUrl: provider.pricingUrl ?? known?.pricingUrl ?? provider.signupUrl
583
+ };
584
+ if (known) return {
585
+ description,
586
+ pricing: known.pricing,
587
+ pricingUrl: provider.pricingUrl ?? known.pricingUrl ?? provider.signupUrl
588
+ };
589
+ return {
590
+ description,
591
+ pricing: missingPricing(locale),
592
+ pricingUrl: provider.pricingUrl ?? provider.signupUrl
593
+ };
438
594
  }
439
595
  //#endregion
440
596
  //#region src/client.tsx
@@ -454,32 +610,129 @@ function editable(settings) {
454
610
  const { revision: _revision, ...rest } = settings;
455
611
  return rest;
456
612
  }
613
+ const STATUS_REFRESH_MS = 3e3;
614
+ function isPageHidden(hidden) {
615
+ if (hidden) return true;
616
+ return typeof document !== "undefined" && document.visibilityState === "hidden";
617
+ }
618
+ function canStartStatusRead(input) {
619
+ if (!input.mounted) return false;
620
+ if (input.source === "poll") return !input.hidden && !input.busy && !input.reordering && !input.reading;
621
+ return true;
622
+ }
623
+ function isExternalConfigurationUrl(url) {
624
+ return /^https:\/\//i.test(url);
625
+ }
626
+ function shouldApplyPolledStatus(input) {
627
+ if (input.hidden || input.busy || input.reordering) return false;
628
+ if (input.requestId !== input.latestRequestId) return false;
629
+ if (input.currentRevision !== void 0 && input.incomingRevision < input.currentRevision) return false;
630
+ return true;
631
+ }
632
+ function bindStatusRefresh(options) {
633
+ const intervalMs = options.intervalMs ?? 3e3;
634
+ let inFlight = false;
635
+ let stopped = false;
636
+ const run = async () => {
637
+ if (stopped || inFlight || options.isHidden()) return;
638
+ inFlight = true;
639
+ try {
640
+ await options.tick();
641
+ } finally {
642
+ inFlight = false;
643
+ }
644
+ };
645
+ const timer = (options.setIntervalFn ?? setInterval)(() => {
646
+ run();
647
+ }, intervalMs);
648
+ const onVisible = () => {
649
+ if (!options.isHidden()) run();
650
+ };
651
+ const add = options.addListener ?? ((type, listener) => {
652
+ if (typeof document !== "undefined") document.addEventListener(type, listener);
653
+ });
654
+ const remove = options.removeListener ?? ((type, listener) => {
655
+ if (typeof document !== "undefined") document.removeEventListener(type, listener);
656
+ });
657
+ add("visibilitychange", onVisible);
658
+ return () => {
659
+ stopped = true;
660
+ (options.clearIntervalFn ?? clearInterval)(timer);
661
+ remove("visibilitychange", onVisible);
662
+ };
663
+ }
457
664
  function isProviderOn(status, provider) {
458
- const enabled = provider.kind === "search" ? status.searchActive : status.fetchActive;
459
- const selected = status.settings[provider.kind === "search" ? "searchProvider" : "fetchProvider"];
460
- return enabled && selected === provider.id;
665
+ if (provider.kind === "fetch") return status.fetchActive && status.settings.fetchProvider === provider.id && provider.configured;
666
+ return status.searchActive && selectedSearchBackend(status)?.id === provider.id;
461
667
  }
462
- function searchBackends(status) {
463
- const all = status.providers.filter((provider) => provider.kind === "search");
464
- 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) ?? []);
668
+ function dormantSearchPlaceholder(id) {
669
+ return {
670
+ id,
671
+ label: id,
672
+ description: "",
673
+ billing: "unknown",
674
+ kind: "search",
675
+ configured: false,
676
+ calls: 0,
677
+ failures: 0,
678
+ configurationOwned: false
679
+ };
680
+ }
681
+ function isRegisteredSearch(status, id) {
682
+ return status.providers.some((provider) => provider.kind === "search" && provider.id === id);
465
683
  }
466
684
  function catalogSearchBackends(status) {
467
- const enabled = searchBackends(status);
468
- const rest = status.providers.filter((provider) => provider.kind === "search" && !enabled.some((row) => row.id === provider.id));
469
- return [...enabled, ...rest];
685
+ const byId = new Map(status.providers.filter((provider) => provider.kind === "search").map((provider) => [provider.id, provider]));
686
+ const rows = [];
687
+ const seen = /* @__PURE__ */ new Set();
688
+ for (const id of status.settings.searchOrder) {
689
+ if (!id || seen.has(id)) continue;
690
+ seen.add(id);
691
+ rows.push(byId.get(id) ?? dormantSearchPlaceholder(id));
692
+ }
693
+ for (const provider of status.providers) {
694
+ if (provider.kind !== "search" || seen.has(provider.id)) continue;
695
+ rows.push(provider);
696
+ }
697
+ return rows;
698
+ }
699
+ function searchBackends(status) {
700
+ const participating = new Set(status.settings.searchOrder);
701
+ return catalogSearchBackends(status).filter((provider) => participating.has(provider.id));
470
702
  }
471
703
  function selectedSearchBackend(status) {
472
- const backends = searchBackends(status);
704
+ const backends = searchBackends(status).filter((provider) => isRegisteredSearch(status, provider.id));
473
705
  const id = pickActiveSearch(backends.map((provider) => provider.id), (id) => Boolean(backends.find((provider) => provider.id === id)?.configured));
474
706
  return backends.find((provider) => provider.id === id);
475
707
  }
476
- function ExternalLink({ url, label, children }) {
708
+ function droppedDormantIds(settings, order, providers) {
709
+ const known = new Set(providers.filter((provider) => provider.kind === "search").map((provider) => provider.id));
710
+ return [...new Set(settings.searchOrder.filter((id) => id && !known.has(id) && !order.includes(id)))];
711
+ }
712
+ function configurationAccess(provider) {
713
+ if (ownsProviderConfiguration(provider)) return { kind: "owned" };
714
+ const url = safeConfigurationUrl(provider.configurationUrl);
715
+ if (url) return {
716
+ kind: "link",
717
+ url
718
+ };
719
+ if (provider.configurationOwner?.trim()) return {
720
+ kind: "owner",
721
+ name: provider.configurationOwner
722
+ };
723
+ return { kind: "unknown" };
724
+ }
725
+ function AppLink({ url, label, children }) {
726
+ const external = isExternalConfigurationUrl(url);
477
727
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
478
728
  href: url,
479
- target: "_blank",
480
- rel: "noreferrer noopener",
729
+ ...external ? {
730
+ target: "_blank",
731
+ rel: "noreferrer noopener"
732
+ } : {},
481
733
  "aria-label": label,
482
734
  onClick: (event) => {
735
+ if (!external) return;
483
736
  const bridge = globalThis.dshWindow;
484
737
  if (bridge?.openExternal) {
485
738
  event.preventDefault();
@@ -495,9 +748,13 @@ function canEnableSearch(order, providers, keys, writable) {
495
748
  if (!provider) return false;
496
749
  if (provider.configured) return true;
497
750
  const typed = (keys[provider.id] ?? "").trim();
498
- return Boolean(provider.credentialRef && typed && writable[provider.credentialRef] !== false);
751
+ return Boolean(ownsProviderConfiguration(provider) && provider.credentialRef && typed && writable[provider.credentialRef] !== false);
499
752
  });
500
753
  }
754
+ function ownsProviderConfiguration(provider) {
755
+ if (typeof provider.configurationOwned === "boolean") return provider.configurationOwned;
756
+ return !provider.configurationOwner || provider.configurationOwner === "@klarkxy/dsh-web-search-manager";
757
+ }
501
758
  function nextSearchEnabled(currentlyEnabled, nextOrder, providers, keys, writable) {
502
759
  return currentlyEnabled && canEnableSearch(nextOrder, providers, keys, writable);
503
760
  }
@@ -549,11 +806,14 @@ const copy = {
549
806
  dragPrefix: "拖动排序 ",
550
807
  dragHint: "拖动排序,或用上下方向键",
551
808
  inUse: "当前使用",
809
+ available: "可用",
810
+ unavailable: "暂不可用",
811
+ dormant: "未加载,保留选择",
552
812
  signup: "去注册",
553
813
  pricing: "费用说明",
554
814
  website: "官网",
815
+ openConfig: "打开配置",
555
816
  needsKey: "待配置 Key",
556
- unavailable: "暂不可用",
557
817
  sharedKey: "与其他设置共用凭据",
558
818
  keyConfigured: "已配置,留空不替换",
559
819
  keyPlaceholder: "输入 API Key",
@@ -567,7 +827,11 @@ const copy = {
567
827
  maxResults: "每条查询结果上限",
568
828
  maxQueries: "每次调用查询上限",
569
829
  timeoutMs: "请求超时(毫秒)",
570
- maxFetchChars: "正文字符上限"
830
+ maxFetchChars: "正文字符上限",
831
+ fetchTitle: "网页读取",
832
+ fetchProvider: "读取服务",
833
+ ownerSettings: "配置入口:",
834
+ unknownOwner: "请在服务所属插件中配置"
571
835
  },
572
836
  en: {
573
837
  tabsLabel: "Web search settings",
@@ -589,11 +853,14 @@ const copy = {
589
853
  dragPrefix: "Reorder ",
590
854
  dragHint: "Drag to reorder, or use arrow keys",
591
855
  inUse: "In use",
856
+ available: "Available",
857
+ unavailable: "Unavailable",
858
+ dormant: "Not loaded; selection kept",
592
859
  signup: "Sign up",
593
860
  pricing: "Pricing",
594
861
  website: "Website",
862
+ openConfig: "Open configuration",
595
863
  needsKey: "Key required",
596
- unavailable: "Unavailable",
597
864
  sharedKey: "Shares a credential with other settings",
598
865
  keyConfigured: "Configured; empty keeps it",
599
866
  keyPlaceholder: "Enter API key",
@@ -607,14 +874,38 @@ const copy = {
607
874
  maxResults: "Max results/query",
608
875
  maxQueries: "Max queries/call",
609
876
  timeoutMs: "Request timeout (ms)",
610
- maxFetchChars: "Max text chars"
877
+ maxFetchChars: "Max text chars",
878
+ fetchTitle: "Read web pages",
879
+ fetchProvider: "Fetch service",
880
+ ownerSettings: "Configure in: ",
881
+ unknownOwner: "Configure this service in its plugin"
882
+ }
883
+ };
884
+ var UiError = class extends Error {
885
+ key;
886
+ constructor(key) {
887
+ super(key);
888
+ this.key = key;
611
889
  }
612
890
  };
891
+ function uiMessage(message, locale, providers = []) {
892
+ if (typeof message === "string") return localErrorText(message, locale);
893
+ if ("key" in message) return copy[locale][message.key];
894
+ if ("sources" in message) return testOkText(locale, message.sources);
895
+ if ("moved" in message) {
896
+ const provider = providers.find((item) => item.id === message.moved);
897
+ return movedText(locale, provider ? providerDisplayLabel(provider, locale) : message.moved, message.position);
898
+ }
899
+ return message.invalid.map((key) => {
900
+ const [, min, max] = LIMITS.find((item) => item[0] === key);
901
+ return limitInvalidText(locale, copy[locale][key], min, max);
902
+ }).join(" ");
903
+ }
613
904
  function movedText(locale, label, position) {
614
905
  return locale === "zh" ? `${label} 已移至第 ${position} 位` : `${label} moved to position ${position}`;
615
906
  }
616
907
  function pricingCheckedText(locale, date) {
617
- return locale === "zh" ? `价格核对于 ${date},额度与费用以供应商账户为准。` : `Prices checked on ${date}; your provider account is authoritative.`;
908
+ return locale === "zh" ? `内置适配器价格核对于 ${date},额度与费用以供应商账户为准。` : `Bundled adapter prices checked on ${date}; your provider account is authoritative.`;
618
909
  }
619
910
  function testOkText(locale, sources) {
620
911
  return locale === "zh" ? `连接正常,返回 ${sources} 条来源。` : `Connection OK: ${sources} sources returned.`;
@@ -655,13 +946,25 @@ function parseLimit(raw, min, max) {
655
946
  * Settings written by an immediate action (a switch or a reorder). It keeps the
656
947
  * saved limits and never includes typed-but-unsaved keys or limit drafts.
657
948
  */
658
- function immediateUpdate(settings, on, order) {
949
+ function immediateUpdate(settings, on, order, providers, remove = []) {
950
+ let searchOrder = order;
951
+ if (providers) {
952
+ const known = new Set(providers.filter((provider) => provider.kind === "search").map((provider) => provider.id));
953
+ const removed = new Set(remove);
954
+ const following = order.filter((id) => !removed.has(id));
955
+ if (following.some((id) => !known.has(id))) searchOrder = following;
956
+ else {
957
+ searchOrder = settings.searchOrder.flatMap((id) => {
958
+ if (removed.has(id)) return [];
959
+ return known.has(id) ? following.splice(0, 1) : [id];
960
+ });
961
+ searchOrder.push(...following);
962
+ }
963
+ }
659
964
  return {
660
965
  ...editable(settings),
661
- searchOrder: order,
662
- searchEnabled: on,
663
- fetchProvider: "http",
664
- fetchEnabled: on
966
+ searchOrder,
967
+ searchEnabled: on
665
968
  };
666
969
  }
667
970
  function limitTexts(settings) {
@@ -679,7 +982,7 @@ function useHostLocale(client) {
679
982
  }
680
983
  /** Keyboard reorders are batched: persist once the arrows stop for this long. */
681
984
  const REORDER_DEBOUNCE_MS = 500;
682
- function NetworkSearchSettings({ client }) {
985
+ function NetworkSearchSettings({ client, hidden }) {
683
986
  const locale = useHostLocale(client);
684
987
  const text = copy[locale];
685
988
  const tabsId = (0, react.useId)();
@@ -703,6 +1006,17 @@ function NetworkSearchSettings({ client }) {
703
1006
  const timerRef = (0, react.useRef)(void 0);
704
1007
  const handleRefs = (0, react.useRef)(/* @__PURE__ */ new Map());
705
1008
  const refocusRef = (0, react.useRef)(null);
1009
+ const busyRef = (0, react.useRef)(false);
1010
+ const hiddenRef = (0, react.useRef)(hidden);
1011
+ hiddenRef.current = hidden;
1012
+ const requestSeq = (0, react.useRef)(0);
1013
+ const mountedRef = (0, react.useRef)(true);
1014
+ const readingRef = (0, react.useRef)(false);
1015
+ const readingRequest = (0, react.useRef)(0);
1016
+ const statusRead = (0, react.useRef)(Promise.resolve());
1017
+ const readGateRef = (0, react.useRef)(void 0);
1018
+ if (!readGateRef.current) readGateRef.current = createPluginReadGate();
1019
+ const readGate = readGateRef.current;
706
1020
  async function call(endpoint, payload = {}) {
707
1021
  return unwrap(await client.connection.rpc.call(WEB_SEARCH_RPC_CHANNEL, endpoint, payload));
708
1022
  }
@@ -718,30 +1032,109 @@ function NetworkSearchSettings({ client }) {
718
1032
  });
719
1033
  if (resetDraft) setInvalid({});
720
1034
  }
721
- async function load(resetDraft = false) {
722
- const next = await call("status");
723
- adopt(next, resetDraft);
724
- setLoadError("");
725
- const refs = next.providers.flatMap((provider) => provider.credentialRef ? [provider.credentialRef] : []);
726
- if (refs.length) {
727
- const credentials = unwrap(await client.remote.credentials.describe(refs));
728
- setWritable(Object.fromEntries(Object.entries(credentials).map(([ref, value]) => [ref, value.writable])));
1035
+ function pollBlocked() {
1036
+ return !canStartStatusRead({
1037
+ source: "poll",
1038
+ mounted: mountedRef.current,
1039
+ hidden: isPageHidden(hiddenRef.current),
1040
+ busy: busyRef.current,
1041
+ reordering: pendingRef.current !== null,
1042
+ reading: readingRef.current
1043
+ });
1044
+ }
1045
+ async function describeOwned(next, mine) {
1046
+ const refs = next.providers.flatMap((provider) => ownsProviderConfiguration(provider) && provider.credentialRef ? [provider.credentialRef] : []);
1047
+ if (!refs.length) return;
1048
+ const credentials = unwrap(await readGate.run(() => client.remote.credentials.describe(refs)));
1049
+ if (!mountedRef.current || mine !== requestSeq.current) return;
1050
+ setWritable(Object.fromEntries(Object.entries(credentials).map(([ref, value]) => [ref, value.writable])));
1051
+ }
1052
+ async function load(resetDraft = false, source = "user") {
1053
+ if (source === "poll" && !readGate.canRead()) return;
1054
+ if (source === "user") {
1055
+ readGate.reset();
1056
+ requestSeq.current += 1;
1057
+ }
1058
+ while (readingRef.current) {
1059
+ if (source === "poll" || !mountedRef.current) return;
1060
+ await statusRead.current;
1061
+ }
1062
+ if (!mountedRef.current) return;
1063
+ if (source === "poll" && pollBlocked()) return;
1064
+ const mine = ++requestSeq.current;
1065
+ readingRef.current = true;
1066
+ readingRequest.current = mine;
1067
+ let release = () => {};
1068
+ statusRead.current = new Promise((resolve) => {
1069
+ release = resolve;
1070
+ });
1071
+ try {
1072
+ const next = unwrap(await readGate.run(() => client.connection.rpc.call(WEB_SEARCH_RPC_CHANNEL, "status", {})));
1073
+ if (!mountedRef.current || mine !== requestSeq.current) return next;
1074
+ if (source === "poll" && !shouldApplyPolledStatus({
1075
+ requestId: mine,
1076
+ latestRequestId: requestSeq.current,
1077
+ busy: busyRef.current,
1078
+ reordering: pendingRef.current !== null,
1079
+ hidden: isPageHidden(hiddenRef.current),
1080
+ currentRevision: statusRef.current?.settings.revision,
1081
+ incomingRevision: next.settings.revision
1082
+ })) return next;
1083
+ adopt(next, resetDraft);
1084
+ setLoadError("");
1085
+ await describeOwned(next, mine);
1086
+ return next;
1087
+ } catch (cause) {
1088
+ if (!mountedRef.current || mine !== requestSeq.current) return;
1089
+ setLoadError(cause instanceof Error && cause.message ? cause.message : { key: "failed" });
1090
+ if (source === "poll") return;
1091
+ throw cause;
1092
+ } finally {
1093
+ if (readingRequest.current === mine) readingRef.current = false;
1094
+ release();
729
1095
  }
730
- return next;
731
1096
  }
732
1097
  (0, react.useEffect)(() => {
733
- load().catch((cause) => setLoadError(cause instanceof Error && cause.message ? cause.message : text.failed));
1098
+ mountedRef.current = true;
1099
+ readGate.reset();
1100
+ load().catch((cause) => setLoadError(cause instanceof Error && cause.message ? cause.message : { key: "failed" }));
1101
+ const bind = () => bindStatusRefresh({
1102
+ isHidden: () => isPageHidden(hiddenRef.current),
1103
+ tick: async () => {
1104
+ await load(false, "poll").catch(() => {});
1105
+ }
1106
+ });
1107
+ let stop = bind();
1108
+ const stopConnection = client.connection.generation?.subscribe(() => {
1109
+ readGate.reset();
1110
+ requestSeq.current += 1;
1111
+ readingRef.current = false;
1112
+ statusRead.current = Promise.resolve();
1113
+ stop();
1114
+ stop = bind();
1115
+ load(false, "poll");
1116
+ });
1117
+ return () => {
1118
+ mountedRef.current = false;
1119
+ readGate.reset();
1120
+ requestSeq.current += 1;
1121
+ stop();
1122
+ stopConnection?.();
1123
+ if (timerRef.current === void 0) return;
1124
+ clearTimeout(timerRef.current);
1125
+ const current = statusRef.current;
1126
+ const next = pendingRef.current;
1127
+ if (current && next) call("update", {
1128
+ settings: immediateUpdate(current.settings, current.settings.searchEnabled, next, current.providers),
1129
+ expectedRevision: current.settings.revision
1130
+ }).catch(() => {});
1131
+ };
734
1132
  }, [client]);
735
- (0, react.useEffect)(() => () => {
736
- if (timerRef.current === void 0) return;
737
- clearTimeout(timerRef.current);
738
- const current = statusRef.current;
739
- const next = pendingRef.current;
740
- if (current && next) call("update", {
741
- settings: immediateUpdate(current.settings, current.settings.searchEnabled, next),
742
- expectedRevision: current.settings.revision
743
- }).catch(() => {});
744
- }, []);
1133
+ (0, react.useEffect)(() => {
1134
+ if (hidden) return;
1135
+ if (!statusRef.current) return;
1136
+ load(false, "poll").catch(() => {});
1137
+ }, [hidden]);
745
1138
  (0, react.useLayoutEffect)(() => {
746
1139
  const id = refocusRef.current;
747
1140
  if (!id) return;
@@ -750,20 +1143,24 @@ function NetworkSearchSettings({ client }) {
750
1143
  if (handle && document.activeElement !== handle) handle.focus();
751
1144
  });
752
1145
  async function action(run) {
1146
+ readGate.reset();
1147
+ requestSeq.current += 1;
1148
+ busyRef.current = true;
753
1149
  setBusy(true);
754
1150
  setNote("");
755
1151
  setError("");
756
1152
  try {
757
1153
  await run();
758
1154
  } catch (cause) {
759
- setError(cause instanceof Error && cause.message ? cause.message : text.failed);
1155
+ setError(cause instanceof UiError ? { key: cause.key } : cause instanceof Error && cause.message ? cause.message : { key: "failed" });
760
1156
  await load().catch(() => {});
761
1157
  } finally {
1158
+ busyRef.current = false;
762
1159
  setBusy(false);
763
1160
  }
764
1161
  }
765
1162
  /** Switches and reorders: settings only, applied at once. */
766
- function runImmediate(on, order) {
1163
+ function runImmediate(on, order, remove = []) {
767
1164
  clearTimeout(timerRef.current);
768
1165
  timerRef.current = void 0;
769
1166
  pendingRef.current = null;
@@ -771,7 +1168,7 @@ function NetworkSearchSettings({ client }) {
771
1168
  const current = statusRef.current;
772
1169
  if (!current) return;
773
1170
  const next = await call("update", {
774
- settings: immediateUpdate(current.settings, on, order),
1171
+ settings: immediateUpdate(current.settings, on, order, current.providers, remove),
775
1172
  expectedRevision: current.settings.revision
776
1173
  });
777
1174
  setStatus(next);
@@ -787,8 +1184,8 @@ function NetworkSearchSettings({ client }) {
787
1184
  if (provider.kind !== "search" || only && provider.id !== only.id) continue;
788
1185
  const snapshot = keys[provider.id] ?? "";
789
1186
  const typed = snapshot.trim();
790
- if (!provider.credentialRef || !typed) continue;
791
- if (writable[provider.credentialRef] === false) throw new Error(text.readOnly);
1187
+ if (!ownsProviderConfiguration(provider) || !provider.credentialRef || !typed) continue;
1188
+ if (writable[provider.credentialRef] === false) throw new UiError("readOnly");
792
1189
  unwrap(await client.remote.credentials.set(provider.credentialRef, typed));
793
1190
  setKeys((current) => current[provider.id] === snapshot ? {
794
1191
  ...current,
@@ -797,21 +1194,21 @@ function NetworkSearchSettings({ client }) {
797
1194
  }
798
1195
  }
799
1196
  async function removeKey(provider) {
800
- if (!provider.credentialRef || provider.credentialShared || !status) return;
1197
+ if (!ownsProviderConfiguration(provider) || !provider.credentialRef || provider.credentialShared || !status) return;
801
1198
  unwrap(await client.remote.credentials.unset(provider.credentialRef));
802
1199
  setKeys((current) => ({
803
1200
  ...current,
804
1201
  [provider.id]: ""
805
1202
  }));
806
1203
  await load();
807
- setNote(text.keyDeleted);
1204
+ setNote({ key: "keyDeleted" });
808
1205
  }
809
1206
  if (!status || !limitDraft) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
810
1207
  className: "web-search-settings ws-root dsh-ui-panel",
811
1208
  children: [loadError ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
812
1209
  role: "alert",
813
1210
  className: "dsh-ui-error",
814
- children: [text.loadFailed, loadError]
1211
+ children: [text.loadFailed, uiMessage(loadError, locale, status?.providers)]
815
1212
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
816
1213
  role: "status",
817
1214
  className: "dsh-ui-hint",
@@ -850,7 +1247,10 @@ function NetworkSearchSettings({ client }) {
850
1247
  pendingRef.current = next;
851
1248
  setPendingOrder(next);
852
1249
  if (keyboard) refocusRef.current = id;
853
- setSortAnnouncement(movedText(locale, backends.find((provider) => provider.id === id)?.label ?? id, nextIndex + 1));
1250
+ setSortAnnouncement({
1251
+ moved: id,
1252
+ position: nextIndex + 1
1253
+ });
854
1254
  clearTimeout(timerRef.current);
855
1255
  timerRef.current = setTimeout(() => {
856
1256
  timerRef.current = void 0;
@@ -871,13 +1271,13 @@ function NetworkSearchSettings({ client }) {
871
1271
  const value = parseLimit(limitDraft[key], min, max);
872
1272
  if (value === void 0) {
873
1273
  nextInvalid[key] = true;
874
- messages.push(limitInvalidText(locale, text[key], min, max));
1274
+ messages.push(key);
875
1275
  } else parsed[key] = value;
876
1276
  }
877
1277
  setInvalid(nextInvalid);
878
1278
  if (messages.length) {
879
1279
  setNote("");
880
- setError(messages.join(" "));
1280
+ setError({ invalid: messages });
881
1281
  return;
882
1282
  }
883
1283
  action(async () => {
@@ -886,14 +1286,12 @@ function NetworkSearchSettings({ client }) {
886
1286
  adopt(await call("update", {
887
1287
  settings: {
888
1288
  ...editable(current.settings),
889
- ...parsed,
890
- fetchProvider: "http",
891
- fetchEnabled: current.settings.searchEnabled
1289
+ ...parsed
892
1290
  },
893
1291
  expectedRevision: current.settings.revision
894
1292
  }), true);
895
1293
  await load();
896
- setNote(text.saved);
1294
+ setNote({ key: "saved" });
897
1295
  });
898
1296
  }
899
1297
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
@@ -920,30 +1318,46 @@ function NetworkSearchSettings({ client }) {
920
1318
  className: "ws-sr-only",
921
1319
  role: "status",
922
1320
  "aria-live": "polite",
923
- children: sortAnnouncement
1321
+ children: uiMessage(sortAnnouncement, locale, saved.providers)
924
1322
  }),
925
1323
  error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
926
1324
  role: "alert",
927
1325
  className: "dsh-ui-error",
928
- children: error
1326
+ children: uiMessage(error, locale, saved.providers)
929
1327
  }),
1328
+ loadError ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1329
+ className: "dsh-ui-stack",
1330
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1331
+ role: "alert",
1332
+ className: "dsh-ui-error",
1333
+ children: [text.loadFailed, uiMessage(loadError, locale, status?.providers)]
1334
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1335
+ variant: "outline",
1336
+ size: "sm",
1337
+ disabled: busy,
1338
+ onClick: () => void action(async () => {
1339
+ await load();
1340
+ }),
1341
+ children: text.reconnect
1342
+ })]
1343
+ }) : null,
930
1344
  note && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
931
1345
  role: "status",
932
1346
  className: "dsh-ui-notice",
933
- children: note
1347
+ children: uiMessage(note, locale, saved.providers)
934
1348
  }),
935
1349
  saved.storageFailed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
936
1350
  role: "alert",
937
1351
  className: "dsh-ui-banner dsh-ui-banner--danger",
938
1352
  children: text.storageFailed
939
1353
  }),
940
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1354
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
941
1355
  role: "tabpanel",
942
1356
  id: tabsId + "-providers-panel",
943
1357
  "aria-labelledby": tabsId + "-providers-tab",
944
1358
  hidden: tab !== "providers",
945
1359
  tabIndex: 0,
946
- children: backends.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1360
+ children: [backends.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
947
1361
  className: "dsh-ui-empty",
948
1362
  children: text.empty
949
1363
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
@@ -972,17 +1386,23 @@ function NetworkSearchSettings({ client }) {
972
1386
  children: backends.map((provider) => {
973
1387
  const participating = order.includes(provider.id);
974
1388
  const rank = order.indexOf(provider.id);
975
- const active = saved.searchActive && selected?.id === provider.id;
976
- const info = searchProviderInfo(provider);
977
- const needsKey = Boolean(provider.credentialRef);
1389
+ const dormant = !isRegisteredSearch(saved, provider.id);
1390
+ const active = !dormant && saved.searchActive && selected?.id === provider.id;
1391
+ const info = searchProviderInfo(provider, locale);
1392
+ const owned = ownsProviderConfiguration(provider);
1393
+ const access = configurationAccess(provider);
1394
+ const needsKey = owned && Boolean(provider.credentialRef);
978
1395
  const shared = Boolean(provider.credentialShared);
979
- const showKey = needsKey && participating && (!shared || !provider.configured);
1396
+ const showKey = !dormant && needsKey && participating && (!shared || !provider.configured);
980
1397
  const canSort = participating && order.length > 1;
981
1398
  const readOnly = writable[provider.credentialRef ?? ""] === false;
982
1399
  const typedKey = (keys[provider.id] ?? "").trim();
1400
+ const availability = dormant ? "dormant" : provider.configured ? "available" : "unavailable";
983
1401
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
984
1402
  className: active ? "is-active" : void 0,
985
1403
  "data-testid": `web-search-rank-${provider.id}`,
1404
+ "data-availability": availability,
1405
+ "data-in-use": active || void 0,
986
1406
  "data-dragging": dragSource === provider.id || void 0,
987
1407
  "data-drop": dropTarget === provider.id && dragSource ? order.indexOf(dragSource) < rank ? "after" : "before" : void 0,
988
1408
  onDragOver: (event) => {
@@ -1050,7 +1470,7 @@ function NetworkSearchSettings({ client }) {
1050
1470
  }),
1051
1471
  draggable: !busy,
1052
1472
  "aria-disabled": busy,
1053
- "aria-label": text.dragPrefix + provider.label,
1473
+ "aria-label": text.dragPrefix + providerDisplayLabel(provider, locale),
1054
1474
  title: text.dragHint,
1055
1475
  onDragStart: (event) => {
1056
1476
  if (busy) {
@@ -1082,35 +1502,49 @@ function NetworkSearchSettings({ client }) {
1082
1502
  children: [
1083
1503
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
1084
1504
  className: "dsh-ui-heading",
1085
- children: provider.label
1505
+ children: providerDisplayLabel(provider, locale)
1506
+ }),
1507
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1508
+ className: "ws-provider-note",
1509
+ 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 })]
1086
1510
  }),
1087
1511
  active && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
1088
1512
  tone: "success",
1089
1513
  children: text.inUse
1090
1514
  }),
1091
- provider.signupUrl && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExternalLink, {
1515
+ provider.signupUrl && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppLink, {
1092
1516
  url: provider.signupUrl,
1093
- label: provider.label + " " + text.signup,
1517
+ label: providerDisplayLabel(provider, locale) + " " + text.signup,
1094
1518
  children: text.signup
1095
1519
  })
1096
1520
  ]
1097
1521
  }),
1098
1522
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1099
1523
  className: "web-search-provider-info",
1100
- 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, {
1524
+ 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, {
1101
1525
  url: info.pricingUrl,
1102
- label: provider.label + " " + text.pricing,
1526
+ label: providerDisplayLabel(provider, locale) + " " + text.pricing,
1103
1527
  children: provider.id === "ddg" ? text.website : text.pricing
1104
1528
  })] })] })]
1105
1529
  }),
1106
- participating && !provider.configured ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1107
- className: "ws-provider-note",
1108
- 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 })]
1530
+ access.kind === "link" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1531
+ className: "dsh-ui-hint",
1532
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppLink, {
1533
+ url: access.url,
1534
+ label: text.openConfig,
1535
+ children: text.openConfig
1536
+ })
1537
+ }) : access.kind === "owner" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1538
+ className: "dsh-ui-hint",
1539
+ children: [text.ownerSettings, access.name]
1540
+ }) : access.kind === "unknown" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1541
+ className: "dsh-ui-hint",
1542
+ children: text.unknownOwner
1109
1543
  }) : null,
1110
- shared && participating && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", {
1544
+ shared && participating && owned ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", {
1111
1545
  className: "dsh-ui-meta",
1112
- children: provider.credentialHint ?? text.sharedKey
1113
- }),
1546
+ children: provider.credentialHint === "与模型设置共用 Key" ? locale === "zh" ? provider.credentialHint : "Shares a key with model settings" : provider.credentialHint ?? text.sharedKey
1547
+ }) : null,
1114
1548
  showKey ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1115
1549
  className: "dsh-ui-field",
1116
1550
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -1139,7 +1573,7 @@ function NetworkSearchSettings({ client }) {
1139
1573
  onClick: () => void action(async () => {
1140
1574
  await saveKeys(provider);
1141
1575
  await load();
1142
- setNote(text.keySaved);
1576
+ setNote({ key: "keySaved" });
1143
1577
  }),
1144
1578
  children: text.saveKey
1145
1579
  }) : null, needsKey && !shared && provider.configured ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfirmButton, {
@@ -1155,11 +1589,11 @@ function NetworkSearchSettings({ client }) {
1155
1589
  className: "web-search-rank-move",
1156
1590
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
1157
1591
  checked: participating,
1158
- label: provider.label,
1592
+ label: providerDisplayLabel(provider, locale),
1159
1593
  disabled: busy,
1160
1594
  onChange: (next) => {
1161
1595
  const following = next ? [...order, provider.id] : order.filter((id) => id !== provider.id);
1162
- runImmediate(nextSearchEnabled(saved.settings.searchEnabled, following, saved.providers, {}, writable), following);
1596
+ runImmediate(nextSearchEnabled(saved.settings.searchEnabled, following, saved.providers, {}, writable), following, droppedDormantIds(saved.settings, following, saved.providers));
1163
1597
  }
1164
1598
  })
1165
1599
  })
@@ -1167,10 +1601,10 @@ function NetworkSearchSettings({ client }) {
1167
1601
  }, provider.id);
1168
1602
  })
1169
1603
  }),
1170
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1604
+ backends.some(usesBundledPricing) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1171
1605
  className: "dsh-ui-hint",
1172
1606
  children: pricingCheckedText(locale, PROVIDER_PRICING_CHECKED)
1173
- }),
1607
+ }) : null,
1174
1608
  on ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1175
1609
  className: "dsh-ui-actions",
1176
1610
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
@@ -1181,13 +1615,110 @@ function NetworkSearchSettings({ client }) {
1181
1615
  await saveKeys();
1182
1616
  const result = await call("test");
1183
1617
  await load();
1184
- setNote(testOkText(locale, result.sources));
1618
+ setNote({ sources: result.sources });
1185
1619
  }),
1186
1620
  children: text.test
1187
1621
  })
1188
1622
  }) : null
1189
1623
  ]
1190
- })
1624
+ }), (saved.providers.some((provider) => provider.kind === "fetch") || saved.settings.fetchProvider) && (() => {
1625
+ const fetchId = saved.settings.fetchProvider;
1626
+ const fetchSelected = saved.providers.find((provider) => provider.kind === "fetch" && provider.id === fetchId);
1627
+ const fetchDormant = Boolean(fetchId) && !fetchSelected;
1628
+ const fetchActive = Boolean(fetchSelected && saved.fetchActive && fetchSelected.configured);
1629
+ const fetchAccess = fetchSelected ? configurationAccess(fetchSelected) : fetchDormant ? { kind: "unknown" } : void 0;
1630
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
1631
+ className: "dsh-ui-card dsh-ui-card--flat",
1632
+ "data-testid": "web-search-fetch",
1633
+ "data-availability": fetchDormant ? "dormant" : fetchSelected?.configured ? "available" : "unavailable",
1634
+ "data-in-use": fetchActive || void 0,
1635
+ children: [
1636
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
1637
+ className: "ws-card-head",
1638
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1639
+ className: "dsh-ui-title",
1640
+ children: text.fetchTitle
1641
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1642
+ className: "ws-provider-note",
1643
+ children: [
1644
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: fetchDormant ? "idle" : fetchSelected?.configured ? "done" : "warning" }),
1645
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: fetchDormant ? text.dormant : fetchSelected?.configured ? text.available : text.unavailable }),
1646
+ fetchActive ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
1647
+ tone: "success",
1648
+ children: text.inUse
1649
+ }) : null
1650
+ ]
1651
+ })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
1652
+ checked: saved.settings.fetchEnabled,
1653
+ label: text.fetchTitle,
1654
+ disabled: busy || reordering,
1655
+ onChange: (next) => void action(async () => {
1656
+ const current = statusRef.current;
1657
+ adopt(await call("update", {
1658
+ settings: {
1659
+ ...editable(current.settings),
1660
+ fetchEnabled: next
1661
+ },
1662
+ expectedRevision: current.settings.revision
1663
+ }));
1664
+ await load();
1665
+ })
1666
+ })]
1667
+ }),
1668
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1669
+ className: "dsh-ui-field",
1670
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1671
+ className: "dsh-ui-label",
1672
+ children: text.fetchProvider
1673
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1674
+ className: "dsh-ui-select",
1675
+ "aria-label": text.fetchProvider,
1676
+ value: fetchId,
1677
+ disabled: busy || reordering,
1678
+ onChange: (event) => {
1679
+ const id = event.target.value;
1680
+ action(async () => {
1681
+ const current = statusRef.current;
1682
+ adopt(await call("update", {
1683
+ settings: {
1684
+ ...editable(current.settings),
1685
+ fetchProvider: id
1686
+ },
1687
+ expectedRevision: current.settings.revision
1688
+ }));
1689
+ await load();
1690
+ });
1691
+ },
1692
+ children: [fetchDormant && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
1693
+ value: fetchId,
1694
+ children: [
1695
+ fetchId,
1696
+ " · ",
1697
+ text.dormant
1698
+ ]
1699
+ }), saved.providers.filter((provider) => provider.kind === "fetch").map((provider) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
1700
+ value: provider.id,
1701
+ children: [providerDisplayLabel(provider, locale), provider.configured ? "" : " · " + text.unavailable]
1702
+ }, provider.id))]
1703
+ })]
1704
+ }),
1705
+ fetchAccess?.kind === "link" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1706
+ className: "dsh-ui-hint",
1707
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppLink, {
1708
+ url: fetchAccess.url,
1709
+ label: text.openConfig,
1710
+ children: text.openConfig
1711
+ })
1712
+ }) : fetchAccess?.kind === "owner" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1713
+ className: "dsh-ui-hint",
1714
+ children: [text.ownerSettings, fetchAccess.name]
1715
+ }) : fetchAccess?.kind === "unknown" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1716
+ className: "dsh-ui-hint",
1717
+ children: text.unknownOwner
1718
+ }) : null
1719
+ ]
1720
+ });
1721
+ })()]
1191
1722
  }),
1192
1723
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1193
1724
  role: "tabpanel",
@@ -1329,20 +1860,34 @@ function apply(ctx) {
1329
1860
  ctx.effect(() => client.slots.inject("plugins.bundle.config", () => client.slots.register({
1330
1861
  name: "plugins.bundle.config",
1331
1862
  key: "@klarkxy/dsh-web-search-manager"
1332
- }, () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NetworkSearchSettings, { client }))), "web-search-manager.settings");
1863
+ }, (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NetworkSearchSettings, {
1864
+ client,
1865
+ hidden: props?.hidden
1866
+ }))), "web-search-manager.settings");
1333
1867
  }
1334
1868
  //#endregion
1335
1869
  exports.NetworkSearchSettings = NetworkSearchSettings;
1870
+ exports.STATUS_REFRESH_MS = STATUS_REFRESH_MS;
1336
1871
  exports.apply = apply;
1872
+ exports.bindStatusRefresh = bindStatusRefresh;
1337
1873
  exports.canEnableSearch = canEnableSearch;
1874
+ exports.canStartStatusRead = canStartStatusRead;
1338
1875
  exports.catalogSearchBackends = catalogSearchBackends;
1876
+ exports.configurationAccess = configurationAccess;
1877
+ exports.dormantSearchPlaceholder = dormantSearchPlaceholder;
1878
+ exports.droppedDormantIds = droppedDormantIds;
1339
1879
  exports.immediateUpdate = immediateUpdate;
1340
1880
  exports.inject = inject;
1881
+ exports.isExternalConfigurationUrl = isExternalConfigurationUrl;
1882
+ exports.isPageHidden = isPageHidden;
1341
1883
  exports.isProviderOn = isProviderOn;
1884
+ exports.isRegisteredSearch = isRegisteredSearch;
1342
1885
  exports.name = name;
1343
1886
  exports.nextSearchEnabled = nextSearchEnabled;
1887
+ exports.ownsProviderConfiguration = ownsProviderConfiguration;
1344
1888
  exports.parseLimit = parseLimit;
1345
1889
  exports.searchBackends = searchBackends;
1346
1890
  exports.selectedSearchBackend = selectedSearchBackend;
1891
+ exports.shouldApplyPolledStatus = shouldApplyPolledStatus;
1347
1892
 
1348
1893
  //# sourceMappingURL=client.inner.cjs.map