Skip to content

Releases

v1.3.0 — 只做检索:装好即生效,只留一个设置 | Plain search provider: works on install, one setting

Choose a tag to compare

@Dingpenghui-good Dingpenghui-good released this 01 Nov 12:20

中文

本版把插件收敛为一个纯粹的检索提供方:装好后直接生效,不需要改任何配置,也不需要调参。

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 --noEmit 0 错误
  • 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 results
  • numResults → overrode the count the host web_search tool asked for
  • baseURL → 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 --noEmit clean
  • tsdown build passes
  • smoke-test.mjs passes (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.0

Restart 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 — 可观测性:让「装了却没生效」有迹可循

Choose a tag to compare

@Dingpenghui-good Dingpenghui-good released this 18 Oct 04:47

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 该会话开始查天气

问题不在插件代码,而在两点叠加:

  1. cordis.patch.yml 只在 DSH 进程启动时读取一次。 进程先启动、配置后
    写入,插件层就不会被加载,而搜索会安静地继续走内置后端,不报任何错。
  2. 插件全程没有任何可观测性。 注册、被寻址、调用、降级、失败——每一步
    都是无声的。用户既看不到「没加载」,也看不到「加载了但没被选中」。

结果是:插件本身完全正常,用户却只能靠猜。

本次改动

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 passed
  • smoke-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 — 修复设置页输入框无法输入

Choose a tag to compare

@Dingpenghui-good Dingpenghui-good released this 18 Oct 04:35

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 — 插件页配置区占位符误导修复

Choose a tag to compare

@Dingpenghui-good Dingpenghui-good released this 18 Oct 04:26

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(无代码变更)

Choose a tag to compare

@Dingpenghui-good Dingpenghui-good released this 18 Oct 04:18

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 版本钉死,
    修复在 DSH 0.2.1-rc.2 上被安装门禁拒绝的问题;
  • peer 仅保留 cordis ~4.0.5 / schemastery ~3.18.5;
  • smoke-test.mjs 兼容性回归护栏。

详见 RELEASE_NOTES_v1.2.3.md。

关于默认配置:安装后所有人一致

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 无需写进配置,每次搜索时按以下顺序惰性解析:

  1. 行 config.apiKey(仅在你自己的 profile 覆盖层里显式配置时才存在);
  2. 宿主进程环境变量 SERPER_API_KEY;
  3. 凭证库引用 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 上安装被拒绝

Choose a tag to compare

@Dingpenghui-good Dingpenghui-good released this 18 Oct 01:27

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 构建。

修复

  1. engines.dsh 单一来源:">=0.2.0-rc.2 <0.3.0-0"。存在 engines.dsh 时
    宿主只依据它判定,不再被 peer 上的精确版本误伤。
  2. 移除对宿主运行时包的 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 运行时一致)。
  3. 依赖与构建对齐运行时:devDependencies 全部升到 0.2.1-rc.2
    (含 7 个 client 平台包),cordis 升到 4.0.5、schemastery 升到 3.18.5。
  4. 回归护栏: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(修复配置热读取与插件页渲染)

Choose a tag to compare

@Dingpenghui-good Dingpenghui-good released this 17 Oct 08:07

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-web
  • pnpm run test:本地回环 HTTP server 上验证请求体(q/gl/num、X-Api-Key)、organic → sources 映射、429 降级、取消不降级、volatile 包装解包、缺 key 降级;并校验客户端 bundle 只 require 模块表说明符
  • 用真实的 0.2.0-rc.2 WebError 类跑通一次真实 Serper 搜索:available() → search() → 3 条 sources,取消被包装为 WebError{code:'WEB_ABORTED'} 且 instanceof WebError === true

未验证项(如实说明)

  1. 客户端界面的可视化验证:本会话无法访问 Web GUI(鉴权),因此设置页在「插件」页上的实际渲染没有截图确认;已通过构建产物与运行时契约核对。
  2. 类型声明的来源: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

Choose a tag to compare

@Dingpenghui-good Dingpenghui-good released this 03 Oct 06:48

发布说明 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_SCHEDULER Symbol 跨实例查不到,宿主调度器取到 undefined,触发 Cannot read properties of undefined (reading 'prepare') 整轮崩溃(详见 10-03 崩溃调查报告)。
    • 改动:dependencies: {};peerDependencies 新增 dsh-web、schemastery(标 optional);peerDependencies 保留 cordis(宿主必备,不标 optional)。
    • 影响:插件自身代码行为不变,只改变了"谁提供这些包"——现在统一由 DSH 宿主提供,单实例,Symbol 身份不分裂。
  • 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 不可用时自动降级

Choose a tag to compare

@Dingpenghui-good Dingpenghui-good released this 01 Oct 13:14

@dingpenghui/dsh-web-search-serper v1.1.0

行为变更版本:安装后自动接管 web.searchProvider,并在 Serper 不可用时自动降级到其它搜索提供方。

中文

修复:装了却完全不生效

1.0.1 及更早版本存在一个静默失效问题:按 README 安装后,插件虽然注册成功,
但 web_search 永远不会调用它,而且不报任何错——用户只会觉得"装了没效果",
无从排查。

原因有两个:

  1. dsh-base(所有 profile 共享的基础层)把 web 行固定为
    searchProvider: deepseek-official,而 web seam 的选择语义是
    「配了 id 就只用它、失败也不换人」——插件注册的 serper 永远轮不到;
  2. 本包自带的 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:

  1. dsh-base (the layer shared by every profile) pins the web row 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
    registered serper provider could never be selected;
  2. the package's own cordis.patch.yml only did - insert: and never pointed
    web.searchProvider at 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 config

Installing 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: http must 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 the deepseek-official provider shipped with dsh-base;
  • Not on cancel — a user cancellation (WEB_ABORTED) is rethrown as-is;
  • Both down — throws WEB_PROVIDER_ERROR naming both the primary and the
    fallback failure, with the fallback error on the cause chain.

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
    to serper. If you do not want Serper, override - id: web in your own
    profile patch and point it back at deepseek-official.
  • If you previously patched - id: web by hand to work around the 1.0.1
    defect, remove that manual block after upgrading, so uninstalling the plugin
    later does not leave a dangling serper reference (which would fail with
    WEB_PROVIDER_CONFIGURED_MISSING).

Install / upgrade

dsh plugin --profile web add @dingpenghui/dsh-web-search-serper@1.1.0

The 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

Choose a tag to compare

@Dingpenghui-good Dingpenghui-good released this 01 Oct 09:38

@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.1

1.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.md and
    dingpenghui-dsh-web-search-serper-0.3.1.tgz from the 0.3.1 era

Install

dsh plugin --profile web add @dingpenghui/dsh-web-search-serper@1.0.1

1.0.0 and 1.0.1 are equivalent on npm; prefer 1.0.1 so the installed version
matches the git tag.