@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 +15 -0
- package/lib/backends/searxng.js +2 -1
- package/lib/client.js +9 -1
- package/lib/index.js +3 -0
- package/lib/provider.js +20 -1
- package/package.json +1 -1
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 服务或环境变量):
|
package/lib/backends/searxng.js
CHANGED
|
@@ -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
|
|
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
|
-
...(
|
|
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.
|
|
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",
|