Repository navigation
Releases
v1.3.0 — 只做检索:装好即生效,只留一个设置 | Plain search provider: works on install, one setting
中文
本版把插件收敛为一个纯粹的检索提供方:装好后直接生效,不需要改任何配置,也不需要调参。
1. 装好即生效(v1.2.8 起)
apply() 注册完 provider 后,直接把 ctx.web.config.searchProvider 设为 serper。web seam 在每次 search() 时解析选择、不缓存,因此无需重启。
原先靠 patch 里整行替换 web 行的接管方式已移除——它会被 profile 的 patch 层静默顶掉,且在配置晚于进程启动写入时完全不生效。
2. 只保留一个设置项(v1.3.0)
移除 gl / cr / numResults / baseURL。这些开关只会让检索在用户无感知的情况下变样:
gl: cn+ 英文技术查询 → 结果全是中文站点numResults→ 覆盖宿主web_search工具下发的条数baseURL→ 等于允许把 key 发往任意地址
现在:端点固定为 https://google.serper.dev,条数由宿主工具决定,区域跟随 Serper 默认。老 profile 里残留的这些值不会再被读取。
3. 尊重显式选择
$DSH_WEB_SEARCH_PROVIDER 非空且不是 serper 时插件让位。Serper 不可用时自动降级到其它已注册的搜索后端。
验证
tsc --noEmit0 错误tsdown构建通过smoke-test.mjs全部通过(含接管 / 让位 / 幂等 / 脏 config 隔离护栏)- 真实 Serper 端到端检索通过
升级
dsh plugin --profile desktop add @dingpenghui/dsh-web-search-serper@1.3.0升级后重启一次宿主。gl / cr / numResults / baseURL 已不再读取,可从 profile 中删除。
English
This release turns the plugin into a plain search provider: it works right after install, with nothing to configure and nothing to tune.
1. Works right after install (since v1.2.8)
apply() sets ctx.web.config.searchProvider to serper immediately after registering the provider. The seam resolves the selection on every search() call and caches nothing, so no restart is needed.
The old patch-based whole-row takeover of the web row was removed: a profile patch layer could silently cancel it, and it never applied when configuration landed after process start.
2. Only one setting remains (v1.3.0)
Removed gl / cr / numResults / baseURL. Those knobs only changed results without the user noticing:
gl: cn+ an English technical query → all-Chinese resultsnumResults→ overrode the count the hostweb_searchtool asked forbaseURL→ allowed the key to be sent to any address
Now the endpoint is pinned to https://google.serper.dev, the result count comes from the host tool, and region follows Serper's default. Values left over in an old profile are no longer read.
3. An explicit choice is respected
The plugin stands down when $DSH_WEB_SEARCH_PROVIDER is set to anything other than serper, and falls back to another registered provider when Serper fails.
Verified
tsc --noEmitcleantsdownbuild passessmoke-test.mjspasses (takeover, opt-out, idempotence, and legacy-config isolation guards)- Live end-to-end Serper search succeeds
Upgrade
dsh plugin --profile desktop add @dingpenghui/dsh-web-search-serper@1.3.0Restart the host once after upgrading. gl / cr / numResults / baseURL are no longer read and can be removed from your profile.
Install / 安装:dsh plugin --profile <name> add @dingpenghui/dsh-web-search-serper@1.3.0
v1.2.7 — 可观测性:让「装了却没生效」有迹可循
v1.2.7 — 可观测性:让「装了却没生效」不再无迹可寻
背景:一次真实的误判
有用户反馈:天气查询没有走 Serper。排查后发现,插件其实安装正常、版本正常、
配置也写进了 cordis.patch.yml,但当前 DSH 进程从未加载它。时间线是:
| 时间 | 事件 |
|---|---|
| 12:26 | 卸载旧版 |
| 12:27 | 安装 1.2.5 |
| 12:32 | cordis.patch.yml 被改写(含 web-search-serper 配置) |
| 12:36 | 该会话开始查天气 |
问题不在插件代码,而在两点叠加:
cordis.patch.yml只在 DSH 进程启动时读取一次。 进程先启动、配置后
写入,插件层就不会被加载,而搜索会安静地继续走内置后端,不报任何错。- 插件全程没有任何可观测性。 注册、被寻址、调用、降级、失败——每一步
都是无声的。用户既看不到「没加载」,也看不到「加载了但没被选中」。
结果是:插件本身完全正常,用户却只能靠猜。
本次改动
1. 注册与路由的日志留痕
apply() 成功时记录一行(这是「插件真的在本进程生效」的唯一证据):
web-search-serper: registered search provider "serper" (key source: credential)
并主动检测两种「加载了但不会被调用」的情形,各给出一条警告:
web-search-serper: web.searchProvider is "deepseek-official", not "serper" —
this plugin is loaded but will not be used until the web row selects it
web-search-serper: the web row does not declare searchProvider —
this plugin is registered but the seam will not route searches to it
2. 降级与失败写日志
此前降级是静默的——用户以为在用 Serper,实际是别的后端给的结果:
web-search-serper: fell back to "deepseek-official" — serper failed: <原因>
web-search-serper: search failed and no usable fallback — <原因>
3. 新增 readRuntimeStatus()
import { readRuntimeStatus } from '@dingpenghui/dsh-web-search-serper'
readRuntimeStatus()返回:
{
registered: boolean // 插件层是否已加载并注册到 seam
calls: number // 本进程累计调用次数
serperHits: number // 其中真正走 Serper 成功的次数
fallbacks: number // 其中发生降级的次数
last?: {
outcome: 'serper-ok' | 'fallback-ok' | 'failed' | 'aborted'
servedBy: string // 本次实际提供结果的 provider id
at: number
durationMs: number
error?: string // 截断至 300 字符
keySource: 'config' | 'env' | 'credential' | 'missing'
}
}判定方式:
registered: false→ 插件层没加载,重启 DSH;registered: true但calls: 0→ 加载了但没被寻址(searchProvider未指向
serper,或从未搜索);keySource用于区分「我以为配了 key」与实际读到的来源。
该快照不包含 key 值本身,只有来源枚举,可安全打日志或贴进 issue。
4. 文档
README.md / README.zh.md 新增「排障:装了、配了,为什么搜索还是不走
Serper?」一节,给出 5 步定位流程,并说明 DSH 配置只读一次的时机问题。
不变的部分
- 搜索行为完全未变:仍是「优先 Serper,失败自动降级」;
- 默认配置未变(
cordis.patch.yml依旧不含任何 key); - 未新增任何运行时依赖。
验证
pnpm run typecheck通过pnpm run build通过pnpm test通过:OK all serper plugin contract checks passedsmoke-test.mjs新增可观测性回归护栏:apply()后readRuntimeStatus().registered === true;- 至少记录一次调用,且
last.outcome/last.keySource落在已知枚举内; - 状态快照不得泄漏 key 值;
- 未调用
apply()的独立模块实例报告registered: false、calls: 0。
升级
dsh plugin --profile desktop add @dingpenghui/dsh-web-search-serper@1.2.7升级后重启宿主。重启后可在日志里确认这一行:
web-search-serper: registered search provider "serper"
v1.2.6 — 修复设置页输入框无法输入
v1.2.6 — 修复设置页输入框无法输入(受控组件未订阅 store)
问题
设置页的 API Key、端点基址、国家/地区代码、结果数量全部无法输入:
输入框可以聚焦、可以看到光标,但敲键后内容不出现,或立刻被清空。
v1.2.5 修的是同一页面的只读视图(占位符误导),并未触及本问题;
本版修复的是可编辑表单本身。
根因
src/client/plugin-detail-page.tsx 的 SerperSettingsForm 创建了
SettingsFormModel 与其 SnapshotStore,但从未订阅该 store:
const [page, setPage] = React.useState(() => createPage(props.form))
// ...
const state = page.model.shell() // 渲染时现取一次
const actions = page.model.actions() // ...宿主 SettingsFormModel 的状态推送机制是:
// @deepseek-ai/dsh-client-ui-primitives 的 SettingsFormModel
constructor(scope, specs, secretSpecs) {
this.unsubscribe = scope.subscribe(() => { this.publish() })
}
bind(project) {
const store = createSnapshotStore(project())
this.listeners.add(() => { store.set(project()) })
return store
}
publish() { for (const listener of this.listeners) listener() }即:actions.edit() → model.stage() → model.publish(),而 publish()
只把新状态写进 bind() 返回的 SnapshotStore,不会触发 React 重渲染。
由于组件没有订阅该 store,React 始终拿不到更新的 text 值。而宿主的
SettingsValueField 是受控输入:
// @deepseek-ai/dsh-client-ui-primitives 的 SettingsValueField
<input
type="text"
value={e.text}
onChange={d => e.onEdit(d.target.value)}
/>于是形成死锁:用户按键 → onEdit → model 内部 staged 更新 → 无重渲染 →
value 仍是旧值 → 输入被回滚。表现为「完全无法输入」。
注:该缺陷不影响只读视图(
plugins.bundle.config),因为那里用的是普通
<input readOnly>,不依赖 model 的订阅。
修复
用 useSyncExternalStore 把组件接到真正的 store 上:
const subscribed = React.useSyncExternalStore(
page.store.subscribe,
page.store.getSnapshot,
page.store.getSnapshot,
)
const state = subscribed
const actions = page.model.actions()page.model.field(key) 仍在渲染期调用,因此能读到最新 staged 值;
订阅建立后,publish() 才能驱动重渲染,输入得以回显。
同时清理 SettingsForm 的 state 传参,直接传递订阅得到的快照。
验证
pnpm run typecheck通过pnpm run build通过(client bundle 16.61 kB → 16.79 kB)pnpm test通过:OK all serper plugin contract checks passed- 实机验证:替换本地已安装包的
lib/client.js并重启 DSH Desktop
0.2.1-rc.2,设置页各字段均可正常输入。
升级
dsh plugin --profile desktop add @dingpenghui/dsh-web-search-serper@1.2.6升级后需重启宿主(client bundle 为 ESM,进程内缓存不会自动刷新;
仅刷新页面不够)。
关于 v1.2.5
v1.2.5 修复的是只读视图的占位符误导与提示文案,与本问题无关,未引入
回归。两个版本请都升级到 1.2.6。
v1.2.5 — 插件页配置区占位符误导修复
v1.2.5 — 修复插件页配置区把占位符伪装成已填值
问题
在插件列表里点插件名进入的插件页,API Key 一栏会显示 ••••••••,
看起来像「key 已配置」,但该输入框无法输入:
- 那个圆点是
placeholder占位符(src/shared.ts的
SERPER_SETTINGS_FIELDS中apiKey的placeholder: '••••••••'),
实际config.apiKey为空; - 该页面在设计上就是只读的,无法在此编辑。
用户因此很容易误判成「已配置好」或「界面坏了」。
根因
1. 只读分支存在无效读取(恒为空)
src/client/plugin-detail-page.tsx 的只读分支:
if (form === undefined) {
const value = form?.state.value // form 已在上一行判定为 undefined,恒为 undefined
const section = value !== null && typeof value === 'object' ? value : {}在已判定 form === undefined 的分支里又用 form?. 读取,结果 section
恒为空对象——即便宿主将来传了值也显示不出来。此为死代码 + 逻辑错误。
2. 只读视图渲染了 placeholder
只读 input 同时渲染 value 与 placeholder。当值为空时浏览器显示
placeholder,对 secret 字段(type="password")尤其误导:••••••••
与真实密码掩码外观完全一致。
3. 提示文案指向不明确
原文案「请在插件行的「配置」页中修改并保存」没有说明从哪进入。
平台事实(为什么不能直接把插件页改成可编辑)
宿主 @deepseek-ai/dsh-client-ui-plugin-manager 渲染两条不同路径:
// 插件页(点插件名进入):不传 form
renderSlot("plugins.bundle.config", { view: "page" }, { entryKey: pkg.name })
// 行配置页(点行上的「配置」):传 form
renderSlot("plugins.row.config", { view: "page", form }, { entryKey: key })plugins.bundle.config 的 props 里没有 form,插件无从获得 mutate
通道,因此该页面只读是平台契约,插件侧无法绕过。
可编辑入口:插件页下方行列表中该插件那一行(按钮 aria-label 为
「配置 」,右侧带 ›),点击后进入 plugins.row.config,此处
才携带 form,可编辑并保存。
修复
| 位置 | 修改 |
|---|---|
plugin-detail-page.tsx 只读分支 |
删除无效的 form?.state.value,改为显式空快照并注明原因 |
plugin-detail-page.tsx 只读 input |
移除 placeholder,避免 secret 字段伪装成已填值;补充 aria-disabled;提示容器加 role="note" |
locales/index.ts(zh / en) |
page.readOnlyHint 改为明确的分步指引:此页只读,请返回插件列表、在该插件行点击「配置」 |
可编辑表单(
plugins.row.config)的行为未改动,本次只影响只读视图。
验证
pnpm run typecheck通过pnpm run build通过(client bundle 16.58 kB → 16.61 kB)pnpm test通过:OK all serper plugin contract checks passed
升级
dsh plugin --profile desktop add @dingpenghui/dsh-web-search-serper@1.2.5升级后需重启宿主(client bundle 是 ESM,进程内缓存不会自动刷新)。
v1.2.4 — 补发 v1.2.3 到 npm(无代码变更)
v1.2.4 — 补发 v1.2.3 到 npm(无代码变更)
为什么有这版
v1.2.3 的修复只推送到了 GitHub(tag v1.2.3),从未发布到 npm。
npm registry 上的 latest 一直停留在 1.2.2,因此:
dsh plugin add @dingpenghui/dsh-web-search-serper # 实际装到 1.2.2会装到不含兼容性修复的版本。1.2.4 与 1.2.3 的代码、依赖、默认配置
完全一致,仅用于把修复同步到 npm,让 latest 重新对齐。
包含的内容(即 v1.2.3 的全部修复)
- 兼容范围由
engines.dsh单一表达:>=0.2.0-rc.2 <0.3.0-0; - 移除对宿主运行时包(
@deepseek-ai/dsh-web/dsh-llm)的 peer 版本钉死,
修复在 DSH0.2.1-rc.2上被安装门禁拒绝的问题; - peer 仅保留
cordis ~4.0.5/schemastery ~3.18.5; smoke-test.mjs兼容性回归护栏。
关于默认配置:安装后所有人一致
cordis.patch.yml(随包分发,files 白名单包含)中的默认配置不含任何
API Key,所有安装者拿到的默认层完全相同:
- id: web
name: '@deepseek-ai/dsh-web'
config:
searchProvider: serper
fetchProvider: http
- insert:
- id: web-search-serper
name: '@dingpenghui/dsh-web-search-serper'API Key 无需写进配置,每次搜索时按以下顺序惰性解析:
- 行
config.apiKey(仅在你自己的 profile 覆盖层里显式配置时才存在); - 宿主进程环境变量
SERPER_API_KEY; - 凭证库引用
SERPER_API_KEY($DSH_HOME/.credentials.yaml的refs:)。
也就是说:默认配置对所有人相同;个人密钥只应放在环境变量或凭证库,
不要写进 composition 的config.apiKey,更不要提交到仓库。
升级
dsh plugin --profile desktop add @dingpenghui/dsh-web-search-serper@1.2.4或直接在插件管理器里安装 @dingpenghui/dsh-web-search-serper@1.2.4。
v1.2.3 — 修复 DSH 0.2.1-rc.2 上安装被拒绝
v1.2.3 — 修复安装被 DSH 兼容性门禁拒绝
问题
在 DSH 0.2.1-rc.2 上安装 1.1.1 会被直接拒绝:
dsh: installation rejected: Plugin @dingpenghui/dsh-web-search-serper@1.1.1
is incompatible with dsh 0.2.1-rc.2: requirements {"@deepseek-ai/dsh-web":"0.2.0-rc.2"}
dsh: restored package.json, pnpm-lock.yaml, and node_modules.
web_search 因此一直落在 dsh-base 自带的 deepseek-official 上,本插件从未生效。
根因
DSH 的安装门禁在 @deepseek-ai/dsh-app-boot 的 evaluatePluginCompatibility:
- 若清单里有
engines.dsh,只看它,peerDependencies被忽略; - 否则逐个检查
@deepseek-ai/dsh与@deepseek-ai/dsh-*的peerDependencies,
判定式为semver.satisfies(runtime, range, { includePrerelease: true })。
1.1.1 没有 engines.dsh,且把 @deepseek-ai/dsh-web 钉成精确版本
0.2.0-rc.2。即使开启 includePrerelease,0.2.1-rc.2 也不满足 0.2.0-rc.2,
因此判定不满足并被拒绝安装。
注:
0.2.1-rc.2的 web seam 实际只有两处内部变化——Config字段改为
volatile()、search()/fetch()在调用时惰性读取配置;WebSearchProvider
契约、WebError、resolveProvider与capSources的语义均未改变。
也就是说代码本来就能跑,被拦下的纯粹是版本声明。这一点已用从运行
app.asar 中抽取(SHA-256 校验)的dsh-web@0.2.1-rc.2逐行比对确认,并在真实
0.2.1-rc.2类型下通过tsc与tsdown构建。
修复
engines.dsh单一来源:">=0.2.0-rc.2 <0.3.0-0"。存在engines.dsh时
宿主只依据它判定,不再被 peer 上的精确版本误伤。- 移除对宿主运行时包的 peer 声明:
@deepseek-ai/dsh-web/
@deepseek-ai/dsh-llm由 DSH 宿主提供,不应再用 peer 钉版本;peer 只保留
cordis/schemastery(cordis ~4.0.5、schemastery ~3.18.5,与
0.2.1-rc.2运行时一致)。 - 依赖与构建对齐运行时:
devDependencies全部升到0.2.1-rc.2
(含 7 个 client 平台包),cordis升到4.0.5、schemastery升到3.18.5。 - 回归护栏:
smoke-test.mjs新增兼容性断言,优先调用宿主自己的
evaluatePluginCompatibility(宿主不可用时回退到语义等价的实现),要求
本清单在0.2.0-rc.2/0.2.1-rc.2上可安装、在0.3.0/0.1.5上被拒绝,
并禁止把@deepseek-ai/dsh-web重新钉回peerDependencies。
验证
在 DSH Desktop 0.2.1-rc.2 上:
plugin_manager install_bundle→application: applied,warnings: [];
宿主 inspect 显示web-search-serper行已挂载且 Config schema 正常;- 走真实
web_search工具返回结果; - 针对已安装包的 12 项检查全部通过:
真实打 Serper 端点(带X-Api-Key,非降级路径)、maxResults由 seam 截断、
丢弃无link条目、上游 500 自动降级、无 key 且无降级目标时抛
WEB_PROVIDER_CONFIGURED_UNAVAILABLE、真实凭据调用成功、取消信号映射为
WEB_ABORTED且不降级。
升级
dsh plugin --profile desktop add @dingpenghui/dsh-web-search-serper@1.2.3或直接在插件管理器里安装 @dingpenghui/dsh-web-search-serper@1.2.3。
1.2.2 及更早版本在 0.2.1-rc.2 上不会被门禁拦下,但仍建议升级到 1.2.3
以获得正确的 engines.dsh 声明与回归护栏。
v1.2.2 — 适配 DSH 0.2.0-rc.2(修复配置热读取与插件页渲染)
v1.2.2 — 适配 DSH 0.2.0-rc.2
目标运行时
本版本以 DSH 0.2.0-rc.2 为目标:
| 依赖 | 版本 |
|---|---|
@deepseek-ai/dsh-web |
0.2.0-rc.2(普通依赖) |
@deepseek-ai/dsh-llm |
0.2.0-rc.2(普通依赖) |
@deepseek-ai/cordis |
~4.0.4(peer) |
@deepseek-ai/schemastery |
~3.18.4(peer,可选) |
dsh-web / dsh-llm 声明为普通依赖而非 peer,是为了不让:
Plugin @dingpenghui/dsh-web-search-serper is incompatible with dsh <runtime>:
requirements {"@deepseek-ai/dsh-web":"..."}
这类兼容性门禁挡住加载(该门禁只检查以 @deepseek-ai/dsh- 开头的 peer)。
相对 1.1.1 的修复
| 位置 | 1.1.1 | 1.2.2 |
|---|---|---|
| 错误类型 | 从 dsh-web import WebError,跨版本安装时类身份可能不同 |
用 seam 的 WebError 作为基类(SerperWebError extends WebError),instanceof WebError 与宿主一致,code/cause 语义不变 |
| 取消判定 | error instanceof WebError && code === 'WEB_ABORTED' |
改为 instanceof HarnessError && code === 'WEB_ABORTED',跨模块副本也成立 |
| 行配置读取 | 只处理裸值 | 额外解包 ctx.settings.describe() 里的 volatile() 包装({ get(): T }),与宿主 plainConfig 同语义——否则用户保存的 key/端点会静默失效 |
| 客户端页面 | 自建 staged form,假定 plugins.bundle.config 会收到 form |
按 0.2.0-rc.2 的真实渲染:plugins.bundle.config 不传 form(只读概览),plugins.row.config 才传 form(用 SettingsFormModel + SettingsForm 走宿主 mutate 保存) |
| 页面挂载 | 直接在 apply() 注册 slot |
configForms.whileServed([ns], …):命名空间被 serve 才出现,缺失时降级为直接注册并提示 |
| 客户端依赖面 | 会 require('@deepseek-ai/dsh-client-ui-slots') 等模块表里不存在的包 |
只 require react / react/jsx-runtime / @deepseek-ai/dsh-client-ui-primitives |
| 端点拼接 | baseURL 末尾有 / 时拼出 //search |
统一去掉尾部 / |
| 类型 | 依赖官方包的类型声明 | 0.2.0-rc.2 的客户端包不带 .d.ts 且该版本线未在 npm 发布类型包,因此 src/contracts.d.ts / src/client/*.d.ts 按运行时实现逐项声明所消费的契约 |
兼容性
| DSH 运行时 | 插件版本 |
|---|---|
0.2.0-rc.x(已验证 0.2.0-rc.2 的完整类型面与运行时) |
1.2.2 |
0.1.7-rc.x |
1.2.0 |
0.2.1-rc.x |
尚未支持(该版本线的客户端公共包未发布,无法取得类型或运行时依赖) |
验证
pnpm run typecheck:0 错误(基于src/contracts.d.ts的本地契约声明)pnpm run build:产物只外置schemastery/dsh-llm/dsh-webpnpm run test:本地回环 HTTP server 上验证请求体(q/gl/num、X-Api-Key)、organic → sources 映射、429 降级、取消不降级、volatile 包装解包、缺 key 降级;并校验客户端 bundle 只 require 模块表说明符- 用真实的
0.2.0-rc.2WebError类跑通一次真实 Serper 搜索:available()→search()→ 3 条 sources,取消被包装为WebError{code:'WEB_ABORTED'}且instanceof WebError === true
未验证项(如实说明)
- 客户端界面的可视化验证:本会话无法访问 Web GUI(鉴权),因此设置页在「插件」页上的实际渲染没有截图确认;已通过构建产物与运行时契约核对。
- 类型声明的来源:
src/contracts.d.ts与src/client/primitives.d.ts/store.d.ts是按安装包内的lib/*.js逆向写出的最小契约,不是官方类型;官方若在某版本线补齐类型声明,应替换为本包对该包的正式依赖。
升级方式
dsh plugin add @dingpenghui/dsh-web-search-serper@1.2.2覆盖安装后完全重启 DSH(插件代码是 ESM,进程内缓存不会热刷新)。
v1.1.1
发布说明 v1.1.1
日期: 2026-10-03
修复 / Fixes
-
package.json依赖模型重构:@deepseek-ai/dsh-web@0.2.0-rc.2与@deepseek-ai/schemastery@^3.18.4从运行时dependencies移到peerDependencies(标optional: true),仅保留@deepseek-ai/cordis作为 peer。- 根因:本插件是 DSH 宿主扩展,运行时在宿主进程内执行。宿主已经把
@deepseek-ai/dsh-web/schemastery加载进同一进程作为宿主模块。本插件若再以dependencies携带同版本包,pnpm 会在插件树里再实例化一份同包,dsh-tools内部的TOOL_RUNTIME_SCHEDULERSymbol 跨实例查不到,宿主调度器取到undefined,触发Cannot read properties of undefined (reading 'prepare')整轮崩溃(详见 10-03 崩溃调查报告)。 - 改动:
dependencies: {};peerDependencies新增dsh-web、schemastery(标 optional);peerDependencies保留cordis(宿主必备,不标 optional)。 - 影响:插件自身代码行为不变,只改变了"谁提供这些包"——现在统一由 DSH 宿主提供,单实例,Symbol 身份不分裂。
- 根因:本插件是 DSH 宿主扩展,运行时在宿主进程内执行。宿主已经把
-
tsdown.config.ts客户端外部化@deepseek-ai/dsh-web:CLIENT_EXTERNALS新增'@deepseek-ai/dsh-web',避免客户端 bundle 把dsh-web打包进client.js,与宿主保持单实例。
不变 / Unchanged
/supply-chain命令、web-search-serper斜杠命令、ctx.web.search拦截逻辑、Serper key 解析链(config → env → credentials store)、自动降级到deepseek-official全部保持不变。smoke-test.mjs不变(仅验证 provider 注册/调用路径)。
升级指引 / Upgrade
- 从
1.1.0升到1.1.1无 breaking change,直接pnpm update @dingpenghui/dsh-web-search-serper即可。 - 插件安装后宿主行为不变,但不再在插件树里携带
dsh-web/schemastery副本。
v1.1.0 — 接管 web.searchProvider,Serper 不可用时自动降级
@dingpenghui/dsh-web-search-serper v1.1.0
行为变更版本:安装后自动接管 web.searchProvider,并在 Serper 不可用时自动降级到其它搜索提供方。
中文
修复:装了却完全不生效
1.0.1 及更早版本存在一个静默失效问题:按 README 安装后,插件虽然注册成功,
但 web_search 永远不会调用它,而且不报任何错——用户只会觉得"装了没效果",
无从排查。
原因有两个:
dsh-base(所有 profile 共享的基础层)把web行固定为
searchProvider: deepseek-official,而 web seam 的选择语义是
「配了 id 就只用它、失败也不换人」——插件注册的serper永远轮不到;- 本包自带的
cordis.patch.yml只做了- insert:,从未把web.searchProvider
指过来。
新增:开箱接管
本包现在在自己的 cordis.patch.yml 里同时覆盖 - id: web:
- id: web
name: '@deepseek-ai/dsh-web'
config:
searchProvider: serper # 覆盖 dsh-base 默认的 deepseek-official
fetchProvider: http # 必须重述:patch 会整行替换 config作为 bundle 安装即自动接管,无需手动修改 profile 配置。
profile 自己的 cordis.patch.yml 在 bundle 层之后应用、优先级更高,
仍可覆盖回其它搜索后端。
fetchProvider: http必须重述——DSH 的 patch 语义是「整行替换 config」,
漏掉它会抹掉 dsh-base 的 fetch 配置。
新增:失败自动降级
- 优先 Serper:只要配置就绪(有 key、端点合法)一律走 Serper;
- 降级:Serper 因任何原因失败(无 key、401/403 鉴权失败、429 限流、5xx、
网络异常、端点配置错误等)时,自动改用 seam 上其它已注册且可用的搜索提供方,
通常是dsh-base自带的deepseek-official; - 取消不降级:用户主动取消(
WEB_ABORTED)直接上抛; - 两侧都失败:上抛
WEB_PROVIDER_ERROR,消息同时包含主后端与降级后端的
失败原因,降级错误挂在cause链上。
降级在插件内部完成,因为 DSH 的 web seam 自身不做回退(配置了 id 就只用它)。
相应地,available() 在「Serper 侧不可用、但存在可降级目标」时仍返回 true,
否则 seam 会在进入 search() 之前就以 WEB_PROVIDER_CONFIGURED_UNAVAILABLE
拦下,降级根本无从发生。
升级注意
- 存量用户:升级后搜索后端会从
deepseek-official切换到serper。
若你并不想用 Serper,在自己的 profile patch 里覆盖- id: web指回
deepseek-official即可。 - 若你此前因 1.0.1 的缺陷手动在 profile patch 里补过
- id: web,升级后
建议删掉那段手动配置,避免将来卸载插件时留下指向serper的悬空配置
(会报WEB_PROVIDER_CONFIGURED_MISSING)。
安装 / 升级
dsh plugin --profile web add @dingpenghui/dsh-web-search-serper@1.1.0默认端点仍为 https://google.serper.dev(不是 https://serper.dev——后者是
官网,调用会 404)。
English
Behavior-changing release: the bundle now claims web.searchProvider on
install and falls back to another search provider whenever Serper is unavailable.
Fixed: installed but silently never used
1.0.1 and earlier had a silent no-op defect: after following the README, the
plugin registered successfully yet web_search never invoked it — and
reported no error at all, leaving users with "installed but nothing happens"
and no way to diagnose it.
Two causes:
dsh-base(the layer shared by every profile) pins thewebrow to
searchProvider: deepseek-official, and the seam's selection rule is
"a configured id is the only candidate — never a fallback on failure", so the
registeredserperprovider could never be selected;- the package's own
cordis.patch.ymlonly did- insert:and never pointed
web.searchProviderat itself.
Added: takeover out of the box
The package's cordis.patch.yml now also overrides - id: web:
- id: web
name: '@deepseek-ai/dsh-web'
config:
searchProvider: serper # overrides dsh-base's deepseek-official
fetchProvider: http # must be restated: a patch replaces the whole configInstalling it as a bundle claims the seam automatically — no manual profile
edits required. Your profile's own cordis.patch.yml is applied after the
bundle layer and wins, so overriding - id: web there still switches backends.
fetchProvider: httpmust be restated: a DSH patch replaces the row's whole
config, and omitting it would drop dsh-base's fetch configuration.
Added: automatic fallback
- Preferred — Serper is used whenever it is ready (key present, endpoint valid);
- Fallback — if Serper fails for any reason (missing key, 401/403 auth
failure, 429 rate limit, 5xx, network error, misconfigured endpoint, …), the
request goes to another registered and usable search provider on the seam,
normally thedeepseek-officialprovider shipped withdsh-base; - Not on cancel — a user cancellation (
WEB_ABORTED) is rethrown as-is; - Both down — throws
WEB_PROVIDER_ERRORnaming both the primary and the
fallback failure, with the fallback error on thecausechain.
The fallback lives inside the plugin, because the DSH web seam does not fall
back by itself (a configured id is the only candidate). Accordingly,
available() still returns true when Serper is unusable but a fallback target
exists — otherwise the seam would reject the provider with
WEB_PROVIDER_CONFIGURED_UNAVAILABLE before search() ever ran.
Upgrade notes
- Existing installs: after upgrading, search moves from
deepseek-official
toserper. If you do not want Serper, override- id: webin your own
profile patch and point it back atdeepseek-official. - If you previously patched
- id: webby hand to work around the 1.0.1
defect, remove that manual block after upgrading, so uninstalling the plugin
later does not leave a danglingserperreference (which would fail with
WEB_PROVIDER_CONFIGURED_MISSING).
Install / upgrade
dsh plugin --profile web add @dingpenghui/dsh-web-search-serper@1.1.0The default endpoint remains https://google.serper.dev — not
https://serper.dev, which is the marketing site and returns 404.
@dingpenghui/dsh-web-search-serper v1.0.1 — npm 与 tag 对齐 / npm-tag alignment
@dingpenghui/dsh-web-search-serper v1.0.1
npm 版本与 git tag 的对齐版本。无功能性变更:lib/ 产物与 1.0.0 一致。
中文
为什么发 1.0.1
1.0.0 的代码(含把包名改回 scoped 的提交 7836949)已经发布到 npm,但 git tag
v1.0.0 仍指向改名之前的提交 801dce6。也就是说 git checkout v1.0.0 拿到的
是无 scope 的包名 dsh-web-search-serper —— 那个名字在 npm 上属于其他作者
(risyasin),既无法发布也无法安装。
移动一个已经发布过 Release 的 tag 等于改写公开历史,因此改为发布 1.0.1:
tag v1.0.1 与 npm 1.0.1 指向同一份代码。
变更
- 无功能性变更;与 1.0.0 的构建产物一致
- 仓库清理:删除 0.3.1 时代遗留的
RELEASE_NOTES.md与
dingpenghui-dsh-web-search-serper-0.3.1.tgz
安装
dsh plugin --profile web add @dingpenghui/dsh-web-search-serper@1.0.11.0.0 与 1.0.1 在 npm 上内容等价,装哪个都可以;建议用 1.0.1 以便与 tag 对齐。
English
Version that re-aligns the npm release with a git tag. No functional change:
the lib/ output is identical to 1.0.0.
Why 1.0.1
The 1.0.0 code — including commit 7836949, which renamed the package back to the
scoped name — was already published to npm, but git tag v1.0.0 still points at
801dce6, the commit before the rename. So git checkout v1.0.0 yields the
unscoped name dsh-web-search-serper, which on npm belongs to another author
(risyasin): it can be neither published nor installed.
Moving a tag that already backs a published Release would rewrite public history,
so 1.0.1 is released instead: tag v1.0.1 and npm 1.0.1 point at the same code.
Changes
- No functional change; the build output matches 1.0.0
- Repository cleanup: removed the leftover
RELEASE_NOTES.mdand
dingpenghui-dsh-web-search-serper-0.3.1.tgzfrom the 0.3.1 era
Install
dsh plugin --profile web add @dingpenghui/dsh-web-search-serper@1.0.11.0.0 and 1.0.1 are equivalent on npm; prefer 1.0.1 so the installed version
matches the git tag.