dsh-free-search 0.9.3 → 0.9.5
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 +42 -4
- package/lib/client.js +1 -1
- package/lib/index.js +56 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -260,6 +260,25 @@ Error: configured web provider "ddg" is not registered
|
|
|
260
260
|
|
|
261
261
|
作用于 **Bing**(`adlt`)、**DuckDuckGo HTML**(`adlt`)、**DuckDuckGo Lite**(`adlt`);其它引擎没有对应参数,不受影响。设置页有下拉框,也可直接在 config 里写 `safeSearch: moderate`。
|
|
262
262
|
|
|
263
|
+
#### fetch provider 接管(与其它 fetch 插件冲突)
|
|
264
|
+
|
|
265
|
+
插件运行时会尝试接管 `web.fetchProvider`:
|
|
266
|
+
|
|
267
|
+
- 未设置、或仍是出厂默认 `http` → 接管为 `fs-quality-fetch`(直连 → exa contents → firecrawl → crawl4ai);
|
|
268
|
+
- 已被**显式指向其它 provider**(例如 `playwright`)→ **不抢占**,只记一条日志(进程内只打一次)。
|
|
269
|
+
|
|
270
|
+
如果你同时装了也会注册 fetch provider 的插件(例如 `dsh-web-fetch-playwright`、`dsh-chatgpt-subscription`),**请在 profile 的 `cordis.patch.yml` 里明确让一方退出**:
|
|
271
|
+
|
|
272
|
+
```yaml
|
|
273
|
+
- id: web
|
|
274
|
+
config:
|
|
275
|
+
fetchProvider: playwright # 用别人的 fetch;本插件只接管 search
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
否则启动阶段可能出现「互相写 `web` config → web 服务重建 → 插件重复 apply」的循环(见 issue #82)。
|
|
279
|
+
|
|
280
|
+
`searchProvider` 的规则相同:未设置或仍是 `deepseek-official` 时接管,显式指定其它 provider 时不抢占。
|
|
281
|
+
|
|
263
282
|
#### 配置文件
|
|
264
283
|
|
|
265
284
|
DSH 0.1.7-rc.1 起,配置跟随 profile 的插件条目保存:设置页与 `/free-search-engine` 都会写入当前 profile 的 `cordis.patch.yml` 中 `web-search-free`(`dsh-free-search`)条目的 `config`。
|
|
@@ -436,7 +455,7 @@ Search engine test:
|
|
|
436
455
|
|
|
437
456
|
#### 离线自检断言(tools/)
|
|
438
457
|
|
|
439
|
-
仓库自带 **17 个**零依赖断言脚本(合计 **
|
|
458
|
+
仓库自带 **17 个**零依赖断言脚本(合计 **504 项**),**不需要 API key、不联网、不需要测试框架**,Node ≥ 20 即可跑:
|
|
440
459
|
|
|
441
460
|
| 脚本 | 覆盖 | 运行(项数) |
|
|
442
461
|
|---|---|---|
|
|
@@ -456,7 +475,7 @@ Search engine test:
|
|
|
456
475
|
| `assert-killtree-win32.mjs` | Windows 子进程树终止(#74):不再依赖 `taskkill` | `node tools/assert-killtree-win32.mjs`(8) |
|
|
457
476
|
| `assert-killtree-portable.mjs` | 跨平台 `killTree` 便携版契约 | `node tools/assert-killtree-portable.mjs`(5) |
|
|
458
477
|
| `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
|
|
478
|
+
| `assert-provider-takeover.mjs` | provider 接管:旧属性 API 与 rc.2 的 `config.*` ref 两套兼容、`settings.mutate(web)` 单批写入、显式指定的 provider 不被抢占、apply 重入/重复注册不再中断、重复日志去重 | `node tools/assert-provider-takeover.mjs`(9) |
|
|
460
479
|
|
|
461
480
|
任何一个失败会以非 0 退出码结束并列出失败项;把某个旧版本文件路径作为参数传进去,可以看到修复前必红(用来证明断言不是空转)。改动发布相关逻辑后建议先跑一遍再发版。
|
|
462
481
|
|
|
@@ -737,6 +756,25 @@ Cooldowns are per-process (reloading the plugin or restarting clears them) and a
|
|
|
737
756
|
|
|
738
757
|
It applies to **Bing** (`adlt`), **DuckDuckGo HTML** (`adlt`), and **DuckDuckGo Lite** (`adlt`). Other engines have no such parameter and are unaffected. There is a dropdown in the settings page; you can also write `safeSearch: moderate` in the config.
|
|
739
758
|
|
|
759
|
+
#### Fetch provider takeover (conflicts with other fetch plugins)
|
|
760
|
+
|
|
761
|
+
At runtime the plugin tries to take over `web.fetchProvider`:
|
|
762
|
+
|
|
763
|
+
- unset, or still the factory default `http` → taken over as `fs-quality-fetch` (direct → exa contents → firecrawl → crawl4ai);
|
|
764
|
+
- **explicitly pointed at another provider** (e.g. `playwright`) → **not stolen**, one log line only (once per process).
|
|
765
|
+
|
|
766
|
+
If you also run a plugin that registers a fetch provider (`dsh-web-fetch-playwright`, `dsh-chatgpt-subscription`, …), **make one of them step aside** in the profile's `cordis.patch.yml`:
|
|
767
|
+
|
|
768
|
+
```yaml
|
|
769
|
+
- id: web
|
|
770
|
+
config:
|
|
771
|
+
fetchProvider: playwright # use theirs; this plugin only takes over search
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
Otherwise startup can loop on "write the `web` config → the web service rebuilds → the plugin re-applies" (see issue #82).
|
|
775
|
+
|
|
776
|
+
`searchProvider` follows the same rule: taken over when unset or still `deepseek-official`, never when explicitly pointed elsewhere.
|
|
777
|
+
|
|
740
778
|
#### Configuration File
|
|
741
779
|
|
|
742
780
|
Since DSH 0.1.7-rc.1 the configuration is stored with the profile's plugin entry: the settings page and `/free-search-engine` both write the `config` of the `web-search-free` (`dsh-free-search`) entry in the active profile's `cordis.patch.yml`.
|
|
@@ -913,7 +951,7 @@ Restart `dsh web` after switching to apply changes.
|
|
|
913
951
|
|
|
914
952
|
#### Offline self-check assertions (`tools/`)
|
|
915
953
|
|
|
916
|
-
**Seventeen** zero-dependency assertion scripts ship with the repo (**
|
|
954
|
+
**Seventeen** zero-dependency assertion scripts ship with the repo (**504 assertions** total): **no API keys, no network, no test framework**, Node ≥ 20:
|
|
917
955
|
|
|
918
956
|
| Script | Covers | Run (count) |
|
|
919
957
|
|---|---|---|
|
|
@@ -933,7 +971,7 @@ Restart `dsh web` after switching to apply changes.
|
|
|
933
971
|
| `assert-killtree-win32.mjs` | Windows process-tree termination (#74): no longer relies on `taskkill` | `node tools/assert-killtree-win32.mjs` (8) |
|
|
934
972
|
| `assert-killtree-portable.mjs` | portable `killTree` contract | `node tools/assert-killtree-portable.mjs` (5) |
|
|
935
973
|
| `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)`,
|
|
974
|
+
| `assert-provider-takeover.mjs` | provider takeover: both the legacy property API and the rc.2 `config.*` refs, a single batched `settings.mutate(web)`, explicit foreign providers left untouched, apply re-entry / duplicate registrations tolerated, and repeated logs deduplicated | `node tools/assert-provider-takeover.mjs` (9) |
|
|
937
975
|
|
|
938
976
|
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
977
|
|
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.
|
|
391
|
+
const PLUGIN_VERSION = "0.9.5";
|
|
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,17 @@ 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();
|
|
93
|
+
// 同一条接管/冲突日志在一个进程里只打一次:rc.2 的启动重入会把同样的 warn 刷到 65ms 一条,
|
|
94
|
+
// 日志噪音会盖住真正的问题(#82)。
|
|
95
|
+
const loggedOnce = new Set();
|
|
96
|
+
const logOnce = (log, level, message) => {
|
|
97
|
+
if (loggedOnce.has(message)) return;
|
|
98
|
+
loggedOnce.add(message);
|
|
99
|
+
const sink = level === "warn" ? log?.warn : log?.info;
|
|
100
|
+
if (typeof sink === "function") sink.call(log, message);
|
|
101
|
+
};
|
|
91
102
|
// 意见 A:zhihu_global / zhihu_site 需要 ZHIHU_API_KEY,不属于免 key 引擎(不得进入 FREE_ENGINES)
|
|
92
103
|
const FREE_ENGINES = ["ddg", "ddg-lite", "bing", "searxng", "anysearch"];
|
|
93
104
|
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 +256,7 @@ function routeEngines(query, { timeRange, disabledEngines = [], fallbackOrder =
|
|
|
245
256
|
}
|
|
246
257
|
|
|
247
258
|
// 当前插件版本(发布时与 package.json 同步;跟随上游 v0.7.5,不在 PR 里 bump)
|
|
248
|
-
const PLUGIN_VERSION = "0.9.
|
|
259
|
+
const PLUGIN_VERSION = "0.9.5";
|
|
249
260
|
// 检查更新的 npm registry 元数据地址(dsh-free-search 是 npmjs 上的公开包)
|
|
250
261
|
const NPM_REGISTRY_URL = "https://registry.npmjs.org/dsh-free-search/latest";
|
|
251
262
|
const PLUGIN_NPM_URL = "https://www.npmjs.com/package/dsh-free-search";
|
|
@@ -3420,6 +3431,26 @@ function resolveConfig(config) {
|
|
|
3420
3431
|
}
|
|
3421
3432
|
|
|
3422
3433
|
function apply(ctx, config) {
|
|
3434
|
+
// 重入保护:同一个 ctx 上 apply 只跑一次。rc.2 的 boot 里出现过未 dispose 的重入,
|
|
3435
|
+
// 会撞上下面的重复注册(provider / route),导致 entry 反复失败把 boot 拖死。
|
|
3436
|
+
if (appliedContexts.has(ctx)) {
|
|
3437
|
+
ctx.logger?.warn?.("free-search: apply() already ran on this context — skipping the re-entry");
|
|
3438
|
+
return;
|
|
3439
|
+
}
|
|
3440
|
+
appliedContexts.add(ctx);
|
|
3441
|
+
// 注册类调用统一做幂等:已存在 / 重复就跳过并记日志,而不是抛错中断 apply。
|
|
3442
|
+
const registerOnce = (call, provider, label) => {
|
|
3443
|
+
try {
|
|
3444
|
+
return call();
|
|
3445
|
+
} catch (error) {
|
|
3446
|
+
const message = String(error?.message ?? error);
|
|
3447
|
+
if (error?.code === "WEB_DUPLICATE_PROVIDER" || /already registered|duplicate/i.test(message)) {
|
|
3448
|
+
ctx.logger?.warn?.(`free-search: ${label} "${provider.id}" is already registered — reusing it`);
|
|
3449
|
+
return () => {};
|
|
3450
|
+
}
|
|
3451
|
+
throw error;
|
|
3452
|
+
}
|
|
3453
|
+
};
|
|
3423
3454
|
const current = () => resolveConfig(config);
|
|
3424
3455
|
const logger = ctx.logger;
|
|
3425
3456
|
// credentials 服务可能晚于本插件挂载(跨 bundle 顺序),运行期动态获取而不是 apply 时缓存
|
|
@@ -3916,6 +3947,15 @@ function apply(ctx, config) {
|
|
|
3916
3947
|
});
|
|
3917
3948
|
// Re-render the dynamic system-prompt engine list whenever this entry's config changes.
|
|
3918
3949
|
sctx.on("settings/document-updated", (ns) => {
|
|
3950
|
+
// 诊断 #82:web 的 provider 选择被写入会让 web 服务重建、依赖它的插件重新 apply。
|
|
3951
|
+
// 记一条(每进程一次),方便判断启动阶段的重入是谁触发的。
|
|
3952
|
+
if (String(ns) === "web") {
|
|
3953
|
+
logOnce(
|
|
3954
|
+
logger,
|
|
3955
|
+
"info",
|
|
3956
|
+
"free-search: the web config was updated — a provider write rebuilds the web service and re-applies this plugin"
|
|
3957
|
+
);
|
|
3958
|
+
}
|
|
3919
3959
|
if (String(ns) === FREE_SEARCH_NS && typeof refreshPrompt === "function") refreshPrompt();
|
|
3920
3960
|
});
|
|
3921
3961
|
});
|
|
@@ -3932,7 +3972,7 @@ function apply(ctx, config) {
|
|
|
3932
3972
|
return runEngineTest(engine, query, timeRange);
|
|
3933
3973
|
},
|
|
3934
3974
|
getCredentials
|
|
3935
|
-
).map((route) => sctx.webServer.register(route));
|
|
3975
|
+
).map((route) => registerOnce(() => sctx.webServer.register(route), { id: route.path }, "bridge route"));
|
|
3936
3976
|
return () => {
|
|
3937
3977
|
for (const dispose of disposers) dispose();
|
|
3938
3978
|
};
|
|
@@ -3963,13 +4003,13 @@ function apply(ctx, config) {
|
|
|
3963
4003
|
else web.fetchProviderId = id;
|
|
3964
4004
|
};
|
|
3965
4005
|
|
|
3966
|
-
ctx.web.registerSearchProvider(provider);
|
|
4006
|
+
registerOnce(() => ctx.web.registerSearchProvider(provider), provider, "search provider");
|
|
3967
4007
|
|
|
3968
4008
|
// web_fetch 质量链接管:注册 fs-quality-fetch(直连 → exa contents → firecrawl → crawl4ai),
|
|
3969
4009
|
// 未设置 fetchProviderId、或仍是出厂默认 http 时接管;已被显式指向其他 provider 时不抢占。
|
|
3970
4010
|
// 与 web_search 的接管规则同构:运行时兜底,不依赖 bundle patch 是否落地。
|
|
3971
4011
|
const fetchProvider = createFetchProvider({ resolveApiKey, logger });
|
|
3972
|
-
ctx.web.registerFetchProvider(fetchProvider);
|
|
4012
|
+
registerOnce(() => ctx.web.registerFetchProvider(fetchProvider), fetchProvider, "fetch provider");
|
|
3973
4013
|
|
|
3974
4014
|
// 接管必须先等 settings 服务就绪:rc.2 起 provider 选择是 config 的 volatile ref,
|
|
3975
4015
|
// 只能通过 settings.mutate 写;apply 期间 settings 尚未挂载,直接取会永远拿不到。
|
|
@@ -3980,13 +4020,17 @@ function apply(ctx, config) {
|
|
|
3980
4020
|
const fetchSelection = providerSelection(ctx.web, "fetchProvider");
|
|
3981
4021
|
if (!fetchSelection.current || fetchSelection.current === "http") {
|
|
3982
4022
|
takeOverOps(ctx.web, "fetchProvider", fetchProvider.id, ops);
|
|
3983
|
-
|
|
4023
|
+
logOnce(
|
|
4024
|
+
logger,
|
|
4025
|
+
"info",
|
|
3984
4026
|
fetchSelection.current === undefined || fetchSelection.current === null
|
|
3985
4027
|
? `free-search: web.fetchProvider was unset, taking over as "${fetchProvider.id}" (direct → exa → firecrawl → crawl4ai)`
|
|
3986
4028
|
: `free-search: taking over from the default fetch provider "${fetchSelection.current}" as "${fetchProvider.id}" (direct → exa → firecrawl → crawl4ai)`
|
|
3987
4029
|
);
|
|
3988
4030
|
} else if (fetchSelection.current !== fetchProvider.id) {
|
|
3989
|
-
|
|
4031
|
+
logOnce(
|
|
4032
|
+
logger,
|
|
4033
|
+
"warn",
|
|
3990
4034
|
`free-search: fetch is served by provider "${fetchSelection.current}", not "${fetchProvider.id}" — web_fetch keeps its current provider. ` +
|
|
3991
4035
|
`To use this plugin's fetch chain, set this in the profile's cordis.patch.yml:\n` +
|
|
3992
4036
|
`- id: web\n config:\n fetchProvider: ${fetchProvider.id}`
|
|
@@ -4002,13 +4046,17 @@ function apply(ctx, config) {
|
|
|
4002
4046
|
const searchSelection = providerSelection(ctx.web, "searchProvider");
|
|
4003
4047
|
if (!searchSelection.current || searchSelection.current === OFFICIAL_SEARCH_PROVIDER_ID) {
|
|
4004
4048
|
takeOverOps(ctx.web, "searchProvider", provider.id, ops);
|
|
4005
|
-
|
|
4049
|
+
logOnce(
|
|
4050
|
+
logger,
|
|
4051
|
+
"info",
|
|
4006
4052
|
searchSelection.current === undefined || searchSelection.current === null
|
|
4007
4053
|
? `free-search: web.searchProvider was unset, taking over as "${provider.id}"`
|
|
4008
4054
|
: `free-search: taking over from the default provider "${searchSelection.current}" as "${provider.id}"`
|
|
4009
4055
|
);
|
|
4010
4056
|
} else if (searchSelection.current !== provider.id) {
|
|
4011
|
-
|
|
4057
|
+
logOnce(
|
|
4058
|
+
logger,
|
|
4059
|
+
"warn",
|
|
4012
4060
|
`free-search: search is served by provider "${searchSelection.current}", not "${provider.id}" — web_search will NOT use this plugin. ` +
|
|
4013
4061
|
`Add this to the profile's cordis.patch.yml (patch replaces the whole entry config, so keep any other web.* fields):\n` +
|
|
4014
4062
|
`- id: web\n config:\n searchProvider: ${provider.id}\n fetchProvider: http`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-free-search",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.5",
|
|
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",
|