dsh-free-search 0.9.3 → 0.9.4

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
@@ -436,7 +436,7 @@ Search engine test:
436
436
 
437
437
  #### 离线自检断言(tools/)
438
438
 
439
- 仓库自带 **17 个**零依赖断言脚本(合计 **501 项**),**不需要 API key、不联网、不需要测试框架**,Node ≥ 20 即可跑:
439
+ 仓库自带 **17 个**零依赖断言脚本(合计 **503 项**),**不需要 API key、不联网、不需要测试框架**,Node ≥ 20 即可跑:
440
440
 
441
441
  | 脚本 | 覆盖 | 运行(项数) |
442
442
  |---|---|---|
@@ -456,7 +456,7 @@ Search engine test:
456
456
  | `assert-killtree-win32.mjs` | Windows 子进程树终止(#74):不再依赖 `taskkill` | `node tools/assert-killtree-win32.mjs`(8) |
457
457
  | `assert-killtree-portable.mjs` | 跨平台 `killTree` 便携版契约 | `node tools/assert-killtree-portable.mjs`(5) |
458
458
  | `assert-parallel-mcp.mjs` | Parallel MCP 引擎:25 s 超时/调用方取消覆盖**响应体读取**、共享 SSE 解析器(通知/多行帧)、6 类错误路径、REST 精确时间过滤 | `node tools/assert-parallel-mcp.mjs`(15) |
459
- | `assert-provider-takeover.mjs` | provider 接管:旧属性 API 与 rc.2 的 `config.*` ref 两套兼容、`settings.mutate(web)` 单批写入、显式指定的 provider 不被抢占 | `node tools/assert-provider-takeover.mjs`(6) |
459
+ | `assert-provider-takeover.mjs` | provider 接管:旧属性 API 与 rc.2 的 `config.*` ref 两套兼容、`settings.mutate(web)` 单批写入、显式指定的 provider 不被抢占,以及 apply 重入/重复注册不再中断 | `node tools/assert-provider-takeover.mjs`(8) |
460
460
 
461
461
  任何一个失败会以非 0 退出码结束并列出失败项;把某个旧版本文件路径作为参数传进去,可以看到修复前必红(用来证明断言不是空转)。改动发布相关逻辑后建议先跑一遍再发版。
462
462
 
@@ -913,7 +913,7 @@ Restart `dsh web` after switching to apply changes.
913
913
 
914
914
  #### Offline self-check assertions (`tools/`)
915
915
 
916
- **Seventeen** zero-dependency assertion scripts ship with the repo (**501 assertions** total): **no API keys, no network, no test framework**, Node ≥ 20:
916
+ **Seventeen** zero-dependency assertion scripts ship with the repo (**503 assertions** total): **no API keys, no network, no test framework**, Node ≥ 20:
917
917
 
918
918
  | Script | Covers | Run (count) |
919
919
  |---|---|---|
@@ -933,7 +933,7 @@ Restart `dsh web` after switching to apply changes.
933
933
  | `assert-killtree-win32.mjs` | Windows process-tree termination (#74): no longer relies on `taskkill` | `node tools/assert-killtree-win32.mjs` (8) |
934
934
  | `assert-killtree-portable.mjs` | portable `killTree` contract | `node tools/assert-killtree-portable.mjs` (5) |
935
935
  | `assert-parallel-mcp.mjs` | Parallel MCP engine: the 25 s timeout / caller abort covers the **body read**, shared SSE parser (notifications, multiline frames), six error paths, REST precise time filtering | `node tools/assert-parallel-mcp.mjs` (15) |
936
- | `assert-provider-takeover.mjs` | provider takeover: both the legacy property API and the rc.2 `config.*` refs, a single batched `settings.mutate(web)`, and explicit foreign providers left untouched | `node tools/assert-provider-takeover.mjs` (6) |
936
+ | `assert-provider-takeover.mjs` | provider takeover: both the legacy property API and the rc.2 `config.*` refs, a single batched `settings.mutate(web)`, and explicit foreign providers left untouched | `node tools/assert-provider-takeover.mjs` (8) |
937
937
 
938
938
  Any failure exits non-zero with the failing items listed. Passing the path of an older file shows the pre-fix failures (proving the assertions are not vacuous). Worth running before a release.
939
939
 
package/lib/client.js CHANGED
@@ -388,7 +388,7 @@ window.__ModuleLoader__.load({
388
388
  };
389
389
  const tt = (lang) => I18N[lang === "en" ? "en" : "zh"];
390
390
  // 当前插件版本(与 lib/index.js 的 PLUGIN_VERSION 保持一致;跟随上游 v0.7.5)
391
- const PLUGIN_VERSION = "0.9.3";
391
+ const PLUGIN_VERSION = "0.9.4";
392
392
  const ENGINES = [
393
393
  { id: "auto", label: "Auto · 智能路由", badge: "AUTO", link: null },
394
394
  { id: "multi", label: "Multi Search · 多引擎", badge: "MULTI", link: null },
package/lib/index.js CHANGED
@@ -88,6 +88,8 @@ const MARKET_TO_LANG = {
88
88
  // declares), not the old `$DSH_HOME/settings.yaml` section name.
89
89
  const FREE_SEARCH_NS = "web-search-free";
90
90
  const BRIDGE_PREFIX = "/api/dsh-free-search-settings";
91
+ // apply() 重入保护:见 apply 顶部的注释。
92
+ const appliedContexts = new WeakSet();
91
93
  // 意见 A:zhihu_global / zhihu_site 需要 ZHIHU_API_KEY,不属于免 key 引擎(不得进入 FREE_ENGINES)
92
94
  const FREE_ENGINES = ["ddg", "ddg-lite", "bing", "searxng", "anysearch"];
93
95
  const ALL_ENGINES = ["ddg", "ddg-lite", "bing", "searxng", "anysearch", "exa", "tavily", "keenable", "firecrawl", "parallel", "perplexity", "serpbase", "serply", "deepseek-official", "deepseek-tools", "you", "baidu", "kimi", "aliyun", "doubao", "tinyfish", "jina", "zhihu_global", "zhihu_site", "openai", "gemini", "claude"];
@@ -245,7 +247,7 @@ function routeEngines(query, { timeRange, disabledEngines = [], fallbackOrder =
245
247
  }
246
248
 
247
249
  // 当前插件版本(发布时与 package.json 同步;跟随上游 v0.7.5,不在 PR 里 bump)
248
- const PLUGIN_VERSION = "0.9.3";
250
+ const PLUGIN_VERSION = "0.9.4";
249
251
  // 检查更新的 npm registry 元数据地址(dsh-free-search 是 npmjs 上的公开包)
250
252
  const NPM_REGISTRY_URL = "https://registry.npmjs.org/dsh-free-search/latest";
251
253
  const PLUGIN_NPM_URL = "https://www.npmjs.com/package/dsh-free-search";
@@ -3420,6 +3422,26 @@ function resolveConfig(config) {
3420
3422
  }
3421
3423
 
3422
3424
  function apply(ctx, config) {
3425
+ // 重入保护:同一个 ctx 上 apply 只跑一次。rc.2 的 boot 里出现过未 dispose 的重入,
3426
+ // 会撞上下面的重复注册(provider / route),导致 entry 反复失败把 boot 拖死。
3427
+ if (appliedContexts.has(ctx)) {
3428
+ ctx.logger?.warn?.("free-search: apply() already ran on this context — skipping the re-entry");
3429
+ return;
3430
+ }
3431
+ appliedContexts.add(ctx);
3432
+ // 注册类调用统一做幂等:已存在 / 重复就跳过并记日志,而不是抛错中断 apply。
3433
+ const registerOnce = (call, provider, label) => {
3434
+ try {
3435
+ return call();
3436
+ } catch (error) {
3437
+ const message = String(error?.message ?? error);
3438
+ if (error?.code === "WEB_DUPLICATE_PROVIDER" || /already registered|duplicate/i.test(message)) {
3439
+ ctx.logger?.warn?.(`free-search: ${label} "${provider.id}" is already registered — reusing it`);
3440
+ return () => {};
3441
+ }
3442
+ throw error;
3443
+ }
3444
+ };
3423
3445
  const current = () => resolveConfig(config);
3424
3446
  const logger = ctx.logger;
3425
3447
  // credentials 服务可能晚于本插件挂载(跨 bundle 顺序),运行期动态获取而不是 apply 时缓存
@@ -3932,7 +3954,7 @@ function apply(ctx, config) {
3932
3954
  return runEngineTest(engine, query, timeRange);
3933
3955
  },
3934
3956
  getCredentials
3935
- ).map((route) => sctx.webServer.register(route));
3957
+ ).map((route) => registerOnce(() => sctx.webServer.register(route), { id: route.path }, "bridge route"));
3936
3958
  return () => {
3937
3959
  for (const dispose of disposers) dispose();
3938
3960
  };
@@ -3963,13 +3985,13 @@ function apply(ctx, config) {
3963
3985
  else web.fetchProviderId = id;
3964
3986
  };
3965
3987
 
3966
- ctx.web.registerSearchProvider(provider);
3988
+ registerOnce(() => ctx.web.registerSearchProvider(provider), provider, "search provider");
3967
3989
 
3968
3990
  // web_fetch 质量链接管:注册 fs-quality-fetch(直连 → exa contents → firecrawl → crawl4ai),
3969
3991
  // 未设置 fetchProviderId、或仍是出厂默认 http 时接管;已被显式指向其他 provider 时不抢占。
3970
3992
  // 与 web_search 的接管规则同构:运行时兜底,不依赖 bundle patch 是否落地。
3971
3993
  const fetchProvider = createFetchProvider({ resolveApiKey, logger });
3972
- ctx.web.registerFetchProvider(fetchProvider);
3994
+ registerOnce(() => ctx.web.registerFetchProvider(fetchProvider), fetchProvider, "fetch provider");
3973
3995
 
3974
3996
  // 接管必须先等 settings 服务就绪:rc.2 起 provider 选择是 config 的 volatile ref,
3975
3997
  // 只能通过 settings.mutate 写;apply 期间 settings 尚未挂载,直接取会永远拿不到。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-free-search",
3
- "version": "0.9.3",
3
+ "version": "0.9.4",
4
4
  "description": "Free web search for DeepSeek Harness: 27 engines (Bing/DuckDuckGo/AnySearch/SearXNG/Exa/Tavily/Keenable/Firecrawl/Parallel keyless; Zhihu global/site via MCP and Perplexity/SerpBase/DeepSeek (official + native tools/search)/You.com/Baidu/Kimi/Aliyun/Doubao with key; OpenAI/Gemini/Claude model built-in search, explicit-only) + auto smart routing + global engine enable/disable & fallback priority + editable zh/en language pools + multi mode (parallel full language-pool merge) + multi_search cross-source tool + failure-aware fallback (quota/auth cooldown, transient retry, fallbackOn) + time filtering + platform search + domain blocklist (blockedDomains) + web_fetch, with web settings UI.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",