@klarkxy/dsh-web-search-manager 0.2.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -405,6 +405,29 @@ ${scope} {
405
405
  & .dsh-ui-select:disabled { color: var(--dsw-alias-label-tertiary); cursor: default; }
406
406
  & .dsh-ui-select[aria-invalid='true'] { border-color: var(--dsw-alias-state-error-primary); }
407
407
 
408
+ /* --- Native textarea. The primitives ship no textarea either, so a settings
409
+ * prompt keeps the platform element and the same field shell as the select:
410
+ * 0.5px l4, radius-md, layer-3, 13/1.5 type. It grows vertically instead of
411
+ * locking to the select's 34px row. --- */
412
+ & .dsh-ui-textarea {
413
+ box-sizing: border-box;
414
+ width: 100%;
415
+ min-width: 0;
416
+ resize: vertical;
417
+ padding: 8px 12px;
418
+ border: 0.5px solid var(--dsw-alias-border-l4);
419
+ border-radius: var(--dsw-radius-md);
420
+ background: var(--dsw-alias-bg-layer-3);
421
+ font: inherit;
422
+ font-size: 13px;
423
+ line-height: 1.5;
424
+ color: var(--dsw-alias-label-primary);
425
+ }
426
+ & .dsh-ui-textarea:focus-visible { outline: none; border-color: var(--dsw-alias-state-business-primary); }
427
+ & .dsh-ui-textarea:disabled { color: var(--dsw-alias-label-tertiary); cursor: default; }
428
+ & .dsh-ui-textarea::placeholder { color: var(--dsw-alias-label-dimmed); }
429
+ & .dsh-ui-textarea[aria-invalid='true'] { border-color: var(--dsw-alias-state-error-primary); }
430
+
408
431
  /* Long panels scroll inside themselves instead of growing the host page. */
409
432
  & .dsh-ui-panel { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
410
433
  & .dsh-ui-panel-wide { max-width: 100%; }
@@ -444,8 +467,8 @@ function createPluginReadGate(now = Date.now) {
444
467
  //#endregion
445
468
  //#region src/contracts.ts
446
469
  const WEB_SEARCH_RPC_CHANNEL = "/web-search-manager";
447
- function pickActiveSearch(order, configured) {
448
- return order.find(configured) ?? "";
470
+ function providerKey(kind, id) {
471
+ return `${kind}:${id}`;
449
472
  }
450
473
  function safeConfigurationUrl(value) {
451
474
  if (typeof value !== "string" || !value || /[\u0000-\u0020\\]/.test(value)) return;
@@ -462,6 +485,21 @@ function safeConfigurationUrl(value) {
462
485
  //#region src/provider-info.ts
463
486
  const PROVIDER_PRICING_CHECKED = "2026-09-21";
464
487
  const providerInfo = {
488
+ perplexity: {
489
+ description: "通过 Perplexity Search API 获取网页来源,不生成回答。",
490
+ pricing: "按 Search API 请求计费;实际价格与额度以官方账户为准。",
491
+ pricingUrl: "https://docs.perplexity.ai/docs/getting-started/pricing"
492
+ },
493
+ you: {
494
+ description: "通过 You.com 获取网页与新闻结果,不请求额外全文提取。",
495
+ pricing: "按 Web Search API 请求计费;实际价格与额度以官方账户为准。",
496
+ pricingUrl: "https://you.com/docs/administration/billing"
497
+ },
498
+ "firecrawl-fetch": {
499
+ description: "读取网页 Markdown;与 Firecrawl 搜索共用 Key。",
500
+ pricing: "网页读取使用 Firecrawl 积分,与搜索分别计费;实际消耗以官方账户为准。",
501
+ pricingUrl: "https://www.firecrawl.dev/pricing"
502
+ },
465
503
  ddg: {
466
504
  description: "搜索 DuckDuckGo 公开网页,无需注册或 API Key。",
467
505
  pricing: "免费使用,不产生搜索 API 费用。",
@@ -504,6 +542,18 @@ const providerInfo = {
504
542
  }
505
543
  };
506
544
  const englishInfo = {
545
+ perplexity: {
546
+ description: "Get web sources through the Perplexity Search API without generating an answer.",
547
+ pricing: "Charged per Search API request; current prices and credits depend on your official account."
548
+ },
549
+ you: {
550
+ description: "Get web and news results through You.com without requesting extra full-page extraction.",
551
+ pricing: "Charged per Web Search API request; current prices and credits depend on your official account."
552
+ },
553
+ "firecrawl-fetch": {
554
+ description: "Read page Markdown using the same key as Firecrawl search.",
555
+ pricing: "Page reads use Firecrawl credits and are charged separately from search; your official account determines usage."
556
+ },
507
557
  ddg: {
508
558
  description: "Search public DuckDuckGo pages without registration or an API key.",
509
559
  pricing: "Free to use; no search API fees."
@@ -538,6 +588,9 @@ const englishInfo = {
538
588
  }
539
589
  };
540
590
  const bundledDescriptions = {
591
+ perplexity: ["Search API;返回网页来源,不生成回答。", "Search API returning web sources without generating an answer."],
592
+ you: ["网页与新闻搜索;不额外提取全文。", "Web and news search without extra full-page extraction."],
593
+ "firecrawl-fetch": ["读取网页 Markdown;与 Firecrawl 搜索共用 Key。", "Read page Markdown using the same key as Firecrawl search."],
541
594
  ddg: ["无需注册。默认后端。", "No registration required. Default backend."],
542
595
  "deepseek-official": ["使用已配置的 DeepSeek API Key,可能产生额外搜索费用。", "Uses the configured DeepSeek API key; additional search fees may apply."],
543
596
  exa: ["独立 Search API。", "Independent Search API."],
@@ -557,6 +610,7 @@ function providerDisplayLabel(provider, locale) {
557
610
  }
558
611
  function bundledCatalogId(provider) {
559
612
  if (!(typeof provider.configurationOwned === "boolean" ? provider.configurationOwned : !provider.configurationOwner || provider.configurationOwner === "@klarkxy/dsh-web-search-manager")) return;
613
+ if (provider.kind === "fetch" && provider.id === "firecrawl") return "firecrawl-fetch";
560
614
  return provider.id === "deepseek-managed" ? "deepseek-official" : provider.id;
561
615
  }
562
616
  function missingPricing(locale) {
@@ -565,7 +619,11 @@ function missingPricing(locale) {
565
619
  function usesBundledPricing(provider) {
566
620
  if (provider.pricing?.trim()) return false;
567
621
  const catalogId = bundledCatalogId(provider);
568
- return Boolean(catalogId && Object.hasOwn(providerInfo, catalogId));
622
+ return Boolean(catalogId && ![
623
+ "perplexity",
624
+ "you",
625
+ "firecrawl-fetch"
626
+ ].includes(catalogId) && Object.hasOwn(providerInfo, catalogId));
569
627
  }
570
628
  function searchProviderInfo(provider, locale = "zh") {
571
629
  const catalogId = bundledCatalogId(provider);
@@ -662,52 +720,61 @@ function bindStatusRefresh(options) {
662
720
  };
663
721
  }
664
722
  function isProviderOn(status, provider) {
665
- if (provider.kind === "fetch") return status.fetchActive && status.settings.fetchProvider === provider.id && provider.configured;
723
+ if (provider.kind === "fetch") return status.fetchActive && selectedBackend(status, "fetch")?.id === provider.id;
666
724
  return status.searchActive && selectedSearchBackend(status)?.id === provider.id;
667
725
  }
668
726
  function dormantSearchPlaceholder(id) {
727
+ return dormantPlaceholder(id, "search");
728
+ }
729
+ function dormantPlaceholder(id, kind) {
669
730
  return {
670
731
  id,
671
732
  label: id,
672
733
  description: "",
673
734
  billing: "unknown",
674
- kind: "search",
735
+ kind,
675
736
  configured: false,
676
737
  calls: 0,
677
738
  failures: 0,
678
739
  configurationOwned: false
679
740
  };
680
741
  }
742
+ function isRegistered(status, id, kind) {
743
+ return status.providers.some((provider) => provider.kind === kind && provider.id === id);
744
+ }
681
745
  function isRegisteredSearch(status, id) {
682
- return status.providers.some((provider) => provider.kind === "search" && provider.id === id);
746
+ return isRegistered(status, id, "search");
683
747
  }
684
- function catalogSearchBackends(status) {
685
- const byId = new Map(status.providers.filter((provider) => provider.kind === "search").map((provider) => [provider.id, provider]));
748
+ function providerOrder(settings, kind) {
749
+ return kind === "search" ? settings.searchOrder : settings.fetchOrder;
750
+ }
751
+ function catalogBackends(status, kind) {
752
+ const byId = new Map(status.providers.filter((provider) => provider.kind === kind).map((provider) => [provider.id, provider]));
686
753
  const rows = [];
687
754
  const seen = /* @__PURE__ */ new Set();
688
- for (const id of status.settings.searchOrder) {
755
+ for (const id of providerOrder(status.settings, kind)) {
689
756
  if (!id || seen.has(id)) continue;
690
757
  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);
758
+ rows.push(byId.get(id) ?? dormantPlaceholder(id, kind));
696
759
  }
760
+ for (const provider of byId.values()) if (!seen.has(provider.id)) rows.push(provider);
697
761
  return rows;
698
762
  }
763
+ function catalogSearchBackends(status) {
764
+ return catalogBackends(status, "search");
765
+ }
699
766
  function searchBackends(status) {
700
- const participating = new Set(status.settings.searchOrder);
701
- return catalogSearchBackends(status).filter((provider) => participating.has(provider.id));
767
+ return catalogSearchBackends(status).filter((provider) => status.settings.searchOrder.includes(provider.id));
768
+ }
769
+ function selectedBackend(status, kind) {
770
+ return providerOrder(status.settings, kind).flatMap((id) => status.providers.find((provider) => provider.kind === kind && provider.id === id) ?? []).find((provider) => provider.configured);
702
771
  }
703
772
  function selectedSearchBackend(status) {
704
- const backends = searchBackends(status).filter((provider) => isRegisteredSearch(status, provider.id));
705
- const id = pickActiveSearch(backends.map((provider) => provider.id), (id) => Boolean(backends.find((provider) => provider.id === id)?.configured));
706
- return backends.find((provider) => provider.id === id);
773
+ return selectedBackend(status, "search");
707
774
  }
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)))];
775
+ function droppedDormantIds(settings, order, providers, kind = "search") {
776
+ const known = new Set(providers.filter((provider) => provider.kind === kind).map((provider) => provider.id));
777
+ return [...new Set(providerOrder(settings, kind).filter((id) => id && !known.has(id) && !order.includes(id)))];
711
778
  }
712
779
  function configurationAccess(provider) {
713
780
  if (ownsProviderConfiguration(provider)) return { kind: "owned" };
@@ -788,8 +855,9 @@ function ConfirmButton(props) {
788
855
  const copy = {
789
856
  zh: {
790
857
  tabsLabel: "网络搜索设置",
791
- tabProviders: "搜索服务",
792
- tabLimits: "请求限制",
858
+ tabSearch: "web-search",
859
+ tabFetch: "web-fetch",
860
+ tabLimits: "设置",
793
861
  loading: "正在读取设置…",
794
862
  loadFailed: "读取设置失败:",
795
863
  reconnect: "重新连接",
@@ -830,13 +898,16 @@ const copy = {
830
898
  maxFetchChars: "正文字符上限",
831
899
  fetchTitle: "网页读取",
832
900
  fetchProvider: "读取服务",
901
+ fetchBackends: "读取后端",
902
+ needFetchBackend: "需选择并配置读取服务。",
833
903
  ownerSettings: "配置入口:",
834
904
  unknownOwner: "请在服务所属插件中配置"
835
905
  },
836
906
  en: {
837
907
  tabsLabel: "Web search settings",
838
- tabProviders: "Search services",
839
- tabLimits: "Request limits",
908
+ tabSearch: "web-search",
909
+ tabFetch: "web-fetch",
910
+ tabLimits: "Settings",
840
911
  loading: "Loading settings…",
841
912
  loadFailed: "Could not load settings: ",
842
913
  reconnect: "Reconnect",
@@ -877,6 +948,8 @@ const copy = {
877
948
  maxFetchChars: "Max text chars",
878
949
  fetchTitle: "Read web pages",
879
950
  fetchProvider: "Fetch service",
951
+ fetchBackends: "Fetch backends",
952
+ needFetchBackend: "Choose and configure a fetch service.",
880
953
  ownerSettings: "Configure in: ",
881
954
  unknownOwner: "Configure this service in its plugin"
882
955
  }
@@ -893,7 +966,7 @@ function uiMessage(message, locale, providers = []) {
893
966
  if ("key" in message) return copy[locale][message.key];
894
967
  if ("sources" in message) return testOkText(locale, message.sources);
895
968
  if ("moved" in message) {
896
- const provider = providers.find((item) => item.id === message.moved);
969
+ const provider = providers.find((item) => item.id === message.moved && item.kind === message.kind);
897
970
  return movedText(locale, provider ? providerDisplayLabel(provider, locale) : message.moved, message.position);
898
971
  }
899
972
  return message.invalid.map((key) => {
@@ -946,15 +1019,15 @@ function parseLimit(raw, min, max) {
946
1019
  * Settings written by an immediate action (a switch or a reorder). It keeps the
947
1020
  * saved limits and never includes typed-but-unsaved keys or limit drafts.
948
1021
  */
949
- function immediateUpdate(settings, on, order, providers, remove = []) {
1022
+ function immediateUpdate(settings, on, order, providers, remove = [], kind = "search") {
950
1023
  let searchOrder = order;
951
1024
  if (providers) {
952
- const known = new Set(providers.filter((provider) => provider.kind === "search").map((provider) => provider.id));
1025
+ const known = new Set(providers.filter((provider) => provider.kind === kind).map((provider) => provider.id));
953
1026
  const removed = new Set(remove);
954
1027
  const following = order.filter((id) => !removed.has(id));
955
1028
  if (following.some((id) => !known.has(id))) searchOrder = following;
956
1029
  else {
957
- searchOrder = settings.searchOrder.flatMap((id) => {
1030
+ searchOrder = providerOrder(settings, kind).flatMap((id) => {
958
1031
  if (removed.has(id)) return [];
959
1032
  return known.has(id) ? following.splice(0, 1) : [id];
960
1033
  });
@@ -963,8 +1036,13 @@ function immediateUpdate(settings, on, order, providers, remove = []) {
963
1036
  }
964
1037
  return {
965
1038
  ...editable(settings),
966
- searchOrder,
967
- searchEnabled: on
1039
+ ...kind === "search" ? {
1040
+ searchOrder,
1041
+ searchEnabled: on
1042
+ } : {
1043
+ fetchOrder: searchOrder,
1044
+ fetchEnabled: on
1045
+ }
968
1046
  };
969
1047
  }
970
1048
  function limitTexts(settings) {
@@ -986,7 +1064,7 @@ function NetworkSearchSettings({ client, hidden }) {
986
1064
  const locale = useHostLocale(client);
987
1065
  const text = copy[locale];
988
1066
  const tabsId = (0, react.useId)();
989
- const [tab, setTab] = (0, react.useState)("providers");
1067
+ const [tab, setTab] = (0, react.useState)("search");
990
1068
  const [status, setStatus] = (0, react.useState)();
991
1069
  const [limitDraft, setLimitDraft] = (0, react.useState)();
992
1070
  const [invalid, setInvalid] = (0, react.useState)({});
@@ -1003,6 +1081,7 @@ function NetworkSearchSettings({ client, hidden }) {
1003
1081
  const statusRef = (0, react.useRef)(status);
1004
1082
  statusRef.current = status;
1005
1083
  const pendingRef = (0, react.useRef)(null);
1084
+ const dragRef = (0, react.useRef)(null);
1006
1085
  const timerRef = (0, react.useRef)(void 0);
1007
1086
  const handleRefs = (0, react.useRef)(/* @__PURE__ */ new Map());
1008
1087
  const refocusRef = (0, react.useRef)(null);
@@ -1038,7 +1117,7 @@ function NetworkSearchSettings({ client, hidden }) {
1038
1117
  mounted: mountedRef.current,
1039
1118
  hidden: isPageHidden(hiddenRef.current),
1040
1119
  busy: busyRef.current,
1041
- reordering: pendingRef.current !== null,
1120
+ reordering: pendingRef.current !== null || dragRef.current !== null,
1042
1121
  reading: readingRef.current
1043
1122
  });
1044
1123
  }
@@ -1075,7 +1154,7 @@ function NetworkSearchSettings({ client, hidden }) {
1075
1154
  requestId: mine,
1076
1155
  latestRequestId: requestSeq.current,
1077
1156
  busy: busyRef.current,
1078
- reordering: pendingRef.current !== null,
1157
+ reordering: pendingRef.current !== null || dragRef.current !== null,
1079
1158
  hidden: isPageHidden(hiddenRef.current),
1080
1159
  currentRevision: statusRef.current?.settings.revision,
1081
1160
  incomingRevision: next.settings.revision
@@ -1125,7 +1204,7 @@ function NetworkSearchSettings({ client, hidden }) {
1125
1204
  const current = statusRef.current;
1126
1205
  const next = pendingRef.current;
1127
1206
  if (current && next) call("update", {
1128
- settings: immediateUpdate(current.settings, current.settings.searchEnabled, next, current.providers),
1207
+ settings: immediateUpdate(current.settings, next.kind === "search" ? current.settings.searchEnabled : current.settings.fetchEnabled, next.order, current.providers, [], next.kind),
1129
1208
  expectedRevision: current.settings.revision
1130
1209
  }).catch(() => {});
1131
1210
  };
@@ -1143,6 +1222,7 @@ function NetworkSearchSettings({ client, hidden }) {
1143
1222
  if (handle && document.activeElement !== handle) handle.focus();
1144
1223
  });
1145
1224
  async function action(run) {
1225
+ if (busyRef.current) return;
1146
1226
  readGate.reset();
1147
1227
  requestSeq.current += 1;
1148
1228
  busyRef.current = true;
@@ -1160,18 +1240,17 @@ function NetworkSearchSettings({ client, hidden }) {
1160
1240
  }
1161
1241
  }
1162
1242
  /** Switches and reorders: settings only, applied at once. */
1163
- function runImmediate(on, order, remove = []) {
1243
+ function runImmediate(on, order, remove = [], kind = "search") {
1164
1244
  clearTimeout(timerRef.current);
1165
1245
  timerRef.current = void 0;
1166
1246
  pendingRef.current = null;
1167
1247
  action(async () => {
1168
1248
  const current = statusRef.current;
1169
1249
  if (!current) return;
1170
- const next = await call("update", {
1171
- settings: immediateUpdate(current.settings, on, order, current.providers, remove),
1250
+ adopt(await call("update", {
1251
+ settings: immediateUpdate(current.settings, on, order, current.providers, remove, kind),
1172
1252
  expectedRevision: current.settings.revision
1173
- });
1174
- setStatus(next);
1253
+ }));
1175
1254
  await load();
1176
1255
  }).then(() => {
1177
1256
  if (pendingRef.current === null) setPendingOrder(null);
@@ -1181,24 +1260,27 @@ function NetworkSearchSettings({ client, hidden }) {
1181
1260
  async function saveKeys(only) {
1182
1261
  if (!status) return;
1183
1262
  for (const provider of status.providers) {
1184
- if (provider.kind !== "search" || only && provider.id !== only.id) continue;
1185
- const snapshot = keys[provider.id] ?? "";
1263
+ if (only && providerKey(provider.kind, provider.id) !== providerKey(only.kind, only.id)) continue;
1264
+ const snapshot = keys[providerKey(provider.kind, provider.id)] ?? "";
1186
1265
  const typed = snapshot.trim();
1187
1266
  if (!ownsProviderConfiguration(provider) || !provider.credentialRef || !typed) continue;
1188
1267
  if (writable[provider.credentialRef] === false) throw new UiError("readOnly");
1189
1268
  unwrap(await client.remote.credentials.set(provider.credentialRef, typed));
1190
- setKeys((current) => current[provider.id] === snapshot ? {
1269
+ setKeys((current) => current[providerKey(provider.kind, provider.id)] === snapshot ? {
1191
1270
  ...current,
1192
- [provider.id]: ""
1271
+ [providerKey(provider.kind, provider.id)]: ""
1193
1272
  } : current);
1194
1273
  }
1195
1274
  }
1275
+ async function saveKeysForKind(kind) {
1276
+ for (const provider of statusRef.current?.providers ?? []) if (provider.kind === kind) await saveKeys(provider);
1277
+ }
1196
1278
  async function removeKey(provider) {
1197
1279
  if (!ownsProviderConfiguration(provider) || !provider.credentialRef || provider.credentialShared || !status) return;
1198
1280
  unwrap(await client.remote.credentials.unset(provider.credentialRef));
1199
1281
  setKeys((current) => ({
1200
1282
  ...current,
1201
- [provider.id]: ""
1283
+ [providerKey(provider.kind, provider.id)]: ""
1202
1284
  }));
1203
1285
  await load();
1204
1286
  setNote({ key: "keyDeleted" });
@@ -1227,28 +1309,30 @@ function NetworkSearchSettings({ client, hidden }) {
1227
1309
  }) : null]
1228
1310
  });
1229
1311
  const saved = status;
1230
- const savedOrder = searchBackends(saved).map((provider) => provider.id);
1231
- const order = pendingOrder ?? savedOrder;
1232
- const catalog = catalogSearchBackends(saved);
1233
- const backends = [...order.flatMap((id) => catalog.find((provider) => provider.id === id) ?? []), ...catalog.filter((provider) => !order.includes(provider.id))];
1234
- const selected = selectedSearchBackend(saved);
1235
- const on = saved.settings.searchEnabled;
1236
- const canEnable = canEnableSearch(order, saved.providers, {}, writable);
1237
1312
  const savedLimits = limitTexts(saved.settings);
1238
1313
  const limitsDirty = LIMITS.some(([key]) => limitDraft[key] !== savedLimits[key]);
1239
1314
  const keysDirty = Object.values(keys).some((value) => value.trim());
1240
- const reordering = pendingOrder !== null;
1241
- function move(id, nextIndex, keyboard = false) {
1315
+ const reordering = pendingOrder !== null || dragSource !== null;
1316
+ function move(kind, id, nextIndex, keyboard = false) {
1317
+ if (pendingRef.current && pendingRef.current.kind !== kind) return;
1318
+ const order = pendingRef.current?.order ?? providerOrder(statusRef.current.settings, kind);
1242
1319
  const index = order.indexOf(id);
1243
1320
  if (busy || index < 0 || nextIndex < 0 || nextIndex >= order.length || index === nextIndex) return;
1244
1321
  const next = [...order];
1245
1322
  const [item] = next.splice(index, 1);
1246
1323
  next.splice(nextIndex, 0, item);
1247
- pendingRef.current = next;
1248
- setPendingOrder(next);
1249
- if (keyboard) refocusRef.current = id;
1324
+ pendingRef.current = {
1325
+ kind,
1326
+ order: next
1327
+ };
1328
+ setPendingOrder({
1329
+ kind,
1330
+ order: next
1331
+ });
1332
+ if (keyboard) refocusRef.current = providerKey(kind, id);
1250
1333
  setSortAnnouncement({
1251
1334
  moved: id,
1335
+ kind,
1252
1336
  position: nextIndex + 1
1253
1337
  });
1254
1338
  clearTimeout(timerRef.current);
@@ -1256,10 +1340,11 @@ function NetworkSearchSettings({ client, hidden }) {
1256
1340
  timerRef.current = void 0;
1257
1341
  const batched = pendingRef.current;
1258
1342
  const current = statusRef.current;
1259
- if (batched && current) runImmediate(current.settings.searchEnabled, batched);
1343
+ if (batched && current) runImmediate(batched.kind === "search" ? current.settings.searchEnabled : current.settings.fetchEnabled, batched.order, [], batched.kind);
1260
1344
  }, keyboard ? REORDER_DEBOUNCE_MS : 0);
1261
1345
  }
1262
1346
  function endDrag() {
1347
+ dragRef.current = null;
1263
1348
  setDragSource(null);
1264
1349
  setDropTarget(null);
1265
1350
  }
@@ -1302,17 +1387,26 @@ function NetworkSearchSettings({ client, hidden }) {
1302
1387
  value: tab,
1303
1388
  onChange: setTab,
1304
1389
  label: text.tabsLabel,
1305
- items: [{
1306
- value: "providers",
1307
- label: text.tabProviders,
1308
- id: tabsId + "-providers-tab",
1309
- panelId: tabsId + "-providers-panel"
1310
- }, {
1311
- value: "limits",
1312
- label: text.tabLimits,
1313
- id: tabsId + "-limits-tab",
1314
- panelId: tabsId + "-limits-panel"
1315
- }]
1390
+ items: [
1391
+ {
1392
+ value: "search",
1393
+ label: text.tabSearch,
1394
+ id: tabsId + "-search-tab",
1395
+ panelId: tabsId + "-search-panel"
1396
+ },
1397
+ {
1398
+ value: "fetch",
1399
+ label: text.tabFetch,
1400
+ id: tabsId + "-fetch-tab",
1401
+ panelId: tabsId + "-fetch-panel"
1402
+ },
1403
+ {
1404
+ value: "limits",
1405
+ label: text.tabLimits,
1406
+ id: tabsId + "-limits-tab",
1407
+ panelId: tabsId + "-limits-panel"
1408
+ }
1409
+ ]
1316
1410
  }),
1317
1411
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1318
1412
  className: "ws-sr-only",
@@ -1351,374 +1445,294 @@ function NetworkSearchSettings({ client, hidden }) {
1351
1445
  className: "dsh-ui-banner dsh-ui-banner--danger",
1352
1446
  children: text.storageFailed
1353
1447
  }),
1354
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1355
- role: "tabpanel",
1356
- id: tabsId + "-providers-panel",
1357
- "aria-labelledby": tabsId + "-providers-tab",
1358
- hidden: tab !== "providers",
1359
- tabIndex: 0,
1360
- children: [backends.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1361
- className: "dsh-ui-empty",
1362
- children: text.empty
1363
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
1364
- className: "web-search-card dsh-ui-card dsh-ui-card--flat",
1365
- "data-testid": "web-search-tool",
1366
- "data-on": on ? "true" : "false",
1367
- children: [
1368
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
1369
- className: "ws-card-head",
1370
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1371
- className: "dsh-ui-title",
1372
- children: text.toolTitle
1373
- }), !canEnable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1374
- className: "dsh-ui-hint",
1375
- children: text.needBackend
1376
- })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
1377
- checked: on,
1378
- label: text.toolTitle,
1379
- disabled: busy || !on && !canEnable,
1380
- onChange: (next) => runImmediate(next, order)
1381
- })]
1382
- }),
1383
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ol", {
1384
- className: "web-search-rank",
1385
- "aria-label": text.backends,
1386
- children: backends.map((provider) => {
1387
- const participating = order.includes(provider.id);
1388
- const rank = order.indexOf(provider.id);
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);
1395
- const shared = Boolean(provider.credentialShared);
1396
- const showKey = !dormant && needsKey && participating && (!shared || !provider.configured);
1397
- const canSort = participating && order.length > 1;
1398
- const readOnly = writable[provider.credentialRef ?? ""] === false;
1399
- const typedKey = (keys[provider.id] ?? "").trim();
1400
- const availability = dormant ? "dormant" : provider.configured ? "available" : "unavailable";
1401
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
1402
- className: active ? "is-active" : void 0,
1403
- "data-testid": `web-search-rank-${provider.id}`,
1404
- "data-availability": availability,
1405
- "data-in-use": active || void 0,
1406
- "data-dragging": dragSource === provider.id || void 0,
1407
- "data-drop": dropTarget === provider.id && dragSource ? order.indexOf(dragSource) < rank ? "after" : "before" : void 0,
1408
- onDragOver: (event) => {
1409
- if (busy || !canSort || !dragSource || dragSource === provider.id) return;
1410
- event.preventDefault();
1411
- event.dataTransfer.dropEffect = "move";
1412
- setDropTarget(provider.id);
1413
- },
1414
- onDragLeave: (event) => {
1415
- if (!event.currentTarget.contains(event.relatedTarget)) setDropTarget(null);
1416
- },
1417
- onDrop: (event) => {
1418
- if (busy || !canSort || !dragSource) return;
1419
- event.preventDefault();
1420
- move(dragSource, rank);
1421
- endDrag();
1422
- },
1423
- children: [
1424
- canSort ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1425
- variant: "ghost",
1426
- size: "sm",
1427
- className: "web-search-drag-handle",
1428
- ref: (element) => {
1429
- if (element) handleRefs.current.set(provider.id, element);
1430
- else handleRefs.current.delete(provider.id);
1431
- },
1432
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
1433
- width: "16",
1434
- height: "20",
1435
- viewBox: "0 0 16 20",
1436
- fill: "currentColor",
1437
- "aria-hidden": "true",
1438
- children: [
1439
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1440
- cx: "5",
1441
- cy: "5",
1442
- r: "1.5"
1443
- }),
1444
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1445
- cx: "11",
1446
- cy: "5",
1447
- r: "1.5"
1448
- }),
1449
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1450
- cx: "5",
1451
- cy: "10",
1452
- r: "1.5"
1453
- }),
1454
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1455
- cx: "11",
1456
- cy: "10",
1457
- r: "1.5"
1458
- }),
1459
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1460
- cx: "5",
1461
- cy: "15",
1462
- r: "1.5"
1463
- }),
1464
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1465
- cx: "11",
1466
- cy: "15",
1467
- r: "1.5"
1468
- })
1469
- ]
1470
- }),
1471
- draggable: !busy,
1472
- "aria-disabled": busy,
1473
- "aria-label": text.dragPrefix + providerDisplayLabel(provider, locale),
1474
- title: text.dragHint,
1475
- onDragStart: (event) => {
1476
- if (busy) {
1477
- event.preventDefault();
1478
- return;
1479
- }
1480
- event.dataTransfer.effectAllowed = "move";
1481
- event.dataTransfer.setData("application/x-dsh-search-provider", provider.id);
1482
- const row = event.currentTarget.closest("li");
1483
- if (row) event.dataTransfer.setDragImage(row, 16, 16);
1484
- setDragSource(provider.id);
1485
- setSortAnnouncement("");
1486
- },
1487
- onDragEnd: endDrag,
1488
- onKeyDown: (event) => {
1489
- if (event.key !== "ArrowUp" && event.key !== "ArrowDown") return;
1490
- event.preventDefault();
1491
- move(provider.id, rank + (event.key === "ArrowUp" ? -1 : 1), true);
1492
- }
1493
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1494
- className: "web-search-rank-index",
1495
- children: participating ? rank + 1 : "—"
1496
- }),
1497
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1498
- className: "web-search-provider",
1499
- children: [
1500
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1501
- className: "web-search-provider-heading",
1502
- children: [
1503
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
1504
- className: "dsh-ui-heading",
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 })]
1510
- }),
1511
- active && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
1512
- tone: "success",
1513
- children: text.inUse
1514
- }),
1515
- provider.signupUrl && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppLink, {
1516
- url: provider.signupUrl,
1517
- label: providerDisplayLabel(provider, locale) + " " + text.signup,
1518
- children: text.signup
1519
- })
1520
- ]
1521
- }),
1522
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1523
- className: "web-search-provider-info",
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, {
1525
- url: info.pricingUrl,
1526
- label: providerDisplayLabel(provider, locale) + " " + text.pricing,
1527
- children: provider.id === "ddg" ? text.website : text.pricing
1528
- })] })] })]
1529
- }),
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
1543
- }) : null,
1544
- shared && participating && owned ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", {
1545
- className: "dsh-ui-meta",
1546
- children: provider.credentialHint === "与模型设置共用 Key" ? locale === "zh" ? provider.credentialHint : "Shares a key with model settings" : provider.credentialHint ?? text.sharedKey
1547
- }) : null,
1548
- showKey ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1549
- className: "dsh-ui-field",
1550
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1551
- className: "dsh-ui-label",
1552
- children: "API Key"
1553
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
1554
- className: "ws-input",
1555
- type: "password",
1556
- autoComplete: "off",
1557
- spellCheck: false,
1558
- disabled: busy || readOnly,
1559
- value: keys[provider.id] ?? "",
1560
- placeholder: provider.configured ? text.keyConfigured : text.keyPlaceholder,
1561
- onChange: (event) => setKeys((current) => ({
1562
- ...current,
1563
- [provider.id]: event.target.value
1564
- }))
1565
- })]
1566
- }) : null,
1567
- showKey || needsKey && !shared && provider.configured ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1568
- className: "dsh-ui-actions",
1569
- children: [showKey ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1570
- variant: "outline",
1571
- size: "sm",
1572
- disabled: busy || readOnly || !typedKey,
1573
- onClick: () => void action(async () => {
1574
- await saveKeys(provider);
1575
- await load();
1576
- setNote({ key: "keySaved" });
1577
- }),
1578
- children: text.saveKey
1579
- }) : null, needsKey && !shared && provider.configured ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfirmButton, {
1580
- disabled: busy || readOnly,
1581
- label: text.deleteKey,
1582
- confirmLabel: text.confirmDelete,
1583
- onConfirm: () => void action(() => removeKey(provider))
1584
- }) : null]
1585
- }) : null
1586
- ]
1587
- }),
1588
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1589
- className: "web-search-rank-move",
1590
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
1591
- checked: participating,
1592
- label: providerDisplayLabel(provider, locale),
1593
- disabled: busy,
1594
- onChange: (next) => {
1595
- const following = next ? [...order, provider.id] : order.filter((id) => id !== provider.id);
1596
- runImmediate(nextSearchEnabled(saved.settings.searchEnabled, following, saved.providers, {}, writable), following, droppedDormantIds(saved.settings, following, saved.providers));
1597
- }
1598
- })
1599
- })
1600
- ]
1601
- }, provider.id);
1602
- })
1603
- }),
1604
- backends.some(usesBundledPricing) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1605
- className: "dsh-ui-hint",
1606
- children: pricingCheckedText(locale, PROVIDER_PRICING_CHECKED)
1607
- }) : null,
1608
- on ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1609
- className: "dsh-ui-actions",
1610
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1611
- variant: "outline",
1612
- size: "md",
1613
- disabled: busy || reordering,
1614
- onClick: () => void action(async () => {
1615
- await saveKeys();
1616
- const result = await call("test");
1617
- await load();
1618
- setNote({ sources: result.sources });
1619
- }),
1620
- children: text.test
1621
- })
1622
- }) : null
1623
- ]
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,
1448
+ ["search", "fetch"].map((kind) => {
1449
+ const order = pendingOrder?.kind === kind ? pendingOrder.order : providerOrder(saved.settings, kind);
1450
+ const catalog = catalogBackends(saved, kind);
1451
+ const backends = [...order.flatMap((id) => catalog.find((provider) => provider.id === id) ?? []), ...catalog.filter((provider) => !order.includes(provider.id))];
1452
+ const selected = selectedBackend(saved, kind);
1453
+ const on = kind === "search" ? saved.settings.searchEnabled : saved.settings.fetchEnabled;
1454
+ const canEnable = order.some((id) => saved.providers.some((provider) => provider.kind === kind && provider.id === id && provider.configured));
1455
+ const sortingOther = pendingOrder !== null && pendingOrder.kind !== kind;
1456
+ const locked = busy || sortingOther || Boolean(dragSource && !dragSource.startsWith(kind + ":"));
1457
+ const title = kind === "search" ? text.toolTitle : text.fetchTitle;
1458
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1459
+ role: "tabpanel",
1460
+ id: tabsId + "-" + kind + "-panel",
1461
+ "aria-labelledby": tabsId + "-" + kind + "-tab",
1462
+ hidden: tab !== kind,
1463
+ tabIndex: 0,
1464
+ children: backends.length === 0 ? kind === "search" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1465
+ className: "dsh-ui-empty",
1466
+ children: text.empty
1467
+ }) : null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
1468
+ className: "web-search-card dsh-ui-card dsh-ui-card--flat",
1469
+ "data-testid": kind === "search" ? "web-search-tool" : "web-search-fetch",
1470
+ "data-on": on ? "true" : "false",
1635
1471
  children: [
1636
1472
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
1637
1473
  className: "ws-card-head",
1638
1474
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1639
1475
  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
- ]
1476
+ children: title
1477
+ }), !canEnable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1478
+ className: "dsh-ui-hint",
1479
+ children: kind === "search" ? text.needBackend : text.needFetchBackend
1651
1480
  })] }), /* @__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
- })
1481
+ checked: on,
1482
+ label: title,
1483
+ disabled: locked || dragSource !== null || !on && !canEnable,
1484
+ onChange: (next) => runImmediate(next, order, [], kind)
1666
1485
  })]
1667
1486
  }),
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,
1487
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ol", {
1488
+ className: "web-search-rank",
1489
+ "aria-label": kind === "search" ? text.backends : text.fetchBackends,
1490
+ children: backends.map((provider) => {
1491
+ const participating = order.includes(provider.id);
1492
+ const rank = order.indexOf(provider.id);
1493
+ const dormant = !isRegistered(saved, provider.id, kind);
1494
+ const active = !dormant && (kind === "search" ? saved.searchActive : saved.fetchActive) && selected?.id === provider.id;
1495
+ const info = kind === "search" || ownsProviderConfiguration(provider) && provider.id === "firecrawl" ? searchProviderInfo(provider, locale) : {
1496
+ description: ownsProviderConfiguration(provider) && provider.id === "http-managed" && provider.description === "优先匿名 Jina Reader 读取,失败后本机 HTTP 直连;无需 Key。" && locale === "en" ? "Reads with anonymous Jina Reader first, then direct HTTP if needed; no key required." : provider.description,
1497
+ pricing: provider.pricing ?? "",
1498
+ pricingUrl: provider.pricingUrl
1499
+ };
1500
+ const owned = ownsProviderConfiguration(provider);
1501
+ const access = configurationAccess(provider);
1502
+ const needsKey = owned && Boolean(provider.credentialRef);
1503
+ const shared = Boolean(provider.credentialShared);
1504
+ const showKey = !dormant && needsKey && participating && (!shared || !provider.configured);
1505
+ const canSort = participating && order.length > 1;
1506
+ const readOnly = writable[provider.credentialRef ?? ""] === false;
1507
+ const typedKey = (keys[providerKey(provider.kind, provider.id)] ?? "").trim();
1508
+ const availability = dormant ? "dormant" : provider.configured ? "available" : "unavailable";
1509
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
1510
+ className: active ? "is-active" : void 0,
1511
+ "data-testid": `web-${kind}-rank-${provider.id}`,
1512
+ "data-availability": availability,
1513
+ "data-in-use": active || void 0,
1514
+ "data-dragging": dragSource === providerKey(kind, provider.id) || void 0,
1515
+ "data-drop": dropTarget === providerKey(kind, provider.id) && dragSource ? order.indexOf(dragSource.slice(kind.length + 1)) < rank ? "after" : "before" : void 0,
1516
+ onDragOver: (event) => {
1517
+ if (locked || !canSort || !dragSource?.startsWith(kind + ":") || dragSource === providerKey(kind, provider.id)) return;
1518
+ event.preventDefault();
1519
+ event.dataTransfer.dropEffect = "move";
1520
+ setDropTarget(providerKey(kind, provider.id));
1521
+ },
1522
+ onDragLeave: (event) => {
1523
+ if (!event.currentTarget.contains(event.relatedTarget)) setDropTarget(null);
1524
+ },
1525
+ onDrop: (event) => {
1526
+ if (locked || !canSort || !dragSource?.startsWith(kind + ":")) return;
1527
+ event.preventDefault();
1528
+ move(kind, dragSource.slice(kind.length + 1), rank);
1529
+ endDrag();
1530
+ },
1694
1531
  children: [
1695
- fetchId,
1696
- " · ",
1697
- text.dormant
1532
+ canSort ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1533
+ variant: "ghost",
1534
+ size: "sm",
1535
+ className: "web-search-drag-handle",
1536
+ ref: (element) => {
1537
+ if (element) handleRefs.current.set(providerKey(kind, provider.id), element);
1538
+ else handleRefs.current.delete(providerKey(kind, provider.id));
1539
+ },
1540
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
1541
+ width: "16",
1542
+ height: "20",
1543
+ viewBox: "0 0 16 20",
1544
+ fill: "currentColor",
1545
+ "aria-hidden": "true",
1546
+ children: [
1547
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1548
+ cx: "5",
1549
+ cy: "5",
1550
+ r: "1.5"
1551
+ }),
1552
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1553
+ cx: "11",
1554
+ cy: "5",
1555
+ r: "1.5"
1556
+ }),
1557
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1558
+ cx: "5",
1559
+ cy: "10",
1560
+ r: "1.5"
1561
+ }),
1562
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1563
+ cx: "11",
1564
+ cy: "10",
1565
+ r: "1.5"
1566
+ }),
1567
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1568
+ cx: "5",
1569
+ cy: "15",
1570
+ r: "1.5"
1571
+ }),
1572
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1573
+ cx: "11",
1574
+ cy: "15",
1575
+ r: "1.5"
1576
+ })
1577
+ ]
1578
+ }),
1579
+ draggable: !locked,
1580
+ "aria-disabled": locked,
1581
+ "aria-label": text.dragPrefix + providerDisplayLabel(provider, locale),
1582
+ title: text.dragHint,
1583
+ onDragStart: (event) => {
1584
+ if (locked || pendingRef.current && pendingRef.current.kind !== kind) {
1585
+ event.preventDefault();
1586
+ return;
1587
+ }
1588
+ event.dataTransfer.effectAllowed = "move";
1589
+ event.dataTransfer.setData(`application/x-dsh-${kind}-provider`, provider.id);
1590
+ const row = event.currentTarget.closest("li");
1591
+ if (row) event.dataTransfer.setDragImage(row, 16, 16);
1592
+ dragRef.current = providerKey(kind, provider.id);
1593
+ requestSeq.current += 1;
1594
+ setDragSource(providerKey(kind, provider.id));
1595
+ setSortAnnouncement("");
1596
+ },
1597
+ onDragEnd: endDrag,
1598
+ onKeyDown: (event) => {
1599
+ if (event.key !== "ArrowUp" && event.key !== "ArrowDown") return;
1600
+ event.preventDefault();
1601
+ move(kind, provider.id, rank + (event.key === "ArrowUp" ? -1 : 1), true);
1602
+ }
1603
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1604
+ className: "web-search-rank-index",
1605
+ children: participating ? rank + 1 : "—"
1606
+ }),
1607
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1608
+ className: "web-search-provider",
1609
+ children: [
1610
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1611
+ className: "web-search-provider-heading",
1612
+ children: [
1613
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
1614
+ className: "dsh-ui-heading",
1615
+ children: providerDisplayLabel(provider, locale)
1616
+ }),
1617
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1618
+ className: "ws-provider-note",
1619
+ 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 })]
1620
+ }),
1621
+ active && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
1622
+ tone: "success",
1623
+ children: text.inUse
1624
+ }),
1625
+ provider.signupUrl && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppLink, {
1626
+ url: provider.signupUrl,
1627
+ label: providerDisplayLabel(provider, locale) + " " + text.signup,
1628
+ children: text.signup
1629
+ })
1630
+ ]
1631
+ }),
1632
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1633
+ className: "web-search-provider-info",
1634
+ children: [info.description ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: info.description }) : null, info.pricing || info.pricingUrl ? /* @__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, {
1635
+ url: info.pricingUrl,
1636
+ label: providerDisplayLabel(provider, locale) + " " + text.pricing,
1637
+ children: provider.id === "ddg" ? text.website : text.pricing
1638
+ })] })] }) : null]
1639
+ }),
1640
+ access.kind === "link" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1641
+ className: "dsh-ui-hint",
1642
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppLink, {
1643
+ url: access.url,
1644
+ label: text.openConfig,
1645
+ children: text.openConfig
1646
+ })
1647
+ }) : access.kind === "owner" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1648
+ className: "dsh-ui-hint",
1649
+ children: [text.ownerSettings, access.name]
1650
+ }) : access.kind === "unknown" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1651
+ className: "dsh-ui-hint",
1652
+ children: text.unknownOwner
1653
+ }) : null,
1654
+ shared && participating && owned ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", {
1655
+ className: "dsh-ui-meta",
1656
+ children: provider.credentialHint === "与模型设置共用 Key" ? locale === "zh" ? provider.credentialHint : "Shares a key with model settings" : owned && provider.id === "firecrawl" && locale === "en" ? "Search and page reads share a Firecrawl key" : provider.credentialHint ?? text.sharedKey
1657
+ }) : null,
1658
+ showKey ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1659
+ className: "dsh-ui-field",
1660
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1661
+ className: "dsh-ui-label",
1662
+ children: "API Key"
1663
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
1664
+ className: "ws-input",
1665
+ type: "password",
1666
+ autoComplete: "off",
1667
+ spellCheck: false,
1668
+ disabled: busy || reordering || readOnly,
1669
+ value: keys[providerKey(provider.kind, provider.id)] ?? "",
1670
+ placeholder: provider.configured ? text.keyConfigured : text.keyPlaceholder,
1671
+ onChange: (event) => setKeys((current) => ({
1672
+ ...current,
1673
+ [providerKey(provider.kind, provider.id)]: event.target.value
1674
+ }))
1675
+ })]
1676
+ }) : null,
1677
+ showKey || needsKey && !shared && provider.configured ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1678
+ className: "dsh-ui-actions",
1679
+ children: [showKey ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1680
+ variant: "outline",
1681
+ size: "sm",
1682
+ disabled: busy || reordering || readOnly || !typedKey,
1683
+ onClick: () => void action(async () => {
1684
+ await saveKeys(provider);
1685
+ await load();
1686
+ setNote({ key: "keySaved" });
1687
+ }),
1688
+ children: text.saveKey
1689
+ }) : null, needsKey && !shared && provider.configured ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfirmButton, {
1690
+ disabled: busy || reordering || readOnly,
1691
+ label: text.deleteKey,
1692
+ confirmLabel: text.confirmDelete,
1693
+ onConfirm: () => void action(() => removeKey(provider))
1694
+ }) : null]
1695
+ }) : null
1696
+ ]
1697
+ }),
1698
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1699
+ className: "web-search-rank-move",
1700
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
1701
+ checked: participating,
1702
+ label: providerDisplayLabel(provider, locale),
1703
+ disabled: locked || dragSource !== null,
1704
+ onChange: (next) => {
1705
+ const following = next ? [...order, provider.id] : order.filter((id) => id !== provider.id);
1706
+ runImmediate(on && following.some((id) => saved.providers.some((item) => item.kind === kind && item.id === id && item.configured)), following, droppedDormantIds(saved.settings, following, saved.providers, kind), kind);
1707
+ }
1708
+ })
1709
+ })
1698
1710
  ]
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
- })]
1711
+ }, provider.id);
1712
+ })
1704
1713
  }),
1705
- fetchAccess?.kind === "link" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1714
+ kind === "search" && backends.some(usesBundledPricing) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1706
1715
  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
1716
+ children: pricingCheckedText(locale, PROVIDER_PRICING_CHECKED)
1717
+ }) : null,
1718
+ on && kind === "search" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1719
+ className: "dsh-ui-actions",
1720
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1721
+ variant: "outline",
1722
+ size: "md",
1723
+ disabled: busy || reordering,
1724
+ onClick: () => void action(async () => {
1725
+ await saveKeysForKind("search");
1726
+ const result = await call("test");
1727
+ await load();
1728
+ setNote({ sources: result.sources });
1729
+ }),
1730
+ children: text.test
1711
1731
  })
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
1732
  }) : null
1719
1733
  ]
1720
- });
1721
- })()]
1734
+ })
1735
+ }, kind);
1722
1736
  }),
1723
1737
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1724
1738
  role: "tabpanel",