@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.
package/lib/client.js CHANGED
@@ -473,8 +473,8 @@ function createPluginReadGate(now = Date.now) {
473
473
  //#endregion
474
474
  //#region src/contracts.ts
475
475
  const WEB_SEARCH_RPC_CHANNEL = "/web-search-manager";
476
- function pickActiveSearch(order, configured) {
477
- return order.find(configured) ?? "";
476
+ function providerKey(kind, id) {
477
+ return `${kind}:${id}`;
478
478
  }
479
479
  function safeConfigurationUrl(value) {
480
480
  if (typeof value !== "string" || !value || /[\u0000-\u0020\\]/.test(value)) return;
@@ -491,6 +491,21 @@ function safeConfigurationUrl(value) {
491
491
  //#region src/provider-info.ts
492
492
  const PROVIDER_PRICING_CHECKED = "2026-09-21";
493
493
  const providerInfo = {
494
+ perplexity: {
495
+ description: "通过 Perplexity Search API 获取网页来源,不生成回答。",
496
+ pricing: "按 Search API 请求计费;实际价格与额度以官方账户为准。",
497
+ pricingUrl: "https://docs.perplexity.ai/docs/getting-started/pricing"
498
+ },
499
+ you: {
500
+ description: "通过 You.com 获取网页与新闻结果,不请求额外全文提取。",
501
+ pricing: "按 Web Search API 请求计费;实际价格与额度以官方账户为准。",
502
+ pricingUrl: "https://you.com/docs/administration/billing"
503
+ },
504
+ "firecrawl-fetch": {
505
+ description: "读取网页 Markdown;与 Firecrawl 搜索共用 Key。",
506
+ pricing: "网页读取使用 Firecrawl 积分,与搜索分别计费;实际消耗以官方账户为准。",
507
+ pricingUrl: "https://www.firecrawl.dev/pricing"
508
+ },
494
509
  ddg: {
495
510
  description: "搜索 DuckDuckGo 公开网页,无需注册或 API Key。",
496
511
  pricing: "免费使用,不产生搜索 API 费用。",
@@ -533,6 +548,18 @@ const providerInfo = {
533
548
  }
534
549
  };
535
550
  const englishInfo = {
551
+ perplexity: {
552
+ description: "Get web sources through the Perplexity Search API without generating an answer.",
553
+ pricing: "Charged per Search API request; current prices and credits depend on your official account."
554
+ },
555
+ you: {
556
+ description: "Get web and news results through You.com without requesting extra full-page extraction.",
557
+ pricing: "Charged per Web Search API request; current prices and credits depend on your official account."
558
+ },
559
+ "firecrawl-fetch": {
560
+ description: "Read page Markdown using the same key as Firecrawl search.",
561
+ pricing: "Page reads use Firecrawl credits and are charged separately from search; your official account determines usage."
562
+ },
536
563
  ddg: {
537
564
  description: "Search public DuckDuckGo pages without registration or an API key.",
538
565
  pricing: "Free to use; no search API fees."
@@ -567,6 +594,9 @@ const englishInfo = {
567
594
  }
568
595
  };
569
596
  const bundledDescriptions = {
597
+ perplexity: ["Search API;返回网页来源,不生成回答。", "Search API returning web sources without generating an answer."],
598
+ you: ["网页与新闻搜索;不额外提取全文。", "Web and news search without extra full-page extraction."],
599
+ "firecrawl-fetch": ["读取网页 Markdown;与 Firecrawl 搜索共用 Key。", "Read page Markdown using the same key as Firecrawl search."],
570
600
  ddg: ["无需注册。默认后端。", "No registration required. Default backend."],
571
601
  "deepseek-official": ["使用已配置的 DeepSeek API Key,可能产生额外搜索费用。", "Uses the configured DeepSeek API key; additional search fees may apply."],
572
602
  exa: ["独立 Search API。", "Independent Search API."],
@@ -586,6 +616,7 @@ function providerDisplayLabel(provider, locale) {
586
616
  }
587
617
  function bundledCatalogId(provider) {
588
618
  if (!(typeof provider.configurationOwned === "boolean" ? provider.configurationOwned : !provider.configurationOwner || provider.configurationOwner === "@klarkxy/dsh-web-search-manager")) return;
619
+ if (provider.kind === "fetch" && provider.id === "firecrawl") return "firecrawl-fetch";
589
620
  return provider.id === "deepseek-managed" ? "deepseek-official" : provider.id;
590
621
  }
591
622
  function missingPricing(locale) {
@@ -594,7 +625,11 @@ function missingPricing(locale) {
594
625
  function usesBundledPricing(provider) {
595
626
  if (provider.pricing?.trim()) return false;
596
627
  const catalogId = bundledCatalogId(provider);
597
- return Boolean(catalogId && Object.hasOwn(providerInfo, catalogId));
628
+ return Boolean(catalogId && ![
629
+ "perplexity",
630
+ "you",
631
+ "firecrawl-fetch"
632
+ ].includes(catalogId) && Object.hasOwn(providerInfo, catalogId));
598
633
  }
599
634
  function searchProviderInfo(provider, locale = "zh") {
600
635
  const catalogId = bundledCatalogId(provider);
@@ -691,52 +726,61 @@ function bindStatusRefresh(options) {
691
726
  };
692
727
  }
693
728
  function isProviderOn(status, provider) {
694
- if (provider.kind === "fetch") return status.fetchActive && status.settings.fetchProvider === provider.id && provider.configured;
729
+ if (provider.kind === "fetch") return status.fetchActive && selectedBackend(status, "fetch")?.id === provider.id;
695
730
  return status.searchActive && selectedSearchBackend(status)?.id === provider.id;
696
731
  }
697
732
  function dormantSearchPlaceholder(id) {
733
+ return dormantPlaceholder(id, "search");
734
+ }
735
+ function dormantPlaceholder(id, kind) {
698
736
  return {
699
737
  id,
700
738
  label: id,
701
739
  description: "",
702
740
  billing: "unknown",
703
- kind: "search",
741
+ kind,
704
742
  configured: false,
705
743
  calls: 0,
706
744
  failures: 0,
707
745
  configurationOwned: false
708
746
  };
709
747
  }
748
+ function isRegistered(status, id, kind) {
749
+ return status.providers.some((provider) => provider.kind === kind && provider.id === id);
750
+ }
710
751
  function isRegisteredSearch(status, id) {
711
- return status.providers.some((provider) => provider.kind === "search" && provider.id === id);
752
+ return isRegistered(status, id, "search");
712
753
  }
713
- function catalogSearchBackends(status) {
714
- const byId = new Map(status.providers.filter((provider) => provider.kind === "search").map((provider) => [provider.id, provider]));
754
+ function providerOrder(settings, kind) {
755
+ return kind === "search" ? settings.searchOrder : settings.fetchOrder;
756
+ }
757
+ function catalogBackends(status, kind) {
758
+ const byId = new Map(status.providers.filter((provider) => provider.kind === kind).map((provider) => [provider.id, provider]));
715
759
  const rows = [];
716
760
  const seen = /* @__PURE__ */ new Set();
717
- for (const id of status.settings.searchOrder) {
761
+ for (const id of providerOrder(status.settings, kind)) {
718
762
  if (!id || seen.has(id)) continue;
719
763
  seen.add(id);
720
- rows.push(byId.get(id) ?? dormantSearchPlaceholder(id));
721
- }
722
- for (const provider of status.providers) {
723
- if (provider.kind !== "search" || seen.has(provider.id)) continue;
724
- rows.push(provider);
764
+ rows.push(byId.get(id) ?? dormantPlaceholder(id, kind));
725
765
  }
766
+ for (const provider of byId.values()) if (!seen.has(provider.id)) rows.push(provider);
726
767
  return rows;
727
768
  }
769
+ function catalogSearchBackends(status) {
770
+ return catalogBackends(status, "search");
771
+ }
728
772
  function searchBackends(status) {
729
- const participating = new Set(status.settings.searchOrder);
730
- return catalogSearchBackends(status).filter((provider) => participating.has(provider.id));
773
+ return catalogSearchBackends(status).filter((provider) => status.settings.searchOrder.includes(provider.id));
774
+ }
775
+ function selectedBackend(status, kind) {
776
+ return providerOrder(status.settings, kind).flatMap((id) => status.providers.find((provider) => provider.kind === kind && provider.id === id) ?? []).find((provider) => provider.configured);
731
777
  }
732
778
  function selectedSearchBackend(status) {
733
- const backends = searchBackends(status).filter((provider) => isRegisteredSearch(status, provider.id));
734
- const id = pickActiveSearch(backends.map((provider) => provider.id), (id) => Boolean(backends.find((provider) => provider.id === id)?.configured));
735
- return backends.find((provider) => provider.id === id);
779
+ return selectedBackend(status, "search");
736
780
  }
737
- function droppedDormantIds(settings, order, providers) {
738
- const known = new Set(providers.filter((provider) => provider.kind === "search").map((provider) => provider.id));
739
- return [...new Set(settings.searchOrder.filter((id) => id && !known.has(id) && !order.includes(id)))];
781
+ function droppedDormantIds(settings, order, providers, kind = "search") {
782
+ const known = new Set(providers.filter((provider) => provider.kind === kind).map((provider) => provider.id));
783
+ return [...new Set(providerOrder(settings, kind).filter((id) => id && !known.has(id) && !order.includes(id)))];
740
784
  }
741
785
  function configurationAccess(provider) {
742
786
  if (ownsProviderConfiguration(provider)) return { kind: "owned" };
@@ -817,8 +861,9 @@ function ConfirmButton(props) {
817
861
  const copy = {
818
862
  zh: {
819
863
  tabsLabel: "网络搜索设置",
820
- tabProviders: "搜索服务",
821
- tabLimits: "请求限制",
864
+ tabSearch: "web-search",
865
+ tabFetch: "web-fetch",
866
+ tabLimits: "设置",
822
867
  loading: "正在读取设置…",
823
868
  loadFailed: "读取设置失败:",
824
869
  reconnect: "重新连接",
@@ -859,13 +904,16 @@ const copy = {
859
904
  maxFetchChars: "正文字符上限",
860
905
  fetchTitle: "网页读取",
861
906
  fetchProvider: "读取服务",
907
+ fetchBackends: "读取后端",
908
+ needFetchBackend: "需选择并配置读取服务。",
862
909
  ownerSettings: "配置入口:",
863
910
  unknownOwner: "请在服务所属插件中配置"
864
911
  },
865
912
  en: {
866
913
  tabsLabel: "Web search settings",
867
- tabProviders: "Search services",
868
- tabLimits: "Request limits",
914
+ tabSearch: "web-search",
915
+ tabFetch: "web-fetch",
916
+ tabLimits: "Settings",
869
917
  loading: "Loading settings…",
870
918
  loadFailed: "Could not load settings: ",
871
919
  reconnect: "Reconnect",
@@ -906,6 +954,8 @@ const copy = {
906
954
  maxFetchChars: "Max text chars",
907
955
  fetchTitle: "Read web pages",
908
956
  fetchProvider: "Fetch service",
957
+ fetchBackends: "Fetch backends",
958
+ needFetchBackend: "Choose and configure a fetch service.",
909
959
  ownerSettings: "Configure in: ",
910
960
  unknownOwner: "Configure this service in its plugin"
911
961
  }
@@ -922,7 +972,7 @@ function uiMessage(message, locale, providers = []) {
922
972
  if ("key" in message) return copy[locale][message.key];
923
973
  if ("sources" in message) return testOkText(locale, message.sources);
924
974
  if ("moved" in message) {
925
- const provider = providers.find((item) => item.id === message.moved);
975
+ const provider = providers.find((item) => item.id === message.moved && item.kind === message.kind);
926
976
  return movedText(locale, provider ? providerDisplayLabel(provider, locale) : message.moved, message.position);
927
977
  }
928
978
  return message.invalid.map((key) => {
@@ -975,15 +1025,15 @@ function parseLimit(raw, min, max) {
975
1025
  * Settings written by an immediate action (a switch or a reorder). It keeps the
976
1026
  * saved limits and never includes typed-but-unsaved keys or limit drafts.
977
1027
  */
978
- function immediateUpdate(settings, on, order, providers, remove = []) {
1028
+ function immediateUpdate(settings, on, order, providers, remove = [], kind = "search") {
979
1029
  let searchOrder = order;
980
1030
  if (providers) {
981
- const known = new Set(providers.filter((provider) => provider.kind === "search").map((provider) => provider.id));
1031
+ const known = new Set(providers.filter((provider) => provider.kind === kind).map((provider) => provider.id));
982
1032
  const removed = new Set(remove);
983
1033
  const following = order.filter((id) => !removed.has(id));
984
1034
  if (following.some((id) => !known.has(id))) searchOrder = following;
985
1035
  else {
986
- searchOrder = settings.searchOrder.flatMap((id) => {
1036
+ searchOrder = providerOrder(settings, kind).flatMap((id) => {
987
1037
  if (removed.has(id)) return [];
988
1038
  return known.has(id) ? following.splice(0, 1) : [id];
989
1039
  });
@@ -992,8 +1042,13 @@ function immediateUpdate(settings, on, order, providers, remove = []) {
992
1042
  }
993
1043
  return {
994
1044
  ...editable(settings),
995
- searchOrder,
996
- searchEnabled: on
1045
+ ...kind === "search" ? {
1046
+ searchOrder,
1047
+ searchEnabled: on
1048
+ } : {
1049
+ fetchOrder: searchOrder,
1050
+ fetchEnabled: on
1051
+ }
997
1052
  };
998
1053
  }
999
1054
  function limitTexts(settings) {
@@ -1015,7 +1070,7 @@ function NetworkSearchSettings({ client, hidden }) {
1015
1070
  const locale = useHostLocale(client);
1016
1071
  const text = copy[locale];
1017
1072
  const tabsId = (0, react.useId)();
1018
- const [tab, setTab] = (0, react.useState)("providers");
1073
+ const [tab, setTab] = (0, react.useState)("search");
1019
1074
  const [status, setStatus] = (0, react.useState)();
1020
1075
  const [limitDraft, setLimitDraft] = (0, react.useState)();
1021
1076
  const [invalid, setInvalid] = (0, react.useState)({});
@@ -1032,6 +1087,7 @@ function NetworkSearchSettings({ client, hidden }) {
1032
1087
  const statusRef = (0, react.useRef)(status);
1033
1088
  statusRef.current = status;
1034
1089
  const pendingRef = (0, react.useRef)(null);
1090
+ const dragRef = (0, react.useRef)(null);
1035
1091
  const timerRef = (0, react.useRef)(void 0);
1036
1092
  const handleRefs = (0, react.useRef)(/* @__PURE__ */ new Map());
1037
1093
  const refocusRef = (0, react.useRef)(null);
@@ -1067,7 +1123,7 @@ function NetworkSearchSettings({ client, hidden }) {
1067
1123
  mounted: mountedRef.current,
1068
1124
  hidden: isPageHidden(hiddenRef.current),
1069
1125
  busy: busyRef.current,
1070
- reordering: pendingRef.current !== null,
1126
+ reordering: pendingRef.current !== null || dragRef.current !== null,
1071
1127
  reading: readingRef.current
1072
1128
  });
1073
1129
  }
@@ -1104,7 +1160,7 @@ function NetworkSearchSettings({ client, hidden }) {
1104
1160
  requestId: mine,
1105
1161
  latestRequestId: requestSeq.current,
1106
1162
  busy: busyRef.current,
1107
- reordering: pendingRef.current !== null,
1163
+ reordering: pendingRef.current !== null || dragRef.current !== null,
1108
1164
  hidden: isPageHidden(hiddenRef.current),
1109
1165
  currentRevision: statusRef.current?.settings.revision,
1110
1166
  incomingRevision: next.settings.revision
@@ -1154,7 +1210,7 @@ function NetworkSearchSettings({ client, hidden }) {
1154
1210
  const current = statusRef.current;
1155
1211
  const next = pendingRef.current;
1156
1212
  if (current && next) call("update", {
1157
- settings: immediateUpdate(current.settings, current.settings.searchEnabled, next, current.providers),
1213
+ settings: immediateUpdate(current.settings, next.kind === "search" ? current.settings.searchEnabled : current.settings.fetchEnabled, next.order, current.providers, [], next.kind),
1158
1214
  expectedRevision: current.settings.revision
1159
1215
  }).catch(() => {});
1160
1216
  };
@@ -1172,6 +1228,7 @@ function NetworkSearchSettings({ client, hidden }) {
1172
1228
  if (handle && document.activeElement !== handle) handle.focus();
1173
1229
  });
1174
1230
  async function action(run) {
1231
+ if (busyRef.current) return;
1175
1232
  readGate.reset();
1176
1233
  requestSeq.current += 1;
1177
1234
  busyRef.current = true;
@@ -1189,18 +1246,17 @@ function NetworkSearchSettings({ client, hidden }) {
1189
1246
  }
1190
1247
  }
1191
1248
  /** Switches and reorders: settings only, applied at once. */
1192
- function runImmediate(on, order, remove = []) {
1249
+ function runImmediate(on, order, remove = [], kind = "search") {
1193
1250
  clearTimeout(timerRef.current);
1194
1251
  timerRef.current = void 0;
1195
1252
  pendingRef.current = null;
1196
1253
  action(async () => {
1197
1254
  const current = statusRef.current;
1198
1255
  if (!current) return;
1199
- const next = await call("update", {
1200
- settings: immediateUpdate(current.settings, on, order, current.providers, remove),
1256
+ adopt(await call("update", {
1257
+ settings: immediateUpdate(current.settings, on, order, current.providers, remove, kind),
1201
1258
  expectedRevision: current.settings.revision
1202
- });
1203
- setStatus(next);
1259
+ }));
1204
1260
  await load();
1205
1261
  }).then(() => {
1206
1262
  if (pendingRef.current === null) setPendingOrder(null);
@@ -1210,24 +1266,27 @@ function NetworkSearchSettings({ client, hidden }) {
1210
1266
  async function saveKeys(only) {
1211
1267
  if (!status) return;
1212
1268
  for (const provider of status.providers) {
1213
- if (provider.kind !== "search" || only && provider.id !== only.id) continue;
1214
- const snapshot = keys[provider.id] ?? "";
1269
+ if (only && providerKey(provider.kind, provider.id) !== providerKey(only.kind, only.id)) continue;
1270
+ const snapshot = keys[providerKey(provider.kind, provider.id)] ?? "";
1215
1271
  const typed = snapshot.trim();
1216
1272
  if (!ownsProviderConfiguration(provider) || !provider.credentialRef || !typed) continue;
1217
1273
  if (writable[provider.credentialRef] === false) throw new UiError("readOnly");
1218
1274
  unwrap(await client.remote.credentials.set(provider.credentialRef, typed));
1219
- setKeys((current) => current[provider.id] === snapshot ? {
1275
+ setKeys((current) => current[providerKey(provider.kind, provider.id)] === snapshot ? {
1220
1276
  ...current,
1221
- [provider.id]: ""
1277
+ [providerKey(provider.kind, provider.id)]: ""
1222
1278
  } : current);
1223
1279
  }
1224
1280
  }
1281
+ async function saveKeysForKind(kind) {
1282
+ for (const provider of statusRef.current?.providers ?? []) if (provider.kind === kind) await saveKeys(provider);
1283
+ }
1225
1284
  async function removeKey(provider) {
1226
1285
  if (!ownsProviderConfiguration(provider) || !provider.credentialRef || provider.credentialShared || !status) return;
1227
1286
  unwrap(await client.remote.credentials.unset(provider.credentialRef));
1228
1287
  setKeys((current) => ({
1229
1288
  ...current,
1230
- [provider.id]: ""
1289
+ [providerKey(provider.kind, provider.id)]: ""
1231
1290
  }));
1232
1291
  await load();
1233
1292
  setNote({ key: "keyDeleted" });
@@ -1256,28 +1315,30 @@ function NetworkSearchSettings({ client, hidden }) {
1256
1315
  }) : null]
1257
1316
  });
1258
1317
  const saved = status;
1259
- const savedOrder = searchBackends(saved).map((provider) => provider.id);
1260
- const order = pendingOrder ?? savedOrder;
1261
- const catalog = catalogSearchBackends(saved);
1262
- const backends = [...order.flatMap((id) => catalog.find((provider) => provider.id === id) ?? []), ...catalog.filter((provider) => !order.includes(provider.id))];
1263
- const selected = selectedSearchBackend(saved);
1264
- const on = saved.settings.searchEnabled;
1265
- const canEnable = canEnableSearch(order, saved.providers, {}, writable);
1266
1318
  const savedLimits = limitTexts(saved.settings);
1267
1319
  const limitsDirty = LIMITS.some(([key]) => limitDraft[key] !== savedLimits[key]);
1268
1320
  const keysDirty = Object.values(keys).some((value) => value.trim());
1269
- const reordering = pendingOrder !== null;
1270
- function move(id, nextIndex, keyboard = false) {
1321
+ const reordering = pendingOrder !== null || dragSource !== null;
1322
+ function move(kind, id, nextIndex, keyboard = false) {
1323
+ if (pendingRef.current && pendingRef.current.kind !== kind) return;
1324
+ const order = pendingRef.current?.order ?? providerOrder(statusRef.current.settings, kind);
1271
1325
  const index = order.indexOf(id);
1272
1326
  if (busy || index < 0 || nextIndex < 0 || nextIndex >= order.length || index === nextIndex) return;
1273
1327
  const next = [...order];
1274
1328
  const [item] = next.splice(index, 1);
1275
1329
  next.splice(nextIndex, 0, item);
1276
- pendingRef.current = next;
1277
- setPendingOrder(next);
1278
- if (keyboard) refocusRef.current = id;
1330
+ pendingRef.current = {
1331
+ kind,
1332
+ order: next
1333
+ };
1334
+ setPendingOrder({
1335
+ kind,
1336
+ order: next
1337
+ });
1338
+ if (keyboard) refocusRef.current = providerKey(kind, id);
1279
1339
  setSortAnnouncement({
1280
1340
  moved: id,
1341
+ kind,
1281
1342
  position: nextIndex + 1
1282
1343
  });
1283
1344
  clearTimeout(timerRef.current);
@@ -1285,10 +1346,11 @@ function NetworkSearchSettings({ client, hidden }) {
1285
1346
  timerRef.current = void 0;
1286
1347
  const batched = pendingRef.current;
1287
1348
  const current = statusRef.current;
1288
- if (batched && current) runImmediate(current.settings.searchEnabled, batched);
1349
+ if (batched && current) runImmediate(batched.kind === "search" ? current.settings.searchEnabled : current.settings.fetchEnabled, batched.order, [], batched.kind);
1289
1350
  }, keyboard ? REORDER_DEBOUNCE_MS : 0);
1290
1351
  }
1291
1352
  function endDrag() {
1353
+ dragRef.current = null;
1292
1354
  setDragSource(null);
1293
1355
  setDropTarget(null);
1294
1356
  }
@@ -1331,17 +1393,26 @@ function NetworkSearchSettings({ client, hidden }) {
1331
1393
  value: tab,
1332
1394
  onChange: setTab,
1333
1395
  label: text.tabsLabel,
1334
- items: [{
1335
- value: "providers",
1336
- label: text.tabProviders,
1337
- id: tabsId + "-providers-tab",
1338
- panelId: tabsId + "-providers-panel"
1339
- }, {
1340
- value: "limits",
1341
- label: text.tabLimits,
1342
- id: tabsId + "-limits-tab",
1343
- panelId: tabsId + "-limits-panel"
1344
- }]
1396
+ items: [
1397
+ {
1398
+ value: "search",
1399
+ label: text.tabSearch,
1400
+ id: tabsId + "-search-tab",
1401
+ panelId: tabsId + "-search-panel"
1402
+ },
1403
+ {
1404
+ value: "fetch",
1405
+ label: text.tabFetch,
1406
+ id: tabsId + "-fetch-tab",
1407
+ panelId: tabsId + "-fetch-panel"
1408
+ },
1409
+ {
1410
+ value: "limits",
1411
+ label: text.tabLimits,
1412
+ id: tabsId + "-limits-tab",
1413
+ panelId: tabsId + "-limits-panel"
1414
+ }
1415
+ ]
1345
1416
  }),
1346
1417
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1347
1418
  className: "ws-sr-only",
@@ -1380,374 +1451,294 @@ function NetworkSearchSettings({ client, hidden }) {
1380
1451
  className: "dsh-ui-banner dsh-ui-banner--danger",
1381
1452
  children: text.storageFailed
1382
1453
  }),
1383
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1384
- role: "tabpanel",
1385
- id: tabsId + "-providers-panel",
1386
- "aria-labelledby": tabsId + "-providers-tab",
1387
- hidden: tab !== "providers",
1388
- tabIndex: 0,
1389
- children: [backends.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1390
- className: "dsh-ui-empty",
1391
- children: text.empty
1392
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
1393
- className: "web-search-card dsh-ui-card dsh-ui-card--flat",
1394
- "data-testid": "web-search-tool",
1395
- "data-on": on ? "true" : "false",
1396
- children: [
1397
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
1398
- className: "ws-card-head",
1399
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1400
- className: "dsh-ui-title",
1401
- children: text.toolTitle
1402
- }), !canEnable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1403
- className: "dsh-ui-hint",
1404
- children: text.needBackend
1405
- })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
1406
- checked: on,
1407
- label: text.toolTitle,
1408
- disabled: busy || !on && !canEnable,
1409
- onChange: (next) => runImmediate(next, order)
1410
- })]
1411
- }),
1412
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ol", {
1413
- className: "web-search-rank",
1414
- "aria-label": text.backends,
1415
- children: backends.map((provider) => {
1416
- const participating = order.includes(provider.id);
1417
- const rank = order.indexOf(provider.id);
1418
- const dormant = !isRegisteredSearch(saved, provider.id);
1419
- const active = !dormant && saved.searchActive && selected?.id === provider.id;
1420
- const info = searchProviderInfo(provider, locale);
1421
- const owned = ownsProviderConfiguration(provider);
1422
- const access = configurationAccess(provider);
1423
- const needsKey = owned && Boolean(provider.credentialRef);
1424
- const shared = Boolean(provider.credentialShared);
1425
- const showKey = !dormant && needsKey && participating && (!shared || !provider.configured);
1426
- const canSort = participating && order.length > 1;
1427
- const readOnly = writable[provider.credentialRef ?? ""] === false;
1428
- const typedKey = (keys[provider.id] ?? "").trim();
1429
- const availability = dormant ? "dormant" : provider.configured ? "available" : "unavailable";
1430
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
1431
- className: active ? "is-active" : void 0,
1432
- "data-testid": `web-search-rank-${provider.id}`,
1433
- "data-availability": availability,
1434
- "data-in-use": active || void 0,
1435
- "data-dragging": dragSource === provider.id || void 0,
1436
- "data-drop": dropTarget === provider.id && dragSource ? order.indexOf(dragSource) < rank ? "after" : "before" : void 0,
1437
- onDragOver: (event) => {
1438
- if (busy || !canSort || !dragSource || dragSource === provider.id) return;
1439
- event.preventDefault();
1440
- event.dataTransfer.dropEffect = "move";
1441
- setDropTarget(provider.id);
1442
- },
1443
- onDragLeave: (event) => {
1444
- if (!event.currentTarget.contains(event.relatedTarget)) setDropTarget(null);
1445
- },
1446
- onDrop: (event) => {
1447
- if (busy || !canSort || !dragSource) return;
1448
- event.preventDefault();
1449
- move(dragSource, rank);
1450
- endDrag();
1451
- },
1452
- children: [
1453
- canSort ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1454
- variant: "ghost",
1455
- size: "sm",
1456
- className: "web-search-drag-handle",
1457
- ref: (element) => {
1458
- if (element) handleRefs.current.set(provider.id, element);
1459
- else handleRefs.current.delete(provider.id);
1460
- },
1461
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
1462
- width: "16",
1463
- height: "20",
1464
- viewBox: "0 0 16 20",
1465
- fill: "currentColor",
1466
- "aria-hidden": "true",
1467
- children: [
1468
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1469
- cx: "5",
1470
- cy: "5",
1471
- r: "1.5"
1472
- }),
1473
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1474
- cx: "11",
1475
- cy: "5",
1476
- r: "1.5"
1477
- }),
1478
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1479
- cx: "5",
1480
- cy: "10",
1481
- r: "1.5"
1482
- }),
1483
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1484
- cx: "11",
1485
- cy: "10",
1486
- r: "1.5"
1487
- }),
1488
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1489
- cx: "5",
1490
- cy: "15",
1491
- r: "1.5"
1492
- }),
1493
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1494
- cx: "11",
1495
- cy: "15",
1496
- r: "1.5"
1497
- })
1498
- ]
1499
- }),
1500
- draggable: !busy,
1501
- "aria-disabled": busy,
1502
- "aria-label": text.dragPrefix + providerDisplayLabel(provider, locale),
1503
- title: text.dragHint,
1504
- onDragStart: (event) => {
1505
- if (busy) {
1506
- event.preventDefault();
1507
- return;
1508
- }
1509
- event.dataTransfer.effectAllowed = "move";
1510
- event.dataTransfer.setData("application/x-dsh-search-provider", provider.id);
1511
- const row = event.currentTarget.closest("li");
1512
- if (row) event.dataTransfer.setDragImage(row, 16, 16);
1513
- setDragSource(provider.id);
1514
- setSortAnnouncement("");
1515
- },
1516
- onDragEnd: endDrag,
1517
- onKeyDown: (event) => {
1518
- if (event.key !== "ArrowUp" && event.key !== "ArrowDown") return;
1519
- event.preventDefault();
1520
- move(provider.id, rank + (event.key === "ArrowUp" ? -1 : 1), true);
1521
- }
1522
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1523
- className: "web-search-rank-index",
1524
- children: participating ? rank + 1 : "—"
1525
- }),
1526
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1527
- className: "web-search-provider",
1528
- children: [
1529
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1530
- className: "web-search-provider-heading",
1531
- children: [
1532
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
1533
- className: "dsh-ui-heading",
1534
- children: providerDisplayLabel(provider, locale)
1535
- }),
1536
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1537
- className: "ws-provider-note",
1538
- 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 })]
1539
- }),
1540
- active && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
1541
- tone: "success",
1542
- children: text.inUse
1543
- }),
1544
- provider.signupUrl && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppLink, {
1545
- url: provider.signupUrl,
1546
- label: providerDisplayLabel(provider, locale) + " " + text.signup,
1547
- children: text.signup
1548
- })
1549
- ]
1550
- }),
1551
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1552
- className: "web-search-provider-info",
1553
- 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, {
1554
- url: info.pricingUrl,
1555
- label: providerDisplayLabel(provider, locale) + " " + text.pricing,
1556
- children: provider.id === "ddg" ? text.website : text.pricing
1557
- })] })] })]
1558
- }),
1559
- access.kind === "link" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1560
- className: "dsh-ui-hint",
1561
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppLink, {
1562
- url: access.url,
1563
- label: text.openConfig,
1564
- children: text.openConfig
1565
- })
1566
- }) : access.kind === "owner" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1567
- className: "dsh-ui-hint",
1568
- children: [text.ownerSettings, access.name]
1569
- }) : access.kind === "unknown" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1570
- className: "dsh-ui-hint",
1571
- children: text.unknownOwner
1572
- }) : null,
1573
- shared && participating && owned ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", {
1574
- className: "dsh-ui-meta",
1575
- children: provider.credentialHint === "与模型设置共用 Key" ? locale === "zh" ? provider.credentialHint : "Shares a key with model settings" : provider.credentialHint ?? text.sharedKey
1576
- }) : null,
1577
- showKey ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1578
- className: "dsh-ui-field",
1579
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1580
- className: "dsh-ui-label",
1581
- children: "API Key"
1582
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
1583
- className: "ws-input",
1584
- type: "password",
1585
- autoComplete: "off",
1586
- spellCheck: false,
1587
- disabled: busy || readOnly,
1588
- value: keys[provider.id] ?? "",
1589
- placeholder: provider.configured ? text.keyConfigured : text.keyPlaceholder,
1590
- onChange: (event) => setKeys((current) => ({
1591
- ...current,
1592
- [provider.id]: event.target.value
1593
- }))
1594
- })]
1595
- }) : null,
1596
- showKey || needsKey && !shared && provider.configured ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1597
- className: "dsh-ui-actions",
1598
- children: [showKey ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1599
- variant: "outline",
1600
- size: "sm",
1601
- disabled: busy || readOnly || !typedKey,
1602
- onClick: () => void action(async () => {
1603
- await saveKeys(provider);
1604
- await load();
1605
- setNote({ key: "keySaved" });
1606
- }),
1607
- children: text.saveKey
1608
- }) : null, needsKey && !shared && provider.configured ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfirmButton, {
1609
- disabled: busy || readOnly,
1610
- label: text.deleteKey,
1611
- confirmLabel: text.confirmDelete,
1612
- onConfirm: () => void action(() => removeKey(provider))
1613
- }) : null]
1614
- }) : null
1615
- ]
1616
- }),
1617
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1618
- className: "web-search-rank-move",
1619
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
1620
- checked: participating,
1621
- label: providerDisplayLabel(provider, locale),
1622
- disabled: busy,
1623
- onChange: (next) => {
1624
- const following = next ? [...order, provider.id] : order.filter((id) => id !== provider.id);
1625
- runImmediate(nextSearchEnabled(saved.settings.searchEnabled, following, saved.providers, {}, writable), following, droppedDormantIds(saved.settings, following, saved.providers));
1626
- }
1627
- })
1628
- })
1629
- ]
1630
- }, provider.id);
1631
- })
1632
- }),
1633
- backends.some(usesBundledPricing) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1634
- className: "dsh-ui-hint",
1635
- children: pricingCheckedText(locale, PROVIDER_PRICING_CHECKED)
1636
- }) : null,
1637
- on ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1638
- className: "dsh-ui-actions",
1639
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1640
- variant: "outline",
1641
- size: "md",
1642
- disabled: busy || reordering,
1643
- onClick: () => void action(async () => {
1644
- await saveKeys();
1645
- const result = await call("test");
1646
- await load();
1647
- setNote({ sources: result.sources });
1648
- }),
1649
- children: text.test
1650
- })
1651
- }) : null
1652
- ]
1653
- }), (saved.providers.some((provider) => provider.kind === "fetch") || saved.settings.fetchProvider) && (() => {
1654
- const fetchId = saved.settings.fetchProvider;
1655
- const fetchSelected = saved.providers.find((provider) => provider.kind === "fetch" && provider.id === fetchId);
1656
- const fetchDormant = Boolean(fetchId) && !fetchSelected;
1657
- const fetchActive = Boolean(fetchSelected && saved.fetchActive && fetchSelected.configured);
1658
- const fetchAccess = fetchSelected ? configurationAccess(fetchSelected) : fetchDormant ? { kind: "unknown" } : void 0;
1659
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
1660
- className: "dsh-ui-card dsh-ui-card--flat",
1661
- "data-testid": "web-search-fetch",
1662
- "data-availability": fetchDormant ? "dormant" : fetchSelected?.configured ? "available" : "unavailable",
1663
- "data-in-use": fetchActive || void 0,
1454
+ ["search", "fetch"].map((kind) => {
1455
+ const order = pendingOrder?.kind === kind ? pendingOrder.order : providerOrder(saved.settings, kind);
1456
+ const catalog = catalogBackends(saved, kind);
1457
+ const backends = [...order.flatMap((id) => catalog.find((provider) => provider.id === id) ?? []), ...catalog.filter((provider) => !order.includes(provider.id))];
1458
+ const selected = selectedBackend(saved, kind);
1459
+ const on = kind === "search" ? saved.settings.searchEnabled : saved.settings.fetchEnabled;
1460
+ const canEnable = order.some((id) => saved.providers.some((provider) => provider.kind === kind && provider.id === id && provider.configured));
1461
+ const sortingOther = pendingOrder !== null && pendingOrder.kind !== kind;
1462
+ const locked = busy || sortingOther || Boolean(dragSource && !dragSource.startsWith(kind + ":"));
1463
+ const title = kind === "search" ? text.toolTitle : text.fetchTitle;
1464
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1465
+ role: "tabpanel",
1466
+ id: tabsId + "-" + kind + "-panel",
1467
+ "aria-labelledby": tabsId + "-" + kind + "-tab",
1468
+ hidden: tab !== kind,
1469
+ tabIndex: 0,
1470
+ children: backends.length === 0 ? kind === "search" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1471
+ className: "dsh-ui-empty",
1472
+ children: text.empty
1473
+ }) : null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
1474
+ className: "web-search-card dsh-ui-card dsh-ui-card--flat",
1475
+ "data-testid": kind === "search" ? "web-search-tool" : "web-search-fetch",
1476
+ "data-on": on ? "true" : "false",
1664
1477
  children: [
1665
1478
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
1666
1479
  className: "ws-card-head",
1667
1480
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1668
1481
  className: "dsh-ui-title",
1669
- children: text.fetchTitle
1670
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1671
- className: "ws-provider-note",
1672
- children: [
1673
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: fetchDormant ? "idle" : fetchSelected?.configured ? "done" : "warning" }),
1674
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: fetchDormant ? text.dormant : fetchSelected?.configured ? text.available : text.unavailable }),
1675
- fetchActive ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
1676
- tone: "success",
1677
- children: text.inUse
1678
- }) : null
1679
- ]
1482
+ children: title
1483
+ }), !canEnable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1484
+ className: "dsh-ui-hint",
1485
+ children: kind === "search" ? text.needBackend : text.needFetchBackend
1680
1486
  })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
1681
- checked: saved.settings.fetchEnabled,
1682
- label: text.fetchTitle,
1683
- disabled: busy || reordering,
1684
- onChange: (next) => void action(async () => {
1685
- const current = statusRef.current;
1686
- adopt(await call("update", {
1687
- settings: {
1688
- ...editable(current.settings),
1689
- fetchEnabled: next
1690
- },
1691
- expectedRevision: current.settings.revision
1692
- }));
1693
- await load();
1694
- })
1487
+ checked: on,
1488
+ label: title,
1489
+ disabled: locked || dragSource !== null || !on && !canEnable,
1490
+ onChange: (next) => runImmediate(next, order, [], kind)
1695
1491
  })]
1696
1492
  }),
1697
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1698
- className: "dsh-ui-field",
1699
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1700
- className: "dsh-ui-label",
1701
- children: text.fetchProvider
1702
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1703
- className: "dsh-ui-select",
1704
- "aria-label": text.fetchProvider,
1705
- value: fetchId,
1706
- disabled: busy || reordering,
1707
- onChange: (event) => {
1708
- const id = event.target.value;
1709
- action(async () => {
1710
- const current = statusRef.current;
1711
- adopt(await call("update", {
1712
- settings: {
1713
- ...editable(current.settings),
1714
- fetchProvider: id
1715
- },
1716
- expectedRevision: current.settings.revision
1717
- }));
1718
- await load();
1719
- });
1720
- },
1721
- children: [fetchDormant && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
1722
- value: fetchId,
1493
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ol", {
1494
+ className: "web-search-rank",
1495
+ "aria-label": kind === "search" ? text.backends : text.fetchBackends,
1496
+ children: backends.map((provider) => {
1497
+ const participating = order.includes(provider.id);
1498
+ const rank = order.indexOf(provider.id);
1499
+ const dormant = !isRegistered(saved, provider.id, kind);
1500
+ const active = !dormant && (kind === "search" ? saved.searchActive : saved.fetchActive) && selected?.id === provider.id;
1501
+ const info = kind === "search" || ownsProviderConfiguration(provider) && provider.id === "firecrawl" ? searchProviderInfo(provider, locale) : {
1502
+ 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,
1503
+ pricing: provider.pricing ?? "",
1504
+ pricingUrl: provider.pricingUrl
1505
+ };
1506
+ const owned = ownsProviderConfiguration(provider);
1507
+ const access = configurationAccess(provider);
1508
+ const needsKey = owned && Boolean(provider.credentialRef);
1509
+ const shared = Boolean(provider.credentialShared);
1510
+ const showKey = !dormant && needsKey && participating && (!shared || !provider.configured);
1511
+ const canSort = participating && order.length > 1;
1512
+ const readOnly = writable[provider.credentialRef ?? ""] === false;
1513
+ const typedKey = (keys[providerKey(provider.kind, provider.id)] ?? "").trim();
1514
+ const availability = dormant ? "dormant" : provider.configured ? "available" : "unavailable";
1515
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
1516
+ className: active ? "is-active" : void 0,
1517
+ "data-testid": `web-${kind}-rank-${provider.id}`,
1518
+ "data-availability": availability,
1519
+ "data-in-use": active || void 0,
1520
+ "data-dragging": dragSource === providerKey(kind, provider.id) || void 0,
1521
+ "data-drop": dropTarget === providerKey(kind, provider.id) && dragSource ? order.indexOf(dragSource.slice(kind.length + 1)) < rank ? "after" : "before" : void 0,
1522
+ onDragOver: (event) => {
1523
+ if (locked || !canSort || !dragSource?.startsWith(kind + ":") || dragSource === providerKey(kind, provider.id)) return;
1524
+ event.preventDefault();
1525
+ event.dataTransfer.dropEffect = "move";
1526
+ setDropTarget(providerKey(kind, provider.id));
1527
+ },
1528
+ onDragLeave: (event) => {
1529
+ if (!event.currentTarget.contains(event.relatedTarget)) setDropTarget(null);
1530
+ },
1531
+ onDrop: (event) => {
1532
+ if (locked || !canSort || !dragSource?.startsWith(kind + ":")) return;
1533
+ event.preventDefault();
1534
+ move(kind, dragSource.slice(kind.length + 1), rank);
1535
+ endDrag();
1536
+ },
1723
1537
  children: [
1724
- fetchId,
1725
- " · ",
1726
- text.dormant
1538
+ canSort ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1539
+ variant: "ghost",
1540
+ size: "sm",
1541
+ className: "web-search-drag-handle",
1542
+ ref: (element) => {
1543
+ if (element) handleRefs.current.set(providerKey(kind, provider.id), element);
1544
+ else handleRefs.current.delete(providerKey(kind, provider.id));
1545
+ },
1546
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
1547
+ width: "16",
1548
+ height: "20",
1549
+ viewBox: "0 0 16 20",
1550
+ fill: "currentColor",
1551
+ "aria-hidden": "true",
1552
+ children: [
1553
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1554
+ cx: "5",
1555
+ cy: "5",
1556
+ r: "1.5"
1557
+ }),
1558
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1559
+ cx: "11",
1560
+ cy: "5",
1561
+ r: "1.5"
1562
+ }),
1563
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1564
+ cx: "5",
1565
+ cy: "10",
1566
+ r: "1.5"
1567
+ }),
1568
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1569
+ cx: "11",
1570
+ cy: "10",
1571
+ r: "1.5"
1572
+ }),
1573
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1574
+ cx: "5",
1575
+ cy: "15",
1576
+ r: "1.5"
1577
+ }),
1578
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1579
+ cx: "11",
1580
+ cy: "15",
1581
+ r: "1.5"
1582
+ })
1583
+ ]
1584
+ }),
1585
+ draggable: !locked,
1586
+ "aria-disabled": locked,
1587
+ "aria-label": text.dragPrefix + providerDisplayLabel(provider, locale),
1588
+ title: text.dragHint,
1589
+ onDragStart: (event) => {
1590
+ if (locked || pendingRef.current && pendingRef.current.kind !== kind) {
1591
+ event.preventDefault();
1592
+ return;
1593
+ }
1594
+ event.dataTransfer.effectAllowed = "move";
1595
+ event.dataTransfer.setData(`application/x-dsh-${kind}-provider`, provider.id);
1596
+ const row = event.currentTarget.closest("li");
1597
+ if (row) event.dataTransfer.setDragImage(row, 16, 16);
1598
+ dragRef.current = providerKey(kind, provider.id);
1599
+ requestSeq.current += 1;
1600
+ setDragSource(providerKey(kind, provider.id));
1601
+ setSortAnnouncement("");
1602
+ },
1603
+ onDragEnd: endDrag,
1604
+ onKeyDown: (event) => {
1605
+ if (event.key !== "ArrowUp" && event.key !== "ArrowDown") return;
1606
+ event.preventDefault();
1607
+ move(kind, provider.id, rank + (event.key === "ArrowUp" ? -1 : 1), true);
1608
+ }
1609
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1610
+ className: "web-search-rank-index",
1611
+ children: participating ? rank + 1 : "—"
1612
+ }),
1613
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1614
+ className: "web-search-provider",
1615
+ children: [
1616
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1617
+ className: "web-search-provider-heading",
1618
+ children: [
1619
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
1620
+ className: "dsh-ui-heading",
1621
+ children: providerDisplayLabel(provider, locale)
1622
+ }),
1623
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1624
+ className: "ws-provider-note",
1625
+ 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 })]
1626
+ }),
1627
+ active && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
1628
+ tone: "success",
1629
+ children: text.inUse
1630
+ }),
1631
+ provider.signupUrl && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppLink, {
1632
+ url: provider.signupUrl,
1633
+ label: providerDisplayLabel(provider, locale) + " " + text.signup,
1634
+ children: text.signup
1635
+ })
1636
+ ]
1637
+ }),
1638
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1639
+ className: "web-search-provider-info",
1640
+ 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, {
1641
+ url: info.pricingUrl,
1642
+ label: providerDisplayLabel(provider, locale) + " " + text.pricing,
1643
+ children: provider.id === "ddg" ? text.website : text.pricing
1644
+ })] })] }) : null]
1645
+ }),
1646
+ access.kind === "link" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1647
+ className: "dsh-ui-hint",
1648
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppLink, {
1649
+ url: access.url,
1650
+ label: text.openConfig,
1651
+ children: text.openConfig
1652
+ })
1653
+ }) : access.kind === "owner" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1654
+ className: "dsh-ui-hint",
1655
+ children: [text.ownerSettings, access.name]
1656
+ }) : access.kind === "unknown" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1657
+ className: "dsh-ui-hint",
1658
+ children: text.unknownOwner
1659
+ }) : null,
1660
+ shared && participating && owned ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", {
1661
+ className: "dsh-ui-meta",
1662
+ 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
1663
+ }) : null,
1664
+ showKey ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1665
+ className: "dsh-ui-field",
1666
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1667
+ className: "dsh-ui-label",
1668
+ children: "API Key"
1669
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
1670
+ className: "ws-input",
1671
+ type: "password",
1672
+ autoComplete: "off",
1673
+ spellCheck: false,
1674
+ disabled: busy || reordering || readOnly,
1675
+ value: keys[providerKey(provider.kind, provider.id)] ?? "",
1676
+ placeholder: provider.configured ? text.keyConfigured : text.keyPlaceholder,
1677
+ onChange: (event) => setKeys((current) => ({
1678
+ ...current,
1679
+ [providerKey(provider.kind, provider.id)]: event.target.value
1680
+ }))
1681
+ })]
1682
+ }) : null,
1683
+ showKey || needsKey && !shared && provider.configured ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1684
+ className: "dsh-ui-actions",
1685
+ children: [showKey ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1686
+ variant: "outline",
1687
+ size: "sm",
1688
+ disabled: busy || reordering || readOnly || !typedKey,
1689
+ onClick: () => void action(async () => {
1690
+ await saveKeys(provider);
1691
+ await load();
1692
+ setNote({ key: "keySaved" });
1693
+ }),
1694
+ children: text.saveKey
1695
+ }) : null, needsKey && !shared && provider.configured ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfirmButton, {
1696
+ disabled: busy || reordering || readOnly,
1697
+ label: text.deleteKey,
1698
+ confirmLabel: text.confirmDelete,
1699
+ onConfirm: () => void action(() => removeKey(provider))
1700
+ }) : null]
1701
+ }) : null
1702
+ ]
1703
+ }),
1704
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1705
+ className: "web-search-rank-move",
1706
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
1707
+ checked: participating,
1708
+ label: providerDisplayLabel(provider, locale),
1709
+ disabled: locked || dragSource !== null,
1710
+ onChange: (next) => {
1711
+ const following = next ? [...order, provider.id] : order.filter((id) => id !== provider.id);
1712
+ 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);
1713
+ }
1714
+ })
1715
+ })
1727
1716
  ]
1728
- }), saved.providers.filter((provider) => provider.kind === "fetch").map((provider) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
1729
- value: provider.id,
1730
- children: [providerDisplayLabel(provider, locale), provider.configured ? "" : " · " + text.unavailable]
1731
- }, provider.id))]
1732
- })]
1717
+ }, provider.id);
1718
+ })
1733
1719
  }),
1734
- fetchAccess?.kind === "link" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1720
+ kind === "search" && backends.some(usesBundledPricing) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1735
1721
  className: "dsh-ui-hint",
1736
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppLink, {
1737
- url: fetchAccess.url,
1738
- label: text.openConfig,
1739
- children: text.openConfig
1722
+ children: pricingCheckedText(locale, PROVIDER_PRICING_CHECKED)
1723
+ }) : null,
1724
+ on && kind === "search" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1725
+ className: "dsh-ui-actions",
1726
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1727
+ variant: "outline",
1728
+ size: "md",
1729
+ disabled: busy || reordering,
1730
+ onClick: () => void action(async () => {
1731
+ await saveKeysForKind("search");
1732
+ const result = await call("test");
1733
+ await load();
1734
+ setNote({ sources: result.sources });
1735
+ }),
1736
+ children: text.test
1740
1737
  })
1741
- }) : fetchAccess?.kind === "owner" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1742
- className: "dsh-ui-hint",
1743
- children: [text.ownerSettings, fetchAccess.name]
1744
- }) : fetchAccess?.kind === "unknown" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1745
- className: "dsh-ui-hint",
1746
- children: text.unknownOwner
1747
1738
  }) : null
1748
1739
  ]
1749
- });
1750
- })()]
1740
+ })
1741
+ }, kind);
1751
1742
  }),
1752
1743
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1753
1744
  role: "tabpanel",