dsh-free-search 0.9.0 → 0.9.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 +118 -36
- package/lib/client.js +82 -7
- package/lib/index.js +164 -49
- package/lib/multi_policy.js +271 -207
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -29,21 +29,23 @@ dsh 默认的搜索 provider 依赖 DeepSeek 官方 API key(`DEEPSEEK_API_KEY`
|
|
|
29
29
|
### 特性
|
|
30
30
|
|
|
31
31
|
- **零成本** —— 多个免费引擎,无需 key、无需注册
|
|
32
|
-
- **多引擎可选**:DuckDuckGo(html/lite)、Bing、SearXNG(元搜索,支持自定义实例)、AnySearch、Exa、Tavily、Keenable、Firecrawl、Parallel、Perplexity、SerpBase、Serply、DeepSeek 官方、You.com、百度千帆、Kimi、阿里云百炼、火山联网搜索(豆包搜索)、知乎全网/站内(`zhihu_global`/`zhihu_site`,经知乎开放平台 MCP)、OpenAI / Gemini / Claude 模型内置搜索
|
|
32
|
+
- **多引擎可选**:DuckDuckGo(html/lite)、Bing、SearXNG(元搜索,支持自定义实例)、AnySearch、Exa、Tavily、Keenable、Firecrawl、Parallel、Perplexity、SerpBase、Serply、DeepSeek 官方、DeepSeek 原生搜索 API、You.com、百度千帆、Kimi、阿里云百炼、火山联网搜索(豆包搜索)、知乎全网/站内(`zhihu_global`/`zhihu_site`,经知乎开放平台 MCP)、OpenAI / Gemini / Claude 模型内置搜索
|
|
33
33
|
- **网页设置页** —— 引擎切换 + API key 配置(UI 中 key 脱敏显示"已配置")+ 中英文切换;入口在左侧「插件」页的组件行配置(`plugins.row.config`,DSH 0.1.7-rc.1+)
|
|
34
34
|
- **弹出式切换命令** —— 聊天框输入 `/free-search-engine`,弹出引擎选择窗口,点选即切换(等效设置页 + 保存)
|
|
35
35
|
- **引擎测试** —— `free_search_test` 工具让 agent 一键测试所有引擎;设置页也有"测试引擎"按钮(直测当前引擎,不走回退链,付费引擎无 key 会明确报错)
|
|
36
36
|
- **全局引擎开关与回退优先级** —— 设置页可勾选/取消引擎(取消后全局生效:普通搜索 / Auto / `advanced_search` / `multi_search` / 引擎测试都不再用它),并可用 ↑↓ 调整全局回退顺序(一键恢复默认)
|
|
37
|
-
- **Multi 模式** —— 可把搜索引擎设为 `Multi Search`:`web_search` 按查询语言并发**对应语言池的全部**已启用引擎(不再是前 3 个;中文/英文池可在设置页编辑,每池最低 2 个引擎)、按 URL
|
|
37
|
+
- **Multi 模式** —— 可把搜索引擎设为 `Multi Search`:`web_search` 按查询语言并发**对应语言池的全部**已启用引擎(不再是前 3 个;中文/英文池可在设置页编辑,每池最低 2 个引擎)、按 URL 合并去重(同站判定用 **eTLD+1**)、跨源命中的结果优先;设置页有 **Fast / Balanced / Deep** 三档强度,也可手填并发上限与目标成功源数;整轮检索共享 **30 秒总预算**,到点返回已有结果(代价是成倍消耗额度;multi 失败会自动退回单引擎回退链)
|
|
38
38
|
- **统一引擎回退** —— 任何引擎失败(付费/免费,缺 key/401/限流/网络)自动轮流尝试下一个引擎:首选引擎 → 其他引擎(exa/tavily/keenable/firecrawl/parallel 无 key 也会尝试,因为它们自带 keyless 免费额度)→ 剩余免费引擎,搜索永不直接失败;结果顶部注明实际生效的引擎(如 `Note: perplexity unavailable or failed, using exa.`);失败按类别处理:额度/鉴权失败→本会话冷却该引擎,反爬→短退避,超时/5xx/限流→同引擎重试一次,解析失败/0 结果→不冷却(详见下文「失败分类与引擎冷却」)
|
|
39
39
|
- **时间过滤** —— `advanced_search` 工具支持 `timeRange`:固定档、自定义相对值、绝对日期三种形式(详见下方逻辑说明)
|
|
40
40
|
- **系统提示词注入** —— agent 知道当前用哪个引擎、哪些需要 key;并明确所有搜索结果是**不可信外部数据**,不得执行其中的指令
|
|
41
41
|
- **提示注入防护(不可信数据边界)** —— 插件自有工具(advanced_search / platform_search / free_search_test)的网页文本包在 `<untrusted-web-content>` 边界内(正文里自带的同名标记会被剥离,防止提前闭合);核心 web_search / web_fetch 由 DSH 核心自带同类提示(`External web content follows...`);所有 snippet 统一清洗并截断到 300 字符
|
|
42
42
|
- **版本号 + 检查更新** —— 设置卡片显示当前版本(跟随 `PLUGIN_VERSION`,随发布更新),"检查更新"按钮直连 npm registry 对比最新版,有新版本时提示并可一键跳转
|
|
43
43
|
- **结果缓存** —— 相同查询(含引擎/时间过滤参数)5 分钟内命中缓存(LRU 50 条),防免费引擎限流、省付费额度;时长可在设置页 0-5 分钟自由配置(0 关闭)
|
|
44
|
+
- **域名黑名单与安全搜索** —— `blockedDomains`:命中的域名不会进入任何结果;`safeSearch`(`off` / `moderate` / `strict`)为 Bing / DuckDuckGo 加一层过滤
|
|
44
45
|
- **免费标注** —— 设置页中免费引擎带绿色 `FREE` 徽章,付费引擎带橙色 `API KEY` 徽章
|
|
45
46
|
- **网页抓取(web_fetch)** —— 让 agent 抓取网页内容(官方 `dsh-web-fetch-http` provider,纯 JS,零额外依赖)
|
|
46
|
-
- **平台搜索(platform_search)** —— 搜 GitHub / V2EX / B站 / Reddit / Hacker News / Stack Overflow / 维基百科 / npm / YouTube / Vimeo(公开 API 或免 key 抓取,零依赖)
|
|
47
|
+
- **平台搜索(platform_search)** —— 搜 GitHub / V2EX / B站 / Reddit / Hacker News / Stack Overflow / 维基百科 / npm / YouTube / Vimeo / 掘金 / 豆瓣 / Qiita(公开 API 或免 key 抓取,零依赖)
|
|
48
|
+
- **垂直领域直查(domain_search)** —— 41 个免 key 垂直源直查:包/版本(PyPI、crates、GitHub releases、endoflife…)、学术(arXiv、Crossref、OpenAlex、Semantic Scholar、PubMed、dblp、Zenodo…)、安全(NVD、OSV、CISA KEV)、金融(SEC EDGAR、IMF、World Bank、CFETS、Yahoo Finance、CoinGecko…)、**法律(US Federal Register、US CourtListener、openlegaldata、UK legislation)**、新闻(Google News、GDELT)等
|
|
47
49
|
- **视频搜索(video_search)** —— 跨站找视频:Bing Videos + DuckDuckGo Videos(免 key,失败互相回退),返回视频链接、标题与来源/时长等元信息
|
|
48
50
|
- **一次调用完成研究(research)** —— 多引擎并发搜索 → 候选重排 → 分层抓取正文 → 段落选窗 → 带出处的 Markdown 证据包;模型组件(重排/嵌入/决策模型)全部可选,缺失或失败时静默回退规则路径
|
|
49
51
|
- **检索正文缓存(cache_search)** —— 全文检索 `research` 抓过的正文(`node:sqlite` 持久缓存,存储 7 天;`node:sqlite` 不可用时自动降级为 no-op,免 flag 需 ≥ 22.13,22.5–22.12 需 `--experimental-sqlite`)
|
|
@@ -69,7 +71,8 @@ dsh 默认的搜索 provider 依赖 DeepSeek 官方 API key(`DEEPSEEK_API_KEY`
|
|
|
69
71
|
| `perplexity` | Perplexity | 付费 | 需 `PERPLEXITY_API_KEY` |
|
|
70
72
|
| `serpbase` | SerpBase | 付费 | 需 `SERPBASE_API_KEY`(serpbase.dev,注册送 100 次免费额度) |
|
|
71
73
|
| `serply` | Serply | 付费 | 需 `SERPLY_API_KEY`(serply.io,新账户 30 天内 2,500 次免费额度);Google 网页结果,按设置页语言本地化 |
|
|
72
|
-
| `deepseek-official` | DeepSeek 官方 | 付费 | 需 `DEEPSEEK_API_KEY` |
|
|
74
|
+
| `deepseek-official` | DeepSeek 官方 | 付费 | 需 `DEEPSEEK_API_KEY`;走 Anthropic 兼容的 `web_search` 服务端工具,每次搜索是一整个模型轮次(慢、耗 token) |
|
|
75
|
+
| `deepseek-tools` | DeepSeek 原生搜索 API | 付费 | 需 `DEEPSEEK_API_KEY`(与上一档共用);直接 `POST /v1/tools/search`,无模型参与,返回结构化 `results[]`(`title`/`url`/`summary`/`date`),同 query 实测 ~0.3s vs 上一档 ~5-6s |
|
|
73
76
|
| `you` | You.com | 付费 | 需 `YOUCOM_API_KEY`(you.com/platform/api-keys,注册即送免费额度) |
|
|
74
77
|
| `baidu` | 百度千帆 AI 搜索 | 额度 | 需 `BAIDU_API_KEY`(千帆 AI 搜索,每日赠 50 次,超额按量后付费);中文全网,支持时间过滤 |
|
|
75
78
|
| `kimi` | Kimi(Moonshot)联网搜索 | 付费 | 需 `MOONSHOT_API_KEY`(basic 约 ¥0.01/次),返回带正文 chunks 的中文结果;**无免费额度**,适合显式选用或作质量升级池,不建议放进默认并发池 |
|
|
@@ -107,7 +110,7 @@ dsh 默认的搜索 provider 依赖 DeepSeek 官方 API key(`DEEPSEEK_API_KEY`
|
|
|
107
110
|
| ① 无限免 key | `ddg` / `ddg-lite` / `bing` / `searxng` / `anysearch` | — |
|
|
108
111
|
| ② 匿名额度(不配 key 也能跑) | `exa` / `tavily` / `keenable` / `firecrawl` / `parallel` | 配 key 提额 |
|
|
109
112
|
| ③ 免费额度(需 key) | `zhihu_global` / `zhihu_site`(每日 5,000 次试用)、`doubao`(每月 500 次)、`baidu`(每日 50 次)、`aliyun`(新用户 200 次包) | 按量后付费 |
|
|
110
|
-
| ④ 付费升级(需 key 才有产出) | `serpbase`(注册送 100 次)/ `serply`(30 天 2,500 次)/ `you` / `perplexity` / `deepseek-official` / `kimi` | 按量 / 按次 |
|
|
113
|
+
| ④ 付费升级(需 key 才有产出) | `serpbase`(注册送 100 次)/ `serply`(30 天 2,500 次)/ `you` / `perplexity` / `deepseek-official` / `deepseek-tools` / `kimi` | 按量 / 按次 |
|
|
111
114
|
| ⑤ 按次计费(模型内置,显式-only) | `openai` / `gemini` / `claude` | 按 token + 每次搜索 |
|
|
112
115
|
|
|
113
116
|
选默认池的经验:**① 打底 + ③ 打主力**;② 可以进但要知道它在烧匿名额度;④ 只在你确实需要该源时显式选用;⑤ 永不入池。完整的逐引擎额度事实与默认池提案见 [docs/engine-quota-tiers.md](./docs/engine-quota-tiers.md)。
|
|
@@ -192,7 +195,7 @@ Error: configured web provider "ddg" is not registered
|
|
|
192
195
|
- **推荐**:付费引擎 key 建议写入 harness 凭据中心 `~/.dsh/.credentials.yaml`(如 `DEEPSEEK_API_KEY: sk-...`,与官方 LLM provider 一致,一处管理所有 key)。插件读取优先级:凭据中心 > 设置页 > 环境变量,设置页填的 key 仅作为遗留兼容。
|
|
193
196
|
- **Test engine**:直测当前引擎可用性(不走回退链,付费引擎无 key 会明确报错)
|
|
194
197
|
- **Use Bing default**:把当前搜索引擎切回稳定的免费 Bing;`Discard` 只撤销尚未保存的编辑
|
|
195
|
-
- **Platform search**:勾选启用 GitHub / V2EX / Bilibili / Reddit / Hacker News / Stack Overflow / 维基百科 / npm / YouTube / Vimeo 平台搜索(`platform_search` 工具按此过滤)
|
|
198
|
+
- **Platform search**:勾选启用 GitHub / V2EX / Bilibili / Reddit / Hacker News / Stack Overflow / 维基百科 / npm / YouTube / Vimeo / 掘金 / 豆瓣 / Qiita 平台搜索(`platform_search` 工具按此过滤)
|
|
196
199
|
- **EN / 中文**:切换界面语言(默认中文)
|
|
197
200
|
|
|
198
201
|
<table align="center" style="border: none; border-collapse: collapse;">
|
|
@@ -245,6 +248,18 @@ Error: configured web provider "ddg" is not registered
|
|
|
245
248
|
|
|
246
249
|
`fallbackOn` 控制哪些失败类别**允许**触发回退:默认勾选全部六类,即当前的「搜索永不直接失败」;取消某类后,遇到该类失败会**停下并把该引擎的错误报出**,不再换引擎(例如去掉 `invalid-response`:首选引擎返回空/解析失败时直接报错,而不是静默退回低质引擎)。设为空数组 `[]` 则任何失败都立即中止。注意**缺 key、被禁用、不支持时间过滤不算失败**,无论怎样都照常换引擎。设置页有对应勾选框,也可在 config 写 `fallbackOn: [quota, auth, bot-wall, transient, invalid-response, unknown]`。
|
|
247
250
|
|
|
251
|
+
#### 安全搜索(safeSearch)
|
|
252
|
+
|
|
253
|
+
`safeSearch` 控制内置来源是否额外套一层安全过滤,默认 `off`(不加参数,保持引擎自身默认行为):
|
|
254
|
+
|
|
255
|
+
| 值 | 含义 |
|
|
256
|
+
|---|---|
|
|
257
|
+
| `off`(默认) | 不加过滤参数,保持引擎默认行为 |
|
|
258
|
+
| `moderate` | 过滤成人内容 |
|
|
259
|
+
| `strict` | 最严格过滤 |
|
|
260
|
+
|
|
261
|
+
作用于 **Bing**(`adlt`)、**DuckDuckGo HTML**(`adlt`)、**DuckDuckGo Lite**(`adlt`);其它引擎没有对应参数,不受影响。设置页有下拉框,也可直接在 config 里写 `safeSearch: moderate`。
|
|
262
|
+
|
|
248
263
|
#### 配置文件
|
|
249
264
|
|
|
250
265
|
DSH 0.1.7-rc.1 起,配置跟随 profile 的插件条目保存:设置页与 `/free-search-engine` 都会写入当前 profile 的 `cordis.patch.yml` 中 `web-search-free`(`dsh-free-search`)条目的 `config`。
|
|
@@ -253,13 +268,19 @@ DSH 0.1.7-rc.1 起,配置跟随 profile 的插件条目保存:设置页与 `
|
|
|
253
268
|
|
|
254
269
|
```yaml
|
|
255
270
|
# profiles/<profile>/cordis.patch.yml 中该条目的 config:
|
|
256
|
-
provider: bing # ddg / ddg-lite / bing / searxng / anysearch / exa / tavily / keenable / firecrawl / parallel / perplexity / serpbase / serply / deepseek-official / you / baidu / kimi / aliyun / doubao / zhihu_global / zhihu_site / openai / gemini / claude / auto / multi
|
|
271
|
+
provider: bing # ddg / ddg-lite / bing / searxng / anysearch / exa / tavily / keenable / firecrawl / parallel / perplexity / serpbase / serply / deepseek-official / deepseek-tools / you / baidu / kimi / aliyun / doubao / zhihu_global / zhihu_site / openai / gemini / claude / auto / multi
|
|
257
272
|
fallbackOn: [quota, auth, bot-wall, transient, invalid-response, unknown] # 允许触发回退的失败类别;[] = 任何失败都立即中止(默认全部)
|
|
258
273
|
lang: zh # 设置页界面语言(zh / en)
|
|
259
274
|
bingMarket: zh-CN # Bing 市场
|
|
260
275
|
region: cn-zh # DuckDuckGo 区域(可选)
|
|
261
276
|
searxngInstances: # 自定义 SearXNG 实例(可选)
|
|
262
277
|
- https://your-instance.example
|
|
278
|
+
searxngBearerToken: ... # 可选:发送 Authorization: Bearer <token>(设置页「SearXNG Bearer Token」同款)
|
|
279
|
+
safeSearch: off # 安全搜索:off(默认)/ moderate / strict,作用于 bing / ddg / ddg-lite
|
|
280
|
+
blockedDomains: # 域名黑名单(可选):命中的域名不会进入结果
|
|
281
|
+
- spam-example.com
|
|
282
|
+
multiMaxEngines: 0 # multi 并发上限(0 = 不限,跑完整语言池);设置页有 Fast/Balanced/Deep 档位
|
|
283
|
+
multiMinSources: 1 # multi 目标成功源数(只用于触发补跑,不是硬下限)
|
|
263
284
|
exaApiKey: ... # 或通过设置页填写
|
|
264
285
|
tavilyApiKey: ... # 或通过设置页填写
|
|
265
286
|
keenableApiKey: ... # 或通过设置页填写
|
|
@@ -306,10 +327,11 @@ Search engine test:
|
|
|
306
327
|
| SearXNG | `time_range` | ⚠️ 近似 | 同上 |
|
|
307
328
|
| DuckDuckGo / Lite | `df` | ⚠️ 近似 | 同上 |
|
|
308
329
|
| Bing / AnySearch | — | ❌ 忽略 | 无对应参数 |
|
|
330
|
+
| DeepSeek 原生搜索(`deepseek-tools`) | query 语法 `after:` | ✅ 精确 | 端点无独立时间字段,改为把 `after:YYYY-MM-DD` 拼进 query;相对天数按 N 天前折算 |
|
|
309
331
|
|
|
310
332
|
**"最近似档位"映射规则**:`≤2 天 → day`,`≤14 天 → week`,`≤90 天 → month`,否则 `year`。例如 `3d` 在 Tavily 上按 `day` 处理,`2mo` 按 `month` 处理。
|
|
311
333
|
|
|
312
|
-
**引擎链优先级**:当带 timeRange 搜索时,支持时间过滤的引擎(tavily / exa / keenable / firecrawl / parallel / searxng / ddg / ddg-lite)会排到引擎链前面,确保过滤真正生效——即使首选引擎是 bing(不支持过滤),也会先尝试支持过滤的引擎。
|
|
334
|
+
**引擎链优先级**:当带 timeRange 搜索时,支持时间过滤的引擎(tavily / exa / keenable / firecrawl / parallel / searxng / ddg / ddg-lite / deepseek-tools)会排到引擎链前面,确保过滤真正生效——即使首选引擎是 bing(不支持过滤),也会先尝试支持过滤的引擎。
|
|
313
335
|
|
|
314
336
|
示例对话:*"帮我搜最近 3 天关于 DSH 的新闻"* → agent 调用 `advanced_search`,`timeRange: "3d"`。
|
|
315
337
|
|
|
@@ -318,7 +340,19 @@ Search engine test:
|
|
|
318
340
|
当需要对重要问题做**多源交叉验证**、避免单一引擎偏差或单源死锁时,可以让 agent 调用 `multi_search` 工具:
|
|
319
341
|
|
|
320
342
|
- **并发请求**:默认按查询语言并发**对应语言池的全部**引擎(不再是前 3 个;或显式传入 `engines` 列表),各引擎独立解析 API Key 与容错(缺 key 引擎自动跳过,不阻断其他引擎)。
|
|
321
|
-
-
|
|
343
|
+
- **三档强度**(设置页单选按钮,选完即写入配置):
|
|
344
|
+
|
|
345
|
+
| 档位 | 并发上限 | 目标成功源数 |
|
|
346
|
+
|---|---|---|
|
|
347
|
+
| `Fast · 1/1` | 1 | 1 |
|
|
348
|
+
| `Balanced · 3/1` | 3 | 1 |
|
|
349
|
+
| `Deep · 6/2` | 6 | 2 |
|
|
350
|
+
|
|
351
|
+
手填下面的数字即为「自定义」,不会有档位按钮处于选中态。
|
|
352
|
+
- **并发上限**(`multiMaxEngines`,默认 `0` = **不限**,跑完整语言池):这是**天花板**,不是下限——实际并发数取「上限」与「凭据合格引擎数」的较小值;凭据判定在截断**之前**完成,缺 key 的引擎不会白占名额(所以 3 个可用引擎 + 上限 6 仍然全部跑)。
|
|
353
|
+
- **目标成功源数**(`multiMinSources`,默认 `1`):**不是硬下限**。只用来触发补跑——已返回源数不足时,按域名轮转顺序**串行补跑**剩余的合格引擎;补跑跑完或时间到就返回已有结果(拿不满不报错)。
|
|
354
|
+
- **总预算 30 秒**(`SEARCH_BUDGET_MS`):并发扇出 + 全部补跑共享同一个 deadline,到点即停并返回**已经拿到的结果**,不会因为某个慢引擎把整次搜索拖死。
|
|
355
|
+
- **去重与合并**:按规范化 URL 去重(去掉结尾斜杠与已审计的 tracking 参数),多引擎共同命中的条目优先置顶并附带 `seenIn` 命中来源清单(如 `[seen in: bing, exa]`);**同站判定用 eTLD+1**——`a.example.co.uk` 与 `b.example.co.uk` 视为同一站点,域外多样性轮转在**站点**粒度上做,不会被多个子域刷屏。
|
|
322
356
|
- **不可信边界与清洗**:严格遵守 `<untrusted-web-content>` 数据边界,正文 snippet 统一清洗。
|
|
323
357
|
- ⚠️ 注:多源并发会消耗更多 API 配额,建议在需要多角度核验时按需使用。
|
|
324
358
|
|
|
@@ -402,16 +436,26 @@ Search engine test:
|
|
|
402
436
|
|
|
403
437
|
#### 离线自检断言(tools/)
|
|
404
438
|
|
|
405
|
-
仓库自带
|
|
439
|
+
仓库自带 **16 个**零依赖断言脚本(合计 **495 项**),**不需要 API key、不联网、不需要测试框架**,Node ≥ 20 即可跑:
|
|
406
440
|
|
|
407
|
-
| 脚本 | 覆盖 |
|
|
441
|
+
| 脚本 | 覆盖 | 运行(项数) |
|
|
408
442
|
|---|---|---|
|
|
409
|
-
| `assert-engines.mjs` | 引擎清单 / 免费引擎口径 / 语言分池路由 / 池配置字段 / 知乎 URL 剥 utm | `node tools/assert-engines.mjs`(
|
|
410
|
-
| `assert-
|
|
411
|
-
| `assert-
|
|
412
|
-
| `assert-
|
|
413
|
-
| `assert-
|
|
414
|
-
| `assert-
|
|
443
|
+
| `assert-engines.mjs` | 引擎清单 / 免费引擎口径 / 语言分池路由 / 池配置字段 / 知乎 URL 剥 utm / `deepseek-tools` 接线与 snippetCap 放宽 | `node tools/assert-engines.mjs`(41) |
|
|
444
|
+
| `assert-blocked-domains.mjs` | 域名黑名单:解析、匹配、收口,且前后端清洗同口径 | `node tools/assert-blocked-domains.mjs`(69) |
|
|
445
|
+
| `assert-multi-policy.mjs` | multi 执行策略:并发上限是天花板 / 目标成功源非硬下限 / 凭据资格在上限之前分流 / **Fast·Balanced·Deep 档位** | `node tools/assert-multi-policy.mjs`(146) |
|
|
446
|
+
| `assert-result-ranking.mjs` | 合并排序:**eTLD+1 同域判定** / 域名轮转 / `seenIn` 降为末位 / 时效仅带时间过滤时生效 | `node tools/assert-result-ranking.mjs`(31) |
|
|
447
|
+
| `assert-url-norm.mjs` | URL 规范化:只剔已审计的 tracking 参数,保留可能区分资源的参数 | `node tools/assert-url-norm.mjs`(72) |
|
|
448
|
+
| `assert-prompt-contract.mjs` | 系统提示词契约:**检索≠抓取**、不可信边界、模型内置搜索只在显式选中时运行 | `node tools/assert-prompt-contract.mjs`(9) |
|
|
449
|
+
| `assert-research-degradation.mjs` | research 无 key 全降级、`node:sqlite` no-op、90s 预算隔离、缓存目录可配置、`budget.exhausted` 契约名 | `node tools/assert-research-degradation.mjs`(19) |
|
|
450
|
+
| `assert-research-call-wrappers.mjs` | research 四个 `call*` 包装层的替身/底层透传(#75 回归) | `node tools/assert-research-call-wrappers.mjs`(23) |
|
|
451
|
+
| `assert-fetch-chain.mjs` | `fs-quality-fetch` 四层链序、任一层命中即停、exa 失败静默降级 firecrawl、全层失败聚合报错 | `node tools/assert-fetch-chain.mjs`(8) |
|
|
452
|
+
| `assert-single-fetch-layer.mjs` | 抓取层全库只有一份实现、依赖单向、替身键不串层 | `node tools/assert-single-fetch-layer.mjs`(22) |
|
|
453
|
+
| `assert-vertical.mjs` | `domain_search` 41 引擎注册表、每引擎 desc、未知引擎报错、不依赖 research | `node tools/assert-vertical.mjs`(8) |
|
|
454
|
+
| `assert-free-search-test-schema.mjs` | `free_search_test` 输出 schema 与真实产出一致(含 auto/multi 虚拟模式) | `node tools/assert-free-search-test-schema.mjs`(8) |
|
|
455
|
+
| `assert-weather-contract.mjs` | `weather` 工具输出 schema 契约(缺字段裁剪、英文输出、空 geocode 抛错) | `node tools/assert-weather-contract.mjs`(11) |
|
|
456
|
+
| `assert-killtree-win32.mjs` | Windows 子进程树终止(#74):不再依赖 `taskkill` | `node tools/assert-killtree-win32.mjs`(8) |
|
|
457
|
+
| `assert-killtree-portable.mjs` | 跨平台 `killTree` 便携版契约 | `node tools/assert-killtree-portable.mjs`(5) |
|
|
458
|
+
| `assert-parallel-mcp.mjs` | Parallel MCP 引擎:25 s 超时/调用方取消覆盖**响应体读取**、共享 SSE 解析器(通知/多行帧)、6 类错误路径、REST 精确时间过滤 | `node tools/assert-parallel-mcp.mjs`(15) |
|
|
415
459
|
|
|
416
460
|
任何一个失败会以非 0 退出码结束并列出失败项;把某个旧版本文件路径作为参数传进去,可以看到修复前必红(用来证明断言不是空转)。改动发布相关逻辑后建议先跑一遍再发版。
|
|
417
461
|
|
|
@@ -465,16 +509,18 @@ This plugin provides multiple free search engines with automatic fallback, compl
|
|
|
465
509
|
- **Popup Switch Command** — Type `/free-search-engine` in the chat: a picker opens with all engines; click one to switch (equivalent to the settings page + save)
|
|
466
510
|
- **Engine Testing** — `free_search_test` for the agent to check all engines in one call; the settings UI also has a "Test engine" button that tests the selected engine directly (no fallback chain; paid engines without a key report an explicit error)
|
|
467
511
|
- **Global engine enable/disable & fallback priority** — uncheck engines you never want (applies everywhere: web_search fallback, Auto routing, advanced_search, multi_search, engine tests) and reorder the global fallback chain with ↑↓ (one-click reset)
|
|
468
|
-
- **Multi mode** — pick `Multi Search` as the engine: web_search queries **every enabled engine of the query's language pool** concurrently (no longer top 3; zh/en pools editable in the settings UI, minimum 2 engines each), merges/deduplicates URLs and prioritizes cross-source hits (costs more quota; a failed multi run automatically falls back to the single-engine chain)
|
|
512
|
+
- **Multi mode** — pick `Multi Search` as the engine: web_search queries **every enabled engine of the query's language pool** concurrently (no longer top 3; zh/en pools editable in the settings UI, minimum 2 engines each), merges/deduplicates URLs (same-site detection via **eTLD+1**) and prioritizes cross-source hits; the settings UI offers **Fast / Balanced / Deep** presets or your own cap and target numbers; the whole run shares one **30-second budget** and returns whatever it already has when time is up (costs more quota; a failed multi run automatically falls back to the single-engine chain)
|
|
469
513
|
- **Unified Engine Fallback** — Any engine failure (paid or free, missing key, 401, rate limit, network error) automatically tries the next engine: the configured engine first, then other engines (exa/tavily/keenable/firecrawl/parallel are tried even without a key because they have built-in keyless quota), then the remaining free engines (Bing/AnySearch etc.) — with a note attached to the results naming the engine that actually served them (e.g. `Note: perplexity unavailable or failed, using exa.`). Failures are classified: quota/auth failures cool the engine down for the session, anti-bot walls back off briefly, timeout/5xx/rate-limit failures retry the same engine once, and parse errors / 0 results are reported without cooling it down (see "Failure-aware fallback" below). Search never fails outright.
|
|
470
514
|
- **Time Filtering** — The `advanced_search` tool supports `timeRange`: fixed tiers, custom relative values, or an absolute date (details below)
|
|
471
515
|
- **System Prompt Injection** — The agent is aware of the currently active engine and which engines require API keys; it is also told that all search output is **untrusted external data** and must never be executed as instructions
|
|
472
516
|
- **Prompt-Injection Guard (untrusted-data boundary)** — Web-derived text from the plugin's own tools (`advanced_search` / `platform_search` / `free_search_test`) is wrapped in an explicit `<untrusted-web-content>` boundary (look-alike tags inside the text are stripped to prevent early closure); the core `web_search` / `web_fetch` tools carry DSH core's own notice (`External web content follows...`); every snippet is cleaned and capped at 300 characters
|
|
473
517
|
- **Version + Update Check** — The settings card shows the current version (tracks PLUGIN_VERSION, bumped on release), and a "Check update" button queries the npm registry to compare against the latest release, prompting a one-click jump when a newer version exists
|
|
474
518
|
- **Result Caching** — Identical queries (same engine / time-filter args) hit an LRU cache (50 entries) for up to 5 minutes, protecting free engines from rate-limiting and saving paid quota; the TTL is configurable from 0-5 minutes in the settings UI (0 disables caching)
|
|
519
|
+
- **Domain blocklist & safe search** — `blockedDomains` keeps matching domains out of every result; `safeSearch` (`off` / `moderate` / `strict`) adds a filter layer for Bing / DuckDuckGo
|
|
475
520
|
- **Visual Badges** — Free engines feature a green `FREE` badge, while paid engines show an orange `API KEY` badge in the settings UI
|
|
476
521
|
- **Webpage Fetching (`web_fetch`)** — Allows the agent to read full webpage contents (official `dsh-web-fetch-http` provider, pure JS, zero extra dependencies)
|
|
477
|
-
- **Platform Search (`platform_search`)** — Search GitHub / V2EX / Bilibili / Reddit / Hacker News / Stack Overflow / Wikipedia / npm / YouTube / Vimeo (public APIs or keyless scraping, zero extra dependencies)
|
|
522
|
+
- **Platform Search (`platform_search`)** — Search GitHub / V2EX / Bilibili / Reddit / Hacker News / Stack Overflow / Wikipedia / npm / YouTube / Vimeo / Juejin / Douban / Qiita (public APIs or keyless scraping, zero extra dependencies)
|
|
523
|
+
- **Vertical Lookups (`domain_search`)** — Direct lookup in 41 keyless vertical sources: packages/versions (PyPI, crates, GitHub releases, endoflife…), academia (arXiv, Crossref, OpenAlex, Semantic Scholar, PubMed, dblp, Zenodo…), security (NVD, OSV, CISA KEV), finance (SEC EDGAR, IMF, World Bank, CFETS, Yahoo Finance, CoinGecko…), **law (US Federal Register, US CourtListener, openlegaldata, UK legislation)**, news (Google News, GDELT) and more
|
|
478
524
|
- **Video Search (`video_search`)** — Find videos across the web via Bing Videos + DuckDuckGo Videos (keyless, mutual fallback), returning video links with titles and publisher/duration metadata
|
|
479
525
|
- **One-Call Research (`research`)** — Concurrent multi-engine search → candidate rerank → tiered body fetch → window selection → an evidence pack with provenance as Markdown; every model component (rerank / embedding / decision model) is optional and silently falls back to the rule path when missing or failing
|
|
480
526
|
- **Search Fetched Bodies (`cache_search`)** — Full-text search over the bodies `research` already fetched (`node:sqlite` persistent cache, 7-day retention; degrades to a no-op whenever `node:sqlite` is unavailable — unflagged from 22.13, while 22.5–22.12 needs `--experimental-sqlite`)
|
|
@@ -500,7 +546,8 @@ If this plugin has been helpful, a ⭐ on [GitHub](https://github.com/DDDMUC/dsh
|
|
|
500
546
|
| `perplexity` | Perplexity | Paid | Requires `PERPLEXITY_API_KEY` |
|
|
501
547
|
| `serpbase` | SerpBase | Paid | Requires `SERPBASE_API_KEY` (serpbase.dev, 100 free queries on signup) |
|
|
502
548
|
| `serply` | Serply | Paid | Requires `SERPLY_API_KEY` (serply.io, 2,500 free credits for the first 30 days); Google web results localized to the settings language |
|
|
503
|
-
| `deepseek-official` | DeepSeek Official | Paid | Requires `DEEPSEEK_API_KEY` |
|
|
549
|
+
| `deepseek-official` | DeepSeek Official | Paid | Requires `DEEPSEEK_API_KEY`; uses the Anthropic-compatible `web_search` server tool, so every search is a full model turn (slow, token-billed) |
|
|
550
|
+
| `deepseek-tools` | DeepSeek Native Search API | Paid | Requires `DEEPSEEK_API_KEY` (shared with the row above); calls `POST /v1/tools/search` directly, no model involved, returns structured `results[]` (`title`/`url`/`summary`/`date`) — measured ~0.3s vs ~5-6s for the row above on the same query |
|
|
504
551
|
| `you` | You.com | Paid | Requires `YOUCOM_API_KEY` (you.com/platform/api-keys, free tier on signup) |
|
|
505
552
|
| `baidu` | Baidu Qianfan AI Search | Quota | Requires `BAIDU_API_KEY` (Qianfan AI Search, 50 free calls/day then pay-as-you-go); Chinese web-wide, supports time filtering |
|
|
506
553
|
| `kimi` | Kimi (Moonshot) Web Search | Paid | Requires `MOONSHOT_API_KEY` (basic ~¥0.01/call); returns Chinese results with body chunks; **no free quota**, so prefer explicit selection or the escalation pool over the default concurrent pool |
|
|
@@ -539,7 +586,7 @@ The free/paid binary is not what decides **who belongs in a default pool** — t
|
|
|
539
586
|
| ① Unlimited, no key | `ddg` / `ddg-lite` / `bing` / `searxng` / `anysearch` | — |
|
|
540
587
|
| ② Anonymous quota (runs without a key) | `exa` / `tavily` / `keenable` / `firecrawl` / `parallel` | key raises the quota |
|
|
541
588
|
| ③ Free quota (key required) | `zhihu_global` / `zhihu_site` (5,000/day trial), `doubao` (500/month), `baidu` (50/day), `aliyun` (200-call starter pack) | pay-as-you-go |
|
|
542
|
-
| ④ Paid upgrade (needs a key to return anything) | `serpbase` (100 free on signup) / `serply` (2,500 for 30 days) / `you` / `perplexity` / `deepseek-official` / `kimi` | per call / per query |
|
|
589
|
+
| ④ Paid upgrade (needs a key to return anything) | `serpbase` (100 free on signup) / `serply` (2,500 for 30 days) / `you` / `perplexity` / `deepseek-official` / `deepseek-tools` / `kimi` | per call / per query |
|
|
543
590
|
| ⑤ Per-call billing (model built-in, explicit-only) | `openai` / `gemini` / `claude` | per token + per search |
|
|
544
591
|
|
|
545
592
|
Rule of thumb for a default pool: **① as the floor + ③ as the workhorse**; ② is allowed but remember it burns anonymous quota; ④ only when you actually want that source; ⑤ never enters a pool. Per-engine facts and a default-pool proposal live in [docs/engine-quota-tiers.md](./docs/engine-quota-tiers.md).
|
|
@@ -624,7 +671,7 @@ The config page provides:
|
|
|
624
671
|
- **Recommended**: store paid-engine keys in the harness credential center `~/.dsh/.credentials.yaml` (e.g. `DEEPSEEK_API_KEY: sk-...`, same as the official LLM providers — one place for all keys). Resolution order: credentials center > settings page > environment variable; the settings-page fields remain for backward compatibility.
|
|
625
672
|
- **Test engine**: Tests the selected engine directly (no fallback chain; paid engines without a key report an explicit error).
|
|
626
673
|
- **Use Bing default**: stage a switch back to the stable free Bing engine; `Discard` only cancels unsaved edits
|
|
627
|
-
- **Platform search**: check platforms (GitHub / V2EX / Bilibili / Reddit / HN / Stack Overflow / Wikipedia / npm / YouTube / Vimeo) to enable them for the `platform_search` tool (disabled platforms are skipped).
|
|
674
|
+
- **Platform search**: check platforms (GitHub / V2EX / Bilibili / Reddit / HN / Stack Overflow / Wikipedia / npm / YouTube / Vimeo / Juejin / Douban / Qiita) to enable them for the `platform_search` tool (disabled platforms are skipped).
|
|
628
675
|
- **EN / 中文**: toggle the interface language (default Chinese).
|
|
629
676
|
|
|
630
677
|
<table align="center" style="border: none; border-collapse: collapse;">
|
|
@@ -677,6 +724,18 @@ Cooldowns are per-process (reloading the plugin or restarting clears them) and a
|
|
|
677
724
|
|
|
678
725
|
`fallbackOn` decides which failure classes are **allowed** to trigger a fallback: by default all six are checked, i.e. today's "search never fails outright". Unchecking a class makes such a failure **stop the search and surface that engine's error** instead of trying another engine (e.g. drop `invalid-response` and a preferred engine that returns nothing/parses badly fails loudly rather than silently handing you a lower-quality engine's results). An empty array `[]` aborts on any failure. Note that **missing keys, disabled engines and unsupported time filters are not failures** and always advance. There is a matching checkbox group in the settings page, and you can also write `fallbackOn: [quota, auth, bot-wall, transient, invalid-response, unknown]` in the config.
|
|
679
726
|
|
|
727
|
+
#### Safe Search (`safeSearch`)
|
|
728
|
+
|
|
729
|
+
`safeSearch` controls whether the built-in sources apply an extra safety filter. Default is `off` (no parameter is added — each engine keeps its own default):
|
|
730
|
+
|
|
731
|
+
| Value | Meaning |
|
|
732
|
+
|---|---|
|
|
733
|
+
| `off` (default) | no filter parameter; engine default behavior |
|
|
734
|
+
| `moderate` | filter adult content |
|
|
735
|
+
| `strict` | strictest filtering |
|
|
736
|
+
|
|
737
|
+
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.
|
|
738
|
+
|
|
680
739
|
#### Configuration File
|
|
681
740
|
|
|
682
741
|
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`.
|
|
@@ -685,13 +744,19 @@ The old `free-search:` section of `~/.dsh/settings.yaml` is **not** imported by
|
|
|
685
744
|
|
|
686
745
|
```yaml
|
|
687
746
|
# config of that entry in profiles/<profile>/cordis.patch.yml:
|
|
688
|
-
provider: bing # ddg / ddg-lite / bing / searxng / anysearch / exa / tavily / keenable / firecrawl / parallel / perplexity / serpbase / serply / deepseek-official / you / baidu / kimi / aliyun / doubao / zhihu_global / zhihu_site / openai / gemini / claude / auto / multi
|
|
747
|
+
provider: bing # ddg / ddg-lite / bing / searxng / anysearch / exa / tavily / keenable / firecrawl / parallel / perplexity / serpbase / serply / deepseek-official / deepseek-tools / you / baidu / kimi / aliyun / doubao / zhihu_global / zhihu_site / openai / gemini / claude / auto / multi
|
|
689
748
|
fallbackOn: [quota, auth, bot-wall, transient, invalid-response, unknown] # failure classes allowed to trigger fallback; [] = abort on any failure (default: all)
|
|
690
749
|
lang: zh # settings UI language (zh / en)
|
|
691
750
|
bingMarket: zh-CN # Bing market
|
|
692
751
|
region: cn-zh # DuckDuckGo region (optional)
|
|
693
752
|
searxngInstances: # Custom SearXNG instances (optional)
|
|
694
753
|
- https://your-instance.example
|
|
754
|
+
searxngBearerToken: ... # Optional: send Authorization: Bearer <token> (same as the "SearXNG Bearer Token" row in the settings UI)
|
|
755
|
+
safeSearch: off # Safe search: off (default) / moderate / strict; applies to bing / ddg / ddg-lite
|
|
756
|
+
blockedDomains: # Domain blocklist (optional): matching domains never enter the results
|
|
757
|
+
- spam-example.com
|
|
758
|
+
multiMaxEngines: 0 # multi concurrency cap (0 = unlimited, whole language pool); the settings UI has Fast/Balanced/Deep presets
|
|
759
|
+
multiMinSources: 1 # multi target source count (only triggers top-up runs; not a hard minimum)
|
|
695
760
|
exaApiKey: ... # Or configure via the web settings UI
|
|
696
761
|
tavilyApiKey: ... # Or configure via the web settings UI
|
|
697
762
|
keenableApiKey: ... # Or configure via the web settings UI
|
|
@@ -738,10 +803,11 @@ Ask the agent for *"news from the last week"*, *"releases this month"*, *"update
|
|
|
738
803
|
| SearXNG | `time_range` | ⚠️ approximate | same as above |
|
|
739
804
|
| DuckDuckGo / Lite | `df` | ⚠️ approximate | same as above |
|
|
740
805
|
| Bing / AnySearch | — | ❌ ignored | no corresponding parameter |
|
|
806
|
+
| DeepSeek Native Search (`deepseek-tools`) | `after:` query syntax | ✅ precise | the endpoint has no dedicated date field, so `after:YYYY-MM-DD` is appended to the query; relative day counts are converted to an N-days-ago date |
|
|
741
807
|
|
|
742
808
|
**Nearest-tier mapping rule**: `≤2 days → day`, `≤14 days → week`, `≤90 days → month`, otherwise `year`. For example, `3d` becomes `day` on Tavily, and `2mo` becomes `month`.
|
|
743
809
|
|
|
744
|
-
**Engine-chain priority**: when a `timeRange` is present, engines that support time filtering (tavily / exa / keenable / firecrawl / parallel / searxng / ddg / ddg-lite) are moved to the front of the fallback chain, so the filter actually takes effect — even if the preferred engine is bing (which does not support filtering), a filtering-capable engine is tried first.
|
|
810
|
+
**Engine-chain priority**: when a `timeRange` is present, engines that support time filtering (tavily / exa / keenable / firecrawl / parallel / searxng / ddg / ddg-lite / deepseek-tools) are moved to the front of the fallback chain, so the filter actually takes effect — even if the preferred engine is bing (which does not support filtering), a filtering-capable engine is tried first.
|
|
745
811
|
|
|
746
812
|
Example: *"Find DSH news from the last 3 days"* → agent calls `advanced_search` with `timeRange: "3d"`.
|
|
747
813
|
|
|
@@ -750,7 +816,19 @@ Example: *"Find DSH news from the last 3 days"* → agent calls `advanced_search
|
|
|
750
816
|
When you need **cross-source verification** to prevent biases or fail-safes from a single engine, the agent can call `multi_search`:
|
|
751
817
|
|
|
752
818
|
- **Concurrent Execution**: Defaults to querying **every engine of the query's language pool** (no longer top 3; or an explicit list passed via `engines`). Each engine independently resolves API keys and handles errors (missing keys are skipped without failing the batch).
|
|
753
|
-
- **
|
|
819
|
+
- **Three presets** (radio buttons in the settings UI; the choice is written to the config immediately):
|
|
820
|
+
|
|
821
|
+
| Preset | Concurrency cap | Target sources |
|
|
822
|
+
|---|---|---|
|
|
823
|
+
| `Fast · 1/1` | 1 | 1 |
|
|
824
|
+
| `Balanced · 3/1` | 3 | 1 |
|
|
825
|
+
| `Deep · 6/2` | 6 | 2 |
|
|
826
|
+
|
|
827
|
+
Typing your own numbers below the buttons counts as "custom" and no preset appears selected.
|
|
828
|
+
- **Concurrency cap** (`multiMaxEngines`, default `0` = **unlimited**, the whole language pool): this is a **ceiling, not a floor** — the actual fan-out is `min(cap, credential-eligible engines)`. Credential eligibility is resolved **before** truncation, so keyless engines never occupy slots (3 eligible engines with a cap of 6 still run all 3).
|
|
829
|
+
- **Target source count** (`multiMinSources`, default `1`): **not a hard minimum**. It only triggers top-up runs — when too few sources have returned, the remaining eligible engines run **serially** in domain round-robin order; once they are exhausted or time runs out, the results in hand are returned (coming up short is not an error).
|
|
830
|
+
- **30-second total budget** (`SEARCH_BUDGET_MS`): the fan-out and every top-up run share one deadline; at expiry the plugin stops and returns **whatever it already has**, so one slow engine cannot stall the whole search.
|
|
831
|
+
- **Deduplication & Merge**: Normalizes URLs (drops trailing slashes and audited tracking params) and ranks items by the number of engines that found them (`seenIn` counts), with cross-hit results placed at the top; **same-site detection uses eTLD+1** — `a.example.co.uk` and `b.example.co.uk` count as one site, so domain-diversity rotation happens at the **site** level and cannot be flooded by many subdomains.
|
|
754
832
|
- **Untrusted Boundary**: Enforces the `<untrusted-web-content>` safety wrapper and trims snippets.
|
|
755
833
|
- ⚠️ Note: Running multiple engines in parallel consumes more search quota; use on demand when high source diversity is needed.
|
|
756
834
|
|
|
@@ -834,16 +912,26 @@ Restart `dsh web` after switching to apply changes.
|
|
|
834
912
|
|
|
835
913
|
#### Offline self-check assertions (`tools/`)
|
|
836
914
|
|
|
837
|
-
|
|
915
|
+
**Sixteen** zero-dependency assertion scripts ship with the repo (**495 assertions** total): **no API keys, no network, no test framework**, Node ≥ 20:
|
|
838
916
|
|
|
839
|
-
| Script | Covers | Run |
|
|
917
|
+
| Script | Covers | Run (count) |
|
|
840
918
|
|---|---|---|
|
|
841
|
-
| `assert-engines.mjs` | engine registry / free-engine rule / language-pool routing / pool config fields / Zhihu URL utm stripping | `node tools/assert-engines.mjs` (
|
|
919
|
+
| `assert-engines.mjs` | engine registry / free-engine rule / language-pool routing / pool config fields / Zhihu URL utm stripping / `deepseek-tools` wiring and snippetCap widening | `node tools/assert-engines.mjs` (41) |
|
|
920
|
+
| `assert-blocked-domains.mjs` | domain blocklist: parsing, matching, choke points, and byte-identical front/back-end sanitizing | `node tools/assert-blocked-domains.mjs` (69) |
|
|
921
|
+
| `assert-multi-policy.mjs` | multi execution policy: ceiling semantics / target-is-not-a-hard-minimum / credential eligibility before the ceiling / **Fast·Balanced·Deep presets** | `node tools/assert-multi-policy.mjs` (146) |
|
|
922
|
+
| `assert-result-ranking.mjs` | merged ranking: **eTLD+1 same-site detection** / domain round-robin / `seenIn` demoted / freshness only under a time filter | `node tools/assert-result-ranking.mjs` (31) |
|
|
923
|
+
| `assert-url-norm.mjs` | URL normalization: strips only audited tracking params, keeps params that may identify different resources | `node tools/assert-url-norm.mjs` (72) |
|
|
924
|
+
| `assert-prompt-contract.mjs` | system-prompt contract: **search ≠ fetch**, untrusted-data boundary, model engines only when explicitly selected | `node tools/assert-prompt-contract.mjs` (9) |
|
|
842
925
|
| `assert-research-degradation.mjs` | research degrades fully without keys, `node:sqlite` no-op, 90s budget isolation, configurable cache dir, `budget.exhausted` contract names | `node tools/assert-research-degradation.mjs` (19) |
|
|
926
|
+
| `assert-research-call-wrappers.mjs` | the four research `call*` wrappers proxy to stubs / the real layer (#75 regression) | `node tools/assert-research-call-wrappers.mjs` (23) |
|
|
843
927
|
| `assert-fetch-chain.mjs` | `fs-quality-fetch` tier ordering, stop-on-first-success, silent firecrawl degradation, aggregated all-tiers failure | `node tools/assert-fetch-chain.mjs` (8) |
|
|
844
|
-
| `assert-vertical.mjs` | `domain_search` registry of 37 engines, per-engine descriptions, unknown-engine errors, no research dependency | `node tools/assert-vertical.mjs` (8) |
|
|
845
|
-
| `assert-free-search-test-schema.mjs` | `free_search_test` output schema matches real output (incl. auto/multi virtual modes) | `node tools/assert-free-search-test-schema.mjs` (8) |
|
|
846
928
|
| `assert-single-fetch-layer.mjs` | exactly one fetch implementation repo-wide, one-way deps, seams owned by the right layer | `node tools/assert-single-fetch-layer.mjs` (22) |
|
|
929
|
+
| `assert-vertical.mjs` | `domain_search` registry of 41 engines, per-engine descriptions, unknown-engine errors, no research dependency | `node tools/assert-vertical.mjs` (8) |
|
|
930
|
+
| `assert-free-search-test-schema.mjs` | `free_search_test` output schema matches real output (incl. auto/multi virtual modes) | `node tools/assert-free-search-test-schema.mjs` (8) |
|
|
931
|
+
| `assert-weather-contract.mjs` | `weather` output schema contract (field pruning, English output, empty geocode throws) | `node tools/assert-weather-contract.mjs` (11) |
|
|
932
|
+
| `assert-killtree-win32.mjs` | Windows process-tree termination (#74): no longer relies on `taskkill` | `node tools/assert-killtree-win32.mjs` (8) |
|
|
933
|
+
| `assert-killtree-portable.mjs` | portable `killTree` contract | `node tools/assert-killtree-portable.mjs` (5) |
|
|
934
|
+
| `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) |
|
|
847
935
|
|
|
848
936
|
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.
|
|
849
937
|
|
|
@@ -870,9 +958,3 @@ Windows users: The desktop shortcut already includes this configuration (`set NO
|
|
|
870
958
|
### License
|
|
871
959
|
|
|
872
960
|
MIT
|
|
873
|
-
|
|
874
|
-
## safeSearch 安全搜索过滤
|
|
875
|
-
|
|
876
|
-
- 全新配置项 `safeSearch`:`off`(引擎默认,不加参数)/ `moderate` / `strict`
|
|
877
|
-
- 作用于 Bing(adlt)、DuckDuckGo HTML(adlt)、DuckDuckGo Lite(adlt)
|
|
878
|
-
- 默认 `off`:不额外过滤,保持引擎自身默认行为;需要时在「设置 > 插件 > Free Search」切换
|
package/lib/client.js
CHANGED
|
@@ -86,6 +86,12 @@ window.__ModuleLoader__.load({
|
|
|
86
86
|
".dshfs-keyStatus-miss{background:#ff9500}",
|
|
87
87
|
".dshfs-keyStatus-opt{background:#c7c7cc}",
|
|
88
88
|
"@media (prefers-color-scheme: dark){.dshfs-keyStatus-opt{background:#48484a}}",
|
|
89
|
+
".dshfs-presetRow{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:6px}",
|
|
90
|
+
".dshfs-presetBtn{font:inherit;font-size:12px;padding:3px 12px;border-radius:999px;border:1px solid rgba(0,0,0,.18);background:transparent;color:inherit;cursor:pointer;opacity:.6;transition:all 150ms ease}",
|
|
91
|
+
".dshfs-presetBtn:hover:not(:disabled){opacity:.9}",
|
|
92
|
+
".dshfs-presetBtn:disabled{opacity:.3;cursor:default}",
|
|
93
|
+
".dshfs-presetBtn.is-active{opacity:1;font-weight:600;background:rgba(127,127,127,.18);border-color:rgba(0,0,0,.3)}",
|
|
94
|
+
"@media (prefers-color-scheme: dark){.dshfs-presetBtn{border-color:rgba(255,255,255,.22)}.dshfs-presetBtn.is-active{background:rgba(255,255,255,.16);border-color:rgba(255,255,255,.35)}}",
|
|
89
95
|
".dshfs-priorityRow[draggable='true']{cursor:grab}",
|
|
90
96
|
".dshfs-priorityRowDragging{opacity:.45;border-style:dashed}",
|
|
91
97
|
"@media (prefers-color-scheme: dark){.dshfs-platform input{accent-color:#0a84ff}}",
|
|
@@ -214,7 +220,10 @@ window.__ModuleLoader__.load({
|
|
|
214
220
|
multiMaxEngines: "Multi 并发引擎上限(multiMaxEngines)",
|
|
215
221
|
multiMaxEnginesHint: "Multi 模式最多同时查询几个引擎。0 = 不限制(默认,跑整个语言池,与旧版一致)。这是天花板不是配额:池里不够就少跑,不会凑数。",
|
|
216
222
|
multiMinSources: "Multi 目标成功源数(multiMinSources)",
|
|
217
|
-
multiMinSourcesHint: "希望至少几个引擎成功返回。不够时会自动用池外引擎补跑,达标即停。注意这是「尽量达到」而非硬性要求:补跑也达不到时仍会返回已有结果,只在备注里说明,不会让搜索失败。",
|
|
223
|
+
multiMinSourcesHint: "希望至少几个引擎成功返回。不够时会自动用池外引擎补跑,达标即停。注意这是「尽量达到」而非硬性要求:补跑也达不到时仍会返回已有结果,只在备注里说明,不会让搜索失败。",
|
|
224
|
+
multiPreset: "执行档位",
|
|
225
|
+
multiPresetCustom: "自定义",
|
|
226
|
+
multiPresetHint: (now) => `一键把上面两个数字设成常用组合(Fast = 1 并发 / 1 目标,Balanced = 3 / 1,Deep = 6 / 2);手改数字会自动回到「自定义」。当前:${now}。`,
|
|
218
227
|
blockedDomainsLabel: "域名黑名单(blockedDomains)",
|
|
219
228
|
blockedDomainsHint: "一行一个域名,命中的站点及其所有子域都会从搜索结果里剔除(csdn.net 覆盖 blog.csdn.net)。留空=不过滤。可以直接粘贴完整网址,scheme、路径和端口会被自动去掉。",
|
|
220
229
|
blockedDomainsWarn: (corrected, dupes, dropped) => `已忽略 ${dupes} 个重复项、${dropped} 行无法识别;${corrected} 行已自动修正为裸域名。规则对下一次搜索生效,旧结果缓存会同步失效。`,
|
|
@@ -328,6 +337,9 @@ window.__ModuleLoader__.load({
|
|
|
328
337
|
multiMaxEnginesHint: "How many engines Multi may query at once. 0 = no limit (default, runs the whole language pool, same as before). This is a ceiling, not a quota: fewer candidates simply means fewer engines run.",
|
|
329
338
|
multiMinSources: "Multi target successful sources (multiMinSources)",
|
|
330
339
|
multiMinSourcesHint: "How many engines should ideally return results. Below target, extra engines from outside the pool are tried until it is met. This is a target, not a hard minimum: if it still cannot be met, existing results are returned and only a note is added — the search never fails for this reason.",
|
|
340
|
+
multiPreset: "Preset",
|
|
341
|
+
multiPresetCustom: "Custom",
|
|
342
|
+
multiPresetHint: (now) => `One click sets the two numbers above to a common combination (Fast = 1 engine / 1 source, Balanced = 3 / 1, Deep = 6 / 2); editing them switches back to Custom. Current: ${now}.`,
|
|
331
343
|
blockedDomainsLabel: "Domain blocklist (blockedDomains)",
|
|
332
344
|
blockedDomainsHint: "One domain per line. A matched site and all of its subdomains are dropped from search results (csdn.net covers blog.csdn.net). Leave empty to disable. Full URLs are accepted — scheme, path and port are stripped automatically.",
|
|
333
345
|
blockedDomainsWarn: (corrected, dupes, dropped) => `Ignored ${dupes} duplicate(s) and ${dropped} unrecognized line(s); corrected ${corrected} line(s) to bare domains. Rules apply to the next search, and the result cache is invalidated accordingly.`,
|
|
@@ -376,7 +388,7 @@ window.__ModuleLoader__.load({
|
|
|
376
388
|
};
|
|
377
389
|
const tt = (lang) => I18N[lang === "en" ? "en" : "zh"];
|
|
378
390
|
// 当前插件版本(与 lib/index.js 的 PLUGIN_VERSION 保持一致;跟随上游 v0.7.5)
|
|
379
|
-
const PLUGIN_VERSION = "0.9.
|
|
391
|
+
const PLUGIN_VERSION = "0.9.2";
|
|
380
392
|
const ENGINES = [
|
|
381
393
|
{ id: "auto", label: "Auto · 智能路由", badge: "AUTO", link: null },
|
|
382
394
|
{ id: "multi", label: "Multi Search · 多引擎", badge: "MULTI", link: null },
|
|
@@ -394,6 +406,7 @@ window.__ModuleLoader__.load({
|
|
|
394
406
|
{ id: "serpbase", label: "SerpBase · Google", badge: "API KEY", link: "https://serpbase.dev" },
|
|
395
407
|
{ id: "serply", label: "Serply · Google", badge: "API KEY", link: "https://serply.io" },
|
|
396
408
|
{ id: "deepseek-official", label: "DeepSeek Official", badge: "API KEY", link: "https://platform.deepseek.com/api_keys" },
|
|
409
|
+
{ id: "deepseek-tools", label: "DeepSeek Tools API", badge: "API KEY", link: "https://api-docs.deepseek.com/zh-cn/guides/search" },
|
|
397
410
|
{ id: "you", label: "You.com", badge: "API KEY", link: "https://you.com/platform/api-keys" },
|
|
398
411
|
{ id: "baidu", label: "Baidu · 百度千帆", badge: "API KEY", link: "https://console.bce.baidu.com/qianfan" },
|
|
399
412
|
{ id: "kimi", label: "Kimi · Moonshot", badge: "API KEY", link: "https://platform.moonshot.cn" },
|
|
@@ -411,7 +424,7 @@ window.__ModuleLoader__.load({
|
|
|
411
424
|
// 与 lib/index.js 的 EXPLICIT_ONLY_ENGINES 同源:按次计费、只在显式选中时运行,因此不提供入池候选。
|
|
412
425
|
const EXPLICIT_ONLY_ENGINES = ["openai", "gemini", "claude"];
|
|
413
426
|
const MODEL_ENGINES = ENGINES.filter((engine) => EXPLICIT_ONLY_ENGINES.includes(engine.id));
|
|
414
|
-
const DEFAULT_FALLBACK_ORDER = ["exa", "tavily", "keenable", "firecrawl", "parallel", "perplexity", "serpbase", "serply", "deepseek-official", "you", "baidu", "kimi", "aliyun", "doubao", "tinyfish", "jina", "bing", "anysearch", "ddg", "ddg-lite", "searxng", "zhihu_global", "zhihu_site"];
|
|
427
|
+
const DEFAULT_FALLBACK_ORDER = ["exa", "tavily", "keenable", "firecrawl", "parallel", "perplexity", "serpbase", "serply", "deepseek-tools", "deepseek-official", "you", "baidu", "kimi", "aliyun", "doubao", "tinyfish", "jina", "bing", "anysearch", "ddg", "ddg-lite", "searxng", "zhihu_global", "zhihu_site"];
|
|
415
428
|
const normalizeFallbackOrder = (value) => {
|
|
416
429
|
const configured = Array.isArray(value) ? value.filter((id) => ENGINE_IDS.includes(id)) : [];
|
|
417
430
|
return [...new Set([...configured, ...DEFAULT_FALLBACK_ORDER, ...ENGINE_IDS.filter((id) => !EXPLICIT_ONLY_ENGINES.includes(id))])].filter((id) => !EXPLICIT_ONLY_ENGINES.includes(id));
|
|
@@ -604,10 +617,35 @@ window.__ModuleLoader__.load({
|
|
|
604
617
|
const [zhihuKey, setZhihuKey] = react.useState("");
|
|
605
618
|
const [tinyfishKey, setTinyfishKey] = react.useState("");
|
|
606
619
|
const [jinaKey, setJinaKey] = react.useState("");
|
|
620
|
+
const [searxngToken, setSearxngToken] = react.useState("");
|
|
607
621
|
const [platforms, setPlatforms] = react.useState(["github", "v2ex", "bilibili", "reddit", "hn", "stackoverflow", "wikipedia", "npm", "youtube", "vimeo", "juejin", "douban", "qiita"]);
|
|
608
622
|
const [cacheTtl, setCacheTtl] = react.useState(5);
|
|
609
623
|
const [multiMaxEngines, setMultiMaxEngines] = react.useState(0);
|
|
610
624
|
const [multiMinSources, setMultiMinSources] = react.useState(1);
|
|
625
|
+
// 执行档位(#69 §1):与 lib/multi_policy.js 的 MULTI_PRESETS 保持一致。
|
|
626
|
+
// 浏览器半没有 import,故内联这份表;两份的一致性由 assert-multi-policy.mjs 交叉校验钉住。
|
|
627
|
+
const MULTI_PRESET_VALUES = {
|
|
628
|
+
fast: { maxEngines: 1, minSources: 1 },
|
|
629
|
+
balanced: { maxEngines: 3, minSources: 1 },
|
|
630
|
+
deep: { maxEngines: 6, minSources: 2 },
|
|
631
|
+
};
|
|
632
|
+
const presetOf = (max, min) => {
|
|
633
|
+
const m = Math.max(Number(max) || 0, 0);
|
|
634
|
+
const s = Math.max(Number(min) || 1, 1);
|
|
635
|
+
for (const [id, p] of Object.entries(MULTI_PRESET_VALUES)) {
|
|
636
|
+
if (p.maxEngines === m && p.minSources === s) return id;
|
|
637
|
+
}
|
|
638
|
+
return "custom";
|
|
639
|
+
};
|
|
640
|
+
const applyPreset = (id) => {
|
|
641
|
+
const p = MULTI_PRESET_VALUES[id];
|
|
642
|
+
if (!p) return;
|
|
643
|
+
setMultiMaxEngines(p.maxEngines);
|
|
644
|
+
setMultiMinSources(p.minSources);
|
|
645
|
+
setDirty(true);
|
|
646
|
+
setFailed(false);
|
|
647
|
+
setSaveError("");
|
|
648
|
+
};
|
|
611
649
|
const [keysConfigured, setKeysConfigured] = react.useState({});
|
|
612
650
|
// key 存储位置:credentials(凭据中心,默认)| settings(设置页,兼容旧行为)
|
|
613
651
|
const [keyStorage, setKeyStorage] = react.useState("credentials");
|
|
@@ -634,6 +672,7 @@ window.__ModuleLoader__.load({
|
|
|
634
672
|
parallel: [parallelKey, setParallelKey],
|
|
635
673
|
perplexity: [perplexityKey, setPerplexityKey],
|
|
636
674
|
"deepseek-official": [deepseekKey, setDeepseekKey],
|
|
675
|
+
"deepseek-tools": [deepseekKey, setDeepseekKey],
|
|
637
676
|
serpbase: [serpbaseKey, setSerpbaseKey],
|
|
638
677
|
serply: [serplyKey, setSerplyKey],
|
|
639
678
|
you: [youcomKey, setYoucomKey],
|
|
@@ -643,6 +682,7 @@ window.__ModuleLoader__.load({
|
|
|
643
682
|
doubao: [doubaoKey, setDoubaoKey],
|
|
644
683
|
tinyfish: [tinyfishKey, setTinyfishKey],
|
|
645
684
|
jina: [jinaKey, setJinaKey],
|
|
685
|
+
searxng: [searxngToken, setSearxngToken],
|
|
646
686
|
zhihu_global: [zhihuKey, setZhihuKey],
|
|
647
687
|
zhihu_site: [zhihuKey, setZhihuKey],
|
|
648
688
|
openai: [openaiKey, setOpenaiKey],
|
|
@@ -650,9 +690,9 @@ window.__ModuleLoader__.load({
|
|
|
650
690
|
claude: [claudeKey, setClaudeKey],
|
|
651
691
|
};
|
|
652
692
|
// #68 密钥状态可视化:引擎 id → 配置中心键名(少数引擎不同名),三态:已配置/可选/缺失
|
|
653
|
-
const CONFIGURED_KEY_ID = { "deepseek-official": "deepseek", you: "youcom", zhihu_global: "zhihu", zhihu_site: "zhihu" };
|
|
654
|
-
const NO_KEY_ENGINES = ["ddg", "ddg-lite", "bing"
|
|
655
|
-
const OPTIONAL_KEY_ENGINES = ["anysearch", "exa", "tavily", "keenable", "firecrawl", "parallel"];
|
|
693
|
+
const CONFIGURED_KEY_ID = { "deepseek-official": "deepseek", "deepseek-tools": "deepseek", you: "youcom", zhihu_global: "zhihu", zhihu_site: "zhihu" };
|
|
694
|
+
const NO_KEY_ENGINES = ["ddg", "ddg-lite", "bing"];
|
|
695
|
+
const OPTIONAL_KEY_ENGINES = ["anysearch", "exa", "tavily", "keenable", "firecrawl", "parallel", "searxng"];
|
|
656
696
|
const hasConfiguredKey = (id) => {
|
|
657
697
|
const k = CONFIGURED_KEY_ID[id] ?? id;
|
|
658
698
|
return Boolean(keysConfigured[k] || credConfigured[k]);
|
|
@@ -713,6 +753,7 @@ window.__ModuleLoader__.load({
|
|
|
713
753
|
setZhihuKey(v.zhihuApiKey ?? "");
|
|
714
754
|
setTinyfishKey(v.tinyfishApiKey ?? "");
|
|
715
755
|
setJinaKey(v.jinaApiKey ?? "");
|
|
756
|
+
setSearxngToken(v.searxngBearerToken ?? "");
|
|
716
757
|
setPlatforms(Array.isArray(v.platforms) && v.platforms.length > 0 ? v.platforms : ["github", "v2ex", "bilibili", "reddit", "hn", "stackoverflow", "wikipedia", "npm", "youtube", "vimeo", "juejin", "douban", "qiita"]);
|
|
717
758
|
setCacheTtl(v.cacheTtl === undefined ? 5 : Math.min(Math.max(Number(v.cacheTtl) ?? 5, 0), 5));
|
|
718
759
|
// #69:multi 上限 0 = 不限制(旧行为);目标成功源数下限 1
|
|
@@ -744,6 +785,7 @@ window.__ModuleLoader__.load({
|
|
|
744
785
|
if (path === "zhihuApiKey") configured.zhihu = true;
|
|
745
786
|
if (path === "tinyfishApiKey") configured.tinyfish = true;
|
|
746
787
|
if (path === "jinaApiKey") configured.jina = true;
|
|
788
|
+
if (path === "searxngBearerToken") configured.searxng = true;
|
|
747
789
|
}
|
|
748
790
|
}
|
|
749
791
|
setKeysConfigured(configured);
|
|
@@ -755,7 +797,7 @@ window.__ModuleLoader__.load({
|
|
|
755
797
|
const cred = await bridgeCredentialsStatus();
|
|
756
798
|
if (cred.ok) {
|
|
757
799
|
const cc = {};
|
|
758
|
-
const map = { anysearchApiKey: "anysearch", exaApiKey: "exa", tavilyApiKey: "tavily", keenableApiKey: "keenable", firecrawlApiKey: "firecrawl", parallelApiKey: "parallel", perplexityApiKey: "perplexity", deepseekApiKey: "deepseek", serpbaseApiKey: "serpbase", serplyApiKey: "serply", youcomApiKey: "youcom", baiduApiKey: "baidu", kimiApiKey: "kimi", aliyunApiKey: "aliyun", doubaoApiKey: "doubao", openaiApiKey: "openai", geminiApiKey: "gemini", claudeApiKey: "claude", zhihuApiKey: "zhihu", tinyfishApiKey: "tinyfish", jinaApiKey: "jina" };
|
|
800
|
+
const map = { anysearchApiKey: "anysearch", exaApiKey: "exa", tavilyApiKey: "tavily", keenableApiKey: "keenable", firecrawlApiKey: "firecrawl", parallelApiKey: "parallel", perplexityApiKey: "perplexity", deepseekApiKey: "deepseek", serpbaseApiKey: "serpbase", serplyApiKey: "serply", youcomApiKey: "youcom", baiduApiKey: "baidu", kimiApiKey: "kimi", aliyunApiKey: "aliyun", doubaoApiKey: "doubao", openaiApiKey: "openai", geminiApiKey: "gemini", claudeApiKey: "claude", zhihuApiKey: "zhihu", tinyfishApiKey: "tinyfish", jinaApiKey: "jina", searxngBearerToken: "searxng" };
|
|
759
801
|
for (const [k, v] of Object.entries(cred.value.configured ?? {})) {
|
|
760
802
|
if (map[k]) cc[map[k]] = v;
|
|
761
803
|
}
|
|
@@ -813,6 +855,7 @@ window.__ModuleLoader__.load({
|
|
|
813
855
|
["zhihuApiKey", zhihuKey],
|
|
814
856
|
["tinyfishApiKey", tinyfishKey],
|
|
815
857
|
["jinaApiKey", jinaKey],
|
|
858
|
+
["searxngBearerToken", searxngToken],
|
|
816
859
|
];
|
|
817
860
|
if (keyStorage === "credentials") {
|
|
818
861
|
for (const [field, value] of keyFields) {
|
|
@@ -1611,6 +1654,38 @@ react_jsx_runtime.jsx("div", {
|
|
|
1611
1654
|
}),
|
|
1612
1655
|
],
|
|
1613
1656
|
}),
|
|
1657
|
+
react_jsx_runtime.jsx("div", {
|
|
1658
|
+
className: "dshfs-field",
|
|
1659
|
+
children: [
|
|
1660
|
+
react_jsx_runtime.jsx("div", {
|
|
1661
|
+
className: "dshfs-label",
|
|
1662
|
+
children: t.multiPreset,
|
|
1663
|
+
}),
|
|
1664
|
+
react_jsx_runtime.jsx("div", {
|
|
1665
|
+
className: "dshfs-presetRow",
|
|
1666
|
+
children: ["fast", "balanced", "deep"].map((id) => {
|
|
1667
|
+
const p = MULTI_PRESET_VALUES[id];
|
|
1668
|
+
const active = presetOf(multiMaxEngines, multiMinSources) === id;
|
|
1669
|
+
return react_jsx_runtime.jsx("button", {
|
|
1670
|
+
type: "button",
|
|
1671
|
+
className: "dshfs-presetBtn" + (active ? " is-active" : ""),
|
|
1672
|
+
disabled: !ready || saving,
|
|
1673
|
+
"aria-pressed": active,
|
|
1674
|
+
onClick: () => applyPreset(id),
|
|
1675
|
+
children: `${id[0].toUpperCase()}${id.slice(1)} · ${p.maxEngines}/${p.minSources}`,
|
|
1676
|
+
}, id);
|
|
1677
|
+
}),
|
|
1678
|
+
}),
|
|
1679
|
+
react_jsx_runtime.jsx("p", {
|
|
1680
|
+
className: "dshfs-hint",
|
|
1681
|
+
children: t.multiPresetHint(
|
|
1682
|
+
presetOf(multiMaxEngines, multiMinSources) === "custom"
|
|
1683
|
+
? t.multiPresetCustom
|
|
1684
|
+
: presetOf(multiMaxEngines, multiMinSources)
|
|
1685
|
+
),
|
|
1686
|
+
}),
|
|
1687
|
+
],
|
|
1688
|
+
}),
|
|
1614
1689
|
react_jsx_runtime.jsx("div", {
|
|
1615
1690
|
className: "dshfs-field",
|
|
1616
1691
|
children: [
|