dsh-web-search-ocgo
自研的 DeepSeek Harness 网页搜索 provider:走 OpenCode Go(自动注入
x-opencode-session),并补上官方 provider 没有的失败回退、查询缓存、
结果总结与设置页。
- provider id:
opencode-go - loader entry id:
web-search-ocgo - 包名:
dsh-web-search-ocgo - 依赖面:只 import
@deepseek-ai/schemastery一个外部包
为什么自研
原先用的第三方 fork dsh-web-search-opencode-go 有四个硬伤:
- 加载即崩 —— 它静态 import 了
installSettingsSection,而 DSH0.1.7-rc.2的@deepseek-ai/dsh-settings没有这个导出,整个 provider 无法加载。 - 靠旧版本残留侥幸解析 —— 它 import 的
@deepseek-ai/dsh-*从它自己的目录 根本解析不到,是靠~/.dsh/profiles/node_modules/@deepseek-ai/里指向 dsh 0.1.5-rc.3 的旧 symlink 才加载起来的(详见「为什么不 import dsh 包」)。 - 代码是官方 provider 的 drop-in fork,官方升级拿不到好处。
- 本地补丁不持久,任何
pnpm install都会覆盖。
本插件从零重写,并保持对外行为兼容。
与官方 provider 的差异
| 能力 | 官方 dsh-web-search-deepseek |
本插件 |
|---|---|---|
| 端点 | api.deepseek.com/anthropic/v1 |
OpenCode Go opencode.ai/zen/go/v1 |
| 可用模型 | 只认 deepseek-flash / deepseek-v4-pro |
只认 deepseek-v4-flash / deepseek-v4.1-flash |
| 会话头 | — | 自动注入 x-opencode-session + x-opencode-client |
result.content |
完全不填(实测摘要覆盖率 0%) | 取响应 text 块(模型总结),默认截断 2000 字符 |
| 查询缓存 | 无 | TTL 默认 5 分钟 / LRU 50,命中即 0 token |
| 失败回退 | 无 | Go 端点重试耗尽后改打官方端点(默认开) |
| 重试 | 无 | 429 / 5xx / 网络错误退避重试(默认 1 次) |
| 日期规范化 | 原样透传 page_age |
规范成 ISO-8601;解析不出来就丢弃而不是塞假日期 |
| 设置页 | 官方定制卡片(绑定 web-search-deepseek) |
自带(注册 plugins.bundle.config) |
| 会话事件 | 只有请求事件 | 也只有请求事件(响应统计走 logger,见下) |
| 外部依赖 | 多个 @deepseek-ai/dsh-* |
只有 @deepseek-ai/schemastery |
为什么不写响应事件(一个会让整份日志打不开的坑)
早期版本额外写了一个自定义事件 web/ocgo-search-response(延迟 / token /
结果数 / 缓存命中 / 是否回退)。结果一条记录就足以让整份会话日志拒绝加载:
历史加载失败:session "..." contains event type "web/ocgo-search-response" (seq 969)
unknown to this harness and not marked ignorable; refusing to interpret the log
根因有两层:
- harness 的读路径(
dsh-session-persistence的validateStoredEvents)会拒绝任何 不在KNOWN_SESSION_EVENT_TYPES里、又没标ignorable: true的事件类型 —— 这是故意的保守设计:无法判断"省略该事件是否安全"时,宁可整体拒绝, 也不要静默重建一个被掏空的会话。 - 但写侧
Session.append(type, data, ...opts)只接受surfaceOp/sourceEventSeqs,没有ignorable的入口 —— 仓库外插件其实没有正规途径 写"可忽略事件"。官方 provider 同样只写请求事件,不写响应事件。
所以本插件的响应统计改走 logger.info(没有 logger 服务时退化为 console.log),
不再进会话日志。bin/doctor.mjs 会主动扫描历史日志,把旧版留下的未标记事件
扫出来并给出修法。
如果你确实需要把响应统计写进会话日志,唯一安全的做法是给 Session.append
补上 ignorable 支持(改全局 @deepseek-ai/dsh-session,pnpm 重装或升级会覆盖),
并让插件传 { ignorable: true }。本插件不采用:补丁维护成本高于这点诊断数据的收益。
一个反直觉的实测结论:max_uses 不影响成本
32 次请求实测(见 bench/maxuses-report.md):max_uses 从 5 降到 2,
总 prompt 恒定在 ~8.1k,结果条数也不变。根因是每次请求实际只发起
1 次 web_search(usage.server_tool_use.web_search_requests 恒为 1),
max_uses 是上限而非目标值,从未被触及。
所以默认值保持 5;省额度靠查询缓存,不是下调 max_uses。
安装
1. 准备依赖链接
本插件只依赖 @deepseek-ai/schemastery,需要让它能被解析:
ln -sfn ~/.dsh/profiles/web/node_modules/@deepseek-ai/schemastery \
node_modules/@deepseek-ai/schemastery
2. 挂进 profile
ln -sfn "$PWD" ~/.dsh/profiles/web/node_modules/dsh-web-search-ocgo
3. 登记 bundle
在 ~/.dsh/profiles/web/package.json 的 dsh.profile.bundles 里加上
dsh-web-search-ocgo,并在 dependencies 里加:
"dsh-web-search-ocgo": "link:/Users/mac/Documents/workspace/创造模式/dsh-web-search-ocgo"
4. 清理旧链路
从 ~/.dsh/profiles/web/cordis.patch.yml 删掉旧的 web-search-opencode-go 段,
并从 dsh.profile.bundles 移除 dsh-web-search-opencode-go。
5. 重启
dsh web
重启后日志里应出现:
[web-search-ocgo] 已装载 provider id=opencode-go endpoint=https://opencode.ai/zen/go/v1 model=deepseek-v4.1-flash fallback=deepseek-flash
验证
dsh --profile web --dump-config | grep -A5 "id: web$" # searchProvider 应为 opencode-go
node scripts/check.mjs # 清单自检
node --test test/*.test.js # 单元测试
node bin/doctor.mjs # 一键诊断
配置
图形界面
「设置 → 插件」→ 打开 dsh-web-search-ocgo → 配置页。
改动在失焦或回车时立即保存,写入 profile 的 cordis.patch.yml。
YAML
也可以在 ~/.dsh/profiles/web/cordis.patch.yml 里写:
- id: web-search-ocgo
name: dsh-web-search-ocgo
config:
apiKeyEnv: OPENCODE_API_KEY
baseURL: https://opencode.ai/zen/go/v1
model: deepseek-v4.1-flash
fallbackEnabled: true
cacheTtlMs: 300000
answerMaxChars: 2000
所有字段都有 schema 默认值,写多少覆盖多少。
主要配置项
| 字段 | 默认值 | 说明 |
|---|---|---|
apiKeyEnv |
OPENCODE_API_KEY |
凭据中心里的密钥名 |
baseURL |
https://opencode.ai/zen/go/v1 |
Go 端点(不含 /messages) |
model |
deepseek-v4.1-flash |
Go 端点只认 deepseek-v4-flash / deepseek-v4.1-flash |
maxUses |
5 |
实测对成本无影响,不建议下调 |
sessionId |
空(每次启动随机) | 固定会话 id;实测缓存命中不依赖它 |
fallbackEnabled |
true |
Go 失败后回退官方端点(消耗 DeepSeek 余额) |
fallbackModel |
deepseek-flash |
官方端点只认 deepseek-flash / deepseek-v4-pro |
cacheEnabled / cacheTtlMs / cacheMaxEntries |
true / 300000 / 50 |
查询缓存 |
answerMaxChars |
2000 |
result.content 截断;0 = 不返回 |
snippetMaxChars |
300 |
单条摘要截断 |
retryCount |
1 |
429 / 5xx / 网络错误的重试次数 |
为什么不 import dsh 包
实测:从插件自己的目录,@deepseek-ai/dsh-* 一个都解析不到
(8 个里只有 schemastery 能解析):
import FAIL @deepseek-ai/dsh-web ERR_MODULE_NOT_FOUND
import FAIL @deepseek-ai/dsh-credentials ERR_MODULE_NOT_FOUND
import FAIL @deepseek-ai/dsh-session ERR_MODULE_NOT_FOUND
import OK @deepseek-ai/schemastery
把同样的静态 import 写进一个 entry,dsh 日志会出现
failed to import / 1 entry did not activate。
那上游 fork 为什么能加载?因为 Node 从
~/.dsh/profiles/web/node_modules/dsh-web-search-opencode-go/ 向上查找时会命中
~/.dsh/profiles/node_modules/@deepseek-ai/ —— 那里有 242 个 symlink,
全部指向 dsh 0.1.5-rc.3 的旧安装,而不是当前运行的 0.1.7-rc.2。
它一直在用旧版本的 WebError / credentialRef 跑,是一次随时会坏的版本错配。
本插件的对策:依赖面压到最小,只 import schemastery。其余契约全部经 ctx 取
(ctx.get('credentials')、ctx.get('agents')),错误对象按 HarnessError 的
形状手工构造(class extends Error { readonly code },见 lib/errors.js),
process.env 兜底 launch environment。
好处:插件可以放在任意目录,也不受 dsh 升级的版本错配影响。
卸载与回滚
临时回滚到官方链路(不卸载)
在「设置 → 插件」里把 web 这一行的 searchProvider 改回 deepseek-official,
重启即可。官方 provider 一直留在注册表里,就是为这个逃生舱准备的。
完全卸载
- 从
~/.dsh/profiles/web/package.json的dsh.profile.bundles移除dsh-web-search-ocgo - 删除
cordis.patch.yml里web-search-ocgo段,并把web的searchProvider改回deepseek-official - 删除
~/.dsh/profiles/web/node_modules/dsh-web-search-ocgo这个链接 - 重启
dsh web
开发
node scripts/check.mjs # 清单自检(字段、exports、语法、依赖面)
node --test test/*.test.js # 单元测试(40 项)
node bin/doctor.mjs # 诊断:凭据 / 配置 / 依赖 / 语法 / 会话日志遗留污染
node bin/doctor.mjs --probe # 额外真发一次搜索(会花钱)
node bench/dump-response.mjs "查询词" # dump 原始响应结构(需要凭据)
node bench/maxuses-bench.mjs # 复现 max_uses 曲线
目录
| 路径 | 说明 |
|---|---|
index.js |
host 半:Config schema + 装载自检 + 注册 provider |
client.js |
浏览器半:设置页(手写 __ModuleLoader__ bundle,无构建步骤) |
lib/provider.js |
provider 本体:请求、重试、回退、结果映射 |
lib/cache.js |
查询缓存(TTL + LRU) |
lib/errors.js |
与 HarnessError 同形状的错误 |
lib/constants.js |
协议常量与默认值 |
cordis.patch.yml |
bundle 补丁层:改钉 web.searchProvider、插入本 entry |
probe/ |
阶段 0 的一次性探针(可删) |
许可
MIT。衍生关系与上游声明见 NOTICE。
还没有评论,来写第一条。