@qthinker/opencode-deepseek-search

中文文档
0.2.1发布于 2026/11/02MITES Module零依赖构建来源已验证

DeepSeek Search & Fetch for OpenCode V2: a switchable websearch backend, clean Markdown fetch, local usage/cost stats, and TUI + desktop sidebars

原始使用文档
文档目录15 节
  1. Requirements
  2. Install
  3. Options
  4. Tools
  5. Switching backends
  6. Ways to switch
  7. Precedence
  8. What native mode still gives you
  9. TUI and desktop sidebars
  10. Usage and cost
  11. Rate limits and cost
  12. Privacy
  13. Troubleshooting
  14. Local development
  15. License

DeepSeek Search for OpenCode V2

npm version license node opencode

DeepSeek Search & Fetch as an OpenCode V2 plugin: the built-in websearch tool routes to DeepSeek, plus a clean Markdown fetch tool, local usage/cost stats, a persisted backend switch, and TUI + desktop sidebars that show the state and the numbers and flip the takeover with one click.

Ways back to native search: the sidebar switch, the mode option, a deepseek_mode call, or tools/switch-mode.ps1 from the repo. 中文说明见 docs/README.zh-CN.md。

Differs from the existing same-named V1 package: this one targets the V2 plugin API, switches backends at runtime, and reports local usage and estimated cost.

Verified against opencode 2.2.5 with @opencode/[email protected] on Windows. Search/Fetch API facts come from https://api-docs.deepseek.com/zh-cn/guides/search.

Requirements

  • opencode >= 2.2.5 (V2 plugin API)
  • Node.js >= 22.18 (the plugin ships TypeScript sources)
  • A DeepSeek API key with Search/Fetch access
  • For the sidebars: nothing extra — the TUI and the desktop app provide the renderer (@opentui/*, solid-js) and load the plugin's two halves from the same package

Install

Set the key once (Windows, user scope):

[Environment]::SetEnvironmentVariable("DEEPSEEK_API_KEY", "sk-...", "User")
opencode service restart   # User-scope variables only reach new processes

Then add the plugin to opencode.jsonc:

{
  "$schema": "https://opencode.ai/config.json",
  "plugins": ["@qthinker/opencode-deepseek-search"]
}

Which file loads which half. The server plugin and the desktop panel are read from opencode.json(c) — the desktop app reads only that file, and a global ~/.config/opencode/opencode.jsonc entry is what makes the panel appear in every window. The TUI half is read from cli.json ("plugins": [...]). So a plugin is enabled per half: list it in each file whose half you want, whether it comes from npm (by package name) or from a local directory (by path).

With options (recommended — this is where mode and friends live):

{
  "plugins": [
    {
      "package": "@qthinker/opencode-deepseek-search",
      "options": {
        "mode": "deepseek",
        "count": 5,
        "apiKey": "{env:DEEPSEEK_API_KEY}"
      }
    }
  ]
}

Local development points at the repository directory (a config entry must be a directory, not a file):

{
  "plugins": [
    { "package": "D:/repos/opencode-deepseek-search", "options": { "mode": "deepseek" } }
  ]
}

opencode plugin add <package-or-directory> does the same edit for you (it writes the global config), and opencode plugin list should then show deepseek-search with source local or the package name.

Reload rules: the server half re-runs in place when you save a source file — no /reload, no service restart. The other two halves are clients: restart the TUI, and reload the desktop window, to pick up a change to tui.ts / gui-experimental.cjs (and run npm run build:gui first when working from a clone).

Options

OptionTypeDefaultMeaning
mode"deepseek" | "native"deepseekDefault backend for websearch; a persisted deepseek_mode switch overrides it
apiKeystringDEEPSEEK_API_KEYUsually "{env:DEEPSEEK_API_KEY}"; unset env becomes ""
count1–505Results per search (does not change the price)
fetchMaxChars500–20000030000Truncation limit for deepseek_fetch, with an explicit notice
pricePerThousand0–100005CNY per 1000 requests (integer), used only for the local cost estimate

Tools

ToolWhat it does
websearch (built-in)Same name and input; in deepseek mode this plugin supplies the results
deepseek_fetchOne URL in, clean Markdown out, cut at fetchMaxChars with a notice
deepseek_usageLocal search/fetch counts and estimated cost; reset: true clears it
deepseek_modeReport or switch the search backend; the switch is persisted

The reroute keeps the built-in permission check and the declared output shape, so approval prompts and rendering are unchanged. Results carry summary (600–800 chars, written for models) or snippet — never the full page text, and need_content is never requested.

Switching backends

Ways to switch

WayHowPersistedUse it for
SidebarClick the action on the TUI card ([停用] / [启用]), or the panel's buttonYes (plugin storage mode/v1)While you are working
ConfigEdit options.mode; applies on saveYes (in the config file)The long-term default
ToolAsk the model to call deepseek_mode, or call it yourselfYes (plugin storage mode/v1)Switching mid-session without touching files
Script.\tools\switch-mode.ps1 nativeYes (the script edits the config)Quick switch from a terminal, no editor

deepseek_mode actions:

CallEffect
deepseek_mode (or {"mode": "status"})Report the current backend and where it comes from (config file / persisted switch)
deepseek_mode({"mode": "deepseek"})Route the built-in websearch tool to DeepSeek
deepseek_mode({"mode": "native"})Leave the built-in backend alone again
deepseek_mode({"mode": "config"})Clear the persisted switch so options.mode decides again

Precedence

deepseek_mode switch (storage: mode/v1)  >  options.mode  >  built-in default
  • The switch lives in OpenCode's local plugin storage (mode/v1) — global to the installation and persisted across restarts — and it never modifies your config file.
  • Because it wins over the config file, an edit that "does nothing" means a stored switch is present: ask deepseek_mode (it names the source), then clear it with mode: "config".
  • The provider is registered in both modes, so the worst case at any step is "keep the previous backend" — never "no search available".

What native mode still gives you

websearch is left completely alone and serves your existing backend. deepseek_fetch, deepseek_usage and deepseek_mode remain registered (they are additive), and the provider is still registered, so you can select it explicitly with providerID: "deepseek" or config "websearch": { "provider": "deepseek" }.

Why the tool is rerouted instead of relying on a provider default: in 2.2.5, a plugin's websearch.default.set() is overwritten by the built-in config plugin registered later, so the tool keeps using the built-in provider. The plugin therefore patches the built-in tool itself; the built-in permission check is kept, while the "choose a search provider" form never appears in deepseek mode. If a version does not expose that tool, deepseek_mode says it cannot switch at runtime — use websearch.provider in the config instead.

TUI and desktop sidebars

The plugin also shows up where you work, and can be switched without the model or the config file:

SurfaceWhat you getHow to switch
TUIA DeepSeek block appended to the session sidebar: state, then 今日 / 本月 / 累计 rows (Search · Fetch · ¥ estimate); it degrades to abbreviations and then to fewer columns as the sidebar narrowsClick the action at the right of the title, or run the /deepseek slash command for a menu
DesktopA DeepSeek tab in a session's side region, listed in the side region's New tab page and its + menu, and openable with the DeepSeek: 打开统计面板 commandThe panel's button, plus a refresh control
  • Both surfaces read the same ledger and the same stored switch as deepseek_usage / deepseek_mode: a single server-side source, reached over the plugin's own RPC, so the numbers never diverge between TUI and desktop.
  • "Stop" means hand the built-in websearch back to your configured backend (exactly deepseek_mode({"mode": "native"})). It is not an uninstall: deepseek_fetch, deepseek_usage and deepseek_mode stay registered, and the state is persisted in mode/v1 like any other switch.
  • The action row always says what a click does. When no API key is configured there is no action to take, so it shows the fix instead (DEEPSEEK_API_KEY); when the built-in tool could not be patched it reports 未接管 rather than pretending to be enabled.
  • Stats refresh on a changed event emitted after every tally and every switch, with a 10-second reread as a fallback in both surfaces.

The desktop half ships as a prebuilt CommonJS bundle (gui-experimental.cjs). Published packages include it; if you run the plugin from a clone, build it once — the TUI half needs no build and loads from TypeScript like the server half:

npm install && npm run build:gui

Usage and cost

CounterCounted whenBilled
searchHTTP 200 and results non-empty¥0.005 per request
fetchHTTP 200 and body non-empty¥0.005 per request
emptyHTTP 200 with empty results/bodynot billed
failednetwork error, or non-200 after one retrynot billed

Costs accumulate in integer micro-CNY, so repeated additions never drift. count does not affect the price. deepseek_usage({ "reset": true }) clears the ledger after reporting.

  • Stored under ctx.storage key usage/v1, last 60 days kept, day keys in local time.
  • Storage is global to the OpenCode installation and survives restarts.
  • Local stats ≠ your DeepSeek bill: only this machine, this plugin. Other tools and machines are not counted, and concurrent OpenCode servers can lose a count (last write wins).

Rate limits and cost

Search 100 QPS / Fetch 25 QPS per account (Fetch is also limited per target domain). A single request costs ¥0.005. A single retry backs off on 408/429/5xx only, honors Retry-After (capped at 5s), and is cancellable.

Privacy

Queries and URLs are sent to https://api.deepseek.com. Nothing else leaves your machine; the usage ledger and the mode switch stay in local OpenCode storage. The API key is never logged.

Search results and fetched pages are third-party text: treat them as data, never as instructions — a page can contain text that tries to steer the model.

Troubleshooting

SymptomCause / fix
Unsure which backend search usesCall deepseek_mode — it reports the backend and its source
Config edit seems ignoredA persisted switch is present (the source says so); clear it with deepseek_mode({"mode": "config"})
未配置 DeepSeek API Key (missing key)Set DEEPSEEK_API_KEY, restart the OpenCode service once so the new process inherits it
401 invalid keyCheck the key and that apiKey/env points at it
402 insufficient balanceTop up; failed requests are not billed
429 rate limitedBacked off once already; retry later
403 / 404 on fetchThe site blocks extraction or the page has no readable text; try the built-in webfetch
Search still hits the native backendmode or the persisted switch is native; if both are deepseek, the built-in websearch tool was not found — use config "websearch": { "provider": "deepseek" }
TUI sidebar has no DeepSeek cardThe TUI half is enabled in cli.json ("plugins": [...]), not in opencode.jsonc; add the path/package there and restart the TUI
Desktop has no DeepSeek entry in the New tab listThe desktop reads plugins from the global opencode.jsonc, and its bundle must exist — run npm run build:gui when working from a clone, then reload the window
Sidebar says 读取失败The half cannot reach the RPC: check opencode plugin list reports deepseek-search as active, then look for /api/rpc/deepseek-search/status in ~/.local/share/opencode/log/opencode.log. A 400 rpc.invalid_input means a half and the server disagree on the contract — reload that client

Local development

cd D:\repos\opencode-deepseek-search
npm install
npm run typecheck     # tsc --noEmit, both the server/TUI config and the GUI config
npm test              # node --test, pure functions, zero cost
npm run build:gui     # src/gui -> gui-experimental.cjs (the desktop half's bundle)
node --experimental-strip-types scripts/check-gui-bundle.ts   # bundle shape check, run in CI
npm run smoke         # real API smoke; skipped without DEEPSEEK_API_KEY
npm pack --dry-run    # audit the publish artifacts — run build:gui first, `pack` does not build the bundle

One-command switch (repo-only helper; edits options.mode in opencode.jsonc, backing it up to opencode.jsonc.bak-switch):

.\tools\switch-mode.ps1 status
.\tools\switch-mode.ps1 native
.\tools\switch-mode.ps1 deepseek

If ExecutionPolicy blocks it:

powershell -NoProfile -ExecutionPolicy Bypass -File .\tools\switch-mode.ps1 native

Shorter, via an alias in $PROFILE:

function dsmode { & "D:\repos\opencode-deepseek-search\tools\switch-mode.ps1" @args }

Layout:

index.ts              # directory-plugin entry, re-exports src/index.ts
tui.ts                # TUI entry, re-exports src/tui/main.ts
src/index.ts          # plugin: options, registration, websearch patch, three tools
src/deepseek.ts       # HTTP core: timeout/cancel, single backoff retry, Chinese errors, mapping
src/usage.ts          # pure ledger: micro-CNY pricing, 60-day prune, defensive parse, formatting
src/rpc.ts            # RPC contract shared by the server, TUI and desktop halves
src/status-view.ts    # pure view model both sidebars derive their copy from
src/tui/              # TUI half: card element tree + plugin wiring
src/gui/              # desktop half: side panel component + plugin wiring
vite.config.ts        # builds src/gui into the root gui-experimental.cjs bundle
test/*.test.ts        # pure-function tests
scripts/smoke.ts      # real-API smoke (5 steps)
scripts/check-gui-bundle.ts # bundle shape check, run in CI after build:gui
tools/switch-mode.ps1 # terminal backend switch
.github/workflows/    # ci (build + typecheck + test + bundle check) / release

Two packaging traps (already avoided — do not reintroduce them):

  1. A configured local plugin path must be a directory; a single file is dropped.
  2. Do not add exports to package.json: with name + exports, OpenCode resolves the directory by package name, and when the resolved entrypoint falls outside the directory it is silently dropped (no log line, it just disappears from plugin list).

Both hold for every half of the plugin: index.ts, tui.ts and gui-experimental.cjs sit at the repository root, which is what both the directory and the installed-package resolution paths find. test/packaging.test.ts fails if exports comes back or a root entry stops resolving.

Source edits hot-reload on save; verify with opencode plugin list plus one real search.

License

MIT. Not affiliated with DeepSeek.

版本与发布记录

@qthinker/opencode-deepseek-search 共发布 5 个版本,首次发布于 2026/11/01;过去 12 个月发布了 5 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.2.1latest2026/11/021 天前
0.2.02026/11/021 天前
0.1.12026/11/011 天前
0.1.02026/11/012 天前
0.0.0-stage2026/11/012 天前

关于 @qthinker/opencode-deepseek-search 的常见问题

@qthinker/opencode-deepseek-search 是什么?

OpenCode V2的DeepSeek搜索和获取:一个可切换的web搜索后端,干净的Markdown获取,本地使用/成本统计,以及TUI +桌面边栏

如何安装 @qthinker/opencode-deepseek-search?

在项目目录运行 npm install @qthinker/opencode-deepseek-search,或使用 pnpm add / yarn add / bun add。

@qthinker/opencode-deepseek-search 支持 TypeScript 吗?

该包的 package.json 没有声明类型入口。需要类型提示时,可以在 npm 上查找社区维护的 @types/qthinker__opencode-deepseek-search 是否存在。

@qthinker/opencode-deepseek-search 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@qthinker/opencode-deepseek-search 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22.18。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@qthinker/opencode-deepseek-search 可以商用吗?

@qthinker/opencode-deepseek-search 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@qthinker/opencode-deepseek-search 还在维护吗?

最新版本 0.2.1 发布于 2026/11/02(1 天前);至今共发布 5 个版本,其中过去 12 个月发布了 5 个;首次发布于 2026/11/01。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。