@240xu/dsh-websearch 2.8.0 → 2.8.2
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 +19 -0
- package/cordis.patch.yml +10 -0
- package/lib/backends/exa.js +8 -4
- package/lib/index.js +51 -28
- package/lib/provider.js +33 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -115,6 +115,12 @@ pnpm install
|
|
|
115
115
|
|
|
116
116
|
环境变量兜底:未在 credentials 配置时回退读同名环境变量;`DSH_UNIFIED_SEARCH_BACKENDS` 可逗号分隔强制指定启用集合。
|
|
117
117
|
|
|
118
|
+
## v2.8.1 SettingsForms 适配 | What's new in v2.8.1
|
|
119
|
+
|
|
120
|
+
- **0.2.0 设置面板表单**:Config 标记 `meta.volatile`——0.2.0 SettingsForms 从插件 Config 自动生成表单,但 volatileForm 只保留 volatile 字段(此前 websearch 表单为空);现整棵 Config 成为 0.2.0 表单。裁决与考证见 dsh-plugin-hub/docs/reviews/cross-settings-forms.md。
|
|
121
|
+
- 删除 0.2.0 线上与可见 UI 自相矛盾的 "no installSection" console.warn(0.1.5 路径不变)。
|
|
122
|
+
- 补齐全部裸字段(×ApiKeyEnv/×BaseURL/×Model)的 description——0.2.0 表单渲染唯一用户可见文案来自 meta.description。
|
|
123
|
+
|
|
118
124
|
## v2.8.0 查全查多 | What's new in v2.8.0
|
|
119
125
|
|
|
120
126
|
面向「查的多一点、查的全一点」的检索深度升级,全部 feature-gated、默认关:
|
|
@@ -247,3 +253,16 @@ v2.7.0 缓存/熔断/历史三项的依据与刻意取舍(含查证过的反
|
|
|
247
253
|
## License | 许可
|
|
248
254
|
|
|
249
255
|
MIT © 2026 240xu
|
|
256
|
+
|
|
257
|
+
## 2.8.2 · Bug 猎场修复(P1×3 + 回归测试)
|
|
258
|
+
|
|
259
|
+
- **[P1] 零结果伪造失败**:所有后端成功但 0 命中时,原实现落进 `all enabled
|
|
260
|
+
backends failed (0) - `(计数自相矛盾、detail 空)并丢弃 mergedContent(Tavily
|
|
261
|
+
直答)。成功即空 → 正常返回 `{sources:[]}` + history 记录。
|
|
262
|
+
- **[P1] limiter 死锁**:`Promise.resolve(fn())` 先执行 fn——同步抛出(如 baseURL
|
|
263
|
+
校验在建 timeout 前)逃出后 `running` 永不递减,泄漏 ≥ concurrency 后所有任务
|
|
264
|
+
永久排队且超时机制未武装(实测 HUNG exit=13)。改 `Promise.resolve().then(fn)`。
|
|
265
|
+
- **[P1] exa category 漏门控**:tavily/searxng 的 deepCoverage 增强都门控,唯独
|
|
266
|
+
exa 漏了——默认关 deepCoverage 也注入 category 收窄结果,与 README 承诺相反。
|
|
267
|
+
- 回归测试 3 条(零结果/limiter 不死锁带 HUNG 超时护栏/exa 双态门控);
|
|
268
|
+
`createLimiter` 导出供测试。89/89。
|
package/cordis.patch.yml
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
# Bundle patch layer:
|
|
2
|
+
# 1. insert the plugin row (registers the unified search provider)
|
|
3
|
+
# 2. wire the built-in web_search tool to the unified provider (the plugin's
|
|
4
|
+
# whole reason to exist — previously a manual cordis.patch.yml edit, now
|
|
5
|
+
# automatic; revert = remove this row or disable the plugin, the base
|
|
6
|
+
# default (deepseek-official) is one yaml line away).
|
|
1
7
|
- insert:
|
|
2
8
|
- id: dsh-websearch
|
|
3
9
|
name: '@240xu/dsh-websearch'
|
|
10
|
+
- id: web
|
|
11
|
+
name: '@deepseek-ai/dsh-web'
|
|
12
|
+
config:
|
|
13
|
+
searchProvider: unified
|
package/lib/backends/exa.js
CHANGED
|
@@ -42,10 +42,14 @@ export const exaBackend = {
|
|
|
42
42
|
query: request.query,
|
|
43
43
|
numResults: request.maxResults ?? 8,
|
|
44
44
|
};
|
|
45
|
-
// v2.8.0: category inference
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
// v2.8.0: category inference — deepCoverage-gated like tavily (searchDepth
|
|
46
|
+
// advanced) and searxng (category sweep): 2.8.1 审计发现此处漏了门控,
|
|
47
|
+
// 默认关着 deepCoverage 也注入 category,把结果悄悄收窄到单类目,与 README
|
|
48
|
+
// 「deepCoverage 深度覆盖」承诺相反。
|
|
49
|
+
if (request.deepCoverage === true) {
|
|
50
|
+
const category = inferExaCategory(request.query);
|
|
51
|
+
if (category) args.category = category;
|
|
52
|
+
}
|
|
49
53
|
hooks.recordRequest?.({
|
|
50
54
|
endpoint: EXA_ENDPOINT,
|
|
51
55
|
toolName: EXA_TOOL,
|
package/lib/index.js
CHANGED
|
@@ -189,30 +189,48 @@ export const Config = z.object({
|
|
|
189
189
|
enableOpenai: z.boolean().default(false).description(DEFAULT_LABELS.openai),
|
|
190
190
|
|
|
191
191
|
// Credential refs (env-var names; secrets live in the credentials service)
|
|
192
|
-
braveApiKeyEnv: z.string().role("credential-ref").default(DEFAULT_KEY_ENV.brave)
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
192
|
+
braveApiKeyEnv: z.string().role("credential-ref").default(DEFAULT_KEY_ENV.brave)
|
|
193
|
+
.description("Brave API Key 的凭证引用(填环境变量名,实际密钥存凭证库)"),
|
|
194
|
+
tavilyApiKeyEnv: z.string().role("credential-ref").default(DEFAULT_KEY_ENV.tavily)
|
|
195
|
+
.description("Tavily API Key 的凭证引用(填环境变量名,实际密钥存凭证库)"),
|
|
196
|
+
serperApiKeyEnv: z.string().role("credential-ref").default(DEFAULT_KEY_ENV.serper)
|
|
197
|
+
.description("Serper API Key 的凭证引用(填环境变量名,实际密钥存凭证库)"),
|
|
198
|
+
mojeekApiKeyEnv: z.string().role("credential-ref").default(DEFAULT_KEY_ENV.mojeek)
|
|
199
|
+
.description("Mojeek API Key 的凭证引用(填环境变量名,实际密钥存凭证库)"),
|
|
200
|
+
deepseekApiKeyEnv: z.string().role("credential-ref").default(DEFAULT_KEY_ENV.deepseek)
|
|
201
|
+
.description("Deepseek API Key 的凭证引用(填环境变量名,实际密钥存凭证库)"),
|
|
202
|
+
anthropicApiKeyEnv: z.string().role("credential-ref").default(DEFAULT_KEY_ENV.anthropic)
|
|
203
|
+
.description("Anthropic API Key 的凭证引用(填环境变量名,实际密钥存凭证库)"),
|
|
204
|
+
openaiApiKeyEnv: z.string().role("credential-ref").default(DEFAULT_KEY_ENV.openai)
|
|
205
|
+
.description("Openai API Key 的凭证引用(填环境变量名,实际密钥存凭证库)"),
|
|
199
206
|
searxngApiKeyEnv: z.string().role("credential-ref").default("SEARXNG_API_KEY")
|
|
200
|
-
.description("SearXNG 私有实例的 API Key
|
|
207
|
+
.description("SearXNG 私有实例的 API Key 凭证引用(公共实例留空)"),
|
|
201
208
|
|
|
202
209
|
// Base URLs (defaults from built-in registry)
|
|
203
|
-
braveBaseURL: z.string().default(DEFAULT_BASE_URLS.brave)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
210
|
+
braveBaseURL: z.string().default(DEFAULT_BASE_URLS.brave)
|
|
211
|
+
.description("Brave API 端点(默认官方;自托管/代理实例改此项)"),
|
|
212
|
+
tavilyBaseURL: z.string().default(DEFAULT_BASE_URLS.tavily)
|
|
213
|
+
.description("Tavily API 端点(默认官方;自托管/代理实例改此项)"),
|
|
214
|
+
serperBaseURL: z.string().default(DEFAULT_BASE_URLS.serper)
|
|
215
|
+
.description("Serper.dev API 端点(默认官方;自托管/代理实例改此项)"),
|
|
216
|
+
mojeekBaseURL: z.string().default(DEFAULT_BASE_URLS.mojeek)
|
|
217
|
+
.description("Mojeek API 端点(默认官方;自托管/代理实例改此项)"),
|
|
218
|
+
searxngBaseURL: z.string().default(DEFAULT_BASE_URLS.searxng)
|
|
219
|
+
.description("SearXNG API 端点(默认官方;自托管/代理实例改此项)"),
|
|
220
|
+
deepseekBaseURL: z.string().default(DEFAULT_BASE_URLS.deepseek)
|
|
221
|
+
.description("DeepSeek API 端点(默认官方;自托管/代理实例改此项)"),
|
|
222
|
+
anthropicBaseURL: z.string().default(DEFAULT_BASE_URLS.anthropic)
|
|
223
|
+
.description("Anthropic API 端点(默认官方;自托管/代理实例改此项)"),
|
|
224
|
+
openaiBaseURL: z.string().default(DEFAULT_BASE_URLS.openai)
|
|
225
|
+
.description("OpenAI API 端点(默认官方;自托管/代理实例改此项)"),
|
|
211
226
|
|
|
212
227
|
// Models
|
|
213
|
-
deepseekModel: z.string().default(DEFAULT_MODELS.deepseek)
|
|
214
|
-
|
|
215
|
-
|
|
228
|
+
deepseekModel: z.string().default(DEFAULT_MODELS.deepseek)
|
|
229
|
+
.description("DeepSeek 后端使用的模型名"),
|
|
230
|
+
anthropicModel: z.string().default(DEFAULT_MODELS.anthropic)
|
|
231
|
+
.description("Anthropic 后端使用的模型名"),
|
|
232
|
+
openaiModel: z.string().default(DEFAULT_MODELS.openai)
|
|
233
|
+
.description("OpenAI 后端使用的模型名"),
|
|
216
234
|
tavilySearchDepth: z.union(["basic", "advanced"]).default("basic")
|
|
217
235
|
.description("Tavily 搜索深度"),
|
|
218
236
|
|
|
@@ -221,6 +239,15 @@ export const Config = z.object({
|
|
|
221
239
|
.description("显式启用后端列表(优先于上方开关;留空则用开关)"),
|
|
222
240
|
});
|
|
223
241
|
|
|
242
|
+
// 0.2.0 SettingsForms adapter (cross-settings-forms.md ruling): the 0.2.0
|
|
243
|
+
// settings service auto-generates its form from entry.fiber.runtime.Config —
|
|
244
|
+
// but volatileForm (RT dsh-settings lib/index.js:122-138) keeps ONLY fields
|
|
245
|
+
// with meta.volatile; a Config without any yields an empty form. Marking the
|
|
246
|
+
// top object makes the whole Config the form. Direct meta assignment (not
|
|
247
|
+
// .volatile(), which returns a NEW schema and throws on re-wrap) is
|
|
248
|
+
// equivalent and idempotent on both the 0.1.5 and 0.2.0 schemastery lines.
|
|
249
|
+
Config.meta = { ...Config.meta, volatile: true };
|
|
250
|
+
|
|
224
251
|
/**
|
|
225
252
|
* Per-field user-language labels (fe-ui W1 remainder): the settings renderer
|
|
226
253
|
* falls back to camelCase keys when no label is present, so attach one to
|
|
@@ -368,6 +395,7 @@ export function apply(ctx, config) {
|
|
|
368
395
|
ctx.inject(["settings"], (settingsCtx) => {
|
|
369
396
|
const settings = settingsCtx.settings;
|
|
370
397
|
if (typeof settings?.installSection === "function") {
|
|
398
|
+
// 0.1.5 SettingsProvider line: explicit section install (unchanged).
|
|
371
399
|
settings.installSection(ctx, NAMESPACE, Config, config, {
|
|
372
400
|
setSource: (source) => {
|
|
373
401
|
current = source;
|
|
@@ -376,15 +404,10 @@ export function apply(ctx, config) {
|
|
|
376
404
|
});
|
|
377
405
|
return;
|
|
378
406
|
}
|
|
379
|
-
//
|
|
380
|
-
//
|
|
381
|
-
//
|
|
382
|
-
//
|
|
383
|
-
console.warn(
|
|
384
|
-
"[dsh-websearch] settings service has no installSection (DSH >= 0.1.7 SettingsForms line); " +
|
|
385
|
-
"the Settings panel section is not installed. Configure the unified-search namespace via cordis config " +
|
|
386
|
-
"or the client settings card; migration to the SettingsForms API is tracked for a future release.",
|
|
387
|
-
);
|
|
407
|
+
// 0.2.0 SettingsForms line: no installSection; the settings form is
|
|
408
|
+
// auto-generated from the volatile-marked Config above — no action and no
|
|
409
|
+
// warning here (the form IS the section; a warning would contradict the
|
|
410
|
+
// visible UI). Config values still resolve via cordis config.
|
|
388
411
|
});
|
|
389
412
|
|
|
390
413
|
// v2.7.0 stores: disk cache + append-only search history, both under
|
package/lib/provider.js
CHANGED
|
@@ -82,7 +82,8 @@ export function getBackendBreaker(id) {
|
|
|
82
82
|
* Simple concurrency limiter using a semaphore pattern.
|
|
83
83
|
* No external dependency needed.
|
|
84
84
|
*/
|
|
85
|
-
|
|
85
|
+
// 2.8.2: exported for P1 regression tests (limiter sync-throw deadlock)
|
|
86
|
+
export function createLimiter(concurrency) {
|
|
86
87
|
const queue = [];
|
|
87
88
|
let running = 0;
|
|
88
89
|
|
|
@@ -95,7 +96,12 @@ function createLimiter(concurrency) {
|
|
|
95
96
|
while (running < concurrency && queue.length > 0) {
|
|
96
97
|
const { fn, resolve, reject } = queue.shift();
|
|
97
98
|
running++;
|
|
98
|
-
|
|
99
|
+
// 2.8.2(P1):必须 .then(fn) 而非 Promise.resolve(fn())——后者先执行
|
|
100
|
+
// fn(),同步抛出(如 runBackend 的 baseURL 校验在创建 timeout 之前 throw)
|
|
101
|
+
// 会逃出本函数:running 已 ++ 却永不 --(finally 没挂上),leaked 计数
|
|
102
|
+
// ≥ concurrency 后所有后续任务永久排队,且 timeoutId 未创建 → 超时也救不了
|
|
103
|
+
// (实测全挂起 exit=13)。.then(fn) 把同步抛出转为 rejection 落进 finally。
|
|
104
|
+
Promise.resolve().then(fn).then(resolve).catch(reject).finally(() => {
|
|
99
105
|
running--;
|
|
100
106
|
process();
|
|
101
107
|
});
|
|
@@ -435,6 +441,31 @@ export function createUnifiedSearchProvider({ ctx, resolveOptions, backends, cac
|
|
|
435
441
|
};
|
|
436
442
|
}
|
|
437
443
|
|
|
444
|
+
// 2.8.2(P1):零结果 ≠ 全部失败。所有后端成功但确实 0 条命中(冷僻查询/
|
|
445
|
+
// 过滤后为空)时,原实现无条件落入下面的 throw,模型端拿到
|
|
446
|
+
// `all enabled backends failed (0) - `(detail 空、计数自相矛盾),且丢弃已
|
|
447
|
+
// 拿到的 mergedContent(如 Tavily 直答)。成功即空 → 正常返回空结果。
|
|
448
|
+
if (allFailures.length === 0) {
|
|
449
|
+
let emptyContent = perVariant.find((v) => v.mergedContent !== undefined)?.mergedContent;
|
|
450
|
+
if (opts.resultHeader !== false && emptyContent !== undefined) {
|
|
451
|
+
const header = resultHeader(typeof request?.query === "string" && /[\u4e00-\u9fff]/.test(request.query) ? "zh" : "en");
|
|
452
|
+
emptyContent = header + "\n\n" + emptyContent;
|
|
453
|
+
}
|
|
454
|
+
history?.record({
|
|
455
|
+
query: request?.query,
|
|
456
|
+
time: Date.now(),
|
|
457
|
+
resultCount: 0,
|
|
458
|
+
backendsOk: allTelemetry.filter((t) => t.ok).length,
|
|
459
|
+
backendsTotal: allTelemetry.length,
|
|
460
|
+
});
|
|
461
|
+
if (cacheKey && cache) cache.set(cacheKey, { sources: [], content: emptyContent, truncated: false });
|
|
462
|
+
return {
|
|
463
|
+
sources: [],
|
|
464
|
+
...(emptyContent !== undefined ? { content: emptyContent } : {}),
|
|
465
|
+
truncated: false,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
438
469
|
// All enabled backends failed - synthesize one meaningful error that
|
|
439
470
|
// names each backend's own reason so the caller (model or user) can act.
|
|
440
471
|
const detail = allFailures.slice(0, 4)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@240xu/dsh-websearch",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.2",
|
|
4
4
|
"description": "Unified web search provider for DeepSeek Harness (ctx.web): 11 backends — 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. 统一网页搜索聚合:11 后端并发扇出、去重合并,零依赖。See CHANGELOG.md for version history.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|