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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -8,6 +8,47 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
8
8
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
9
9
  let react = require("react");
10
10
  let react_jsx_runtime = require("react/jsx-runtime");
11
+ //#region src/client-error.ts
12
+ /** Local adapter diagnostics only; third-party descriptions remain verbatim. */
13
+ const localErrors = {
14
+ "网络搜索设置格式无效。": "Invalid web search settings format.",
15
+ "未知操作。": "Unknown operation.",
16
+ "网络设置操作失败,请刷新后重试。": "The network settings operation failed. Refresh and try again.",
17
+ "查询须为 1 至 4000 字符。": "The query must contain 1 to 4,000 characters.",
18
+ "网络搜索插件已停止。": "The web search plugin has stopped.",
19
+ "请在设置 → 网络搜索中配置凭据并启用。": "Configure credentials and enable web search in Settings → Web search.",
20
+ "网络访问设置已改变,请重新发起请求。": "Network access settings changed. Start the request again.",
21
+ "网络请求已取消。": "The network request was cancelled.",
22
+ "网络请求已取消或超时。": "The network request was cancelled or timed out.",
23
+ "设置已被其他页面修改,请刷新后重试。": "Settings changed in another page. Refresh and try again.",
24
+ "供应商不支持自定义地址。": "The provider does not support a custom address.",
25
+ "供应商地址须为无凭据、无查询参数的 HTTPS 地址。": "The provider address must use HTTPS without credentials or query parameters.",
26
+ "需选择已安装的供应商并填写有效凭据。": "Choose an installed provider and enter valid credentials.",
27
+ "保存失败,本次运行已暂停网络访问以防意外联网。": "Saving failed. Network access is paused for this run to prevent unintended requests.",
28
+ "供应商未提供错误描述。": "The provider did not supply an error description.",
29
+ "供应商请求失败。": "Provider request failed.",
30
+ "搜索凭据已删除或不可用,请重新配置。": "Search credentials were deleted or are unavailable. Configure them again.",
31
+ "供应商配置不可用。": "Provider configuration is unavailable.",
32
+ "查询无效。": "Invalid query.",
33
+ "结果数须为 1 至 20。": "The result count must be from 1 to 20.",
34
+ "搜索已取消。": "Search cancelled.",
35
+ "Tavily 搜索已取消。": "Tavily search cancelled.",
36
+ "响应格式无效。": "Invalid response shape",
37
+ "响应缺失。": "Missing response",
38
+ "响应过大。": "Response too large",
39
+ "地址无效。": "Invalid endpoint"
40
+ };
41
+ function localErrorText(message, locale) {
42
+ const prefix = message.match(/^(\[(?:search|fetch):[^\]]+\] )(.*)$/s);
43
+ if (prefix) return prefix[1] + localErrorText(prefix[2], locale);
44
+ if (locale === "zh") return Object.entries(localErrors).find(([, english]) => english === message)?.[0] ?? message;
45
+ if (localErrors[message]) return localErrors[message];
46
+ const http = message.match(/^供应商请求失败(HTTP (\d{3}))。$/);
47
+ if (http) return `Provider request failed (HTTP ${http[1]}).`;
48
+ const config = message.match(/^(brave|bocha|firecrawl|serper|Tavily) 配置不可用。$/);
49
+ if (config) return `${config[1]} configuration is unavailable.`;
50
+ return message;
51
+ }
11
52
  Object.values({
12
53
  /** Surfaces, from the page up to the topmost nested card. */
13
54
  surface: [
@@ -305,7 +346,7 @@ ${scope} {
305
346
  border: 0.5px solid var(--dsw-alias-border-l3);
306
347
  border-radius: var(--dsw-radius-md);
307
348
  background: var(--dsw-alias-bg-layer-1);
308
- font-family: var(--ds-font-family-code, ui-monospace, 'Cascadia Mono', Consolas, monospace);
349
+ font-family: var(--ds-font-family-code);
309
350
  font-size: 12px;
310
351
  line-height: 20px;
311
352
  color: var(--dsw-alias-label-secondary);
@@ -468,6 +509,58 @@ const providerInfo = {
468
509
  pricingUrl: "https://docs.tavily.com/documentation/api-credits"
469
510
  }
470
511
  };
512
+ const englishInfo = {
513
+ ddg: {
514
+ description: "Search public DuckDuckGo pages without registration or an API key.",
515
+ pricing: "Free to use; no search API fees."
516
+ },
517
+ bocha: {
518
+ description: "Search Chinese web content, returning titles, links and summaries.",
519
+ pricing: "Charged per request: listed price ¥0.036/request; trial credits depend on official offers."
520
+ },
521
+ brave: {
522
+ description: "Search web pages, news and more with Brave’s independent index.",
523
+ pricing: "$5 in monthly credits (about 1,000 requests); then $5/1,000 requests. A payment card is required to activate."
524
+ },
525
+ "deepseek-official": {
526
+ description: "Call built-in search through a DeepSeek model using the model API key.",
527
+ pricing: "Default Flash token pricing per million: input ¥1–2 (cache hits ¥0.02–0.04), output ¥4–8, varying by peak/off-peak hours. Platform terms govern promotional balance and tool fees."
528
+ },
529
+ exa: {
530
+ description: "Web search for AI with relevant pages and content snippets.",
531
+ pricing: "$20 on signup, then $10 monthly credits; $7/1,000 requests (up to 10 results each), plus $1/1,000 additional results beyond 10."
532
+ },
533
+ firecrawl: {
534
+ description: "Search the web and extract page content.",
535
+ pricing: "1,000 free credits/month; 2 credits per 10 search results. Hobby includes 5,000 credits/month at $16/month billed annually; extra credits cost $5/1,000."
536
+ },
537
+ serper: {
538
+ description: "Get Google web search results.",
539
+ pricing: "2,500 one-time signup requests; $50/50,000 requests ($1/1,000). Paid credits expire after 6 months."
540
+ },
541
+ tavily: {
542
+ description: "Web search for AI, currently using basic search.",
543
+ pricing: "1,000 free credits/month; basic search uses 1 credit/request. Pay as you go: $0.008/credit ($8/1,000 requests)."
544
+ }
545
+ };
546
+ const bundledDescriptions = {
547
+ ddg: ["无需注册。默认后端。", "No registration required. Default backend."],
548
+ "deepseek-official": ["使用已配置的 DeepSeek API Key,可能产生额外搜索费用。", "Uses the configured DeepSeek API key; additional search fees may apply."],
549
+ exa: ["独立 Search API。", "Independent Search API."],
550
+ brave: ["Brave Search API。", "Brave Search API."],
551
+ bocha: ["中文网页搜索 API。", "Chinese web search API."],
552
+ serper: ["Google 结果的 Search API。", "Search API for Google results."],
553
+ firecrawl: ["网页搜索与提取 API。", "Web search and extraction API."],
554
+ tavily: ["Search API;固定 basic 检索,不自动升级搜索深度。", "Search API using basic search; search depth is never upgraded automatically."]
555
+ };
556
+ /** Translate only manager-owned labels; externally supplied metadata stays verbatim. */
557
+ function providerDisplayLabel(provider, locale) {
558
+ if (!bundledCatalogId(provider)) return provider.label;
559
+ if (provider.label === "DeepSeek 搜索") return locale === "zh" ? provider.label : "DeepSeek search";
560
+ if (provider.label === "博查") return locale === "zh" ? provider.label : "Bocha";
561
+ if (provider.label === "网页读取") return locale === "zh" ? provider.label : "Read web pages";
562
+ return provider.label;
563
+ }
471
564
  function bundledCatalogId(provider) {
472
565
  if (!(typeof provider.configurationOwned === "boolean" ? provider.configurationOwned : !provider.configurationOwner || provider.configurationOwner === "@klarkxy/dsh-web-search-manager")) return;
473
566
  return provider.id === "deepseek-managed" ? "deepseek-official" : provider.id;
@@ -482,8 +575,13 @@ function usesBundledPricing(provider) {
482
575
  }
483
576
  function searchProviderInfo(provider, locale = "zh") {
484
577
  const catalogId = bundledCatalogId(provider);
485
- const known = catalogId && Object.hasOwn(providerInfo, catalogId) ? providerInfo[catalogId] : void 0;
486
- const description = provider.description.trim() ? provider.description : known?.description ?? provider.description;
578
+ const base = catalogId && Object.hasOwn(providerInfo, catalogId) ? providerInfo[catalogId] : void 0;
579
+ const known = base && locale === "en" ? {
580
+ ...base,
581
+ ...englishInfo[catalogId]
582
+ } : base;
583
+ const ownedCopy = catalogId ? bundledDescriptions[catalogId] : void 0;
584
+ const description = ownedCopy && provider.description === ownedCopy[0] ? ownedCopy[locale === "zh" ? 0 : 1] : base && provider.description === base.description ? known.description : provider.description.trim() ? provider.description : known?.description ?? provider.description;
487
585
  if (provider.pricing?.trim()) return {
488
586
  description,
489
587
  pricing: provider.pricing,
@@ -789,6 +887,26 @@ const copy = {
789
887
  unknownOwner: "Configure this service in its plugin"
790
888
  }
791
889
  };
890
+ var UiError = class extends Error {
891
+ key;
892
+ constructor(key) {
893
+ super(key);
894
+ this.key = key;
895
+ }
896
+ };
897
+ function uiMessage(message, locale, providers = []) {
898
+ if (typeof message === "string") return localErrorText(message, locale);
899
+ if ("key" in message) return copy[locale][message.key];
900
+ if ("sources" in message) return testOkText(locale, message.sources);
901
+ if ("moved" in message) {
902
+ const provider = providers.find((item) => item.id === message.moved);
903
+ return movedText(locale, provider ? providerDisplayLabel(provider, locale) : message.moved, message.position);
904
+ }
905
+ return message.invalid.map((key) => {
906
+ const [, min, max] = LIMITS.find((item) => item[0] === key);
907
+ return limitInvalidText(locale, copy[locale][key], min, max);
908
+ }).join(" ");
909
+ }
792
910
  function movedText(locale, label, position) {
793
911
  return locale === "zh" ? `${label} 已移至第 ${position} 位` : `${label} moved to position ${position}`;
794
912
  }
@@ -974,7 +1092,7 @@ function NetworkSearchSettings({ client, hidden }) {
974
1092
  return next;
975
1093
  } catch (cause) {
976
1094
  if (!mountedRef.current || mine !== requestSeq.current) return;
977
- setLoadError(cause instanceof Error && cause.message ? cause.message : text.failed);
1095
+ setLoadError(cause instanceof Error && cause.message ? cause.message : { key: "failed" });
978
1096
  if (source === "poll") return;
979
1097
  throw cause;
980
1098
  } finally {
@@ -985,7 +1103,7 @@ function NetworkSearchSettings({ client, hidden }) {
985
1103
  (0, react.useEffect)(() => {
986
1104
  mountedRef.current = true;
987
1105
  readGate.reset();
988
- load().catch((cause) => setLoadError(cause instanceof Error && cause.message ? cause.message : text.failed));
1106
+ load().catch((cause) => setLoadError(cause instanceof Error && cause.message ? cause.message : { key: "failed" }));
989
1107
  const bind = () => bindStatusRefresh({
990
1108
  isHidden: () => isPageHidden(hiddenRef.current),
991
1109
  tick: async () => {
@@ -1040,7 +1158,7 @@ function NetworkSearchSettings({ client, hidden }) {
1040
1158
  try {
1041
1159
  await run();
1042
1160
  } catch (cause) {
1043
- setError(cause instanceof Error && cause.message ? cause.message : text.failed);
1161
+ setError(cause instanceof UiError ? { key: cause.key } : cause instanceof Error && cause.message ? cause.message : { key: "failed" });
1044
1162
  await load().catch(() => {});
1045
1163
  } finally {
1046
1164
  busyRef.current = false;
@@ -1073,7 +1191,7 @@ function NetworkSearchSettings({ client, hidden }) {
1073
1191
  const snapshot = keys[provider.id] ?? "";
1074
1192
  const typed = snapshot.trim();
1075
1193
  if (!ownsProviderConfiguration(provider) || !provider.credentialRef || !typed) continue;
1076
- if (writable[provider.credentialRef] === false) throw new Error(text.readOnly);
1194
+ if (writable[provider.credentialRef] === false) throw new UiError("readOnly");
1077
1195
  unwrap(await client.remote.credentials.set(provider.credentialRef, typed));
1078
1196
  setKeys((current) => current[provider.id] === snapshot ? {
1079
1197
  ...current,
@@ -1089,14 +1207,14 @@ function NetworkSearchSettings({ client, hidden }) {
1089
1207
  [provider.id]: ""
1090
1208
  }));
1091
1209
  await load();
1092
- setNote(text.keyDeleted);
1210
+ setNote({ key: "keyDeleted" });
1093
1211
  }
1094
1212
  if (!status || !limitDraft) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1095
1213
  className: "web-search-settings ws-root dsh-ui-panel",
1096
1214
  children: [loadError ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1097
1215
  role: "alert",
1098
1216
  className: "dsh-ui-error",
1099
- children: [text.loadFailed, loadError]
1217
+ children: [text.loadFailed, uiMessage(loadError, locale, status?.providers)]
1100
1218
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1101
1219
  role: "status",
1102
1220
  className: "dsh-ui-hint",
@@ -1135,7 +1253,10 @@ function NetworkSearchSettings({ client, hidden }) {
1135
1253
  pendingRef.current = next;
1136
1254
  setPendingOrder(next);
1137
1255
  if (keyboard) refocusRef.current = id;
1138
- setSortAnnouncement(movedText(locale, backends.find((provider) => provider.id === id)?.label ?? id, nextIndex + 1));
1256
+ setSortAnnouncement({
1257
+ moved: id,
1258
+ position: nextIndex + 1
1259
+ });
1139
1260
  clearTimeout(timerRef.current);
1140
1261
  timerRef.current = setTimeout(() => {
1141
1262
  timerRef.current = void 0;
@@ -1156,13 +1277,13 @@ function NetworkSearchSettings({ client, hidden }) {
1156
1277
  const value = parseLimit(limitDraft[key], min, max);
1157
1278
  if (value === void 0) {
1158
1279
  nextInvalid[key] = true;
1159
- messages.push(limitInvalidText(locale, text[key], min, max));
1280
+ messages.push(key);
1160
1281
  } else parsed[key] = value;
1161
1282
  }
1162
1283
  setInvalid(nextInvalid);
1163
1284
  if (messages.length) {
1164
1285
  setNote("");
1165
- setError(messages.join(" "));
1286
+ setError({ invalid: messages });
1166
1287
  return;
1167
1288
  }
1168
1289
  action(async () => {
@@ -1176,7 +1297,7 @@ function NetworkSearchSettings({ client, hidden }) {
1176
1297
  expectedRevision: current.settings.revision
1177
1298
  }), true);
1178
1299
  await load();
1179
- setNote(text.saved);
1300
+ setNote({ key: "saved" });
1180
1301
  });
1181
1302
  }
1182
1303
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
@@ -1203,19 +1324,19 @@ function NetworkSearchSettings({ client, hidden }) {
1203
1324
  className: "ws-sr-only",
1204
1325
  role: "status",
1205
1326
  "aria-live": "polite",
1206
- children: sortAnnouncement
1327
+ children: uiMessage(sortAnnouncement, locale, saved.providers)
1207
1328
  }),
1208
1329
  error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1209
1330
  role: "alert",
1210
1331
  className: "dsh-ui-error",
1211
- children: error
1332
+ children: uiMessage(error, locale, saved.providers)
1212
1333
  }),
1213
1334
  loadError ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1214
1335
  className: "dsh-ui-stack",
1215
1336
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1216
1337
  role: "alert",
1217
1338
  className: "dsh-ui-error",
1218
- children: [text.loadFailed, loadError]
1339
+ children: [text.loadFailed, uiMessage(loadError, locale, status?.providers)]
1219
1340
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1220
1341
  variant: "outline",
1221
1342
  size: "sm",
@@ -1229,7 +1350,7 @@ function NetworkSearchSettings({ client, hidden }) {
1229
1350
  note && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1230
1351
  role: "status",
1231
1352
  className: "dsh-ui-notice",
1232
- children: note
1353
+ children: uiMessage(note, locale, saved.providers)
1233
1354
  }),
1234
1355
  saved.storageFailed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1235
1356
  role: "alert",
@@ -1355,7 +1476,7 @@ function NetworkSearchSettings({ client, hidden }) {
1355
1476
  }),
1356
1477
  draggable: !busy,
1357
1478
  "aria-disabled": busy,
1358
- "aria-label": text.dragPrefix + provider.label,
1479
+ "aria-label": text.dragPrefix + providerDisplayLabel(provider, locale),
1359
1480
  title: text.dragHint,
1360
1481
  onDragStart: (event) => {
1361
1482
  if (busy) {
@@ -1387,7 +1508,7 @@ function NetworkSearchSettings({ client, hidden }) {
1387
1508
  children: [
1388
1509
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
1389
1510
  className: "dsh-ui-heading",
1390
- children: provider.label
1511
+ children: providerDisplayLabel(provider, locale)
1391
1512
  }),
1392
1513
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1393
1514
  className: "ws-provider-note",
@@ -1399,7 +1520,7 @@ function NetworkSearchSettings({ client, hidden }) {
1399
1520
  }),
1400
1521
  provider.signupUrl && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppLink, {
1401
1522
  url: provider.signupUrl,
1402
- label: provider.label + " " + text.signup,
1523
+ label: providerDisplayLabel(provider, locale) + " " + text.signup,
1403
1524
  children: text.signup
1404
1525
  })
1405
1526
  ]
@@ -1408,7 +1529,7 @@ function NetworkSearchSettings({ client, hidden }) {
1408
1529
  className: "web-search-provider-info",
1409
1530
  children: [info.description ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: info.description }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [info.pricing, info.pricingUrl && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [" ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppLink, {
1410
1531
  url: info.pricingUrl,
1411
- label: provider.label + " " + text.pricing,
1532
+ label: providerDisplayLabel(provider, locale) + " " + text.pricing,
1412
1533
  children: provider.id === "ddg" ? text.website : text.pricing
1413
1534
  })] })] })]
1414
1535
  }),
@@ -1428,7 +1549,7 @@ function NetworkSearchSettings({ client, hidden }) {
1428
1549
  }) : null,
1429
1550
  shared && participating && owned ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", {
1430
1551
  className: "dsh-ui-meta",
1431
- children: provider.credentialHint ?? text.sharedKey
1552
+ children: provider.credentialHint === "与模型设置共用 Key" ? locale === "zh" ? provider.credentialHint : "Shares a key with model settings" : provider.credentialHint ?? text.sharedKey
1432
1553
  }) : null,
1433
1554
  showKey ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1434
1555
  className: "dsh-ui-field",
@@ -1458,7 +1579,7 @@ function NetworkSearchSettings({ client, hidden }) {
1458
1579
  onClick: () => void action(async () => {
1459
1580
  await saveKeys(provider);
1460
1581
  await load();
1461
- setNote(text.keySaved);
1582
+ setNote({ key: "keySaved" });
1462
1583
  }),
1463
1584
  children: text.saveKey
1464
1585
  }) : null, needsKey && !shared && provider.configured ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfirmButton, {
@@ -1474,7 +1595,7 @@ function NetworkSearchSettings({ client, hidden }) {
1474
1595
  className: "web-search-rank-move",
1475
1596
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
1476
1597
  checked: participating,
1477
- label: provider.label,
1598
+ label: providerDisplayLabel(provider, locale),
1478
1599
  disabled: busy,
1479
1600
  onChange: (next) => {
1480
1601
  const following = next ? [...order, provider.id] : order.filter((id) => id !== provider.id);
@@ -1500,7 +1621,7 @@ function NetworkSearchSettings({ client, hidden }) {
1500
1621
  await saveKeys();
1501
1622
  const result = await call("test");
1502
1623
  await load();
1503
- setNote(testOkText(locale, result.sources));
1624
+ setNote({ sources: result.sources });
1504
1625
  }),
1505
1626
  children: text.test
1506
1627
  })
@@ -1583,7 +1704,7 @@ function NetworkSearchSettings({ client, hidden }) {
1583
1704
  ]
1584
1705
  }), saved.providers.filter((provider) => provider.kind === "fetch").map((provider) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
1585
1706
  value: provider.id,
1586
- children: [provider.label, provider.configured ? "" : " · " + text.unavailable]
1707
+ children: [providerDisplayLabel(provider, locale), provider.configured ? "" : " · " + text.unavailable]
1587
1708
  }, provider.id))]
1588
1709
  })]
1589
1710
  }),
@@ -1,4 +1,5 @@
1
1
  import { WebFetchProvider, WebSearchProvider } from "@deepseek-ai/dsh-web";
2
+ import { DshWebManagementDescriptor } from "@klarkxy/dsh-plugin-kit/contracts";
2
3
  //#region src/contracts.d.ts
3
4
  declare const WEB_SEARCH_RPC_CHANNEL = "/web-search-manager";
4
5
  type RpcResult<T = unknown> = {
@@ -13,31 +14,8 @@ type RpcResult<T = unknown> = {
13
14
  };
14
15
  };
15
16
  type ProviderKind = 'search' | 'fetch';
16
- /** Management metadata only. Execution and source vocabulary belong to ctx.web. */
17
- interface ProviderDescriptor {
18
- readonly id: string;
19
- readonly label: string;
20
- readonly description: string;
21
- readonly defaultBaseURL?: string;
22
- readonly credentialRef?: string;
23
- /** True when the key is shared with another surface. The page must not delete it. */
24
- readonly credentialShared?: boolean;
25
- /** Optional notice naming the settings surface that owns a shared credential. */
26
- readonly credentialHint?: string;
27
- readonly billing: 'request' | 'model-and-tools' | 'none' | 'unknown';
28
- /** Optional provider-supplied billing summary. */
29
- readonly pricing?: string;
30
- /** Public HTTPS page explaining provider charges. */
31
- readonly pricingUrl?: string;
32
- /** Public HTTPS page where the user can create an API key. */
33
- readonly signupUrl?: string;
34
- /** Plugin that owns configuration. Native providers keep their settings there. */
35
- readonly configurationOwner?: string;
36
- /** Explicit settings link supplied by the provider, never inferred from its id. */
37
- readonly configurationUrl?: string;
38
- /** Fixed legacy route migrated by a bundle-owned adapter; not used by native plugins. */
39
- readonly legacyId?: string;
40
- }
17
+ /** Management metadata only. Field contract lives in plugin-kit; execution stays with ctx.web. */
18
+ interface ProviderDescriptor extends DshWebManagementDescriptor {}
41
19
  declare const WEB_MANAGER_OWNER = "@klarkxy/dsh-web-search-manager";
42
20
  /** Optional display metadata on a provider registered directly with ctx.web. */
43
21
  type DiscoverableSearchProvider = WebSearchProvider & {
@@ -1 +1 @@
1
- {"version":3,"file":"contracts.js","names":[],"sources":["../src/contracts.ts"],"sourcesContent":["import type { WebFetchProvider, WebSearchProvider } from '@deepseek-ai/dsh-web'\n\nexport const WEB_SEARCH_RPC_CHANNEL = '/web-search-manager'\nexport type RpcResult<T = unknown> = { ok: true; value: T } | {\n ok: false; error: { code: string; message: string; details: Record<string, unknown> }\n}\nexport type ProviderKind = 'search' | 'fetch'\n/** Management metadata only. Execution and source vocabulary belong to ctx.web. */\nexport interface ProviderDescriptor {\n readonly id: string\n readonly label: string\n readonly description: string\n readonly defaultBaseURL?: string\n readonly credentialRef?: string\n /** True when the key is shared with another surface. The page must not delete it. */\n readonly credentialShared?: boolean\n /** Optional notice naming the settings surface that owns a shared credential. */\n readonly credentialHint?: string\n readonly billing: 'request' | 'model-and-tools' | 'none' | 'unknown'\n /** Optional provider-supplied billing summary. */\n readonly pricing?: string\n /** Public HTTPS page explaining provider charges. */\n readonly pricingUrl?: string\n /** Public HTTPS page where the user can create an API key. */\n readonly signupUrl?: string\n /** Plugin that owns configuration. Native providers keep their settings there. */\n readonly configurationOwner?: string\n /** Explicit settings link supplied by the provider, never inferred from its id. */\n readonly configurationUrl?: string\n /** Fixed legacy route migrated by a bundle-owned adapter; not used by native plugins. */\n readonly legacyId?: string\n}\nexport const WEB_MANAGER_OWNER = '@klarkxy/dsh-web-search-manager'\n/** Optional display metadata on a provider registered directly with ctx.web. */\nexport type DiscoverableSearchProvider = WebSearchProvider & { readonly dshWebManagement?: ProviderDescriptor }\nexport type DiscoverableFetchProvider = WebFetchProvider & { readonly dshWebManagement?: ProviderDescriptor }\nexport interface ProviderOptions {\n readonly apiKey?: string\n readonly baseURL?: string\n readonly timeoutMs: number\n readonly maxFetchChars: number\n}\nexport type SearchProviderFactory = (options: ProviderOptions) => WebSearchProvider\nexport type FetchProviderFactory = (options: ProviderOptions) => WebFetchProvider\n/** Never store credential values here. Keys are resolved by reference per call. */\nexport interface WebSettings {\n revision: number\n searchEnabled: boolean\n fetchEnabled: boolean\n searchProvider: string\n fetchProvider: string\n searchOrder: string[]\n maxResults: number\n maxQueries: number\n timeoutMs: number\n maxFetchChars: number\n endpoints: Record<string, string>\n}\nexport const DEFAULT_SEARCH_ORDER = ['ddg'] as const\n\nexport const defaultSettings = (): WebSettings => ({\n revision: 0, searchEnabled: true, fetchEnabled: true,\n searchProvider: 'ddg', fetchProvider: 'http', searchOrder: [...DEFAULT_SEARCH_ORDER],\n maxResults: 5, maxQueries: 1,\n timeoutMs: 30_000, maxFetchChars: 100_000, endpoints: {},\n})\n\n/** Keyed backends first, keyless last — same idea as OpenClaw auto-detect. */\nexport function defaultSearchOrder(ids: readonly string[], keyed: (id: string) => boolean): string[] {\n return [...ids.filter(keyed), ...ids.filter(id => !keyed(id))]\n}\n\nexport function migrateSearchOrder(settings: { searchOrder?: string[]; searchProvider?: string }): string[] {\n return settings.searchOrder ?? (settings.searchProvider ? [settings.searchProvider] : [...DEFAULT_SEARCH_ORDER])\n}\n\nexport function resolveSearchOrder(\n knownIds: readonly string[],\n settings: Pick<WebSettings, 'searchOrder' | 'searchProvider'>,\n _keyed: (id: string) => boolean,\n): string[] {\n return [...new Set(settings.searchOrder.filter(id => knownIds.includes(id)))]\n}\n\nexport function pickActiveSearch(order: readonly string[], configured: (id: string) => boolean): string {\n return order.find(configured) ?? ''\n}\nexport interface ProviderView extends ProviderDescriptor {\n kind: ProviderKind\n configured: boolean\n /** True only for configuration explicitly owned by this manager bundle. */\n configurationOwned?: boolean\n baseURL?: string\n calls: number\n failures: number\n}\nexport interface WebStatus {\n settings: WebSettings\n providers: ProviderView[]\n searchActive: boolean\n fetchActive: boolean\n storageFailed: boolean\n}\nexport function providerKey(kind: ProviderKind, id: string): string { return `${kind}:${id}` }\nexport function safeConfigurationUrl(value: unknown): string | undefined {\n if (typeof value !== 'string' || !value || /[\\u0000-\\u0020\\\\]/.test(value)) return\n try {\n const url = new URL(value, 'https://dsh.invalid')\n if (url.username || url.password) return\n if ((value.startsWith('/') && !value.startsWith('//')) || value.startsWith('#')) {\n return url.origin === 'https://dsh.invalid' ? value : undefined\n }\n return /^https:\\/\\//i.test(value) && url.protocol === 'https:' ? value : undefined\n } catch { return }\n}\nexport function validateBaseURL(value: string): string {\n const url = new URL(value)\n if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash) {\n throw new Error('供应商地址必须是 HTTPS,且不能包含用户名、密码、查询参数或片段。')\n }\n return url.href.replace(/\\/+$/, '')\n}\n"],"mappings":";AAEA,MAAa,yBAAyB;AA8BtC,MAAa,oBAAoB;AA0BjC,MAAa,uBAAuB,CAAC,KAAK;AAE1C,MAAa,yBAAsC;CACjD,UAAU;CAAG,eAAe;CAAM,cAAc;CAChD,gBAAgB;CAAO,eAAe;CAAQ,aAAa,CAAC,GAAG,oBAAoB;CACnF,YAAY;CAAG,YAAY;CAC3B,WAAW;CAAQ,eAAe;CAAS,WAAW,CAAC;AACzD;;AAGA,SAAgB,mBAAmB,KAAwB,OAA0C;CACnG,OAAO,CAAC,GAAG,IAAI,OAAO,KAAK,GAAG,GAAG,IAAI,QAAO,OAAM,CAAC,MAAM,EAAE,CAAC,CAAC;AAC/D;AAEA,SAAgB,mBAAmB,UAAyE;CAC1G,OAAO,SAAS,gBAAgB,SAAS,iBAAiB,CAAC,SAAS,cAAc,IAAI,CAAC,GAAG,oBAAoB;AAChH;AAEA,SAAgB,mBACd,UACA,UACA,QACU;CACV,OAAO,CAAC,GAAG,IAAI,IAAI,SAAS,YAAY,QAAO,OAAM,SAAS,SAAS,EAAE,CAAC,CAAC,CAAC;AAC9E;AAEA,SAAgB,iBAAiB,OAA0B,YAA6C;CACtG,OAAO,MAAM,KAAK,UAAU,KAAK;AACnC;AAiBA,SAAgB,YAAY,MAAoB,IAAoB;CAAE,OAAO,GAAG,KAAK,GAAG;AAAK;AAC7F,SAAgB,qBAAqB,OAAoC;CACvE,IAAI,OAAO,UAAU,YAAY,CAAC,SAAS,oBAAoB,KAAK,KAAK,GAAG;CAC5E,IAAI;EACF,MAAM,MAAM,IAAI,IAAI,OAAO,qBAAqB;EAChD,IAAI,IAAI,YAAY,IAAI,UAAU;EAClC,IAAK,MAAM,WAAW,GAAG,KAAK,CAAC,MAAM,WAAW,IAAI,KAAM,MAAM,WAAW,GAAG,GAC5E,OAAO,IAAI,WAAW,wBAAwB,QAAQ,KAAA;EAExD,OAAO,eAAe,KAAK,KAAK,KAAK,IAAI,aAAa,WAAW,QAAQ,KAAA;CAC3E,QAAQ;EAAE;CAAO;AACnB;AACA,SAAgB,gBAAgB,OAAuB;CACrD,MAAM,MAAM,IAAI,IAAI,KAAK;CACzB,IAAI,IAAI,aAAa,YAAY,IAAI,YAAY,IAAI,YAAY,IAAI,UAAU,IAAI,MACjF,MAAM,IAAI,MAAM,qCAAqC;CAEvD,OAAO,IAAI,KAAK,QAAQ,QAAQ,EAAE;AACpC"}
1
+ {"version":3,"file":"contracts.js","names":[],"sources":["../src/contracts.ts"],"sourcesContent":["import type { WebFetchProvider, WebSearchProvider } from '@deepseek-ai/dsh-web'\nimport type { DshWebManagementDescriptor } from '@klarkxy/dsh-plugin-kit/contracts'\n\nexport const WEB_SEARCH_RPC_CHANNEL = '/web-search-manager'\nexport type RpcResult<T = unknown> = { ok: true; value: T } | {\n ok: false; error: { code: string; message: string; details: Record<string, unknown> }\n}\nexport type ProviderKind = 'search' | 'fetch'\n/** Management metadata only. Field contract lives in plugin-kit; execution stays with ctx.web. */\nexport interface ProviderDescriptor extends DshWebManagementDescriptor {}\nexport const WEB_MANAGER_OWNER = '@klarkxy/dsh-web-search-manager'\n/** Optional display metadata on a provider registered directly with ctx.web. */\nexport type DiscoverableSearchProvider = WebSearchProvider & { readonly dshWebManagement?: ProviderDescriptor }\nexport type DiscoverableFetchProvider = WebFetchProvider & { readonly dshWebManagement?: ProviderDescriptor }\nexport interface ProviderOptions {\n readonly apiKey?: string\n readonly baseURL?: string\n readonly timeoutMs: number\n readonly maxFetchChars: number\n}\nexport type SearchProviderFactory = (options: ProviderOptions) => WebSearchProvider\nexport type FetchProviderFactory = (options: ProviderOptions) => WebFetchProvider\n/** Never store credential values here. Keys are resolved by reference per call. */\nexport interface WebSettings {\n revision: number\n searchEnabled: boolean\n fetchEnabled: boolean\n searchProvider: string\n fetchProvider: string\n searchOrder: string[]\n maxResults: number\n maxQueries: number\n timeoutMs: number\n maxFetchChars: number\n endpoints: Record<string, string>\n}\nexport const DEFAULT_SEARCH_ORDER = ['ddg'] as const\n\nexport const defaultSettings = (): WebSettings => ({\n revision: 0, searchEnabled: true, fetchEnabled: true,\n searchProvider: 'ddg', fetchProvider: 'http', searchOrder: [...DEFAULT_SEARCH_ORDER],\n maxResults: 5, maxQueries: 1,\n timeoutMs: 30_000, maxFetchChars: 100_000, endpoints: {},\n})\n\n/** Keyed backends first, keyless last — same idea as OpenClaw auto-detect. */\nexport function defaultSearchOrder(ids: readonly string[], keyed: (id: string) => boolean): string[] {\n return [...ids.filter(keyed), ...ids.filter(id => !keyed(id))]\n}\n\nexport function migrateSearchOrder(settings: { searchOrder?: string[]; searchProvider?: string }): string[] {\n return settings.searchOrder ?? (settings.searchProvider ? [settings.searchProvider] : [...DEFAULT_SEARCH_ORDER])\n}\n\nexport function resolveSearchOrder(\n knownIds: readonly string[],\n settings: Pick<WebSettings, 'searchOrder' | 'searchProvider'>,\n _keyed: (id: string) => boolean,\n): string[] {\n return [...new Set(settings.searchOrder.filter(id => knownIds.includes(id)))]\n}\n\nexport function pickActiveSearch(order: readonly string[], configured: (id: string) => boolean): string {\n return order.find(configured) ?? ''\n}\nexport interface ProviderView extends ProviderDescriptor {\n kind: ProviderKind\n configured: boolean\n /** True only for configuration explicitly owned by this manager bundle. */\n configurationOwned?: boolean\n baseURL?: string\n calls: number\n failures: number\n}\nexport interface WebStatus {\n settings: WebSettings\n providers: ProviderView[]\n searchActive: boolean\n fetchActive: boolean\n storageFailed: boolean\n}\nexport function providerKey(kind: ProviderKind, id: string): string { return `${kind}:${id}` }\nexport function safeConfigurationUrl(value: unknown): string | undefined {\n if (typeof value !== 'string' || !value || /[\\u0000-\\u0020\\\\]/.test(value)) return\n try {\n const url = new URL(value, 'https://dsh.invalid')\n if (url.username || url.password) return\n if ((value.startsWith('/') && !value.startsWith('//')) || value.startsWith('#')) {\n return url.origin === 'https://dsh.invalid' ? value : undefined\n }\n return /^https:\\/\\//i.test(value) && url.protocol === 'https:' ? value : undefined\n } catch { return }\n}\nexport function validateBaseURL(value: string): string {\n const url = new URL(value)\n if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash) {\n throw new Error('供应商地址必须是 HTTPS,且不能包含用户名、密码、查询参数或片段。')\n }\n return url.href.replace(/\\/+$/, '')\n}\n"],"mappings":";AAGA,MAAa,yBAAyB;AAOtC,MAAa,oBAAoB;AA0BjC,MAAa,uBAAuB,CAAC,KAAK;AAE1C,MAAa,yBAAsC;CACjD,UAAU;CAAG,eAAe;CAAM,cAAc;CAChD,gBAAgB;CAAO,eAAe;CAAQ,aAAa,CAAC,GAAG,oBAAoB;CACnF,YAAY;CAAG,YAAY;CAC3B,WAAW;CAAQ,eAAe;CAAS,WAAW,CAAC;AACzD;;AAGA,SAAgB,mBAAmB,KAAwB,OAA0C;CACnG,OAAO,CAAC,GAAG,IAAI,OAAO,KAAK,GAAG,GAAG,IAAI,QAAO,OAAM,CAAC,MAAM,EAAE,CAAC,CAAC;AAC/D;AAEA,SAAgB,mBAAmB,UAAyE;CAC1G,OAAO,SAAS,gBAAgB,SAAS,iBAAiB,CAAC,SAAS,cAAc,IAAI,CAAC,GAAG,oBAAoB;AAChH;AAEA,SAAgB,mBACd,UACA,UACA,QACU;CACV,OAAO,CAAC,GAAG,IAAI,IAAI,SAAS,YAAY,QAAO,OAAM,SAAS,SAAS,EAAE,CAAC,CAAC,CAAC;AAC9E;AAEA,SAAgB,iBAAiB,OAA0B,YAA6C;CACtG,OAAO,MAAM,KAAK,UAAU,KAAK;AACnC;AAiBA,SAAgB,YAAY,MAAoB,IAAoB;CAAE,OAAO,GAAG,KAAK,GAAG;AAAK;AAC7F,SAAgB,qBAAqB,OAAoC;CACvE,IAAI,OAAO,UAAU,YAAY,CAAC,SAAS,oBAAoB,KAAK,KAAK,GAAG;CAC5E,IAAI;EACF,MAAM,MAAM,IAAI,IAAI,OAAO,qBAAqB;EAChD,IAAI,IAAI,YAAY,IAAI,UAAU;EAClC,IAAK,MAAM,WAAW,GAAG,KAAK,CAAC,MAAM,WAAW,IAAI,KAAM,MAAM,WAAW,GAAG,GAC5E,OAAO,IAAI,WAAW,wBAAwB,QAAQ,KAAA;EAExD,OAAO,eAAe,KAAK,KAAK,KAAK,IAAI,aAAa,WAAW,QAAQ,KAAA;CAC3E,QAAQ;EAAE;CAAO;AACnB;AACA,SAAgB,gBAAgB,OAAuB;CACrD,MAAM,MAAM,IAAI,IAAI,KAAK;CACzB,IAAI,IAAI,aAAa,YAAY,IAAI,YAAY,IAAI,YAAY,IAAI,UAAU,IAAI,MACjF,MAAM,IAAI,MAAM,qCAAqC;CAEvD,OAAO,IAAI,KAAK,QAAQ,QAAQ,EAAE;AACpC"}
package/lib/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { FetchProviderFactory, ProviderDescriptor, ProviderOptions, SearchProviderFactory, WebSettings, WebStatus } from "./contracts.js";
2
2
  import { WebFetchProvider, WebSearchProvider } from "@deepseek-ai/dsh-web";
3
+ import { WEB_PROVIDER_AVAILABILITY_EVENT } from "@klarkxy/dsh-plugin-kit/contracts";
3
4
  import { Context } from "@deepseek-ai/cordis";
4
5
  //#region src/web-registry.d.ts
5
6
  interface WebRegistry {
@@ -67,7 +68,7 @@ declare function apply(ctx: Context): Promise<void>;
67
68
  declare module '@deepseek-ai/cordis' {
68
69
  interface Events {
69
70
  /** Repository extension: native providers announce a local availability change. */
70
- 'web/provider-availability-updated'(kind: 'search' | 'fetch', id: string): void;
71
+ [WEB_PROVIDER_AVAILABILITY_EVENT](kind: 'search' | 'fetch', id: string): void;
71
72
  }
72
73
  }
73
74
  //#endregion
package/lib/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { WEB_MANAGER_OWNER, WEB_SEARCH_RPC_CHANNEL, defaultSettings, migrateSearchOrder, pickActiveSearch, providerKey, resolveSearchOrder, safeConfigurationUrl, validateBaseURL } from "./contracts.js";
2
2
  import { credentialRef } from "@deepseek-ai/dsh-credentials";
3
3
  import { WebError } from "@deepseek-ai/dsh-web";
4
+ import { WEB_PROVIDER_AVAILABILITY_EVENT } from "@klarkxy/dsh-plugin-kit/contracts";
4
5
  import { DEEPSEEK_DEFAULT_API_VERSION, DEEPSEEK_DEFAULT_BASE_URL, DEEPSEEK_DEFAULT_MAX_TOKENS, DEEPSEEK_DEFAULT_MAX_USES, DEEPSEEK_DEFAULT_MODEL, DeepSeekSearchProvider } from "@deepseek-ai/dsh-web-search-deepseek";
5
6
  import { ExaSearchProvider } from "@deepseek-ai/dsh-web-search-exa";
6
7
  import { HttpFetchProvider } from "@deepseek-ai/dsh-web-fetch-http";
@@ -129,7 +130,7 @@ function providerError(error, secrets = []) {
129
130
  return new WebError(redactDescription(descriptions.join(";") || "供应商未提供错误描述。", secrets), redactDescription(code || "WEB_PROVIDER_ERROR", secrets));
130
131
  }
131
132
  const SEARCH_ENGINE_PAGE = "https://lite.duckduckgo.com/lite/";
132
- const MAX_RESPONSE_BYTES$2 = 2e6;
133
+ const MAX_RESPONSE_BYTES$1 = 2e6;
133
134
  const USER_AGENT = "Mozilla/5.0 (compatible; dsh-editor-search/1.0)";
134
135
  function searchEngineUrl(query, page = SEARCH_ENGINE_PAGE) {
135
136
  const url = new URL(page);
@@ -196,7 +197,7 @@ async function readText(response, signal) {
196
197
  const { value, done } = await reader.read();
197
198
  if (done) break;
198
199
  size += value.length;
199
- if (size > MAX_RESPONSE_BYTES$2) throw new Error("Response too large");
200
+ if (size > MAX_RESPONSE_BYTES$1) throw new Error("Response too large");
200
201
  chunks.push(value);
201
202
  }
202
203
  } finally {
@@ -252,10 +253,10 @@ var SearchEngineProvider = class {
252
253
  }
253
254
  }
254
255
  };
255
- function record$1(value) {
256
+ function record(value) {
256
257
  return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
257
258
  }
258
- async function readJSON$1(response, signal) {
259
+ async function readJSON(response, signal) {
259
260
  const reader = response.body?.getReader();
260
261
  if (!reader) throw new Error("Missing response");
261
262
  let size = 0;
@@ -287,7 +288,7 @@ function httpsEndpoint(base, path = "") {
287
288
  return `${url.href.replace(/\/+$/, "")}${path}`;
288
289
  }
289
290
  function sourceFrom(raw, urlKey, titleKey, snippetKey) {
290
- const row = record$1(raw);
291
+ const row = record(raw);
291
292
  if (typeof row?.[urlKey] !== "string") return;
292
293
  let url;
293
294
  try {
@@ -351,7 +352,7 @@ function createRestSearch(spec) {
351
352
  await response.body?.cancel().catch(() => {});
352
353
  throw new HttpStatusError(response.status);
353
354
  }
354
- const sources = spec.parse(await readJSON$1(response, signal));
355
+ const sources = spec.parse(await readJSON(response, signal));
355
356
  return {
356
357
  sources: sources.slice(0, count),
357
358
  truncated: sources.length > count
@@ -374,7 +375,7 @@ const BraveSearchProvider = createRestSearch({
374
375
  count: String(count)
375
376
  }),
376
377
  parse(payload) {
377
- const results = record$1(record$1(payload)?.web)?.results;
378
+ const results = record(record(payload)?.web)?.results;
378
379
  return Array.isArray(results) ? results.flatMap((row) => sourceFrom(row, "url", "title", "description") ?? []) : [];
379
380
  }
380
381
  });
@@ -394,7 +395,7 @@ const BochaSearchProvider = createRestSearch({
394
395
  summary: false
395
396
  }),
396
397
  parse(payload) {
397
- const pages = record$1(record$1(record$1(payload)?.data)?.webPages)?.value ?? record$1(record$1(payload)?.webPages)?.value;
398
+ const pages = record(record(record(payload)?.data)?.webPages)?.value ?? record(record(payload)?.webPages)?.value;
398
399
  return Array.isArray(pages) ? pages.flatMap((row) => sourceFrom(row, "url", "name", "snippet") ?? []) : [];
399
400
  }
400
401
  });
@@ -412,7 +413,7 @@ const SerperSearchProvider = createRestSearch({
412
413
  num: count
413
414
  }),
414
415
  parse(payload) {
415
- const organic = record$1(payload)?.organic;
416
+ const organic = record(payload)?.organic;
416
417
  return Array.isArray(organic) ? organic.flatMap((row) => sourceFrom(row, "link", "title", "snippet") ?? []) : [];
417
418
  }
418
419
  });
@@ -430,47 +431,12 @@ const FirecrawlSearchProvider = createRestSearch({
430
431
  limit: count
431
432
  }),
432
433
  parse(payload) {
433
- const data = record$1(payload)?.data;
434
+ const data = record(payload)?.data;
434
435
  return Array.isArray(data) ? data.flatMap((row) => sourceFrom(row, "url", "title", "description") ?? []) : [];
435
436
  }
436
437
  });
437
438
  //#endregion
438
439
  //#region src/tavily.ts
439
- const MAX_RESPONSE_BYTES = 2e6;
440
- function endpoint(base) {
441
- const url = new URL(base);
442
- if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash) throw new Error("Invalid endpoint");
443
- return `${url.href.replace(/\/+$/, "")}/search`;
444
- }
445
- function record(value) {
446
- return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
447
- }
448
- async function readJSON(response, signal) {
449
- const reader = response.body?.getReader();
450
- if (!reader) throw new Error("Missing response");
451
- let size = 0;
452
- const chunks = [];
453
- try {
454
- while (true) {
455
- signal?.throwIfAborted();
456
- const { value, done } = await reader.read();
457
- if (done) break;
458
- size += value.length;
459
- if (size > MAX_RESPONSE_BYTES) throw new Error("Response too large");
460
- chunks.push(value);
461
- }
462
- const bytes = new Uint8Array(size);
463
- let offset = 0;
464
- for (const chunk of chunks) {
465
- bytes.set(chunk, offset);
466
- offset += chunk.length;
467
- }
468
- return JSON.parse(new TextDecoder().decode(bytes));
469
- } finally {
470
- await reader.cancel().catch(() => {});
471
- reader.releaseLock();
472
- }
473
- }
474
440
  /** Direct REST adapter. No auxiliary LLM, retries, anonymous fallback or auto-depth. */
475
441
  var TavilySearchProvider = class {
476
442
  options;
@@ -480,7 +446,7 @@ var TavilySearchProvider = class {
480
446
  }
481
447
  available() {
482
448
  try {
483
- endpoint(this.options.baseURL ?? "https://api.tavily.com");
449
+ httpsEndpoint(this.options.baseURL ?? "https://api.tavily.com", "/search");
484
450
  return Boolean(this.options.apiKey.trim());
485
451
  } catch {
486
452
  return false;
@@ -493,7 +459,7 @@ var TavilySearchProvider = class {
493
459
  if (!Number.isInteger(count) || count < 1 || count > 20) throw new WebError("结果数须为 1 至 20。", "WEB_INVALID_REQUEST");
494
460
  try {
495
461
  signal?.throwIfAborted();
496
- const response = await (this.options.fetch ?? fetch)(endpoint(this.options.baseURL ?? "https://api.tavily.com"), {
462
+ const response = await (this.options.fetch ?? fetch)(httpsEndpoint(this.options.baseURL ?? "https://api.tavily.com", "/search"), {
497
463
  method: "POST",
498
464
  redirect: "error",
499
465
  signal,
@@ -522,18 +488,12 @@ var TavilySearchProvider = class {
522
488
  const sources = [];
523
489
  for (const raw of payload.results) {
524
490
  const row = record(raw);
525
- if (typeof row?.url !== "string") continue;
526
- let url;
527
- try {
528
- url = new URL(row.url);
529
- } catch {
530
- continue;
531
- }
532
- if (!["https:", "http:"].includes(url.protocol) || url.username || url.password) continue;
491
+ const source = sourceFrom(row, "url", "title", "content");
492
+ if (!source || !row) continue;
533
493
  sources.push({
534
- url: url.href,
535
- ...typeof row.title === "string" ? { title: row.title.slice(0, 1e3) } : {},
536
- ...typeof row.content === "string" ? { snippet: row.content.slice(0, 8e3) } : {},
494
+ ...source,
495
+ ...row.title === "" ? { title: "" } : {},
496
+ ...row.content === "" ? { snippet: "" } : {},
537
497
  ...typeof row.published_date === "string" && Number.isFinite(Date.parse(row.published_date)) ? { publishedAt: new Date(row.published_date).toISOString() } : {}
538
498
  });
539
499
  }
@@ -1298,7 +1258,7 @@ async function apply(ctx) {
1298
1258
  ctx.on("credentials/reference-updated", () => {
1299
1259
  manager.refresh();
1300
1260
  });
1301
- ctx.on("web/provider-availability-updated", () => {
1261
+ ctx.on(WEB_PROVIDER_AVAILABILITY_EVENT, () => {
1302
1262
  manager.refresh();
1303
1263
  });
1304
1264
  ctx.effect(() => registerBuiltins(ctx.web, {