Skip to content
dsh.fish
Bundle

dsh-web-search-ocgo

自研 DSH 网页搜索 provider:走 OpenCode Go(自动注入 x-opencode-session),带官方端点失败回退、查询缓存、result.content 总结与日期规范化,并自带设置页。

Source
jitang-open
License
MIT
Updated
Updated 18 days ago

Readme

# 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` 有四个硬伤:

1. **加载即崩** —— 它静态 import 了 `installSettingsSection`,而 DSH `0.1.7-rc.2`
   的 `@deepseek-ai/dsh-settings` 没有这个导出,整个 provider 无法加载。
2. **靠旧版本残留侥幸解析** —— 它 import 的 `@deepseek-ai/dsh-*` 从它自己的目录
   根本解析不到,是靠 `~/.dsh/profiles/node_modules/@deepseek-ai/` 里指向
   **dsh 0.1.5-rc.3** 的旧 symlink 才加载起来的(详见「为什么不 import dsh 包」)。
3. **代码是官方 provider 的 drop-in fork**,官方升级拿不到好处。
4. **本地补丁不持久**,任何 `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
```

根因有两层:

1. harness 的读路径(`dsh-session-persistence` 的 `validateStoredEvents`)会拒绝任何
   不在 `KNOWN_SESSION_EVENT_TYPES` 里、又没标 `ignorable: true` 的事件类型 ——
   这是**故意**的保守设计:无法判断"省略该事件是否安全"时,宁可整体拒绝,
   也不要静默重建一个被掏空的会话。
2. 但写侧 `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`,需要让它能被解析:

```bash
ln -sfn ~/.dsh/profiles/web/node_modules/@deepseek-ai/schemastery \
        node_modules/@deepseek-ai/schemastery
```

### 2. 挂进 profile

```bash
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` 里加:

```json
"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. 重启

```bash
dsh web
```

重启后日志里应出现:

```
[web-search-ocgo] 已装载 provider id=opencode-go endpoint=https://opencode.ai/zen/go/v1 model=deepseek-v4.1-flash fallback=deepseek-flash
```

### 验证

```bash
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` 里写:

```yaml
- 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 一直留在注册表里,就是为这个逃生舱准备的。

### 完全卸载

1. 从 `~/.dsh/profiles/web/package.json` 的 `dsh.profile.bundles` 移除 `dsh-web-search-ocgo`
2. 删除 `cordis.patch.yml` 里 `web-search-ocgo` 段,并把 `web` 的 `searchProvider`
   改回 `deepseek-official`
3. 删除 `~/.dsh/profiles/web/node_modules/dsh-web-search-ocgo` 这个链接
4. 重启 `dsh web`

---

## 开发

```bash
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](./NOTICE)。

Install

dsh plugin --profile web add github:jitang-open/dsh-web-search-ocgo

Profile: web

  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source