@240xu/dsh-websearch 2.2.0 → 2.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/README.md CHANGED
@@ -31,6 +31,18 @@ DSH([DeepSeek Harness](https://github.com/deepseek-ai/dsh))原生插件:
31
31
 
32
32
  **Default enabled set**: `["exa", "parallel", "ddg", "searxng"]` — all keyless. The key-gated backends only join when their `available()` returns true (key present + baseURL reachable).
33
33
 
34
+ ### SearXNG 实例 | Public instances
35
+
36
+ SearXNG 默认指向 `https://searx.be`;公共实例可用性随网络环境波动,失败时错误信息会提示换源。
37
+ 在设置面板修改 `searxngBaseURL` 即可切换,常见候选:
38
+
39
+ | 实例 | 备注 |
40
+ |---|---|
41
+ | https://searx.be | 官方默认,部分地区不可达 |
42
+ | https://searx.tiekoetter.com | 稳定性较好 |
43
+ | https://priv.au | 隐私友好 |
44
+ | 自托管 | 最可靠,支持 API Key(`searxngApiKeyEnv`) |
45
+
34
46
  ## Install | 安装
35
47
 
36
48
  ### 方式一:作为 profile 依赖安装(推荐,规范做法)
@@ -81,6 +93,9 @@ pnpm install
81
93
 
82
94
  **v2.1 结果策略**:设置面板新增「结果策略」分区。时间/语言过滤按各后端能力自动映射——Brave `freshness/country/search_lang`、Tavily `time_range`、Serper `tbs/hl/gl`(Google 日期语法)、SearXNG `time_range/language`、DDG `df`;不支持的后端自动忽略对应维度。内部超时以真实原因失败(backend "<id>" timed out after Nms),不再被静默降级为取消;Mojeek 的 API Key 改走 Authorization 头,不再出现在 URL 中。
83
95
 
96
+ **结果健康遥测(默认开)**:每次搜索的 `content` 尾部附一行 `[websearch backends] exa ✓5ms/3 · ddg ✗30000ms`,
97
+ 模型可据此自诊断并建议用户调整设置;设置面板「结果策略」可关闭(`resultTelemetry`)。
98
+
84
99
  **11 个后端开关**:`enableExa` / `enableParallel` / `enableDdg` / `enableSearxng`(默认开);`enableBrave` / `enableTavily` / `enableSerper` / `enableMojeek` / `enableDeepseek` / `enableAnthropic` / `enableOpenai`(默认关)
85
100
 
86
101
  **API Key 授权**(credential-ref,填环境变量名,实际 key 存于 credentials 服务或环境变量):
@@ -66,7 +66,8 @@ export const searxngBackend = {
66
66
  });
67
67
  } catch (error) {
68
68
  if (signal?.aborted === true || isAbortError(error)) throw searchAborted(signal, error);
69
- throw new WebError("SearXNG request failed: " + error, "WEB_PROVIDER_ERROR", { cause: error });
69
+ throw new WebError("SearXNG request failed: " + error +
70
+ " (instance unreachable? point searxngBaseURL at another public instance)", "WEB_PROVIDER_ERROR", { cause: error });
70
71
  }
71
72
 
72
73
  if (!response.ok) {
package/lib/client.js CHANGED
@@ -31,7 +31,7 @@ window.__ModuleLoader__.load({
31
31
  dedupStrategy: ["url", "url+title"],
32
32
  };
33
33
  /** Boolean settings staged as "true"/"false" strings by the selects. */
34
- const BOOLEAN_FIELDS = ["rerank", "safeSearch"];
34
+ const BOOLEAN_FIELDS = ["rerank", "safeSearch", "resultTelemetry"];
35
35
  const KEYLESS = BACKENDS.filter((b) => !b.keyEnv);
36
36
  const KEYED = BACKENDS.filter((b) => b.keyEnv);
37
37
 
@@ -89,6 +89,7 @@ window.__ModuleLoader__.load({
89
89
  dedupTitle: "链接 + 标题(合并转载)",
90
90
  langPlaceholder: "如 en、zh-CN,留空为默认",
91
91
  envProvided: "环境变量提供", readOnly: "只读,无需填写", conflict: "设置已在其他端被修改,请重试", keyFailed: "该 Key 保存失败",
92
+ telemetryLabel: "结果健康遥测",
92
93
  };
93
94
  const en = {
94
95
  nav: "Web Search",
@@ -136,6 +137,7 @@ window.__ModuleLoader__.load({
136
137
  readOnly: "read-only, nothing to fill",
137
138
  conflict: "Changed elsewhere - please retry",
138
139
  keyFailed: "failed to save this key",
140
+ telemetryLabel: "Result health telemetry",
139
141
  };
140
142
 
141
143
  //#region styles
@@ -465,6 +467,12 @@ window.__ModuleLoader__.load({
465
467
  options: boolOptions,
466
468
  onChange: (v) => card.edit("safeSearch", v),
467
469
  }),
470
+ react_jsx_runtime.jsx(SelectField, {
471
+ label: props.t("telemetryLabel"),
472
+ value: selVal("resultTelemetry", "true"),
473
+ options: boolOptions,
474
+ onChange: (v) => card.edit("resultTelemetry", v),
475
+ }),
468
476
  ] }),
469
477
  react_jsx_runtime.jsx("h4", { className: "wsc-sectionTitle", children: props.t("keysTitle") }),
470
478
  react_jsx_runtime.jsx("div", { className: "wsc-grid", children: BACKENDS.filter((b) => b.keyEnv).map((b) =>
package/lib/index.js CHANGED
@@ -141,6 +141,8 @@ export const Config = z.object({
141
141
  .description("去重策略:url 仅按链接;url+title 额外合并同标题的转载镜像"),
142
142
  rerank: z.boolean().default(false)
143
143
  .description("按查询词相关性重排序(确定性、并列时保持后端优先级)"),
144
+ resultTelemetry: z.boolean().default(true)
145
+ .description("在搜索结果尾部附一行后端健康遥测(状态+耗时),模型可据此自诊断"),
144
146
 
145
147
  // Enable toggles (one per backend)
146
148
  enableExa: z.boolean().default(true).description(DEFAULT_LABELS.exa),
@@ -265,6 +267,7 @@ export function resolveOptions(ctx, config) {
265
267
  // v2.1 shaping options consumed by the provider merge pipeline.
266
268
  dedupStrategy: config?.dedupStrategy === "url+title" ? "url+title" : "url",
267
269
  rerank: config?.rerank === true,
270
+ resultTelemetry: config?.resultTelemetry !== false,
268
271
  filters: {
269
272
  recency: typeof config?.recency === "string" && config.recency !== "any" ? config.recency : undefined,
270
273
  lang: language.length > 0 ? language : undefined,
package/lib/provider.js CHANGED
@@ -119,6 +119,7 @@ export function createUnifiedSearchProvider({ ctx, resolveOptions, backends }) {
119
119
  const effectiveRequest =
120
120
  Object.keys(filters).length > 0 ? { ...request, filters } : request;
121
121
 
122
+ const telemetry = [];
122
123
  // Concurrency limiter
123
124
  const limit = createLimiter(opts.concurrency ?? 6);
124
125
 
@@ -128,6 +129,7 @@ export function createUnifiedSearchProvider({ ctx, resolveOptions, backends }) {
128
129
  // reclassified as WEB_PROVIDER_ERROR so timeouts surface as real
129
130
  // failures instead of being silently demoted like user cancellations.
130
131
  const runBackend = ({ id, backend, beOpts, hooks }) => {
132
+ const startedAtMs = Date.now();
131
133
  const controller = new AbortController();
132
134
  const mergedSignal = signal
133
135
  ? AbortSignal.any([signal, controller.signal])
@@ -135,6 +137,16 @@ export function createUnifiedSearchProvider({ ctx, resolveOptions, backends }) {
135
137
  const timeoutId = setTimeout(() => controller.abort(), backendTimeoutMs);
136
138
  return backend
137
139
  .search(effectiveRequest, mergedSignal, beOpts, hooks)
140
+ .then(
141
+ (value) => {
142
+ telemetry.push({ id, ok: true, ms: Date.now() - startedAtMs, count: value?.sources?.length ?? 0 });
143
+ return value;
144
+ },
145
+ (err) => {
146
+ telemetry.push({ id, ok: false, ms: Date.now() - startedAtMs, error: String(err instanceof Error ? err.message : err).slice(0, 80) });
147
+ throw err;
148
+ },
149
+ )
138
150
  .catch((err) => {
139
151
  if (controller.signal.aborted && !(signal && signal.aborted)) {
140
152
  throw new WebError(
@@ -211,9 +223,16 @@ export function createUnifiedSearchProvider({ ctx, resolveOptions, backends }) {
211
223
  // If we got ANY sources, return them even if some backends failed.
212
224
  if (ordered.length > 0) {
213
225
  const sources = ordered.slice(0, maxResults);
226
+ let outContent = mergedContent;
227
+ if (opts.resultTelemetry !== false && telemetry.length > 0) {
228
+ const line = "[websearch backends] " + telemetry
229
+ .map((t) => t.id + (t.ok ? " ✓" : " ✗") + t.ms + "ms" + (t.ok ? "/" + t.count : ""))
230
+ .join(" · ");
231
+ outContent = outContent === undefined ? line : outContent + "\n\n" + line;
232
+ }
214
233
  return {
215
234
  sources,
216
- ...(mergedContent !== undefined ? { content: mergedContent } : {}),
235
+ ...(outContent !== undefined ? { content: outContent } : {}),
217
236
  truncated: ordered.length > maxResults,
218
237
  };
219
238
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@240xu/dsh-websearch",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "Unified web search provider for DeepSeek Harness (ctx.web): 11 backends \u2014 Exa + Parallel + DDG + SearXNG (keyless), DeepSeek + Anthropic + OpenAI + Brave + Tavily + Serper + Mojeek (key-gated via Settings credentials). Registers provider id \"unified\" for zero-config web_search tool.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",