dsh-web-search-plugin 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,57 @@
2
2
 
3
3
  本项目的重要变更都记录在此。格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本号遵循 [Semantic Versioning](https://semver.org/lang/zh-CN/)。
4
4
 
5
+ ## [未发布]
6
+
7
+ ## [0.5.0] - 2026-10-09
8
+
9
+ ### 新增
10
+
11
+ - **适配 DeepSeek Harness `0.2.0-rc.2`**:四个 DSH peerDependencies 提升到 `^0.2.0-rc.2`,`@deepseek-ai/cordis` / `@deepseek-ai/schemastery` 对齐 `~4.0.4` / `~3.18.4`;版本号与 `User-Agent` 更新为 `0.5.0`。
12
+ - DeepSeek 后端支持账号令牌认证:会话 provider 为 `deepseek-account` 时通过 `deepseekAccount.resolveToken()` 获取令牌并以 `x-dsh-auth-token` 发送,未授权时回落到 `DEEPSEEK_API_KEY`。
13
+ - 有会话发起者时,DeepSeek 搜索在派发前记录脱敏请求到会话事件 `web/deepseek-search-llm-request`,与官方 provider 行为一致。
14
+ - 插件列表页与详情页的标题、描述随界面语言切换:`locale/en.json` 与 `locale/zh.json`,经 `"./locale/*.json"` 导出(与官方 bundle 同一机制)。卡片内文案由 `lib/client.js` 的 `zh` / `en` 字典提供。
15
+ - README 面向开源使用者整理:删除仅适用于本机的排查笔记与旧个人包迁移章节,修正与实现脱节的仓库布局与安装说明。
16
+
17
+ ### 移除
18
+
19
+ - **`settings.installSection()` 已从 Harness 移除**:Host 侧改为全部配置字段声明 `volatile()`,并在 `apply` 中通过 `config.<field>.get()` 读取实时值。插件不再调用 `ctx.inject(["settings"])`,也不再维护 `setSource` / `current()` 这条配置来源链。
20
+ - **客户端 `settingsScope` 服务已移除**:浏览器端改用 `ctx.configForms`。设置分区通过 `configForms.whileServed([namespace], register)` 条件注册,Host 未下发该命名空间时不留痕迹。`inject` 中的 `settingsScope` 同步替换为 `configForms`。
21
+ - 设置分区不再自行注册 Host 设置卡:0.2.x 的设置由当前 Profile 的插件配置保存,`client.js` 的分区只负责展示与写入。
22
+
23
+ ### 修复
24
+
25
+ - Exa 请求改用 `numResults` 与对象形态的 `contents.highlights`;SearXNG 不再发送其 API 未定义的 `count` 参数。
26
+ - 插件 `maxResults` 与调用方上限取较小值,避免常规 `web_search` 请求覆盖设置卡上限;未选中的引擎不再解析其凭据引用,设置卡校验引用名格式。
27
+ - Tavily 用量只保留当前凭据的内存数据,换 key 后立即重新对账;手动刷新限速改为 10 秒,自动刷新不受手动限速约束。
28
+ - Firecrawl 搜索按 API 要求发送数组形态的 `sources`;REST 元数据的空 `countParam` 不再被强制改为 `max_results`。
29
+ - DeepSeek 与 REST 后端按配置的 `maxResults` 截取超额结果,并标记 `truncated`。
30
+ - 网页搜索设置分区随 Host 配置命名空间出现和撤销;凭据写入失败时保留草稿并显示失败提示。
31
+ - 同时修改凭据引用名与 API key 时,密钥写入新的引用名;DeepSeek 账号令牌预检响应取消并统一映射错误。
32
+ - Tavily 与 DeepSeek 的空端点配置恢复环境变量回退;Tavily 手动刷新额度增加最短请求间隔。
33
+ - 设置卡把配置改动按同一 revision 原子提交;Host 拒绝写入时保留草稿,保存期间锁定编辑。写入新凭据时清空该引擎的字面量 key 覆盖,使新凭据生效。
34
+ - 更新 Serper 冒烟脚本,使用当前导出的 `RestSearchProvider` 与内置元数据。
35
+
36
+ ### 文档
37
+
38
+ - 澄清本插件注册的接缝 id 是 `dsh-web-search`,`deepseek-official` 只是内部引擎选项;同步修正 bundle patch、卸载说明和会话事件说明。
39
+ - 对齐 README 的端点默认值、环境变量优先级和 API key 保存位置。
40
+
41
+ ### 说明
42
+
43
+ - `cordis.patch.yml` 禁用内置 `web-search-deepseek`,使此 bundle 统一管理 DeepSeek 搜索配置;本插件注册的接缝 id 为 `dsh-web-search`。
44
+ - Bundle patch 仍使用单文件声明(0.1.7 起支持的多文件有序声明为可选能力,未启用)。
45
+
46
+ ## [0.4.1] - 2026-09-10
47
+
48
+ ### 变更
49
+
50
+ - **适配 DeepSeek Harness `0.1.5-rc.1`**:四个 DSH peerDependencies 提升到 `^0.1.5-rc.1`,Node.js 运行要求对齐为 `^22.19.0 || >=24.0.0`。
51
+ - 浏览器端显式注入 `remote.credentials`,避免 API remote 尚未挂载时访问凭据服务的启动顺序竞态。
52
+ - Bundle 覆盖 `web.config` 时保留上游 `fetchProvider: http`,不再因切换搜索 provider 而隐式清除抓取 provider。
53
+ - Brave 的 `proxy` 仅作为 provider 级显式覆盖;留空时继承 DSH 的全局代理分发器,统一支持 `HTTP_PROXY` / `HTTPS_PROXY` / `ALL_PROXY` / `NO_PROXY`。
54
+ - 版本号及 `User-Agent` 更新为 `0.4.1`。
55
+
5
56
  ## [0.4.0] - 2026-09-02
6
57
 
7
58
  ### 变更
@@ -72,7 +123,7 @@
72
123
  - Brave 月限额 `0` 的套餐按控制台 **Capacity** 展示每秒窗口(例如 50 次/秒),不再把月配额渲染成「已用 0 / 0」。Brave 没有花费/credits 接口。
73
124
  - 数字框只接受不小于 1 的整数;非法输入用红色错误文案替换灰色 hint,保存会被拦住。超出 20 的结果数量会夹到上限。
74
125
 
75
- ## [0.2.0] - 2026-08-19
126
+ ## [0.2.0] - 2026-08-18
76
127
 
77
128
  ### 新增
78
129
 
package/README.md CHANGED
@@ -2,39 +2,41 @@
2
2
 
3
3
  面向 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) web 能力接缝(`ctx.web`)的统一网页搜索插件,内置 **DeepSeek(官方,默认)/ Tavily / [Brave Search](https://brave.com/search/api/) / Serper / SerpApi / Exa / SearXNG / Scavio / Firecrawl 九个后端**。本包只向接缝注册 **一个** `WebSearchProvider`,稳定 id 为 `dsh-web-search`。在 **设置 → 网页搜索** 里切换引擎即可,不必再改 `web.searchProvider`。
4
4
 
5
- - **DeepSeek(官方,默认)** — 走 DeepSeek 的 Anthropic 兼容 Messages API(原生 `web_search_20250305` 工具,凭据名 `DEEPSEEK_API_KEY`),一次搜索消耗一个模型轮次。
5
+ - **DeepSeek(官方,默认)** — 走 DeepSeek 的 Anthropic 兼容 Messages API(原生 `web_search_20250305` 工具,支持账号令牌或 `DEEPSEEK_API_KEY`),一次搜索消耗一个模型轮次。
6
6
  - **Tavily** — `keyless`(免费、限流、无需账号)或 `keyed`(`TAVILY_API_KEY`,Bearer token)。keyed 会在设置卡显示额度进度条。
7
7
  - **Brave Search** — `GET https://api.search.brave.com/res/v1/web/search`,请求头 `X-Subscription-Token`(凭据名 `BRAVE_API_KEY`)。一次搜索就是一次 HTTP 请求,不走模型轮次;设置卡按响应头展示 Capacity / 月配额。
8
8
  - **Serper** — Google 结果(`POST /search`,头 `X-API-KEY`,凭据名 `SERPER_API_KEY`)。
9
9
  - **SerpApi** — `GET /search.json`,key 走 query(`api_key=`),凭据名 `SERPAPI_API_KEY`。
10
- - **Exa** — 语义/神经搜索(`POST /search`,Bearer,凭据名 `EXA_API_KEY`)。
11
- - **SearXNG** — 自建/协议免 key 的元搜索(`GET /search?format=json`),可填自托管实例地址。
10
+ - **Exa** — 语义/神经搜索(`POST /search`,Bearer,凭据名 `EXA_API_KEY`),请求 `contents.highlights` 作为结果摘要。
11
+ - **SearXNG** — 自建/协议免 key 的元搜索(`GET /search?format=json`),可填自托管实例地址;实例须启用 JSON 输出。
12
12
  - **Scavio** — Google SERP(`POST /api/v2/google`,Bearer,凭据名 `SCAVIO_API_KEY`),响应与 SerpApi 同构。
13
- - **Firecrawl** — 搜索+抓取(`POST /v2/search`,Bearer,凭据名 `FIRECRAWL_API_KEY`),`sources: ["web"]`。
13
+ - **Firecrawl** — 网页搜索(`POST /v2/search`,Bearer,凭据名 `FIRECRAWL_API_KEY`),`sources: ["web"]`;当前请求没有启用整页抓取。
14
14
  - **DuckDuckGo** — 无官方搜索 API,不提供独立后端;下拉中以禁用项提示:选 SearXNG 并指向聚合了 DuckDuckGo 的实例即可。
15
15
  - **官方 key 跳转** — 需要 key 的内置 provider 在设置卡带「获取 API key ↗」一键跳官方控制台;免 key 的(SearXNG / Tavily keyless)不渲染。
16
- - **不写自定义 session 事件** — 工具结果已经走接缝自己的事件,无需多余信封。
16
+ - **会话事件** — 有会话发起者时,DeepSeek 在派发前记录脱敏模型请求到 `web/deepseek-search-llm-request`;REST 后端不追加自定义事件。
17
17
 
18
18
  ## 特性
19
19
 
20
- - 与官方 `@deepseek-ai/dsh-web-search-deepseek` 相同的提供方约定:`inject: ['web']` + `ctx.settings.installSection` + `ctx.web.registerSearchProvider`。
21
- - 顶层 **设置 → 网页搜索** 分区:两列布局、未保存草稿、保存 toast;结果数量为 1–20 下拉。
20
+ - 与官方 `@deepseek-ai/dsh-web-search-deepseek` 相同的提供方约定:`inject: ['web']` + 全`volatile()` 配置字段 + `ctx.web.registerSearchProvider`。
21
+ - 自定义配置卡片位于 **设置 → 网页搜索**。
22
22
  - **默认引擎为 DeepSeek(官方)**:新装 / 未显式改动时,搜索直接走 `deepseek-official`,不再依赖 keyless 的 Tavily。
23
- - 纯 REST 后端由**静态元数据表 + 通用后端**驱动,新增 provider 即"表里加一行 + 官方跳转链接",无定制执行代码。
23
+ - **9 个后端**中,8 个纯 REST 后端由**静态元数据表 + 通用后端**驱动;只有 DeepSeek 官方走模型轮次。新增内置 REST 引擎还需同步配置字段和客户端引擎列表。
24
24
  - Tavily keyless 无需密钥即可用;Brave 需要订阅 token(若本机已有 `BRAVE_API_KEY` 凭据,可直接复用)。
25
25
  - Tavily keyed / Brave 在设置卡展示额度进度条(DeepSeek 官方与 Tavily keyless / 其余 REST 后端不展示)。
26
26
  - 各引擎结果都规范化为接缝的 `WebSearchResult`(可选 `content` + `sources[]`),按 URL 去重。
27
- - 设置段 `dsh-web-search-plugin` 通过 dsh 的 `settings.installSection` 注册、由 `settings.describe()` 动态暴露,不需要宿主白名单补丁,也不需要自建回环 settings 桥。
27
+ - 配置由当前 Profile 的插件配置保存,全部字段声明为 `volatile()`:改引擎或端点后无需重启插件即可生效。
28
+ - 插件列表页与详情页的标题、描述随界面语言切换(`locale/*.json`);卡片内文案自带中英字典。
28
29
  - 错误映射为 `WEB_PROVIDER_ERROR` / `WEB_ABORTED` / `WEB_PROVIDER_CREDENTIAL_MISSING`。
29
30
 
30
31
  ## 运行要求
31
32
 
32
- - DeepSeek Harness `0.1.2-alpha.4`(最新 main)或更新
33
+ - DeepSeek Harness `0.2.0-rc.2` 或兼容的 `0.2.x` 版本
34
+ - Node.js `^22.19.0` 或 `>=24.0.0`
33
35
  - pnpm,用于通过 `dsh plugin` 把插件装进 profile
34
36
 
35
37
  ## 安装
36
38
 
37
- 本包是 **bundle**:`dsh.bundle.patch` + `cordis.patch.yml` 会插入 Host 行、把 `web.searchProvider` 设为 `dsh-web-search`,并**禁用内置 `web-search-deepseek` Host 插件**(本插件已自行托管 DeepSeek 后端,禁用它能移除旧的「插件 → 网页搜索」卡片、避免重复注册 provider)。没有这一声明时,`dsh plugin add` 只写入依赖,插件不会挂载。
39
+ 本包是 **bundle**:`dsh.bundle.patch` + `cordis.patch.yml` 会插入 Host 行、把 `web.searchProvider` 设为 `dsh-web-search`,并禁用内置 `web-search-deepseek` Host 插件,使此 profile 的 DeepSeek 搜索由本插件统一配置。本插件注册到接缝的 id 是 `dsh-web-search`;`deepseek-official` 是内部引擎选项,和内置插件注册的 id 不同。没有 bundle 声明时,`dsh plugin add` 只写入依赖,插件不会挂载。
38
40
 
39
41
  ### 从 npm 安装
40
42
 
@@ -46,39 +48,39 @@ dsh plugin --profile web add dsh-web-search-plugin
46
48
 
47
49
  ## 配置
48
50
 
49
- 设置卡编辑的是 `dsh-web-search-plugin` 命名空间:
51
+ 设置卡编辑的是 `dsh-web-search-plugin` 命名空间。设置卡中的 API key 输入框把密钥写入凭据服务;若直接在插件配置中填写 `*ApiKey` 字段,则该字面量保存在插件配置中。建议使用设置卡或凭据引用名,避免在配置文件中保存密钥。
50
52
 
51
53
  | 键 | 默认值 | 含义 |
52
54
  |---|---|---|
53
55
  | `provider` | `deepseek-official` | `tavily`、`brave`、`deepseek-official`、`serper`、`serpapi`、`exa`、`searxng`、`scavio` 或 `firecrawl` |
54
56
  | `mode` | `keyless` | 仅 Tavily:`keyless` 或 `keyed` |
55
- | `apiKey` | — | Tavily API key 字面量(走凭据域,不会回显) |
57
+ | `apiKey` | — | Tavily API key 配置字面量;设置卡的 key 输入框写入凭据服务 |
56
58
  | `apiKeyEnv` | `TAVILY_API_KEY` | keyed Tavily 使用的凭据/环境变量名 |
57
- | `baseURL` | `https://api.tavily.com` | Tavily REST 基址;会再拼 `/search` |
58
- | `maxResults` | `8` | 每次搜索返回的源数量,各后端共用。设置卡为 1–20 下拉,超出上限会夹到 20 |
59
+ | `baseURL` | 空 | Tavily REST 基址;留空时依次使用 `TAVILY_BASE_URL` 和 `https://api.tavily.com`,再拼 `/search` |
60
+ | `maxResults` | `8` | 插件返回的源数量上限,与调用方的 `request.maxResults` 取较小值。设置卡为 1–20 下拉;SearXNG 不支持请求级数量参数,只在收到结果后截取 |
59
61
  | `searchDepth` | `basic` | 仅 Tavily:`basic` 或 `advanced` |
60
62
  | `includeAnswer` | `true` | 仅 Tavily:请求生成摘要,写入结果 `content` |
61
63
  | `topic` | `general` | 仅 Tavily:`general` 或 `news` |
62
- | `deepseekApiKey` | — | DeepSeek API key 字面量(走凭据域) |
64
+ | `deepseekApiKey` | — | DeepSeek API key 配置字面量;设置卡的 key 输入框写入凭据服务 |
63
65
  | `deepseekApiKeyEnv` | `DEEPSEEK_API_KEY` | DeepSeek 使用的凭据/环境变量名 |
64
- | `deepseekBaseURL` | `https://api.deepseek.com/anthropic/v1` | DeepSeek Anthropic 兼容 Messages 基址;再拼 `/messages` |
66
+ | `deepseekBaseURL` | 空 | DeepSeek Anthropic 兼容 Messages 基址;留空时依次使用 `DEEPSEEK_SEARCH_BASE_URL` 和 `https://api.deepseek.com/anthropic/v1`,再拼 `/messages` |
65
67
  | `model` | `deepseek-v4-flash` | Anthropic 格式模型名 |
66
68
  | `apiVersion` | `2023-06-01` | `anthropic-version` 请求头 |
67
69
  | `maxTokens` | `4096` | Messages 请求生成 token 上限 |
68
70
  | `maxUses` | `5` | 每次请求 `web_search` 工具的最大调用次数 |
69
- | `braveApiKey` | — | Brave 订阅 token 字面量(走凭据域) |
71
+ | `braveApiKey` | — | Brave 订阅 token 配置字面量;设置卡的 key 输入框写入凭据服务 |
70
72
  | `braveApiKeyEnv` | `BRAVE_API_KEY` | Brave 使用的凭据/环境变量名 |
71
73
  | `braveBaseURL` | `https://api.search.brave.com/res/v1/web/search` | Brave 网页搜索接口 |
72
74
  | `country` | — | Brave 的 `country`(ISO 两位码,如 `cn`);留空使用 Brave 默认 |
73
75
  | `searchLang` | — | Brave 的 `search_lang`(如 `zh-hans`);留空使用 Brave 默认 |
74
76
  | `freshness` | — | Brave 的 `freshness`:`pd` / `pw` / `pm` / `py` |
75
- | `proxy` | — | Brave 使用的 HTTP(S) 代理;留空回退 `HTTPS_PROXY` / `HTTP_PROXY` |
76
- | `serperApiKey` / `serperApiKeyEnv` / `serperBaseURL` | — / `SERPER_API_KEY` / — | Serper 的 key 字面量 / 凭据引用名 / 端点覆盖 |
77
- | `serpapiApiKey` / `serpapiApiKeyEnv` / `serpapiBaseURL` | — / `SERPAPI_API_KEY` / — | SerpApi 的 key 字面量 / 凭据引用名 / 端点覆盖 |
78
- | `exaApiKey` / `exaApiKeyEnv` / `exaBaseURL` | — / `EXA_API_KEY` / — | Exa 的 key 字面量 / 凭据引用名 / 端点覆盖 |
79
- | `searxngBaseURL` | — | SearXNG 自托管实例基址;留空用 `https://searx.be` |
80
- | `scavioApiKey` / `scavioApiKeyEnv` / `scavioBaseURL` | — / `SCAVIO_API_KEY` / — | Scavio 的 key 字面量 / 凭据引用名 / 端点覆盖 |
81
- | `firecrawlApiKey` / `firecrawlApiKeyEnv` / `firecrawlBaseURL` | — / `FIRECRAWL_API_KEY` / — | Firecrawl 的 key 字面量 / 凭据引用名 / 端点覆盖 |
77
+ | `proxy` | — | Brave 使用的 HTTP(S) 代理覆盖;留空继承 DSH 的全局 `HTTP_PROXY` / `HTTPS_PROXY` / `ALL_PROXY` / `NO_PROXY` 策略 |
78
+ | `serperApiKey` / `serperApiKeyEnv` / `serperBaseURL` | — / `SERPER_API_KEY` / `https://google.serper.dev` | Serper 的配置字面量 key / 凭据引用名 / REST 基址 |
79
+ | `serpapiApiKey` / `serpapiApiKeyEnv` / `serpapiBaseURL` | — / `SERPAPI_API_KEY` / `https://serpapi.com` | SerpApi 的配置字面量 key / 凭据引用名 / REST 基址 |
80
+ | `exaApiKey` / `exaApiKeyEnv` / `exaBaseURL` | — / `EXA_API_KEY` / `https://api.exa.ai` | Exa 的配置字面量 key / 凭据引用名 / REST 基址 |
81
+ | `searxngBaseURL` | `https://searx.be` | SearXNG 实例基址;可改为自托管地址 |
82
+ | `scavioApiKey` / `scavioApiKeyEnv` / `scavioBaseURL` | — / `SCAVIO_API_KEY` / `https://api.scavio.dev` | Scavio 的配置字面量 key / 凭据引用名 / REST 基址 |
83
+ | `firecrawlApiKey` / `firecrawlApiKeyEnv` / `firecrawlBaseURL` | — / `FIRECRAWL_API_KEY` / `https://api.firecrawl.dev` | Firecrawl 的配置字面量 key / 凭据引用名 / REST 基址 |
82
84
 
83
85
  ## 未纳入的服务
84
86
 
@@ -91,40 +93,48 @@ dsh plugin --profile web add dsh-web-search-plugin
91
93
  | **SERPJET** | 官网当前不可访问,暂不接入 |
92
94
  | **DuckDuckGo** | 无官方搜索 API(HTML/社区库抓取不符合元数据表"纯 REST"边界)。下拉中有禁用提示,指引经 SearXNG 使用 |
93
95
 
94
- 环境变量:启动时 `DSH_WEB_SEARCH_PROVIDER=dsh-web-search` 会选中本接缝 id。未设置 `baseURL` 时,Tavily 基址回退 `$TAVILY_BASE_URL`。
96
+ 环境变量:启动时 `DSH_WEB_SEARCH_PROVIDER=dsh-web-search` 会选中本接缝 id。`baseURL` 留空时,Tavily 基址回退 `TAVILY_BASE_URL`;`deepseekBaseURL` 留空时回退 `DEEPSEEK_SEARCH_BASE_URL`。
95
97
 
96
98
  ## 额度
97
99
 
98
100
  - **Tavily keyless / DeepSeek 官方**:不展示额度条。前者是免费限流、没有账户配额;后者按次扣费、没有月度限额。
99
- - **Tavily keyed**:搜索时请求 `include_usage`,把本次 credits 累加到 `%DSH_HOME%\storages\dsh-web-search-usage.json`。Host 每 10 分钟(以及设置卡点刷新)调用 `GET /usage`,用 `account.current_plan` / `plan_limit` 做限额,用量取本地累计与远端的较大值。换套餐或远端用量回落会重置本地计数。
101
+ - **Tavily keyed**:搜索时请求 `include_usage`,在内存中累加当前凭据的 credits。Host 定时调用 `GET /usage`,设置卡也可手动刷新;手动刷新之间至少间隔 10 秒,自动对账不受此限制。用 `account.current_plan` / `plan_limit` 做限额,用量取当前凭据的本地累计与远端的较大值;换 key 时清空旧账户用量并立即重新对账。旧 Tavily 额度不再持久化到磁盘。
100
102
  - **Brave**:没有 usage / 花费接口。控制台 **Capacity** 就是响应头里的每秒窗口(例如 50 次/秒)。月限额 `0` 表示不限请求次数,不是额度用完。计费 credits 只能看 [Brave API 控制台](https://api-dashboard.search.brave.com/)。
101
103
  - 浏览器只读 `GET /dsh-web-search/usage`(不直打上游)。进度条:剩余超过 20% 为绿色,不超过 20% 为黄色,不超过 10% 为红色。
102
104
 
103
- ## 切回内置 DeepSeek
105
+ ## 卸载
104
106
 
105
- DeepSeek 已并入本插件(`provider: deepseek-official`),无需切回。若确要恢复 DSH 内置的 DeepSeek host 插件,请在 profile patch 里去掉对 `web-search-deepseek` 的 `disabled` 并把 `searchProvider` 设回 `deepseek-official`;本插件可以继续挂着,只是不会被选中。
107
+ 若要改用 DSH 内置的 DeepSeek host 插件,先卸载本插件及其 bundle patch:
108
+
109
+ ```powershell
110
+ dsh plugin --profile web remove dsh-web-search-plugin
111
+ ```
112
+
113
+ 随后检查 profile 中实际生效的 `web.searchProvider`;若曾手工覆盖为 `dsh-web-search`,将该覆盖改回 `deepseek-official`。
106
114
 
107
115
  ## 工作方式
108
116
 
109
- - **DeepSeek** — `POST {deepseekBaseURL}/messages`,请求头 `x-api-key` / `authorization: Bearer`,工具 `web_search_20250305`。
110
- - **纯 REST 类(Tavily / Brave / Serper / SerpApi / Exa / SearXNG)** — 由静态元数据表(`lib/providers.js`)+ 通用后端(`lib/rest.js`)驱动,无独立定制代码。请求方法 / 路径 / 查询字段名 / 鉴权(bearer / header / none / query)/ 固定参数 / 响应形态全部由表里的行决定。
117
+ - **DeepSeek** — `POST {deepseekBaseURL}/messages`,工具 `web_search_20250305`。会话运行在 DeepSeek 官方账号路由(provider 为 `deepseek-account`)且账号服务允许该端点时,用账号令牌发 `x-dsh-auth-token`;否则回落到 API key(`x-api-key` / `authorization: Bearer`,凭据名 `DEEPSEEK_API_KEY`)。有会话发起者时,在派发前把脱敏请求记录为会话事件 `web/deepseek-search-llm-request`。
118
+ - **纯 REST 类(Tavily / Brave / Serper / SerpApi / Exa / SearXNG / Scavio / Firecrawl)** — 由静态元数据表(`lib/providers.js`)+ 通用后端(`lib/rest.js`)驱动。请求方法 / 路径 / 查询字段名 / 鉴权(bearer / header / none / query)/ 固定参数 / 响应形态由表中的行决定。
111
119
  - **Tavily** — `POST {baseURL}/search`。keyless 发送 `x-tavily-access-mode: keyless`;keyed 发送 `authorization: Bearer <key>`,并带 `include_usage` 回传 credits。
112
120
  - **Brave** — `GET {braveBaseURL}?q=&count=`,请求头 `x-subscription-token`。不向 session 追加自定义事件。
113
- - **Serper / SerpApi / Exa / SearXNG** — 按各自表的 `method` / `auth` / `params` 约定请求。
121
+ - **其余六种 REST 引擎** — 按各自表的 `method` / `auth` / `params` 约定请求。
114
122
  - 非 2xx 映射为 `WEB_PROVIDER_ERROR`;调用方取消映射为 `WEB_ABORTED`。
115
123
 
116
124
  ## 仓库布局
117
125
 
118
126
  ```
119
- lib/index.js Host 插件:Config、按元数据分发提供方、设置段、额度路由
120
- lib/providers.js 静态元数据表:内置 REST provider 的请求/鉴权/响应/官方跳转(纯数据,无定制代码)
121
- lib/rest.js 通用 REST 后端:按元数据驱动请求构造与响应规范化(可选 hooks)
122
- lib/deepseek.js DeepSeek 后端(Anthropic Messages + web_search_20250305,模型工具型专用)
123
- lib/tavily.js Tavily 选项解析 / 响应映射(额度由 Tavily hook 回传)
124
- lib/brave.js Brave 选项解析 / 响应映射(解析 X-RateLimit-*)
127
+ lib/index.js Host 插件:volatile Config、按元数据分发提供方、额度路由
128
+ lib/providers.js 静态元数据表:8 个 REST provider(另加 deepseek.js 的 DeepSeek,共9 个后端)的请求/鉴权/响应/官方跳转(纯数据,无定制代码)
129
+ lib/rest.js 通用 REST 后端:按元数据驱动请求构造与响应规范化(含 Tavily / Brave 的额度与限流 hook)
130
+ lib/deepseek.js DeepSeek 后端(Anthropic Messages + web_search_20250305 + 账号令牌)
131
+ lib/tavily.js Tavily 专用后端与选项解析(当前分发使用 rest 通用后端)
132
+ lib/brave.js Brave 专用后端与选项解析(当前分发使用 rest 通用后端)
125
133
  lib/usage.js Tavily/Brave 用量本地缓存与 /usage 对账
126
134
  lib/shared.js 中止 / 凭据解析辅助
127
- lib/client.js 浏览器 bundle:顶层「网页搜索」分区 + 引擎切换 + 官方 key 跳转 + 额度条
135
+ lib/client.js 浏览器 bundle:设置分区 + 引擎切换 + 官方 key 跳转 + 额度条
136
+ locale/en.json 插件列表页 / 详情页的英文标题与描述
137
+ locale/zh.json 同上,中文
128
138
  cordis.patch.yml Bundle patch:插入 Host 行、设 searchProvider、禁用内置 deepseek
129
139
  ```
130
140
 
@@ -138,14 +148,11 @@ cordis.patch.yml Bundle patch:插入 Host 行、设 searchProvider、禁用
138
148
  dsh plugin --profile web add ".\dsh-web-search-plugin"
139
149
  ```
140
150
 
141
- **Windows 跨盘不要 `dsh plugin add` 绝对路径。** Profile 在 `C:`、仓库在 `D:` 时,它会写成 `link:d:/...`;pnpm 把盘符当成相对路径,junction 会指到 `profiles\web\D:\...` 并链坏。也不要写 `file:D:/...`:pnpm 10 同样会把跨盘绝对路径拼进 profile 目录。
142
-
143
- 正确做法是快照到与 profile **同盘**,再用 `file:`:
151
+ **Windows 跨盘不要 `dsh plugin add` 绝对路径。** Profile 与仓库不在同一盘时,它会写成 `link:` 或 `file:` 加绝对盘符;pnpm 会把盘符当成相对路径,junction 指向一个不存在的目录。做法是先把仓库快照到与 profile **同盘**,再用相对 `file:`:
144
152
 
145
153
  ```powershell
146
154
  $dst = "$env:USERPROFILE\.dsh\profiles\web\.local\dsh-web-search-plugin"
147
- New-Item -ItemType Directory -Force -Path $dst | Out-Null
148
- robocopy "D:\sample\dsh-web-search-plugin" $dst /E /XD node_modules .git .github /NFL /NDL /NJH /NJS
155
+ robocopy "<仓库路径>" $dst /E /XD node_modules .git .github .snapshots .workbuddy docs /NFL /NDL /NJH /NJS
149
156
  ```
150
157
 
151
158
  在 profile 的 `package.json` 里:
@@ -160,51 +167,49 @@ robocopy "D:\sample\dsh-web-search-plugin" $dst /E /XD node_modules .git .github
160
167
  dsh plugin --profile web install
161
168
  ```
162
169
 
163
- `file:` 是快照:改完仓库后要再 robocopy + `install` 并重启 DSH。
170
+ `file:` 是快照:改完仓库后要再 robocopy + `install` 并重启 DSH。改了 `package.json` 的 `files` 字段时,pnpm 可能认为快照未变化而跳过更新,需要先删掉 `node_modules/dsh-web-search-plugin` 再装。
164
171
 
165
172
  用 `dsh --profile web --dump-config` 确认:组成树里应有 `dsh-web-search-plugin` 行,且 `web.searchProvider` 为 `dsh-web-search`。
166
173
 
167
- > **关于 `file:`(本地)安装** — pnpm 会把 `file:` 依赖做成快照。改完本仓库后,需要再同步快照并 `dsh plugin --profile web install`,然后重启。Windows 上 profile 与仓库不在同一盘时,用同盘 `file:.local/...`,不要 `link:` / `file:` 指向另一块盘。
174
+ ### 语法检查
168
175
 
169
- ### 迁移旧后端包
176
+ ```powershell
177
+ npm run check
178
+ npm test
179
+ ```
170
180
 
171
- 若 profile 已经覆盖了 `web`(例如旧包 `@dsh-ltctfer/dsh-web-search-brave` 留下的 `brave-official`),后打的 patch 仍会生效。请改指向本插件,并卸掉旧包:
181
+ 客户端 bundle 必须保持 `window.__ModuleLoader__.load({ id, factory })` 线格式 — 由 `dsh-client-modules` 加载,不是打包器。必须同时导出 `apply` 和 `inject`(`slots`、`locale`、`remote`、`remote.credentials`、`configForms`)。
172
182
 
173
- ```yaml
174
- - id: web
175
- name: '@deepseek-ai/dsh-web'
176
- config:
177
- searchProvider: dsh-web-search
178
- ```
183
+ 配置卡片的宿主就是**设置 → 网页搜索**(`settings.section`,`id: "web-search"`),数据来自 `ctx.configForms.get("dsh-web-search-plugin")`,并通过 `configForms.whileServed([namespace], register)` 条件注册:Host 未下发该命名空间时不留痕迹。
179
184
 
180
- ```powershell
181
- dsh plugin --profile web remove @dsh-ltctfer/dsh-web-search-brave
182
- ```
185
+ 本插件不在插件详情页(`plugins.bundle.config`)显示配置。
183
186
 
184
- 然后在 **设置 → 网页搜索** 里把引擎切到想要的提供方,即可继续使用对应的 API key。
187
+ ## 本地化
185
188
 
186
- ### 语法检查
189
+ 插件列表页与详情页的标题和描述走 `locale/<lang>.json`,随界面语言切换:
187
190
 
188
- ```powershell
189
- npm run check
191
+ ```
192
+ locale/en.json {"meta": {"title": "Web search", "description": "…"}}
193
+ locale/zh.json {"meta": {"title": "网页搜索", "description": "…"}}
190
194
  ```
191
195
 
192
- 客户端 bundle 必须保持 `window.__ModuleLoader__.load({ id, factory })` 线格式 — 由 `dsh-client-modules` 加载,不是打包器。必须同时导出 `apply` 和 `inject`(`slots`、`locale`、`remote`、`settingsScope`),并把分区注册进 `settings.section`(`id: "web-search"`)。
196
+ 卡片内的文案由 `lib/client.js` 的 `zh` / `en` 两份字典提供,改动时须保持键名对齐。
193
197
 
194
198
  ## 发布
195
199
 
196
- 发布由 GitHub Actions(`.github/workflows/publish.yml`)自动完成:**推送 `v*` 标签**(例如 `v0.3.0`)会带 provenance 发到 npm。向 `main` 的普通推送不会发布。
200
+ 发布由 GitHub Actions(`.github/workflows/publish.yml`)自动完成:**推送与 `package.json` 版本一致的 `v*` 标签**会带 provenance 发到 npm。向 `main` 的普通推送不会发布。
197
201
 
198
- 1. 确认 `package.json` 的 `version` 与即将推送的标签一致(例如 `0.3.0` → `v0.3.0`)。
202
+ 1. 确认 `package.json` 的 `version` 与即将推送的标签一致,并在 `CHANGELOG.md` 中为该版本填写非空的 `## [版本号]` 章节。
199
203
  2. 在 GitHub **Settings → Secrets and variables → Actions** 里配置有 publish 权限的 npm automation token(仓库密钥 `NPM_TOKEN`),并允许 Actions 运行。
200
204
  3. 打标签并推送:
201
205
 
202
206
  ```powershell
203
- git tag v0.3.0
204
- git push origin v0.3.0
207
+ $version = node -p "require('./package.json').version"
208
+ git tag "v$version"
209
+ git push origin "v$version"
205
210
  ```
206
211
 
207
- 工作流会先核对标签与 `package.json` 的 `version`,再执行 `npm publish --provenance --access public`。
212
+ 工作流会先核对标签与 `package.json` 的 `version`,确认 `CHANGELOG.md` 中对应版本的发布说明非空,再执行 `npm publish --provenance --access public`。
208
213
 
209
214
  ## 参与贡献
210
215
 
package/cordis.patch.yml CHANGED
@@ -3,18 +3,21 @@
3
3
  # installs the package as a plain library: no loader row, no Host apply, no
4
4
  # `/plugins/<name>/client.js`.
5
5
  #
6
- # `searchProvider` is restated in full because a Cordis patch replaces the
7
- # targeted row's whole `config`. The profile's own cordis.patch.yml still
8
- # wins if it later overrides `web`. The seam id is `dsh-web-search`; the
9
- # backend (DeepSeek / Tavily / Brave) is the plugin's own `provider` setting.
6
+ # `searchProvider` and the upstream `fetchProvider` are restated in full
7
+ # because a Cordis patch replaces the targeted row's whole `config`. The
8
+ # profile's own cordis.patch.yml still wins if it later overrides `web`. The
9
+ # seam id is `dsh-web-search`; the backend (DeepSeek / Tavily / Brave) is the
10
+ # plugin's own `provider` setting.
10
11
  - id: web
11
12
  name: '@deepseek-ai/dsh-web'
12
13
  config:
13
14
  searchProvider: dsh-web-search
15
+ fetchProvider: http
14
16
 
15
- # This plugin re-hosts the DeepSeek backend, so the in-box DeepSeek host
16
- # plugin is redundant: disabling it removes its `deepseek-official` provider
17
- # (no duplicate) and hides the legacy "Plugins → web search" card.
17
+ # This bundle selects its own `dsh-web-search` seam provider and configures
18
+ # DeepSeek through that provider. Disable the in-box DeepSeek host plugin so
19
+ # the profile has one active DeepSeek configuration surface. Its provider id
20
+ # is `deepseek-official`, distinct from this bundle's `dsh-web-search` id.
18
21
  - id: web-search-deepseek
19
22
  disabled: true
20
23
 
package/lib/brave.js CHANGED
@@ -12,7 +12,6 @@
12
12
  */
13
13
  import { ProxyAgent } from "undici";
14
14
  import { WebError } from "@deepseek-ai/dsh-web";
15
- import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
16
15
  import { MAX_RESULTS_CAP, USER_AGENT, isAbortError, isPositiveInteger, resolveApiKey, resolveSecret, searchAborted, throwIfSearchAborted } from "./shared.js";
17
16
 
18
17
  /** Default Brave Search API web-results endpoint. */
@@ -123,9 +122,9 @@ export class BraveSearchProvider {
123
122
  }
124
123
 
125
124
  /**
126
- * Cached undici ProxyAgent for the current proxy URL. Node's fetch does not
127
- * read `HTTPS_PROXY` itself; a configured proxy (or the launch-environment
128
- * fallback) is how DNS for api.search.brave.com can go through a tunnel.
125
+ * Cached undici ProxyAgent for an explicit provider-level proxy override.
126
+ * With no override, fetch inherits the dispatcher installed by DSH so its
127
+ * HTTP_PROXY / HTTPS_PROXY / ALL_PROXY / NO_PROXY policy remains intact.
129
128
  */
130
129
  dispatcher(options) {
131
130
  const proxy = options.proxy;
@@ -142,7 +141,6 @@ export class BraveSearchProvider {
142
141
 
143
142
  /** Project the plugin section into Brave backend options. */
144
143
  export function resolveBraveOptions(ctx, config) {
145
- const environment = launchEnvironmentOf(ctx);
146
144
  return {
147
145
  ...resolveSecret(ctx, {
148
146
  literal: config.braveApiKey,
@@ -153,6 +151,6 @@ export function resolveBraveOptions(ctx, config) {
153
151
  country: config.country,
154
152
  searchLang: config.searchLang,
155
153
  freshness: config.freshness,
156
- proxy: config.proxy ?? environment.get("HTTPS_PROXY")?.value ?? environment.get("HTTP_PROXY")?.value
154
+ proxy: config.proxy
157
155
  };
158
156
  }