9router-web-search
让 dsh 的 web_search / web_fetch 改走本机 9router 服务的搜索与抓取接口,替换内置 provider;关掉开关即可回退到 dsh 内置实现。
- 包名 / entry id / settings 命名空间:
9router-web-search - provider id:
9router(搜索)、9router-fetch(抓取) - 只替换
ctx.web接缝的 provider 层:工具的 JSON schema、系统提示词、结果卡片与 UI 全部不变,模型无感
安装
推荐:从 git 安装(自带补丁,卸载干净)
dsh plugin add github:yifabunemgshoushi-oss/dsh-9router-web-search
插件通过 package.json 的 dsh.bundle.patch 声明自带的 cordis.patch.yml,安装时 dsh 会自动处理两件事,不需要手改 profile:
- 把插件插入 Loader 行;
- 把
web接缝的 provider 指向它。
卸载也会自动清理:app-boot/src/profile-plugins.ts 的 reconcileProfilePlugins 会在包失去 dsh.bundle.patch 声明(或依赖被移除)时,自动把本 bundle 从 profile 的 bundles 列表剔除,补丁层随之消失,web 行回落到 base 的默认 provider(deepseek-official / http),不留悬空引用。
这一点很关键。若插件没有自带补丁、而是在 profile 里手写 entry,卸载器不知道那些手写条目的存在,会残留一条指向已卸载插件的行——插件页会继续显示它,且
web行留下悬空 provider id(可能触发WEB_PROVIDER_CONFIGURED_MISSING)。
密钥怎么给
补丁里不写明文密钥,只声明凭据引用:
apiKeyEnv: NINEROUTER_API_KEY
插件按 字面量 apiKey → credentials 域 → 启动环境变量 的顺序解析(lib/index.js 的 resolveApiKey)。三种给法任选:
- 凭据域(推荐):
dsh的「设置 → 插件 → 9Router 网页搜索」里填 API Key,写入<DSH_HOME>/.credentials.yaml的refs; - 环境变量:设
NINEROUTER_API_KEY(注意要用apiKeyEnv指向的那个名字); - profile 局部覆盖:在 profile 的
cordis.patch.yml里覆盖本行 config 补apiKey。注意config是整段替换,须重述其余字段。
本地开发(link: 挂载)
源码目录:D:\other\dsh-plugins\dsh-9router-web-search(与 dsh-mcp-manager、dsh-settings-extras 同级)。
目录名 ≠ 包名:插件对外身份是包名 9router-web-search(package.json 的 name),目录上的 dsh- 前缀只是本地存放约定,不参与解析。profile 按依赖键建软链,所以依赖键必须写包名。
在 profile 的 package.json 里加一条 link 依赖,再跑 pnpm install 生成软链:
"9router-web-search": "link:D:/other/dsh-plugins/dsh-9router-web-search"
改用(或改用回)link 挂载时,记得把 git 安装时期留下的 profile 手写条目一并清掉,否则同一 id 会有两份定义(后者静默覆盖前者,不报错)。
配置字段
| 字段 | 默认 | 说明 |
|---|---|---|
enabled |
true |
开启时强制 9router,并把 web 的 provider 选择写回 9router;关闭后不再调用 9router,直接走内置实现 |
baseUrl |
http://localhost:20128 |
9router 服务地址。不要带 /v1,插件自己拼 /v1/search、/v1/web/fetch |
apiKey |
空 | 9router 的 API Key。走 credentials 域存储,不写入设置文件 |
apiKeyEnv |
NINEROUTER_API_KEY |
密钥引用名;apiKey 为空时按此名去 credentials / 环境变量解析 |
searchModel |
search-combo |
搜索走 9router 的哪个模型/路由 |
fetchModel |
fetch-combo |
抓取走 9router 的哪个模型/路由 |
searchType |
web |
传给 /v1/search 的 search_type,详见下节 |
timeoutMs |
60000 |
单次搜索或抓取的最长等待时间(毫秒) |
fallbackToBuiltin |
true |
9router 调用失败时,是否自动改用内置实现重试 |
search_type(搜索类型)
可选值
web 与 news 两个。这是 9router 自己配置表单给出的选项(...\9router\app\.next-cli-build\server\app\(dashboard)\dashboard\media-providers\[kind]\[id]\page.js):
{ key: "search_type", label: "Type", type: "select", default: "web", options: ["web", "news"] }
严格说,取值由 9router 当前那条 combo 里每个上游 provider 的 searchTypes 数组决定。/api/v1/search 的取值逻辑是:
searchType: c.search_type || provider.searchTypes?.[0] || "web"
即:你没填 → 用该 provider 的第一个 → 再兜底 "web"。
有什么用
- 选择搜索索引类型:
web= 通用网页索引;news= 新闻/时效优先。 - 副作用是会影响 9router 选择哪条上游 provider。
- 填了不支持的值不报错,上游会忽略它并退回默认,试错成本低。
实测对比
用 search-combo、同一 query 实测(2026-10):
| search_type | 实际命中的上游 | 结果特征 |
|---|---|---|
web |
tavily | 通用网页结果 |
news |
searchapi | 明显偏时事新闻 |
images(非法值,用于验证) |
tavily | 静默退回默认,无报错 |
建议
- 平时保持
web。 - 想要时效性强的新闻结果时切
news。 - 面板里清空该字段会回到插件默认值
web,与 9router 自身的缺省行为一致。
未暴露的其它 9router 参数
9router 的搜索接口还接受 country、language、time_range、offset、domain_filter、content_options、provider_options,但 dsh 的 web 接缝只向下传 query + maxResults,因此本插件未在面板中暴露这些字段。
回退语义
触发条件:enabled = false,或 9router 调用失败且 fallbackToBuiltin = true。另外,enabled = true 时插件会把 web 的 provider 选择强制写回 9router,避免内置开关把执行路径切走(见「0.2.1 适配说明」)。
| 能力 | 回退实现 |
|---|---|
| 抓取 | 匿名 HTTP GET(等价 dsh 内置的 dsh-web-fetch-http),按 content-type 判定 html / text;非 2xx 作为正常结果返回而非抛错 |
| 搜索 | 等价的 DeepSeek 原生搜索(Anthropic 兼容 Messages + web_search_20250305,并显式发送 thinking: { type: 'disabled' })。需要 DEEPSEEK_API_KEY,取不到时如实报 WEB_PROVIDER_CREDENTIAL_MISSING,不会编造结果 |
为什么用常驻注册 + 开关,而不是"卸载即回退":ctx.web 的 provider 选择策略是"显式配置的 id 优先",若插件卸载,接缝会报 WEB_PROVIDER_CONFIGURED_MISSING 而不是回退。所以两个 provider 常驻注册,由 enabled 在执行路径上切换,这样一拨开关就能热回退,无需重启。
设置面板
位置:插件 → 9Router 网页搜索(0.2.0 时代写作「设置 → 插件」,0.2.1 起插件配置统一收在插件页)。
可以改:启用开关、接口地址、API Key、密钥引用名、搜索模型、抓取模型、搜索类型、超时、失败回退开关。改完即热生效,无需重启。
首次安装后,Host 半会热加载生效;但客户端面板需要重启一次 dsh 才会出现(
dsh-client-modules在启动时扫描并生成 boot graph)。
客户端控件是自带的,不依赖 Harness Client 包
lib/client.js 只 require react 与 react/jsx-runtime,设置页用到的控件(开关、取值字段、密钥字段、表单外壳、SettingsFormModel 表单内核)都在文件内的 #region lib/client/ui-port.js 里自带实现,样式经 mountStyles() 注入。
- 为什么不用
@deepseek-ai/dsh-client-ui-primitives:官方references/practices.md:35明确禁止客户端插件 require 任何 Harness Client 包。该包虽是平台种子模块(packages/client/web/src/seed.ts把它注入冻结模块表,当前可用),但其 API 不受 semver 约束、手写 JS 也无类型检查;一旦不兼容,失败形态是整个 slot 条目变空白(控制台slot entry crashed in '<slot>'),而不是一个可降级的错误。自带实现后只依赖react与--dsw-*token,DSH 升级最多让外观降级。 - 类名与样式:统一加
dsh-9rws-前缀以避免与宿主样式冲突;颜色只用--dsw-*主题 token。原包的 CSS Modules 已内联为CSS_TEXT常量。 - 移植范围:只移植本插件用到的能力。原包中
SettingsValueField的help信息按钮、numeric/placeholder透传,以及各控件的className/title透传均未移植(本插件不需要),因此没有无测试覆盖的死代码。 SettingsFormModel的差异:原实现依赖@deepseek-ai/dsh-client-store的createSnapshotStore;自带版改为等价的极小实现(subscribe/getSnapshot/set),以彻底断开该依赖。语义(暂存编辑、保存、修订栅栏、base/user分层、overridden 判定)完全保留。
0.2.1 适配说明(2026-10-21 核对)
已对照 dsh 源码 0.2.1-rc.2(D:\AI_ls\deepseekHarness)核对。宿主契约未变,本插件不需要为 0.2.1 重写:
ctx.web的 provider 接口、registerSearchProvider/registerFetchProvider、WebError与错误码、WebSearchResult/WebFetchResult形状均未变。@deepseek-ai/dsh-credentials的credentialRef/isCredentialRefName/resolve()、@deepseek-ai/dsh-launch-environment的launchEnvironmentOf()均未变;0.2.1 把凭据优先级改为「存储优先于环境」,与本插件「字面量 → credentials → 启动环境」的解析顺序一致。- 客户端
dsh.client清单、window.__ModuleLoader__.load协议、plugins.item槽位、ctx.configForms的getSnapshot/subscribe/mutate均未变(仅删除了本插件未使用的mode字段);react与react/jsx-runtime仍是平台种子模块。 web行的searchProvider/fetchProvider在 0.2.1 变为 volatile(每次搜索/抓取时读取),本插件补丁的写法无需调整。- peer 范围
^0.2.0-rc.2在 0.2.1 的兼容性判定(includePrerelease: true)下仍满足0.2.1-rc.2,不会被判为不兼容。
两处行为变化:
内置搜索回退已同步:0.2.1 的内置 Messages 搜索显式发送
thinking: { type: 'disabled' },避免正文被丢弃的辅助请求白跑思考 token;本插件回退路径已同步。内置开关会被本插件接管:0.2.1 新增
web-deepseek-api(deepseek-search-api/deepseek-fetch-api),内置「网页搜索」页的 Use the new Web Search API 开关会把web.searchProvider/web.fetchProvider写进 profile 的用户层。它与本插件的enabled开关不是一回事:状态 实际执行 本插件开 强制 9router;守卫发现 web选择被改走时写回9router/9router-fetch本插件关 seam 保持当前选择;若仍是 9router,则走插件内部的内置等价实现;内置开关可正常切换 enabled为true时,installWebSelectionGuard订阅settings/document-updated,用settings.describe()读取web的生效值,发现不是 9router 就用settings.update('web', …)写回。写入放在微任务里避免重入;写回值与 bundle 层一致时,settings 会直接删除用户层覆盖,所以内置开关保存后会在下一次刷新被改回 OFF,日志会记录一条restoring web.searchProvider/fetchProvider to 9router。enabled为false时守卫不写入,把选择权交还内置页面。因此,开启插件时不会出现“拨一下内置开关就绕过插件、没 DeepSeek 凭据导致搜索全挂”的情况。例外是
web行被 home patch 或命令行 overlay 覆盖:configEditor.edit会拒绝写入,守卫只能记录警告,无法强制——这是宿主规定的覆盖优先级。此时内置「网页搜索」页会显示 Messages 模式,但实际执行的是 9router;在该页改 Messages 的 baseURL / maxUses 不会影响 9router,只有
DEEPSEEK_API_KEY会被插件的内部回退使用。仍不建议给
web-deepseek-api加disabled: true来藏开关:守卫只在插件启用时生效;插件关闭时,如果用户层已经保存过searchProvider: deepseek-search-api,禁用该行会让web指向未注册的 provider,触发WEB_PROVIDER_CONFIGURED_MISSING。
升级安装提示:0.2.1 起 HMR 模式下对已安装包再次安装会返回
already-installed;更新本插件需先在插件页移除,再重新安装。
维护提示
- 源码目录与挂载方式:源码在
D:\other\dsh-plugins\dsh-9router-web-search。桌面 profile 目前用 git 依赖(github:yifabunemgshoushi-oss/dsh-9router-web-search),profiles/desktop/node_modules/9router-web-search是安装出来的实体副本;本地改完源码后,要么把该依赖临时改成link:D:/other/dsh-plugins/dsh-9router-web-search再pnpm install,要么推送后走插件页更新。两种方式改完都需重启 dsh 才生效。 - 依赖由宿主供给,仓库不携带
node_modules:package.json把插件直接 import 的 4 个包声明为peerDependencies与devDependencies各一份——@deepseek-ai/dsh-credentials、@deepseek-ai/dsh-launch-environment、@deepseek-ai/dsh-web、@deepseek-ai/schemastery。- peer 让 profile 解析器用运行中 dsh 安装里的那一份占据该包名,即使插件目录下没有物理
node_modules也成立(见packages/boot/app-boot/src/profile-resolution/resolver.ts的routeLinked:读 importer 祖先链上每层 manifest 的peerDependencies来判定拦截)。dev 副本供编译器/独立测试使用,运行时不会被加载。 - 必须共用同一实例:
lib/index.js用error instanceof WebError判定错误,两侧各自复制一份会让该判定静默失效。 - 其余传递依赖(
cosmokit、cordis、dsh-brand、dsh-llm、zod等)由宿主安装闭包提供(app.asar内共 289 个@deepseek-ai/*包),无需声明。 node_modules/与temp-dsh/已加入.gitignore。本地若仍留有旧的实体副本,声明 peer 后运行时已优先走宿主那一份,副本只是冗余(可删)。- 升级 dsh 后若出现模块缺失或 API 不存在,核对两个不变量:①
package.json里 peer 声明的每个包名,都仍出现在 dsh 安装闭包中(app.asar内dsh/node_modules/@deepseek-ai/*,本次核对时共 289 个);② 插件直接 import 的每个包名都已声明 peer。历史上profiles/node_modules曾是指向 npm 版 dsh0.1.0-rc.7的 junction 农场,那套缺schemastery.volatile()与dsh-credentials.isCredentialRefName,加载会崩;该农场已于 2026-10-13 清理。
- peer 让 profile 解析器用运行中 dsh 安装里的那一份占据该包名,即使插件目录下没有物理
- 分发方式:本地开发用 profile 的
link:依赖(见上)。要装到别的机器,走 git 源(dsh plugin add github:<用户>/dsh-9router-web-search),由插件管理器在 profile 内跑 pnpm 安装依赖。lib/必须提交:安装前会预检exports指向的文件真实存在,缺构建产物会直接判为失败。 - 命名空间即 entry id:settings 命名空间等于 profile 里的 entry id。若要改名,
cordis.patch.yml的id、lib/index.js的name、lib/client.js的 bundleid与ROUTER_NS必须一起改;旧命名空间下面板写入的用户层配置会变成孤儿。 - 桌面版契约:本插件按 dsh 0.2.0-rc.2 的接口编写,并已核对 0.2.1-rc.2 仍然兼容(见上节「0.2.1 适配说明」)。用于更早的 npm 版 dsh 需要按旧 API 改写。
No comments yet. Be the first to write one.