@klarkxy/dsh-web-search-manager 0.2.5 → 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.
@@ -467,8 +467,8 @@ function createPluginReadGate(now = Date.now) {
467
467
  //#endregion
468
468
  //#region src/contracts.ts
469
469
  const WEB_SEARCH_RPC_CHANNEL = "/web-search-manager";
470
- function pickActiveSearch(order, configured) {
471
- return order.find(configured) ?? "";
470
+ function providerKey(kind, id) {
471
+ return `${kind}:${id}`;
472
472
  }
473
473
  function safeConfigurationUrl(value) {
474
474
  if (typeof value !== "string" || !value || /[\u0000-\u0020\\]/.test(value)) return;
@@ -485,6 +485,21 @@ function safeConfigurationUrl(value) {
485
485
  //#region src/provider-info.ts
486
486
  const PROVIDER_PRICING_CHECKED = "2026-09-21";
487
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
+ },
488
503
  ddg: {
489
504
  description: "搜索 DuckDuckGo 公开网页,无需注册或 API Key。",
490
505
  pricing: "免费使用,不产生搜索 API 费用。",
@@ -527,6 +542,18 @@ const providerInfo = {
527
542
  }
528
543
  };
529
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
+ },
530
557
  ddg: {
531
558
  description: "Search public DuckDuckGo pages without registration or an API key.",
532
559
  pricing: "Free to use; no search API fees."
@@ -561,6 +588,9 @@ const englishInfo = {
561
588
  }
562
589
  };
563
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."],
564
594
  ddg: ["无需注册。默认后端。", "No registration required. Default backend."],
565
595
  "deepseek-official": ["使用已配置的 DeepSeek API Key,可能产生额外搜索费用。", "Uses the configured DeepSeek API key; additional search fees may apply."],
566
596
  exa: ["独立 Search API。", "Independent Search API."],
@@ -580,6 +610,7 @@ function providerDisplayLabel(provider, locale) {
580
610
  }
581
611
  function bundledCatalogId(provider) {
582
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";
583
614
  return provider.id === "deepseek-managed" ? "deepseek-official" : provider.id;
584
615
  }
585
616
  function missingPricing(locale) {
@@ -588,7 +619,11 @@ function missingPricing(locale) {
588
619
  function usesBundledPricing(provider) {
589
620
  if (provider.pricing?.trim()) return false;
590
621
  const catalogId = bundledCatalogId(provider);
591
- 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));
592
627
  }
593
628
  function searchProviderInfo(provider, locale = "zh") {
594
629
  const catalogId = bundledCatalogId(provider);
@@ -685,52 +720,61 @@ function bindStatusRefresh(options) {
685
720
  };
686
721
  }
687
722
  function isProviderOn(status, provider) {
688
- 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;
689
724
  return status.searchActive && selectedSearchBackend(status)?.id === provider.id;
690
725
  }
691
726
  function dormantSearchPlaceholder(id) {
727
+ return dormantPlaceholder(id, "search");
728
+ }
729
+ function dormantPlaceholder(id, kind) {
692
730
  return {
693
731
  id,
694
732
  label: id,
695
733
  description: "",
696
734
  billing: "unknown",
697
- kind: "search",
735
+ kind,
698
736
  configured: false,
699
737
  calls: 0,
700
738
  failures: 0,
701
739
  configurationOwned: false
702
740
  };
703
741
  }
742
+ function isRegistered(status, id, kind) {
743
+ return status.providers.some((provider) => provider.kind === kind && provider.id === id);
744
+ }
704
745
  function isRegisteredSearch(status, id) {
705
- return status.providers.some((provider) => provider.kind === "search" && provider.id === id);
746
+ return isRegistered(status, id, "search");
706
747
  }
707
- function catalogSearchBackends(status) {
708
- 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]));
709
753
  const rows = [];
710
754
  const seen = /* @__PURE__ */ new Set();
711
- for (const id of status.settings.searchOrder) {
755
+ for (const id of providerOrder(status.settings, kind)) {
712
756
  if (!id || seen.has(id)) continue;
713
757
  seen.add(id);
714
- rows.push(byId.get(id) ?? dormantSearchPlaceholder(id));
715
- }
716
- for (const provider of status.providers) {
717
- if (provider.kind !== "search" || seen.has(provider.id)) continue;
718
- rows.push(provider);
758
+ rows.push(byId.get(id) ?? dormantPlaceholder(id, kind));
719
759
  }
760
+ for (const provider of byId.values()) if (!seen.has(provider.id)) rows.push(provider);
720
761
  return rows;
721
762
  }
763
+ function catalogSearchBackends(status) {
764
+ return catalogBackends(status, "search");
765
+ }
722
766
  function searchBackends(status) {
723
- const participating = new Set(status.settings.searchOrder);
724
- 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);
725
771
  }
726
772
  function selectedSearchBackend(status) {
727
- const backends = searchBackends(status).filter((provider) => isRegisteredSearch(status, provider.id));
728
- const id = pickActiveSearch(backends.map((provider) => provider.id), (id) => Boolean(backends.find((provider) => provider.id === id)?.configured));
729
- return backends.find((provider) => provider.id === id);
773
+ return selectedBackend(status, "search");
730
774
  }
731
- function droppedDormantIds(settings, order, providers) {
732
- const known = new Set(providers.filter((provider) => provider.kind === "search").map((provider) => provider.id));
733
- 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)))];
734
778
  }
735
779
  function configurationAccess(provider) {
736
780
  if (ownsProviderConfiguration(provider)) return { kind: "owned" };
@@ -811,8 +855,9 @@ function ConfirmButton(props) {
811
855
  const copy = {
812
856
  zh: {
813
857
  tabsLabel: "网络搜索设置",
814
- tabProviders: "搜索服务",
815
- tabLimits: "请求限制",
858
+ tabSearch: "web-search",
859
+ tabFetch: "web-fetch",
860
+ tabLimits: "设置",
816
861
  loading: "正在读取设置…",
817
862
  loadFailed: "读取设置失败:",
818
863
  reconnect: "重新连接",
@@ -853,13 +898,16 @@ const copy = {
853
898
  maxFetchChars: "正文字符上限",
854
899
  fetchTitle: "网页读取",
855
900
  fetchProvider: "读取服务",
901
+ fetchBackends: "读取后端",
902
+ needFetchBackend: "需选择并配置读取服务。",
856
903
  ownerSettings: "配置入口:",
857
904
  unknownOwner: "请在服务所属插件中配置"
858
905
  },
859
906
  en: {
860
907
  tabsLabel: "Web search settings",
861
- tabProviders: "Search services",
862
- tabLimits: "Request limits",
908
+ tabSearch: "web-search",
909
+ tabFetch: "web-fetch",
910
+ tabLimits: "Settings",
863
911
  loading: "Loading settings…",
864
912
  loadFailed: "Could not load settings: ",
865
913
  reconnect: "Reconnect",
@@ -900,6 +948,8 @@ const copy = {
900
948
  maxFetchChars: "Max text chars",
901
949
  fetchTitle: "Read web pages",
902
950
  fetchProvider: "Fetch service",
951
+ fetchBackends: "Fetch backends",
952
+ needFetchBackend: "Choose and configure a fetch service.",
903
953
  ownerSettings: "Configure in: ",
904
954
  unknownOwner: "Configure this service in its plugin"
905
955
  }
@@ -916,7 +966,7 @@ function uiMessage(message, locale, providers = []) {
916
966
  if ("key" in message) return copy[locale][message.key];
917
967
  if ("sources" in message) return testOkText(locale, message.sources);
918
968
  if ("moved" in message) {
919
- const provider = providers.find((item) => item.id === message.moved);
969
+ const provider = providers.find((item) => item.id === message.moved && item.kind === message.kind);
920
970
  return movedText(locale, provider ? providerDisplayLabel(provider, locale) : message.moved, message.position);
921
971
  }
922
972
  return message.invalid.map((key) => {
@@ -969,15 +1019,15 @@ function parseLimit(raw, min, max) {
969
1019
  * Settings written by an immediate action (a switch or a reorder). It keeps the
970
1020
  * saved limits and never includes typed-but-unsaved keys or limit drafts.
971
1021
  */
972
- function immediateUpdate(settings, on, order, providers, remove = []) {
1022
+ function immediateUpdate(settings, on, order, providers, remove = [], kind = "search") {
973
1023
  let searchOrder = order;
974
1024
  if (providers) {
975
- 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));
976
1026
  const removed = new Set(remove);
977
1027
  const following = order.filter((id) => !removed.has(id));
978
1028
  if (following.some((id) => !known.has(id))) searchOrder = following;
979
1029
  else {
980
- searchOrder = settings.searchOrder.flatMap((id) => {
1030
+ searchOrder = providerOrder(settings, kind).flatMap((id) => {
981
1031
  if (removed.has(id)) return [];
982
1032
  return known.has(id) ? following.splice(0, 1) : [id];
983
1033
  });
@@ -986,8 +1036,13 @@ function immediateUpdate(settings, on, order, providers, remove = []) {
986
1036
  }
987
1037
  return {
988
1038
  ...editable(settings),
989
- searchOrder,
990
- searchEnabled: on
1039
+ ...kind === "search" ? {
1040
+ searchOrder,
1041
+ searchEnabled: on
1042
+ } : {
1043
+ fetchOrder: searchOrder,
1044
+ fetchEnabled: on
1045
+ }
991
1046
  };
992
1047
  }
993
1048
  function limitTexts(settings) {
@@ -1009,7 +1064,7 @@ function NetworkSearchSettings({ client, hidden }) {
1009
1064
  const locale = useHostLocale(client);
1010
1065
  const text = copy[locale];
1011
1066
  const tabsId = (0, react.useId)();
1012
- const [tab, setTab] = (0, react.useState)("providers");
1067
+ const [tab, setTab] = (0, react.useState)("search");
1013
1068
  const [status, setStatus] = (0, react.useState)();
1014
1069
  const [limitDraft, setLimitDraft] = (0, react.useState)();
1015
1070
  const [invalid, setInvalid] = (0, react.useState)({});
@@ -1026,6 +1081,7 @@ function NetworkSearchSettings({ client, hidden }) {
1026
1081
  const statusRef = (0, react.useRef)(status);
1027
1082
  statusRef.current = status;
1028
1083
  const pendingRef = (0, react.useRef)(null);
1084
+ const dragRef = (0, react.useRef)(null);
1029
1085
  const timerRef = (0, react.useRef)(void 0);
1030
1086
  const handleRefs = (0, react.useRef)(/* @__PURE__ */ new Map());
1031
1087
  const refocusRef = (0, react.useRef)(null);
@@ -1061,7 +1117,7 @@ function NetworkSearchSettings({ client, hidden }) {
1061
1117
  mounted: mountedRef.current,
1062
1118
  hidden: isPageHidden(hiddenRef.current),
1063
1119
  busy: busyRef.current,
1064
- reordering: pendingRef.current !== null,
1120
+ reordering: pendingRef.current !== null || dragRef.current !== null,
1065
1121
  reading: readingRef.current
1066
1122
  });
1067
1123
  }
@@ -1098,7 +1154,7 @@ function NetworkSearchSettings({ client, hidden }) {
1098
1154
  requestId: mine,
1099
1155
  latestRequestId: requestSeq.current,
1100
1156
  busy: busyRef.current,
1101
- reordering: pendingRef.current !== null,
1157
+ reordering: pendingRef.current !== null || dragRef.current !== null,
1102
1158
  hidden: isPageHidden(hiddenRef.current),
1103
1159
  currentRevision: statusRef.current?.settings.revision,
1104
1160
  incomingRevision: next.settings.revision
@@ -1148,7 +1204,7 @@ function NetworkSearchSettings({ client, hidden }) {
1148
1204
  const current = statusRef.current;
1149
1205
  const next = pendingRef.current;
1150
1206
  if (current && next) call("update", {
1151
- 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),
1152
1208
  expectedRevision: current.settings.revision
1153
1209
  }).catch(() => {});
1154
1210
  };
@@ -1166,6 +1222,7 @@ function NetworkSearchSettings({ client, hidden }) {
1166
1222
  if (handle && document.activeElement !== handle) handle.focus();
1167
1223
  });
1168
1224
  async function action(run) {
1225
+ if (busyRef.current) return;
1169
1226
  readGate.reset();
1170
1227
  requestSeq.current += 1;
1171
1228
  busyRef.current = true;
@@ -1183,18 +1240,17 @@ function NetworkSearchSettings({ client, hidden }) {
1183
1240
  }
1184
1241
  }
1185
1242
  /** Switches and reorders: settings only, applied at once. */
1186
- function runImmediate(on, order, remove = []) {
1243
+ function runImmediate(on, order, remove = [], kind = "search") {
1187
1244
  clearTimeout(timerRef.current);
1188
1245
  timerRef.current = void 0;
1189
1246
  pendingRef.current = null;
1190
1247
  action(async () => {
1191
1248
  const current = statusRef.current;
1192
1249
  if (!current) return;
1193
- const next = await call("update", {
1194
- 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),
1195
1252
  expectedRevision: current.settings.revision
1196
- });
1197
- setStatus(next);
1253
+ }));
1198
1254
  await load();
1199
1255
  }).then(() => {
1200
1256
  if (pendingRef.current === null) setPendingOrder(null);
@@ -1204,24 +1260,27 @@ function NetworkSearchSettings({ client, hidden }) {
1204
1260
  async function saveKeys(only) {
1205
1261
  if (!status) return;
1206
1262
  for (const provider of status.providers) {
1207
- if (provider.kind !== "search" || only && provider.id !== only.id) continue;
1208
- 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)] ?? "";
1209
1265
  const typed = snapshot.trim();
1210
1266
  if (!ownsProviderConfiguration(provider) || !provider.credentialRef || !typed) continue;
1211
1267
  if (writable[provider.credentialRef] === false) throw new UiError("readOnly");
1212
1268
  unwrap(await client.remote.credentials.set(provider.credentialRef, typed));
1213
- setKeys((current) => current[provider.id] === snapshot ? {
1269
+ setKeys((current) => current[providerKey(provider.kind, provider.id)] === snapshot ? {
1214
1270
  ...current,
1215
- [provider.id]: ""
1271
+ [providerKey(provider.kind, provider.id)]: ""
1216
1272
  } : current);
1217
1273
  }
1218
1274
  }
1275
+ async function saveKeysForKind(kind) {
1276
+ for (const provider of statusRef.current?.providers ?? []) if (provider.kind === kind) await saveKeys(provider);
1277
+ }
1219
1278
  async function removeKey(provider) {
1220
1279
  if (!ownsProviderConfiguration(provider) || !provider.credentialRef || provider.credentialShared || !status) return;
1221
1280
  unwrap(await client.remote.credentials.unset(provider.credentialRef));
1222
1281
  setKeys((current) => ({
1223
1282
  ...current,
1224
- [provider.id]: ""
1283
+ [providerKey(provider.kind, provider.id)]: ""
1225
1284
  }));
1226
1285
  await load();
1227
1286
  setNote({ key: "keyDeleted" });
@@ -1250,28 +1309,30 @@ function NetworkSearchSettings({ client, hidden }) {
1250
1309
  }) : null]
1251
1310
  });
1252
1311
  const saved = status;
1253
- const savedOrder = searchBackends(saved).map((provider) => provider.id);
1254
- const order = pendingOrder ?? savedOrder;
1255
- const catalog = catalogSearchBackends(saved);
1256
- const backends = [...order.flatMap((id) => catalog.find((provider) => provider.id === id) ?? []), ...catalog.filter((provider) => !order.includes(provider.id))];
1257
- const selected = selectedSearchBackend(saved);
1258
- const on = saved.settings.searchEnabled;
1259
- const canEnable = canEnableSearch(order, saved.providers, {}, writable);
1260
1312
  const savedLimits = limitTexts(saved.settings);
1261
1313
  const limitsDirty = LIMITS.some(([key]) => limitDraft[key] !== savedLimits[key]);
1262
1314
  const keysDirty = Object.values(keys).some((value) => value.trim());
1263
- const reordering = pendingOrder !== null;
1264
- 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);
1265
1319
  const index = order.indexOf(id);
1266
1320
  if (busy || index < 0 || nextIndex < 0 || nextIndex >= order.length || index === nextIndex) return;
1267
1321
  const next = [...order];
1268
1322
  const [item] = next.splice(index, 1);
1269
1323
  next.splice(nextIndex, 0, item);
1270
- pendingRef.current = next;
1271
- setPendingOrder(next);
1272
- 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);
1273
1333
  setSortAnnouncement({
1274
1334
  moved: id,
1335
+ kind,
1275
1336
  position: nextIndex + 1
1276
1337
  });
1277
1338
  clearTimeout(timerRef.current);
@@ -1279,10 +1340,11 @@ function NetworkSearchSettings({ client, hidden }) {
1279
1340
  timerRef.current = void 0;
1280
1341
  const batched = pendingRef.current;
1281
1342
  const current = statusRef.current;
1282
- 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);
1283
1344
  }, keyboard ? REORDER_DEBOUNCE_MS : 0);
1284
1345
  }
1285
1346
  function endDrag() {
1347
+ dragRef.current = null;
1286
1348
  setDragSource(null);
1287
1349
  setDropTarget(null);
1288
1350
  }
@@ -1325,17 +1387,26 @@ function NetworkSearchSettings({ client, hidden }) {
1325
1387
  value: tab,
1326
1388
  onChange: setTab,
1327
1389
  label: text.tabsLabel,
1328
- items: [{
1329
- value: "providers",
1330
- label: text.tabProviders,
1331
- id: tabsId + "-providers-tab",
1332
- panelId: tabsId + "-providers-panel"
1333
- }, {
1334
- value: "limits",
1335
- label: text.tabLimits,
1336
- id: tabsId + "-limits-tab",
1337
- panelId: tabsId + "-limits-panel"
1338
- }]
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
+ ]
1339
1410
  }),
1340
1411
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1341
1412
  className: "ws-sr-only",
@@ -1374,374 +1445,294 @@ function NetworkSearchSettings({ client, hidden }) {
1374
1445
  className: "dsh-ui-banner dsh-ui-banner--danger",
1375
1446
  children: text.storageFailed
1376
1447
  }),
1377
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1378
- role: "tabpanel",
1379
- id: tabsId + "-providers-panel",
1380
- "aria-labelledby": tabsId + "-providers-tab",
1381
- hidden: tab !== "providers",
1382
- tabIndex: 0,
1383
- children: [backends.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1384
- className: "dsh-ui-empty",
1385
- children: text.empty
1386
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
1387
- className: "web-search-card dsh-ui-card dsh-ui-card--flat",
1388
- "data-testid": "web-search-tool",
1389
- "data-on": on ? "true" : "false",
1390
- children: [
1391
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
1392
- className: "ws-card-head",
1393
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1394
- className: "dsh-ui-title",
1395
- children: text.toolTitle
1396
- }), !canEnable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1397
- className: "dsh-ui-hint",
1398
- children: text.needBackend
1399
- })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
1400
- checked: on,
1401
- label: text.toolTitle,
1402
- disabled: busy || !on && !canEnable,
1403
- onChange: (next) => runImmediate(next, order)
1404
- })]
1405
- }),
1406
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ol", {
1407
- className: "web-search-rank",
1408
- "aria-label": text.backends,
1409
- children: backends.map((provider) => {
1410
- const participating = order.includes(provider.id);
1411
- const rank = order.indexOf(provider.id);
1412
- const dormant = !isRegisteredSearch(saved, provider.id);
1413
- const active = !dormant && saved.searchActive && selected?.id === provider.id;
1414
- const info = searchProviderInfo(provider, locale);
1415
- const owned = ownsProviderConfiguration(provider);
1416
- const access = configurationAccess(provider);
1417
- const needsKey = owned && Boolean(provider.credentialRef);
1418
- const shared = Boolean(provider.credentialShared);
1419
- const showKey = !dormant && needsKey && participating && (!shared || !provider.configured);
1420
- const canSort = participating && order.length > 1;
1421
- const readOnly = writable[provider.credentialRef ?? ""] === false;
1422
- const typedKey = (keys[provider.id] ?? "").trim();
1423
- const availability = dormant ? "dormant" : provider.configured ? "available" : "unavailable";
1424
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
1425
- className: active ? "is-active" : void 0,
1426
- "data-testid": `web-search-rank-${provider.id}`,
1427
- "data-availability": availability,
1428
- "data-in-use": active || void 0,
1429
- "data-dragging": dragSource === provider.id || void 0,
1430
- "data-drop": dropTarget === provider.id && dragSource ? order.indexOf(dragSource) < rank ? "after" : "before" : void 0,
1431
- onDragOver: (event) => {
1432
- if (busy || !canSort || !dragSource || dragSource === provider.id) return;
1433
- event.preventDefault();
1434
- event.dataTransfer.dropEffect = "move";
1435
- setDropTarget(provider.id);
1436
- },
1437
- onDragLeave: (event) => {
1438
- if (!event.currentTarget.contains(event.relatedTarget)) setDropTarget(null);
1439
- },
1440
- onDrop: (event) => {
1441
- if (busy || !canSort || !dragSource) return;
1442
- event.preventDefault();
1443
- move(dragSource, rank);
1444
- endDrag();
1445
- },
1446
- children: [
1447
- canSort ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1448
- variant: "ghost",
1449
- size: "sm",
1450
- className: "web-search-drag-handle",
1451
- ref: (element) => {
1452
- if (element) handleRefs.current.set(provider.id, element);
1453
- else handleRefs.current.delete(provider.id);
1454
- },
1455
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
1456
- width: "16",
1457
- height: "20",
1458
- viewBox: "0 0 16 20",
1459
- fill: "currentColor",
1460
- "aria-hidden": "true",
1461
- children: [
1462
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1463
- cx: "5",
1464
- cy: "5",
1465
- r: "1.5"
1466
- }),
1467
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1468
- cx: "11",
1469
- cy: "5",
1470
- r: "1.5"
1471
- }),
1472
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1473
- cx: "5",
1474
- cy: "10",
1475
- r: "1.5"
1476
- }),
1477
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1478
- cx: "11",
1479
- cy: "10",
1480
- r: "1.5"
1481
- }),
1482
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1483
- cx: "5",
1484
- cy: "15",
1485
- r: "1.5"
1486
- }),
1487
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1488
- cx: "11",
1489
- cy: "15",
1490
- r: "1.5"
1491
- })
1492
- ]
1493
- }),
1494
- draggable: !busy,
1495
- "aria-disabled": busy,
1496
- "aria-label": text.dragPrefix + providerDisplayLabel(provider, locale),
1497
- title: text.dragHint,
1498
- onDragStart: (event) => {
1499
- if (busy) {
1500
- event.preventDefault();
1501
- return;
1502
- }
1503
- event.dataTransfer.effectAllowed = "move";
1504
- event.dataTransfer.setData("application/x-dsh-search-provider", provider.id);
1505
- const row = event.currentTarget.closest("li");
1506
- if (row) event.dataTransfer.setDragImage(row, 16, 16);
1507
- setDragSource(provider.id);
1508
- setSortAnnouncement("");
1509
- },
1510
- onDragEnd: endDrag,
1511
- onKeyDown: (event) => {
1512
- if (event.key !== "ArrowUp" && event.key !== "ArrowDown") return;
1513
- event.preventDefault();
1514
- move(provider.id, rank + (event.key === "ArrowUp" ? -1 : 1), true);
1515
- }
1516
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1517
- className: "web-search-rank-index",
1518
- children: participating ? rank + 1 : "—"
1519
- }),
1520
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1521
- className: "web-search-provider",
1522
- children: [
1523
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1524
- className: "web-search-provider-heading",
1525
- children: [
1526
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
1527
- className: "dsh-ui-heading",
1528
- children: providerDisplayLabel(provider, locale)
1529
- }),
1530
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1531
- className: "ws-provider-note",
1532
- 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 })]
1533
- }),
1534
- active && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
1535
- tone: "success",
1536
- children: text.inUse
1537
- }),
1538
- provider.signupUrl && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppLink, {
1539
- url: provider.signupUrl,
1540
- label: providerDisplayLabel(provider, locale) + " " + text.signup,
1541
- children: text.signup
1542
- })
1543
- ]
1544
- }),
1545
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1546
- className: "web-search-provider-info",
1547
- 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, {
1548
- url: info.pricingUrl,
1549
- label: providerDisplayLabel(provider, locale) + " " + text.pricing,
1550
- children: provider.id === "ddg" ? text.website : text.pricing
1551
- })] })] })]
1552
- }),
1553
- access.kind === "link" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1554
- className: "dsh-ui-hint",
1555
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppLink, {
1556
- url: access.url,
1557
- label: text.openConfig,
1558
- children: text.openConfig
1559
- })
1560
- }) : access.kind === "owner" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1561
- className: "dsh-ui-hint",
1562
- children: [text.ownerSettings, access.name]
1563
- }) : access.kind === "unknown" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1564
- className: "dsh-ui-hint",
1565
- children: text.unknownOwner
1566
- }) : null,
1567
- shared && participating && owned ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", {
1568
- className: "dsh-ui-meta",
1569
- children: provider.credentialHint === "与模型设置共用 Key" ? locale === "zh" ? provider.credentialHint : "Shares a key with model settings" : provider.credentialHint ?? text.sharedKey
1570
- }) : null,
1571
- showKey ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1572
- className: "dsh-ui-field",
1573
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1574
- className: "dsh-ui-label",
1575
- children: "API Key"
1576
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
1577
- className: "ws-input",
1578
- type: "password",
1579
- autoComplete: "off",
1580
- spellCheck: false,
1581
- disabled: busy || readOnly,
1582
- value: keys[provider.id] ?? "",
1583
- placeholder: provider.configured ? text.keyConfigured : text.keyPlaceholder,
1584
- onChange: (event) => setKeys((current) => ({
1585
- ...current,
1586
- [provider.id]: event.target.value
1587
- }))
1588
- })]
1589
- }) : null,
1590
- showKey || needsKey && !shared && provider.configured ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1591
- className: "dsh-ui-actions",
1592
- children: [showKey ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1593
- variant: "outline",
1594
- size: "sm",
1595
- disabled: busy || readOnly || !typedKey,
1596
- onClick: () => void action(async () => {
1597
- await saveKeys(provider);
1598
- await load();
1599
- setNote({ key: "keySaved" });
1600
- }),
1601
- children: text.saveKey
1602
- }) : null, needsKey && !shared && provider.configured ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfirmButton, {
1603
- disabled: busy || readOnly,
1604
- label: text.deleteKey,
1605
- confirmLabel: text.confirmDelete,
1606
- onConfirm: () => void action(() => removeKey(provider))
1607
- }) : null]
1608
- }) : null
1609
- ]
1610
- }),
1611
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1612
- className: "web-search-rank-move",
1613
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
1614
- checked: participating,
1615
- label: providerDisplayLabel(provider, locale),
1616
- disabled: busy,
1617
- onChange: (next) => {
1618
- const following = next ? [...order, provider.id] : order.filter((id) => id !== provider.id);
1619
- runImmediate(nextSearchEnabled(saved.settings.searchEnabled, following, saved.providers, {}, writable), following, droppedDormantIds(saved.settings, following, saved.providers));
1620
- }
1621
- })
1622
- })
1623
- ]
1624
- }, provider.id);
1625
- })
1626
- }),
1627
- backends.some(usesBundledPricing) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1628
- className: "dsh-ui-hint",
1629
- children: pricingCheckedText(locale, PROVIDER_PRICING_CHECKED)
1630
- }) : null,
1631
- on ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1632
- className: "dsh-ui-actions",
1633
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1634
- variant: "outline",
1635
- size: "md",
1636
- disabled: busy || reordering,
1637
- onClick: () => void action(async () => {
1638
- await saveKeys();
1639
- const result = await call("test");
1640
- await load();
1641
- setNote({ sources: result.sources });
1642
- }),
1643
- children: text.test
1644
- })
1645
- }) : null
1646
- ]
1647
- }), (saved.providers.some((provider) => provider.kind === "fetch") || saved.settings.fetchProvider) && (() => {
1648
- const fetchId = saved.settings.fetchProvider;
1649
- const fetchSelected = saved.providers.find((provider) => provider.kind === "fetch" && provider.id === fetchId);
1650
- const fetchDormant = Boolean(fetchId) && !fetchSelected;
1651
- const fetchActive = Boolean(fetchSelected && saved.fetchActive && fetchSelected.configured);
1652
- const fetchAccess = fetchSelected ? configurationAccess(fetchSelected) : fetchDormant ? { kind: "unknown" } : void 0;
1653
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
1654
- className: "dsh-ui-card dsh-ui-card--flat",
1655
- "data-testid": "web-search-fetch",
1656
- "data-availability": fetchDormant ? "dormant" : fetchSelected?.configured ? "available" : "unavailable",
1657
- "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",
1658
1471
  children: [
1659
1472
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
1660
1473
  className: "ws-card-head",
1661
1474
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1662
1475
  className: "dsh-ui-title",
1663
- children: text.fetchTitle
1664
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1665
- className: "ws-provider-note",
1666
- children: [
1667
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: fetchDormant ? "idle" : fetchSelected?.configured ? "done" : "warning" }),
1668
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: fetchDormant ? text.dormant : fetchSelected?.configured ? text.available : text.unavailable }),
1669
- fetchActive ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
1670
- tone: "success",
1671
- children: text.inUse
1672
- }) : null
1673
- ]
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
1674
1480
  })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
1675
- checked: saved.settings.fetchEnabled,
1676
- label: text.fetchTitle,
1677
- disabled: busy || reordering,
1678
- onChange: (next) => void action(async () => {
1679
- const current = statusRef.current;
1680
- adopt(await call("update", {
1681
- settings: {
1682
- ...editable(current.settings),
1683
- fetchEnabled: next
1684
- },
1685
- expectedRevision: current.settings.revision
1686
- }));
1687
- await load();
1688
- })
1481
+ checked: on,
1482
+ label: title,
1483
+ disabled: locked || dragSource !== null || !on && !canEnable,
1484
+ onChange: (next) => runImmediate(next, order, [], kind)
1689
1485
  })]
1690
1486
  }),
1691
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1692
- className: "dsh-ui-field",
1693
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1694
- className: "dsh-ui-label",
1695
- children: text.fetchProvider
1696
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1697
- className: "dsh-ui-select",
1698
- "aria-label": text.fetchProvider,
1699
- value: fetchId,
1700
- disabled: busy || reordering,
1701
- onChange: (event) => {
1702
- const id = event.target.value;
1703
- action(async () => {
1704
- const current = statusRef.current;
1705
- adopt(await call("update", {
1706
- settings: {
1707
- ...editable(current.settings),
1708
- fetchProvider: id
1709
- },
1710
- expectedRevision: current.settings.revision
1711
- }));
1712
- await load();
1713
- });
1714
- },
1715
- children: [fetchDormant && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
1716
- 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
+ },
1717
1531
  children: [
1718
- fetchId,
1719
- " · ",
1720
- 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
+ })
1721
1710
  ]
1722
- }), saved.providers.filter((provider) => provider.kind === "fetch").map((provider) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
1723
- value: provider.id,
1724
- children: [providerDisplayLabel(provider, locale), provider.configured ? "" : " · " + text.unavailable]
1725
- }, provider.id))]
1726
- })]
1711
+ }, provider.id);
1712
+ })
1727
1713
  }),
1728
- fetchAccess?.kind === "link" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1714
+ kind === "search" && backends.some(usesBundledPricing) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1729
1715
  className: "dsh-ui-hint",
1730
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppLink, {
1731
- url: fetchAccess.url,
1732
- label: text.openConfig,
1733
- 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
1734
1731
  })
1735
- }) : fetchAccess?.kind === "owner" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1736
- className: "dsh-ui-hint",
1737
- children: [text.ownerSettings, fetchAccess.name]
1738
- }) : fetchAccess?.kind === "unknown" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1739
- className: "dsh-ui-hint",
1740
- children: text.unknownOwner
1741
1732
  }) : null
1742
1733
  ]
1743
- });
1744
- })()]
1734
+ })
1735
+ }, kind);
1745
1736
  }),
1746
1737
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1747
1738
  role: "tabpanel",