@qthinker/opencode-deepseek-search 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,33 @@
3
3
  All notable changes to this package. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
4
4
  and the version numbers follow [Semantic Versioning](https://semver.org/).
5
5
 
6
+ ## [0.3.2] - 2026-11-04
7
+
8
+ ### Added
9
+
10
+ - **Account balance in both sidebars**: the server probes `GET /user/balance`
11
+ (<https://api-docs.deepseek.com/zh-cn/api/get-user-balance>) with the same API key and serves a
12
+ cached snapshot through the `status` RPC as an optional `balance` field — 60s TTL, at most one upstream request
13
+ per minute, failures keep the last snapshot, and the balance is absent until the first successful probe. The TUI
14
+ card closes its body with the balance (warning-colored when the account cannot pay, dropped below ~12 columns
15
+ instead of squeezed, shown only in the two states that point at the account); the desktop panel shows an account
16
+ block below the usage table with the granted/topped-up split on hover (shown only when both components are
17
+ non-zero). Manual refresh (the desktop refresh
18
+ button, the TUI dialog's *Refresh*, both retry paths) forces a re-probe; the 10-second polls never do.
19
+ - **Sidebar display overhaul**: the usage ladders now put money before words (the cost survives to much narrower
20
+ widths in every language, and the old words-only rungs that outranked a cheaper money row are gone), below the
21
+ numeric floor the three usage rows collapse into one digit-carrying row instead of bare labels, the TUI body
22
+ gained three text tiers plus a 2-column indent on wider sidebars, the state word goes bold when the glyph is
23
+ dropped, and the desktop panel tightens its table typography (10px chunk gaps, trailing separators removed,
24
+ letter-spacing on Latin labels only, a 12px glyph cell, money chunks that never wrap away from their digits).
25
+
26
+ ### Removed
27
+
28
+ - `tools/switch-mode.ps1`, the terminal helper that rewrote `options.mode` in `opencode.jsonc`. The switch has lived in
29
+ plugin storage (`mode/v1`) since the first release and `deepseek_mode` covers the terminal case, so the script only
30
+ duplicated it — and on Windows PowerShell 5.1 its `Set-Content -Encoding UTF8` wrote a BOM, after which
31
+ `opencode plugin add/update/remove` rejected the whole config as invalid.
32
+
6
33
  ## [0.3.0] - 2026-11-02
7
34
 
8
35
  ### Added
package/README.md CHANGED
@@ -6,18 +6,18 @@
6
6
  [![opencode](https://img.shields.io/badge/opencode-%E2%89%A52.2.5-black)](https://opencode.ai)
7
7
 
8
8
  DeepSeek Search & Fetch as an OpenCode V2 plugin: **the built-in `websearch` tool routes to DeepSeek**, plus a clean
9
- Markdown fetch tool, local usage/cost stats, a **persisted** backend switch, and **TUI + desktop sidebars** that show
10
- the state and the numbers and flip the takeover with one click.
9
+ Markdown fetch tool, local usage/cost stats, your **live account balance**, a **persisted** backend switch, and
10
+ **TUI + desktop sidebars** that show the state, the numbers and the balance, and flip the takeover with one click.
11
11
 
12
- Ways back to native search: the sidebar switch, the `mode` option, a `deepseek_mode` call, or
13
- `tools/switch-mode.ps1` from the repo.
12
+ Ways back to native search: the sidebar switch, the `mode` option, or a `deepseek_mode` call.
14
13
  中文说明见 [docs/README.zh-CN.md](https://github.com/QThinkerJR/opencode-deepseek-search/blob/main/docs/README.zh-CN.md)。
15
14
 
16
15
  Differs from the existing same-named V1 package: this one targets the **V2 plugin API**, switches backends **at
17
16
  runtime**, and reports **local usage and estimated cost**.
18
17
 
19
18
  > Verified against `opencode 2.2.5` with `@opencode/plugin@2.2.5` on Windows. Search/Fetch API facts come from
20
- > <https://api-docs.deepseek.com/zh-cn/guides/search>.
19
+ > <https://api-docs.deepseek.com/zh-cn/guides/search>; the balance endpoint from
20
+ > <https://api-docs.deepseek.com/zh-cn/api/get-user-balance>.
21
21
 
22
22
  ## Requirements
23
23
 
@@ -129,7 +129,6 @@ and `need_content` is never requested.
129
129
  | Sidebar | Click the action on the TUI card (English UI: `[Disable]` / `[Enable]`; Chinese UI: `[停用]` / `[启用]`), or the panel's button | Yes (plugin storage `mode/v1`) | While you are working |
130
130
  | Config | Edit `options.mode`; applies on save | Yes (in the config file) | The long-term default |
131
131
  | Tool | Ask the model to call `deepseek_mode`, or call it yourself | Yes (plugin storage `mode/v1`) | Switching mid-session without touching files |
132
- | Script | `.\tools\switch-mode.ps1 native` | Yes (the script edits the config) | Quick switch from a terminal, no editor |
133
132
 
134
133
  `deepseek_mode` actions:
135
134
 
@@ -184,6 +183,18 @@ The plugin also shows up where you work, and can be switched without the model o
184
183
  than pretending to be enabled.
185
184
  - Stats refresh on a `changed` event emitted after every tally and every switch, with a 10-second reread as a
186
185
  fallback in both surfaces.
186
+ - The body closes with your **account balance**, read from `GET /user/balance` with the same API key
187
+ (<https://api-docs.deepseek.com/zh-cn/api/get-user-balance>) — the usage rows read as the receipt and the account
188
+ truth lands under them. It shows in the two states where the card
189
+ actually points at the account (takeover enabled, or native mode); an unavailable balance gets a warning-colored
190
+ amount, and below ~12 columns the row is dropped rather than squeezed. The server serves a snapshot cached for
191
+ 60 seconds — at most one upstream request per minute, failures keep the last value — and only a manual refresh
192
+ (the desktop refresh button, the TUI menu's *Refresh*, both retry paths) forces a re-probe; the 10-second polls
193
+ never do. The desktop panel shows it as an account block below the usage table, with the granted/topped-up
194
+ split on hover (when the split is meaningful).
195
+ - The usage ladder puts **money before words**: as the sidebar narrows, the cost survives longer than the wordy
196
+ labels, and below the numeric floor the three usage rows collapse into one row that still carries a number
197
+ instead of three bare labels.
187
198
 
188
199
  The desktop half ships as a prebuilt CommonJS bundle (`gui-experimental.cjs`). Published packages include it; if you
189
200
  run the plugin from a clone, build it once — the TUI half needs no build and loads from TypeScript like the server half:
@@ -251,26 +262,6 @@ npm run smoke # real API smoke; skipped without DEEPSEEK_API_KEY
251
262
  npm pack --dry-run # audit the publish artifacts — run build:gui first, `pack` does not build the bundle
252
263
  ```
253
264
 
254
- One-command switch (repo-only helper; edits `options.mode` in `opencode.jsonc`, backing it up to `opencode.jsonc.bak-switch`):
255
-
256
- ```powershell
257
- .\tools\switch-mode.ps1 status
258
- .\tools\switch-mode.ps1 native
259
- .\tools\switch-mode.ps1 deepseek
260
- ```
261
-
262
- If ExecutionPolicy blocks it:
263
-
264
- ```powershell
265
- powershell -NoProfile -ExecutionPolicy Bypass -File .\tools\switch-mode.ps1 native
266
- ```
267
-
268
- Shorter, via an alias in `$PROFILE`:
269
-
270
- ```powershell
271
- function dsmode { & "D:\repos\opencode-deepseek-search\tools\switch-mode.ps1" @args }
272
- ```
273
-
274
265
  Layout:
275
266
 
276
267
  ```
@@ -287,7 +278,6 @@ vite.config.ts # builds src/gui into the root gui-experimental.cjs bundle
287
278
  test/*.test.ts # pure-function tests
288
279
  scripts/smoke.ts # real-API smoke (5 steps)
289
280
  scripts/check-gui-bundle.ts # bundle shape check, run in CI after build:gui
290
- tools/switch-mode.ps1 # terminal backend switch
291
281
  .github/workflows/ # ci (build + typecheck + test + bundle check) / release
292
282
  ```
293
283
 
@@ -1 +1 @@
1
- "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const a=require("solid-js/web"),_=require("@opencode/plugin/gui"),b=require("solid-js"),z="deepseek-search";function B(e){return e==="en"||e==="ja"||e==="zh-CN"}const F={type:"object",properties:{search:{type:"integer"},fetch:{type:"integer"},empty:{type:"integer"},failed:{type:"integer"},costMicro:{type:"integer"}},required:["search","fetch","empty","failed","costMicro"],additionalProperties:!1},Q={id:z,methods:{status:{input:{type:"object",properties:{},additionalProperties:!1},output:{type:"object",properties:{mode:{type:"string",enum:["deepseek","native"]},source:{type:"string",enum:["override","config"]},patched:{type:"boolean"},hasKey:{type:"boolean"},pricePerThousand:{type:"number"},today:{type:"string"},month:{type:"string"},total:{type:"string"},language:{type:"string",enum:["en","ja","zh-CN"]},counts:{type:"object",properties:{today:F,month:F,total:F},required:["today","month","total"],additionalProperties:!1}},required:["mode","source","patched","hasKey","pricePerThousand","today","month","total"],additionalProperties:!1}},setMode:{input:{type:"object",properties:{mode:{type:"string",enum:["deepseek","native","config"]}},required:["mode"],additionalProperties:!1},output:{type:"object",properties:{ok:{type:"boolean"},mode:{type:"string"},message:{type:"string"}},required:["ok","mode","message"],additionalProperties:!1}}},events:{changed:{schema:{type:"object",properties:{}}}}};function K(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function V(e){return e==="deepseek"||e==="native"}function x(e){if(!K(e))return;const{search:t,fetch:o,empty:r,failed:n,costMicro:s}=e;for(const c of[t,o,r,n])if(typeof c!="number"||!Number.isInteger(c)||c<0)return;if(!(typeof s!="number"||!Number.isFinite(s)||s<0))return{search:t,fetch:o,empty:r,failed:n,costMicro:s}}function W(e){if(!K(e))return;const{mode:t,source:o,patched:r,hasKey:n,pricePerThousand:s,today:c,month:h,total:d,language:m,counts:i}=e;if(!V(t)||o!=="override"&&o!=="config"||typeof r!="boolean"||typeof n!="boolean"||typeof s!="number"||!Number.isFinite(s)||typeof c!="string"||typeof h!="string"||typeof d!="string")return;const u={mode:t,source:o,patched:r,hasKey:n,pricePerThousand:s,today:c,month:h,total:d,language:B(m)?m:"en"};if(K(i)){const f=x(i.today),S=x(i.month),g=x(i.total);f&&S&&g&&(u.counts={today:f,month:S,total:g})}return u}function G(e){if(!K(e))return;const{ok:t,mode:o,message:r}=e;if(!(typeof t!="boolean"||typeof o!="string"||typeof r!="string"))return{ok:t,mode:o,message:r}}const J={"state.loading":"Loading…","state.loadingShort":"Loading…","state.failed":"Read failed","state.failedShort":"Failed","state.enabled":"Enabled","state.enabledShort":"On","state.disabled":"Disabled","state.disabledShort":"Off","state.unpatched":"websearch not taken over","state.unpatchedShort":"Not taken over","state.missingKey":"API key missing","state.missingKeyShort":"No API key","action.disable":"Disable","action.enable":"Enable","action.release":"Release","action.retry":"Retry","panel.tooltip.refresh":"Refresh usage","panel.loading":"Reading usage…","panel.label.on":"Disable (release to native search)","panel.label.off":"Enable (take over with DeepSeek)","panel.label.warn":"Release to native search","panel.label.error":"Retry","panel.label.idle":"Retry","command.open":"DeepSeek: Open usage panel","command.toggle":"DeepSeek: Enable/disable search takeover","menu.description":"Search usage and the takeover switch","toast.openSessionFirst":"Open a session first","toast.statusMalformed":"Status payload has the wrong shape","toast.modeMalformed":"Mode switch result has the wrong shape","dialog.title":"DeepSeek search","dialog.enable":"Enable","dialog.enableDesc":"Take over the built-in websearch","dialog.disable":"Disable","dialog.disableDesc":"Return the built-in websearch to the host","dialog.refresh":"Refresh","dialog.refreshDesc":"Re-read status and usage","mode.report":"Current search backend: {mode} (source: {source}){suffix}","mode.source.override":"deepseek_mode switch, persisted","mode.source.config":"config file options.mode","mode.suffix.patched":" · built-in websearch is served by DeepSeek","mode.suffix.notPatched":" · built-in websearch was not found, so nothing is taken over","mode.suffix.native":" · built-in websearch stays native","mode.notFound":'Built-in {tool} tool not found; it cannot be switched at runtime. Set options.mode to "{mode}" in the config file instead.',"mode.configCleared":"Persisted switch cleared; back to the config file ({mode}).","mode.persisted":'Persisted: still {mode} after restarting OpenCode. To return to the config file, call deepseek_mode with mode: "config".',"mode.statusOnly":"status only reports the current mode; it does not switch.","mode.invalid":"mode must be status / deepseek / native / config, got: {raw}","progress.fetch":"DeepSeek Fetch: fetching {url}","fetch.source":"Source: {url}","usage.reset":"Local stats cleared.","usage.report.title":"DeepSeek usage (local counts, estimated per successful request; current mode {mode}; ¥{price}/1000 requests)","usage.report.line":"{label}: searches {search} · fetches {fetch} · empty/failed {skipped} (empty {empty} / failed {failed}) · est. {cost}","usage.report.note":"Note: empty and failed requests are not billed; DeepSeek's account-side bill is authoritative, and these counts exclude other machines or tools.","usage.label.today":"Today","usage.label.month":"Month","usage.label.total":"Total","usage.summary.line":"Search {search} · Fetch {fetch} · empty/failed {skipped} · {cost}","error.missingKey":'DeepSeek API key is not configured. Set the DEEPSEEK_API_KEY environment variable (PowerShell: [Environment]::SetEnvironmentVariable("DEEPSEEK_API_KEY","sk-...","User")), or write "apiKey": "{env:DEEPSEEK_API_KEY}" in the plugin options, then restart opencode.',"error.http.400":"Invalid DeepSeek request parameters (400){upstream}. Queries must be 1–128 characters; Fetch URLs must be valid absolute http(s) URLs.","error.http.401":"Invalid DeepSeek API key (401){upstream}. Check the DEEPSEEK_API_KEY environment variable or options.apiKey.","error.http.402":"Insufficient DeepSeek account balance (402); failed requests are not billed. Top up and try again.","error.http.403":"DeepSeek Fetch was blocked by the target site's security policy (403){upstream}.{hint}","error.http.404":"DeepSeek could not extract the page body (404){upstream}. Check that the link is reachable.{hint}","error.http.408":"DeepSeek request timed out (408); one backoff retry also failed. Try again later.","error.http.413":"DeepSeek request body too large (413). Shorten the input.","error.http.429":"DeepSeek rate limit hit (429; account limits: Search 100 QPS / Fetch 25 QPS); one backoff retry also failed. Try again later.","error.http.5xx":'DeepSeek server error ({status}); one backoff retry also failed. You can set options.mode to "native" to use native search for now.',"error.http.other":"DeepSeek request failed (HTTP {status}){upstream}","error.http.fetchHint":" You can use the built-in webfetch tool for this page.","error.parse":"DeepSeek returned an unparseable response (HTTP {status}). Try again later.","error.aborted":"DeepSeek request cancelled.","error.timeout":"DeepSeek request timed out ({seconds}s). Try again.","error.network":"Cannot reach {base} (network error: {message}). Check your network or proxy settings.","error.validate.queryEmpty":"Empty query: DeepSeek Search needs a non-empty query (1–128 characters).","error.validate.queryLong":"Query too long ({length} characters, limit {max}); shorten it and retry.","error.validate.urlScheme":'Invalid URL: "{url}". DeepSeek Fetch only supports absolute http/https URLs.',"error.validate.urlLong":"URL too long ({length} > {max} characters).","error.validate.urlHost":'URL has no host: "{url}".',"error.validate.urlUserinfo":"URL must not contain a username/password (userinfo).","error.validate.urlParse":'Cannot parse URL: "{url}".',"error.fetch.truncated":"[DeepSeek Fetch truncated] The page is {raw} characters; the limit is {max}, so only the first {max} characters are kept."},X=["{label} Search {search} Fetch {fetch} {cost}","{label} Search {search} Fetch {fetch} {cost}","{label} Search {search} Fetch {fetch}","{label} Search {search} Fetch {fetch}","{label} {search}S {fetch}F {cost}","{label} {search}S {fetch}F","{label} {search}S"],Z=["Set DEEPSEEK_API_KEY and restart","Set DEEPSEEK_API_KEY","Set API KEY","Set KEY"],ee={"state.loading":"読み込み中…","state.loadingShort":"読み込み中…","state.failed":"読み込み失敗","state.failedShort":"失敗","state.enabled":"有効","state.enabledShort":"有効","state.disabled":"無効","state.disabledShort":"無効","state.unpatched":"websearch 未引き継ぎ","state.unpatchedShort":"未引き継ぎ","state.missingKey":"APIキー未設定","state.missingKeyShort":"キー未設定","action.disable":"停止","action.enable":"有効化","action.release":"返上","action.retry":"再試行","panel.tooltip.refresh":"使用量を更新","panel.loading":"使用量を読み込み中…","panel.label.on":"停止(ネイティブ検索に返上)","panel.label.off":"有効化(DeepSeek が引き継ぐ)","panel.label.warn":"ネイティブ検索に返上","panel.label.error":"再試行","panel.label.idle":"再試行","command.open":"DeepSeek: 使用量パネルを開く","command.toggle":"DeepSeek: 検索の引き継ぎを切り替え","menu.description":"検索使用量と引き継ぎスイッチ","toast.openSessionFirst":"先にセッションを開いてください","toast.statusMalformed":"状態データの形式が不正です","toast.modeMalformed":"モード切り替え結果の形式が不正です","dialog.title":"DeepSeek 検索","dialog.enable":"有効化","dialog.enableDesc":"組み込み websearch を引き継ぐ","dialog.disable":"停止","dialog.disableDesc":"組み込み websearch をホストに返上","dialog.refresh":"更新","dialog.refreshDesc":"状態と使用量を再読み込み","mode.report":"現在の検索バックエンド: {mode}(ソース: {source}){suffix}","mode.source.override":"deepseek_mode スイッチ(永続化済み)","mode.source.config":"設定ファイルの options.mode","mode.suffix.patched":" · 組み込み websearch は DeepSeek が処理","mode.suffix.notPatched":" · 組み込み websearch が見つからず、引き継げませんでした","mode.suffix.native":" · 組み込み websearch はネイティブのまま","mode.notFound":"組み込み {tool} ツールが見つからないため、実行中は切り替えできません。設定ファイルで options.mode を「{mode}」にしてください。","mode.configCleared":"永続化スイッチを削除し、設定ファイルの値({mode})に戻しました。","mode.persisted":'永続化しました: OpenCode を再起動しても {mode} のままです。設定ファイルの値に戻すには、deepseek_mode に mode: "config" を渡してください。',"mode.statusOnly":"status は現在のモードを読むだけで、切り替えは行いません。","mode.invalid":"mode は status / deepseek / native / config のいずれかです。受信値: {raw}","progress.fetch":"DeepSeek Fetch 取得中: {url}","fetch.source":"出典: {url}","usage.reset":"ローカル統計をクリアしました。","usage.report.title":"DeepSeek 使用量(ローカル集計・成功リクエストで概算。現在のモード {mode}、単価 ¥{price}/1000回)","usage.report.line":"{label}: 検索 {search} · 取得 {fetch} · 空/失敗 {skipped}(空結果 {empty} / 失敗 {failed})· 推定 {cost}","usage.report.note":"注: 空結果と失敗は課金されません。請求は DeepSeek のアカウント側が正で、この集計は他のマシンや他ツールの呼び出しを含みません。","usage.label.today":"今日","usage.label.month":"今月","usage.label.total":"合計","usage.summary.line":"検索 {search} · 取得 {fetch} · 空/失敗 {skipped} · {cost}","error.missingKey":'DeepSeek APIキーが未設定です。環境変数 DEEPSEEK_API_KEY を設定してください(PowerShell: [Environment]::SetEnvironmentVariable("DEEPSEEK_API_KEY","sk-...","User"))。またはプラグイン options に "apiKey": "{env:DEEPSEEK_API_KEY}" を書き、opencode を再起動してください。',"error.http.400":"DeepSeek のリクエストパラメータが不正です(400){upstream}。検索語は 1〜128 文字、Fetch の URL は有効な http(s) 絶対 URL が必要です。","error.http.401":"DeepSeek APIキーが無効です(401){upstream}。DEEPSEEK_API_KEY 環境変数か options.apiKey を確認してください。","error.http.402":"DeepSeek アカウントの残高が不足しています(402)。失敗したリクエストは課金されません。チャージ後に再試行してください。","error.http.403":"DeepSeek Fetch は対象サイトのセキュリティポリシーに拒否されました(403){upstream}。{hint}","error.http.404":"DeepSeek はこのリンクの本文を抽出できませんでした(404){upstream}。リンクにアクセスできるか確認してください。{hint}","error.http.408":"DeepSeek リクエストがタイムアウトしました(408)。バックオフ再試行も失敗しました。しばらくしてから再試行してください。","error.http.413":"DeepSeek リクエストの本文が大きすぎます(413)。入力を短くしてください。","error.http.429":"DeepSeek のレート制限に達しました(429。アカウント上限: Search 100 QPS / Fetch 25 QPS)。バックオフ再試行も失敗しました。しばらくしてから再試行してください。","error.http.5xx":'DeepSeek のサーバーエラーです({status})。バックオフ再試行も失敗しました。一時的に options.mode を "native" にするとネイティブ検索を使えます。',"error.http.other":"DeepSeek リクエストが失敗しました(HTTP {status}){upstream}","error.http.fetchHint":" このページには組み込み webfetch ツールを使えます。","error.parse":"DeepSeek が解析できないレスポンスを返しました(HTTP {status})。しばらくしてから再試行してください。","error.aborted":"DeepSeek リクエストはキャンセルされました。","error.timeout":"DeepSeek リクエストがタイムアウトしました({seconds} 秒)。再試行してください。","error.network":"{base} に接続できません(ネットワークエラー: {message})。ネットワークかプロキシの設定を確認してください。","error.validate.queryEmpty":"検索語が空です。DeepSeek Search には空でない query(1〜128 文字)が必要です。","error.validate.queryLong":"検索語が長すぎます({length} 文字、上限 {max})。短くして再試行してください。","error.validate.urlScheme":"無効な URL:「{url}」。DeepSeek Fetch は http/https の絶対 URL のみ対応します。","error.validate.urlLong":"URL が長すぎます({length} > {max} 文字)。","error.validate.urlHost":"URL にホスト名がありません:「{url}」。","error.validate.urlUserinfo":"URL にユーザー名/パスワード(userinfo)を含められません。","error.validate.urlParse":"URL を解析できません:「{url}」。","error.fetch.truncated":"[DeepSeek Fetch 切り詰め] 元の本文は {raw} 文字で、上限 {max} 文字を超えています。先頭 {max} 文字のみ保持しました。"},te=["{label} Search {search} Fetch {fetch} {cost}","{label} Search {search} Fetch {fetch} {cost}","{label} Search {search} Fetch {fetch}","{label} Search {search} Fetch {fetch}","{label} {search}S {fetch}F {cost}","{label} {search}S {fetch}F","{label} {search}S"],re=["DEEPSEEK_API_KEY を設定して再起動","DEEPSEEK_API_KEY を設定","API KEY を設定","KEY 設定"],oe={"state.loading":"正在读取…","state.loadingShort":"正在读取…","state.failed":"读取失败","state.failedShort":"读取失败","state.enabled":"已启用","state.enabledShort":"已启用","state.disabled":"已停用","state.disabledShort":"已停用","state.unpatched":"未接管内置 websearch","state.unpatchedShort":"未接管","state.missingKey":"未配置 API Key","state.missingKeyShort":"缺 API Key","action.disable":"停用","action.enable":"启用","action.release":"交还","action.retry":"重试","panel.tooltip.refresh":"刷新用量","panel.loading":"正在读取用量…","panel.label.on":"停用(交还原生搜索)","panel.label.off":"启用(由 DeepSeek 接管)","panel.label.warn":"交还原生搜索","panel.label.error":"重试","panel.label.idle":"重试","command.open":"DeepSeek: 打开统计面板","command.toggle":"DeepSeek: 启用/停用搜索接管","menu.description":"搜索用量与接管开关","toast.openSessionFirst":"请先打开一个会话","toast.statusMalformed":"状态数据格式不符","toast.modeMalformed":"模式切换结果格式不符","dialog.title":"DeepSeek 搜索","dialog.enable":"启用","dialog.enableDesc":"接管内置 websearch","dialog.disable":"停用","dialog.disableDesc":"把内置 websearch 交还宿主","dialog.refresh":"刷新","dialog.refreshDesc":"重新读取状态与统计","mode.report":"当前搜索后端: {mode}(来源: {source}){suffix}","mode.source.override":"deepseek_mode 开关,已持久化","mode.source.config":"配置文件 options.mode","mode.suffix.patched":" · 内置 websearch 已由 DeepSeek 接管","mode.suffix.notPatched":" · 未找到内置 websearch 工具,实际未能接管","mode.suffix.native":" · 内置 websearch 保持原生","mode.notFound":'未找到内置 {tool} 工具,无法在运行时切换。请改用配置文件把 options.mode 设为 "{mode}"。',"mode.configCleared":"已清除持久化开关,回到配置文件设置({mode})。","mode.persisted":'已持久化:重启 OpenCode 后仍为 {mode}。要恢复配置文件设置,调用 deepseek_mode 并传 mode: "config"。',"mode.statusOnly":"status 只读取当前模式,不做切换。","mode.invalid":"mode 只能是 status / deepseek / native / config,收到:{raw}","progress.fetch":"DeepSeek Fetch 抓取中: {url}","fetch.source":"来源: {url}","usage.reset":"已清零本地统计。","usage.report.title":"DeepSeek 用量(本地统计,按成功请求估算;当前模式 {mode};单价 ¥{price}/1000 次)","usage.report.line":"{label}: 搜索 {search} · 抓取 {fetch} · 空/失 {skipped}(空结果 {empty} / 失败 {failed})· 估算 {cost}","usage.report.note":"说明:空结果与失败请求不计费;DeepSeek 以账号侧账单为准,本统计不含其他机器或其他工具的调用。","usage.label.today":"今日","usage.label.month":"本月","usage.label.total":"累计","usage.summary.line":"搜索 {search} · 抓取 {fetch} · 空/失 {skipped} · {cost}","error.missingKey":'未配置 DeepSeek API Key:请设置环境变量 DEEPSEEK_API_KEY(PowerShell: [Environment]::SetEnvironmentVariable("DEEPSEEK_API_KEY","sk-...","User")),或在插件 options 中写 "apiKey": "{env:DEEPSEEK_API_KEY}",然后重启 opencode。',"error.http.400":"DeepSeek 请求参数无效(400){upstream}。搜索词需 1~128 字符;Fetch 的 URL 需为合法的 http(s) 绝对地址。","error.http.401":"DeepSeek API Key 无效(401){upstream}。请检查 DEEPSEEK_API_KEY 环境变量或 options.apiKey。","error.http.402":"DeepSeek 账户余额不足(402),失败请求不计费。请充值后重试。","error.http.403":"DeepSeek Fetch 被目标站点安全策略拒绝(403){upstream}。{hint}","error.http.404":"DeepSeek 无法提取该链接的正文(404){upstream}。请确认链接可访问。{hint}","error.http.408":"DeepSeek 请求超时(408),已退避重试一次仍失败。请稍后重试。","error.http.413":"DeepSeek 请求体过大(413)。请缩短输入。","error.http.429":"DeepSeek 触发限速(429,账号级 Search 100 QPS / Fetch 25 QPS),已退避重试一次仍失败。请稍后再试。","error.http.5xx":'DeepSeek 服务端错误({status}),已退避重试一次仍失败。可把插件 options.mode 改为 "native" 暂用原生搜索。',"error.http.other":"DeepSeek 请求失败(HTTP {status}){upstream}","error.http.fetchHint":" 可改用内置 webfetch 工具抓取该页面。","error.parse":"DeepSeek 返回了无法解析的响应(HTTP {status})。请稍后重试。","error.aborted":"DeepSeek 请求已取消。","error.timeout":"DeepSeek 请求超时({seconds} 秒),请重试。","error.network":"无法连接 {base}(网络错误:{message})。请检查网络或代理设置。","error.validate.queryEmpty":"搜索词为空:DeepSeek Search 需要非空 query(1~128 字符)。","error.validate.queryLong":"搜索词过长({length} 字符,上限 {max}),请精简后重试。","error.validate.urlScheme":"无效 URL:「{url}」。DeepSeek Fetch 仅支持 http/https 绝对地址。","error.validate.urlLong":"URL 过长({length} > {max} 字符)。","error.validate.urlHost":"URL 缺少主机名:「{url}」。","error.validate.urlUserinfo":"URL 不能包含用户名/密码(userinfo)。","error.validate.urlParse":"无法解析 URL:「{url}」。","error.fetch.truncated":"[DeepSeek Fetch 截断提示] 原文共 {raw} 字符,超出上限 {max},此处仅保留前 {max} 字符。"},ae=["{label} Search {search} Fetch {fetch} {cost}","{label} Search {search} Fetch {fetch} {cost}","{label} Search {search} Fetch {fetch}","{label} Search {search} Fetch {fetch}","{label} {search}S {fetch}F {cost}","{label} {search}S {fetch}F","{label} {search}S"],ne=["设置 DEEPSEEK_API_KEY 后重启","设置 API KEY 后重启","设置 API KEY","设置 KEY"],M={en:{messages:J,usageForms:X,hintForms:Z},ja:{messages:ee,usageForms:te,hintForms:re},"zh-CN":{messages:oe,usageForms:ae,hintForms:ne}};function N(e){return M[e]}function se(e){return M[e].messages}function C(e,t={}){return e.replace(/\{(\w+)\}/g,(o,r)=>r in t?String(t[r]):o)}function l(e,t,o){const n=se(e)[t];return n===void 0?t:C(n,o)}const ie="DeepSeek search/fetch",L="DeepSeek",le=2,ce=1,de=24,A=["today","month","total"],q={today:"usage.label.today",month:"usage.label.month",total:"usage.label.total"};function O(e){const t=e.codePointAt(0)??0;return t===0?0:t>=4352&&t<=4447||t>=11904&&t<=42191||t>=44032&&t<=55203||t>=63744&&t<=64255||t>=65040&&t<=65049||t>=65072&&t<=65135||t>=65280&&t<=65376||t>=65504&&t<=65510||t>=127744&&t<=129791||t>=131072&&t<=262141?2:1}function v(e){let t=0;for(const o of e)t+=O(o);return t}function D(e,t){if(t<1)return"";if(v(e)<=t)return e;let o="",r=0;for(const n of e){const s=O(n);if(r+s>t-1)return`${o}…`;o+=n,r+=s}return o}function pe(e,t,o){const r=e.replace(/\s+/g," ").trim();return D(r||l(o,"state.failed"),t)}function H(e){let o=(Number.isFinite(e)&&e>0?e/1e6:0).toFixed(3);for(;o.endsWith("0")&&o.length>4;)o=o.slice(0,-1);return`¥${o}`}function ue(e){const t=A.map(o=>e[o]);return{search:Math.max(...t.map(o=>String(o.search).length)),fetch:Math.max(...t.map(o=>String(o.fetch).length)),cost:Math.max(...t.map(o=>H(o.costMicro).length))}}function he(e,t,o,r,n){const s=l(n,q[e]),c=String(t.search).padStart(o.search," "),h=String(t.fetch).padStart(o.fetch," "),d=H(t.costMicro).padStart(o.cost," "),m={label:s,search:c,fetch:h,cost:d};for(const i of N(n).usageForms){const u=C(i,m);if(v(u)<=r)return u}return v(s)<=r?s:D(s,r)}function fe(e,t,o,r){const n=l(r,q[e]),s=`${n} ${t}`;if(v(s)<=o)return s;const c=t.split(" · ").filter(d=>!d.startsWith("¥")&&!d.startsWith("空/失")),h=[`${n} ${c.join(" ")}`.trimEnd(),`${n} ${c[0]??""}`.trimEnd(),n];for(const d of h)if(v(d)<=o)return d;return D(n,o)}function me(e,t,o){if(t===Number.POSITIVE_INFINITY)return[`${l(o,"usage.label.today")} ${e.today}`,`${l(o,"usage.label.month")} ${e.month}`,`${l(o,"usage.label.total")} ${e.total}`];const r=e.counts;if(r){const n=ue(r);return A.map(s=>he(s,r[s],n,t,o))}return A.map(n=>fe(n,e[n],t,o))}function ge(e,t){const o=`[${e}]`;if(t===Number.POSITIVE_INFINITY||v(o)<=t)return o;const r=t-2;return r>=1?`[${D(e,r)}]`:D(o,t)}function j(e,t,o,r){const n=(d,m)=>{if(o===Number.POSITIVE_INFINITY)return Number.POSITIVE_INFINITY;const i=d?le:0,u=r===void 0||m?0:r+ce;return Math.max(0,o-i-u)},s=[`${ie} · ${e}`,`${L} · ${e}`,`${L} · ${t}`,e,t],c=r===void 0?[!1]:[!1,!0];for(const d of s)for(const m of c)for(const i of[!0,!1])if(v(d)<=n(i,m))return{title:d,glyph:i,actionBelow:m};const h=r!==void 0;return{title:D(t,n(!1,h)),glyph:!1,actionBelow:h}}function be(e,t){const o=N(t).hintForms;if(e===Number.POSITIVE_INFINITY)return o[0]??"";for(const r of o)if(v(r)<=e)return r;return D(o[o.length-1]??"",e)}function P(e,t,o,r){const n=ge(o,r);return{...j(e,t,r,v(n)),action:n}}function Se(e){const{status:t,error:o,language:r}=e,n=e.maxColumns??de,s=t===void 0?[]:me(t,n,r);if(t===void 0){const h=l(r,"action.retry");return o===void 0||o.trim()===""?{...P(l(r,"state.loading"),l(r,"state.loadingShort"),h,n),hint:void 0,clickable:!0,lines:s,tone:"idle"}:{...P(l(r,"state.failed"),l(r,"state.failedShort"),h,n),hint:void 0,clickable:!0,lines:[pe(o,n,r)],tone:"error"}}if(t.patched&&t.hasKey)return{...P(l(r,"state.enabled"),l(r,"state.enabledShort"),l(r,"action.disable"),n),hint:void 0,clickable:!0,lines:s,tone:"on"};if(!t.patched&&t.mode==="deepseek")return{...P(l(r,"state.unpatched"),l(r,"state.unpatchedShort"),l(r,"action.release"),n),hint:void 0,clickable:!0,lines:s,tone:"warn"};if(t.mode==="native")return{...P(l(r,"state.disabled"),l(r,"state.disabledShort"),l(r,"action.enable"),n),hint:void 0,clickable:!0,lines:s,tone:"off"};const c=be(n,r);return{...j(l(r,"state.missingKey"),l(r,"state.missingKeyShort"),n,void 0),action:c,hint:c,clickable:!1,lines:s,tone:"error"}}var U=a.template("<span>"),ye=a.template("<span style=opacity:0.6;font-weight:400>"),ke=a.template("<span style=opacity:0.3;font-weight:400> · "),ve=a.template("<code style=font-size:0.94em>DEEPSEEK_API_KEY"),Ee=a.template('<p style="margin:16px 0 0;font-size:12px;line-height:1.6;opacity:0.68;overflow-wrap:anywhere">'),De=a.template("<div style=margin-top:18px>"),we=a.template('<div style="display:flex;flex-direction:column;height:100%;min-width:0;box-sizing:border-box;padding:16px 16px 24px;overflow-y:auto;overflow-x:hidden"><header style=display:flex;align-items:flex-start;gap:8px><span aria-hidden=true style=flex-shrink:0;font-size:10px;line-height:20px;font-weight:700></span><h2 aria-live=polite style=flex:1;min-width:0;margin:0;font-size:13px;font-weight:600;line-height:20px;overflow-wrap:anywhere>'),Pe=a.template('<p style="margin:14px 0 0;font-size:12px;line-height:1.6;opacity:0.7;overflow-wrap:anywhere">'),_e=a.template("<div style=display:flex;align-items:center;gap:8px;margin-top:14px;opacity:0.7><span style=font-size:12px>"),Ie=a.template('<dl style="margin:16px 0 0">'),Ke=a.template('<div style="display:flex;align-items:baseline;gap:12px;padding:10px 0"><dt style=flex-shrink:0;font-size:11px;letter-spacing:0.08em;line-height:1.9;opacity:0.62></dt><dd style=margin:0;margin-left:auto;min-width:0;display:flex;flex-wrap:wrap;justify-content:flex-end;column-gap:6px;row-gap:2px;font-size:13px;line-height:1.55;font-variant-numeric:tabular-nums;text-align:right>'),Fe=a.template("<span style=min-width:0;overflow-wrap:anywhere>");const xe={on:"●",off:"○",warn:"◐",error:"!",idle:"…"},Ae={dark:{on:"#63d19a",warn:"#e0ad4f",error:"#ee7b74",off:"#8b9198",idle:"#8b9198",rule:"rgba(255,255,255,0.14)"},light:{on:"#1e7a4c",warn:"#8a5f08",error:"#b03931",off:"#6b7076",idle:"#6b7076",rule:"rgba(0,0,0,0.13)"}},$e={on:"panel.label.on",off:"panel.label.off",warn:"panel.label.warn",error:"panel.label.error",idle:"panel.label.idle"},Re={on:"arrow-undo-down",off:"globe",warn:"arrow-undo-down",error:"refresh",idle:"refresh"};function Te(e,t){const o=e.indexOf(" · ");return o<0?(()=>{var r=U();return a.setStyleProperty(r,"color",t),a.insert(r,e),r})():[(()=>{var r=ye();return a.insert(r,()=>e.slice(0,o)),r})(),ke(),(()=>{var r=U();return a.setStyleProperty(r,"color",t),a.insert(r,()=>e.slice(o+3)),r})()]}function Le(e){const t=e.indexOf(" ");return t<0?{label:e,chunks:[]}:{label:e.slice(0,t),chunks:e.slice(t+1).split(" · ")}}function Ue(e,t){const o="DEEPSEEK_API_KEY",r=e.indexOf(o);return r<0?e:[a.memo(()=>e.slice(0,r)),(()=>{var n=ve();return a.setStyleProperty(n,"font-family",t),n})(),a.memo(()=>e.slice(r+o.length))]}function Ye(e){const t=_.usePlugin(),o=()=>e.status?.language??"en",r=b.createMemo(()=>Se({status:e.status,error:e.error,maxColumns:1/0,language:o()})),n=b.createMemo(()=>Ae[t.appearance.mode()]),s=()=>t.appearance.font("mono");return b.onMount(()=>{const c=setInterval(()=>e.onRefresh(),1e4);b.onCleanup(()=>clearInterval(c))}),(()=>{var c=we(),h=c.firstChild,d=h.firstChild,m=d.nextSibling;return a.insert(d,()=>xe[r().tone]),a.insert(m,()=>Te(r().title,n()[r().tone])),a.insert(h,a.createComponent(t.ui.Tooltip,{get value(){return l(o(),"panel.tooltip.refresh")},placement:"bottom",get children(){return a.createComponent(t.ui.IconButton,{icon:"refresh",get label(){return l(o(),"panel.tooltip.refresh")},size:"small",get onClick(){return e.onRefresh}})}}),null),a.insert(c,(()=>{var i=a.memo(()=>e.status===void 0);return()=>i()?a.memo(()=>r().lines.length>0)()?(()=>{var u=Pe();return a.insert(u,()=>r().lines[0]),u})():(()=>{var u=_e(),f=u.firstChild;return a.insert(u,a.createComponent(t.ui.Loader,{size:"small"}),f),a.insert(f,()=>l(o(),"panel.loading")),u})():(()=>{var u=Ie();return a.insert(u,a.createComponent(b.For,{get each(){return r().lines},children:(f,S)=>{const g=Le(f);return(()=>{var p=Ke(),y=p.firstChild,$=y.nextSibling;return a.insert(y,()=>g.label),a.insert($,a.createComponent(b.For,{get each(){return g.chunks},children:(k,I)=>(()=>{var E=Fe();return a.insert(E,k,null),a.insert(E,()=>I()<g.chunks.length-1?" ·":"",null),a.effect(w=>{var R=k.startsWith("¥")?500:400,T=k.startsWith("¥")?1:.8;return R!==w.e&&a.setStyleProperty(E,"font-weight",w.e=R),T!==w.t&&a.setStyleProperty(E,"opacity",w.t=T),w},{e:void 0,t:void 0}),E})()})),a.effect(k=>{var I=S()>0?`1px solid ${n().rule}`:void 0,E=s();return I!==k.e&&a.setStyleProperty(p,"border-top",k.e=I),E!==k.t&&a.setStyleProperty($,"font-family",k.t=E),k},{e:void 0,t:void 0}),p})()}})),a.effect(f=>{var S=`1px solid ${n().rule}`,g=`1px solid ${n().rule}`;return S!==f.e&&a.setStyleProperty(u,"border-top",f.e=S),g!==f.t&&a.setStyleProperty(u,"border-bottom",f.t=g),f},{e:void 0,t:void 0}),u})()})(),null),a.insert(c,a.createComponent(b.Show,{get when(){return!r().clickable},get children(){var i=Ee();return a.insert(i,()=>Ue(r().action,s())),i}}),null),a.insert(c,a.createComponent(b.Show,{get when(){return a.memo(()=>!!r().clickable)()&&r().tone!=="idle"},get children(){var i=De();return a.insert(i,a.createComponent(t.ui.Button,{get variant(){return r().tone==="off"||r().tone==="error"?"contrast":"neutral"},get icon(){return Re[r().tone]},get disabled(){return e.busy},get busy(){return e.busy},get onClick(){return e.onToggle},get children(){return l(o(),$e[r().tone])}})),i}}),null),a.effect(i=>{var u=e.busy||void 0,f=n()[r().tone];return u!==i.e&&a.setAttribute(c,"aria-busy",i.e=u),f!==i.t&&a.setStyleProperty(d,"color",i.t=f),i},{e:void 0,t:void 0}),c})()}const Y={id:"main",title:"DeepSeek"},Me=80,Ne=_.Plugin.define({setup(e){const t=e.client.rpc(Q),[o,r]=b.createSignal(),[n,s]=b.createSignal(),[c,h]=b.createSignal(!1),d=()=>o()?.language??"en",m=p=>(p instanceof Error?p.message:String(p)).slice(0,Me);async function i(){try{const p=W(await t.status({}));p?(r(p),s(void 0)):s(l(d(),"toast.statusMalformed"))}catch(p){s(m(p))}}async function u(p){h(!0);try{const y=G(await t.setMode({mode:p}));y?y.ok||e.toast.show({title:"DeepSeek",description:y.message,variant:"error"}):e.toast.show({title:"DeepSeek",description:l(d(),"toast.modeMalformed"),variant:"error"})}catch(y){e.toast.show({title:"DeepSeek",description:m(y),variant:"error"})}finally{await i(),h(!1)}}function f(){const p=o();if(!p||n()){i();return}u(p.mode==="deepseek"?"native":"deepseek")}e.add(_.Panel,{id:"main",region:"side",mobile:{title:"DeepSeek",order:50,kind:"menu",icon:"globe"},list:p=>p.open.includes(Y.id)?[Y]:[],render:()=>a.createComponent(Ye,{get status(){return o()},get error(){return n()},get busy(){return c()},onToggle:f,onRefresh:()=>{i()}})});const S=()=>{const p=e.sessions.current();if(!p){e.toast.show({title:"DeepSeek",description:l(d(),"toast.openSessionFirst")});return}e.layout.open(`${e.id}:main`,p)};e.add(_.Command,()=>({id:"open",title:l(d(),"command.open"),section:"session",run:S})),e.add(_.MenuItem,()=>{const p=e.sessions.current(),y=p!==void 0&&e.layout.stored(p).includes("main");return{menu:"session.panel",id:"open",title:"DeepSeek",description:l(d(),"menu.description"),icon:"globe",order:60,opened:y,run:S}});const g=t.events.on("changed",()=>{i()});return i(),g}});exports.default=Ne;
1
+ "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const a=require("solid-js/web"),I=require("@opencode/plugin/gui"),k=require("solid-js"),z="deepseek-search";function j(e){return e==="en"||e==="ja"||e==="zh-CN"}const $={type:"object",properties:{search:{type:"integer"},fetch:{type:"integer"},empty:{type:"integer"},failed:{type:"integer"},costMicro:{type:"integer"}},required:["search","fetch","empty","failed","costMicro"],additionalProperties:!1},Q={type:"object",properties:{isAvailable:{type:"boolean"},currency:{type:"string"},totalBalance:{type:"string"},grantedBalance:{type:"string"},toppedUpBalance:{type:"string"},fetchedAt:{type:"number"}},required:["isAvailable","currency","totalBalance","grantedBalance","toppedUpBalance","fetchedAt"],additionalProperties:!1},V={id:z,methods:{status:{input:{type:"object",properties:{force:{type:"boolean"}},additionalProperties:!1},output:{type:"object",properties:{mode:{type:"string",enum:["deepseek","native"]},source:{type:"string",enum:["override","config"]},patched:{type:"boolean"},hasKey:{type:"boolean"},pricePerThousand:{type:"number"},today:{type:"string"},month:{type:"string"},total:{type:"string"},language:{type:"string",enum:["en","ja","zh-CN"]},counts:{type:"object",properties:{today:$,month:$,total:$},required:["today","month","total"],additionalProperties:!1},balance:Q},required:["mode","source","patched","hasKey","pricePerThousand","today","month","total"],additionalProperties:!1}},setMode:{input:{type:"object",properties:{mode:{type:"string",enum:["deepseek","native","config"]}},required:["mode"],additionalProperties:!1},output:{type:"object",properties:{ok:{type:"boolean"},mode:{type:"string"},message:{type:"string"}},required:["ok","mode","message"],additionalProperties:!1}}},events:{changed:{schema:{type:"object",properties:{}}}}};function K(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function G(e){return e==="deepseek"||e==="native"}function A(e){if(!K(e))return;const{search:t,fetch:o,empty:r,failed:n,costMicro:s}=e;for(const l of[t,o,r,n])if(typeof l!="number"||!Number.isInteger(l)||l<0)return;if(!(typeof s!="number"||!Number.isFinite(s)||s<0))return{search:t,fetch:o,empty:r,failed:n,costMicro:s}}function W(e){if(!K(e))return;const{isAvailable:t,currency:o,totalBalance:r,grantedBalance:n,toppedUpBalance:s,fetchedAt:l}=e;if(typeof t=="boolean"&&!(o!=="CNY"&&o!=="USD")){for(const i of[r,n,s])if(typeof i!="string"||i.length===0)return;if(!(typeof l!="number"||!Number.isFinite(l)||l<0))return{isAvailable:t,currency:o,totalBalance:r,grantedBalance:n,toppedUpBalance:s,fetchedAt:l}}}function J(e){if(!K(e))return;const{mode:t,source:o,patched:r,hasKey:n,pricePerThousand:s,today:l,month:i,total:u,language:b,counts:m,balance:f}=e;if(!G(t)||o!=="override"&&o!=="config"||typeof r!="boolean"||typeof n!="boolean"||typeof s!="number"||!Number.isFinite(s)||typeof l!="string"||typeof i!="string"||typeof u!="string")return;const h={mode:t,source:o,patched:r,hasKey:n,pricePerThousand:s,today:l,month:i,total:u,language:j(b)?b:"en"};if(K(m)){const S=A(m.today),d=A(m.month),g=A(m.total);S&&d&&g&&(h.counts={today:S,month:d,total:g})}const p=W(f);return p&&(h.balance=p),h}function X(e){if(!K(e))return;const{ok:t,mode:o,message:r}=e;if(!(typeof t!="boolean"||typeof o!="string"||typeof r!="string"))return{ok:t,mode:o,message:r}}const Z={"state.loading":"Loading…","state.loadingShort":"Loading…","state.failed":"Read failed","state.failedShort":"Failed","state.enabled":"Enabled","state.enabledShort":"On","state.disabled":"Disabled","state.disabledShort":"Off","state.unpatched":"websearch not taken over","state.unpatchedShort":"Not taken over","state.missingKey":"API key missing","state.missingKeyShort":"No API key","action.disable":"Disable","action.enable":"Enable","action.release":"Release","action.retry":"Retry","panel.tooltip.refresh":"Refresh usage","panel.loading":"Reading usage…","panel.label.on":"Disable (release to native search)","panel.label.off":"Enable (take over with DeepSeek)","panel.label.warn":"Release to native search","panel.label.error":"Retry","panel.label.idle":"Retry","command.open":"DeepSeek: Open usage panel","command.toggle":"DeepSeek: Enable/disable search takeover","menu.description":"Search usage and the takeover switch","toast.openSessionFirst":"Open a session first","toast.statusMalformed":"Status payload has the wrong shape","toast.modeMalformed":"Mode switch result has the wrong shape","dialog.title":"DeepSeek search","dialog.enable":"Enable","dialog.enableDesc":"Take over the built-in websearch","dialog.disable":"Disable","dialog.disableDesc":"Return the built-in websearch to the host","dialog.refresh":"Refresh","dialog.refreshDesc":"Re-read status and usage","mode.report":"Current search backend: {mode} (source: {source}){suffix}","mode.source.override":"deepseek_mode switch, persisted","mode.source.config":"config file options.mode","mode.suffix.patched":" · built-in websearch is served by DeepSeek","mode.suffix.notPatched":" · built-in websearch was not found, so nothing is taken over","mode.suffix.native":" · built-in websearch stays native","mode.notFound":'Built-in {tool} tool not found; it cannot be switched at runtime. Set options.mode to "{mode}" in the config file instead.',"mode.configCleared":"Persisted switch cleared; back to the config file ({mode}).","mode.persisted":'Persisted: still {mode} after restarting OpenCode. To return to the config file, call deepseek_mode with mode: "config".',"mode.statusOnly":"status only reports the current mode; it does not switch.","mode.invalid":"mode must be status / deepseek / native / config, got: {raw}","progress.fetch":"DeepSeek Fetch: fetching {url}","fetch.source":"Source: {url}","usage.reset":"Local stats cleared.","usage.report.title":"DeepSeek usage (local counts, estimated per successful request; current mode {mode}; ¥{price}/1000 requests)","usage.report.line":"{label}: searches {search} · fetches {fetch} · empty/failed {skipped} (empty {empty} / failed {failed}) · est. {cost}","usage.report.note":"Note: empty and failed requests are not billed; DeepSeek's account-side bill is authoritative, and these counts exclude other machines or tools.","usage.label.today":"Today","usage.label.month":"Month","usage.label.total":"Total","usage.summary.line":"Search {search} · Fetch {fetch} · empty/failed {skipped} · {cost}","balance.label":"Balance","balance.breakdown":"Granted {granted} · Topped up {topped}","error.missingKey":'DeepSeek API key is not configured. Set the DEEPSEEK_API_KEY environment variable (PowerShell: [Environment]::SetEnvironmentVariable("DEEPSEEK_API_KEY","sk-...","User")), or write "apiKey": "{env:DEEPSEEK_API_KEY}" in the plugin options, then restart opencode.',"error.http.400":"Invalid DeepSeek request parameters (400){upstream}. Queries must be 1–128 characters; Fetch URLs must be valid absolute http(s) URLs.","error.http.401":"Invalid DeepSeek API key (401){upstream}. Check the DEEPSEEK_API_KEY environment variable or options.apiKey.","error.http.402":"Insufficient DeepSeek account balance (402); failed requests are not billed. Top up and try again.","error.http.403":"DeepSeek Fetch was blocked by the target site's security policy (403){upstream}.{hint}","error.http.404":"DeepSeek could not extract the page body (404){upstream}. Check that the link is reachable.{hint}","error.http.408":"DeepSeek request timed out (408); one backoff retry also failed. Try again later.","error.http.413":"DeepSeek request body too large (413). Shorten the input.","error.http.429":"DeepSeek rate limit hit (429; account limits: Search 100 QPS / Fetch 25 QPS); one backoff retry also failed. Try again later.","error.http.5xx":'DeepSeek server error ({status}); one backoff retry also failed. You can set options.mode to "native" to use native search for now.',"error.http.other":"DeepSeek request failed (HTTP {status}){upstream}","error.http.fetchHint":" You can use the built-in webfetch tool for this page.","error.parse":"DeepSeek returned an unparseable response (HTTP {status}). Try again later.","error.aborted":"DeepSeek request cancelled.","error.timeout":"DeepSeek request timed out ({seconds}s). Try again.","error.network":"Cannot reach {base} (network error: {message}). Check your network or proxy settings.","error.validate.queryEmpty":"Empty query: DeepSeek Search needs a non-empty query (1–128 characters).","error.validate.queryLong":"Query too long ({length} characters, limit {max}); shorten it and retry.","error.validate.urlScheme":'Invalid URL: "{url}". DeepSeek Fetch only supports absolute http/https URLs.',"error.validate.urlLong":"URL too long ({length} > {max} characters).","error.validate.urlHost":'URL has no host: "{url}".',"error.validate.urlUserinfo":"URL must not contain a username/password (userinfo).","error.validate.urlParse":'Cannot parse URL: "{url}".',"error.fetch.truncated":"[DeepSeek Fetch truncated] The page is {raw} characters; the limit is {max}, so only the first {max} characters are kept."},ee=["{label} Search {search} Fetch {fetch} {cost}","{label} Search {search} Fetch {fetch} {cost}","{label} {search}S {fetch}F {cost}","{label} {search}S {fetch}F","{label} {search}S"],te=["Balance {amount}","Bal. {amount}"],re=["Set DEEPSEEK_API_KEY and restart","Set DEEPSEEK_API_KEY","Set API KEY","Set KEY"],oe={"state.loading":"読み込み中…","state.loadingShort":"読み込み中…","state.failed":"読み込み失敗","state.failedShort":"失敗","state.enabled":"有効","state.enabledShort":"有効","state.disabled":"無効","state.disabledShort":"無効","state.unpatched":"websearch 未引き継ぎ","state.unpatchedShort":"未引き継ぎ","state.missingKey":"APIキー未設定","state.missingKeyShort":"キー未設定","action.disable":"停止","action.enable":"有効化","action.release":"返上","action.retry":"再試行","panel.tooltip.refresh":"使用量を更新","panel.loading":"使用量を読み込み中…","panel.label.on":"停止(ネイティブ検索に返上)","panel.label.off":"有効化(DeepSeek が引き継ぐ)","panel.label.warn":"ネイティブ検索に返上","panel.label.error":"再試行","panel.label.idle":"再試行","command.open":"DeepSeek: 使用量パネルを開く","command.toggle":"DeepSeek: 検索の引き継ぎを切り替え","menu.description":"検索使用量と引き継ぎスイッチ","toast.openSessionFirst":"先にセッションを開いてください","toast.statusMalformed":"状態データの形式が不正です","toast.modeMalformed":"モード切り替え結果の形式が不正です","dialog.title":"DeepSeek 検索","dialog.enable":"有効化","dialog.enableDesc":"組み込み websearch を引き継ぐ","dialog.disable":"停止","dialog.disableDesc":"組み込み websearch をホストに返上","dialog.refresh":"更新","dialog.refreshDesc":"状態と使用量を再読み込み","mode.report":"現在の検索バックエンド: {mode}(ソース: {source}){suffix}","mode.source.override":"deepseek_mode スイッチ(永続化済み)","mode.source.config":"設定ファイルの options.mode","mode.suffix.patched":" · 組み込み websearch は DeepSeek が処理","mode.suffix.notPatched":" · 組み込み websearch が見つからず、引き継げませんでした","mode.suffix.native":" · 組み込み websearch はネイティブのまま","mode.notFound":"組み込み {tool} ツールが見つからないため、実行中は切り替えできません。設定ファイルで options.mode を「{mode}」にしてください。","mode.configCleared":"永続化スイッチを削除し、設定ファイルの値({mode})に戻しました。","mode.persisted":'永続化しました: OpenCode を再起動しても {mode} のままです。設定ファイルの値に戻すには、deepseek_mode に mode: "config" を渡してください。',"mode.statusOnly":"status は現在のモードを読むだけで、切り替えは行いません。","mode.invalid":"mode は status / deepseek / native / config のいずれかです。受信値: {raw}","progress.fetch":"DeepSeek Fetch 取得中: {url}","fetch.source":"出典: {url}","usage.reset":"ローカル統計をクリアしました。","usage.report.title":"DeepSeek 使用量(ローカル集計・成功リクエストで概算。現在のモード {mode}、単価 ¥{price}/1000回)","usage.report.line":"{label}: 検索 {search} · 取得 {fetch} · 空/失敗 {skipped}(空結果 {empty} / 失敗 {failed})· 推定 {cost}","usage.report.note":"注: 空結果と失敗は課金されません。請求は DeepSeek のアカウント側が正で、この集計は他のマシンや他ツールの呼び出しを含みません。","usage.label.today":"今日","usage.label.month":"今月","usage.label.total":"合計","usage.summary.line":"検索 {search} · 取得 {fetch} · 空/失敗 {skipped} · {cost}","balance.label":"残高","balance.breakdown":"付与 {granted} · チャージ {topped}","error.missingKey":'DeepSeek APIキーが未設定です。環境変数 DEEPSEEK_API_KEY を設定してください(PowerShell: [Environment]::SetEnvironmentVariable("DEEPSEEK_API_KEY","sk-...","User"))。またはプラグイン options に "apiKey": "{env:DEEPSEEK_API_KEY}" を書き、opencode を再起動してください。',"error.http.400":"DeepSeek のリクエストパラメータが不正です(400){upstream}。検索語は 1〜128 文字、Fetch の URL は有効な http(s) 絶対 URL が必要です。","error.http.401":"DeepSeek APIキーが無効です(401){upstream}。DEEPSEEK_API_KEY 環境変数か options.apiKey を確認してください。","error.http.402":"DeepSeek アカウントの残高が不足しています(402)。失敗したリクエストは課金されません。チャージ後に再試行してください。","error.http.403":"DeepSeek Fetch は対象サイトのセキュリティポリシーに拒否されました(403){upstream}。{hint}","error.http.404":"DeepSeek はこのリンクの本文を抽出できませんでした(404){upstream}。リンクにアクセスできるか確認してください。{hint}","error.http.408":"DeepSeek リクエストがタイムアウトしました(408)。バックオフ再試行も失敗しました。しばらくしてから再試行してください。","error.http.413":"DeepSeek リクエストの本文が大きすぎます(413)。入力を短くしてください。","error.http.429":"DeepSeek のレート制限に達しました(429。アカウント上限: Search 100 QPS / Fetch 25 QPS)。バックオフ再試行も失敗しました。しばらくしてから再試行してください。","error.http.5xx":'DeepSeek のサーバーエラーです({status})。バックオフ再試行も失敗しました。一時的に options.mode を "native" にするとネイティブ検索を使えます。',"error.http.other":"DeepSeek リクエストが失敗しました(HTTP {status}){upstream}","error.http.fetchHint":" このページには組み込み webfetch ツールを使えます。","error.parse":"DeepSeek が解析できないレスポンスを返しました(HTTP {status})。しばらくしてから再試行してください。","error.aborted":"DeepSeek リクエストはキャンセルされました。","error.timeout":"DeepSeek リクエストがタイムアウトしました({seconds} 秒)。再試行してください。","error.network":"{base} に接続できません(ネットワークエラー: {message})。ネットワークかプロキシの設定を確認してください。","error.validate.queryEmpty":"検索語が空です。DeepSeek Search には空でない query(1〜128 文字)が必要です。","error.validate.queryLong":"検索語が長すぎます({length} 文字、上限 {max})。短くして再試行してください。","error.validate.urlScheme":"無効な URL:「{url}」。DeepSeek Fetch は http/https の絶対 URL のみ対応します。","error.validate.urlLong":"URL が長すぎます({length} > {max} 文字)。","error.validate.urlHost":"URL にホスト名がありません:「{url}」。","error.validate.urlUserinfo":"URL にユーザー名/パスワード(userinfo)を含められません。","error.validate.urlParse":"URL を解析できません:「{url}」。","error.fetch.truncated":"[DeepSeek Fetch 切り詰め] 元の本文は {raw} 文字で、上限 {max} 文字を超えています。先頭 {max} 文字のみ保持しました。"},ae=["{label} Search {search} Fetch {fetch} {cost}","{label} Search {search} Fetch {fetch} {cost}","{label} {search}S {fetch}F {cost}","{label} {search}S {fetch}F","{label} {search}S"],ne=["残高 {amount}"],se=["DEEPSEEK_API_KEY を設定して再起動","DEEPSEEK_API_KEY を設定","API KEY を設定","KEY 設定"],ie={"state.loading":"正在读取…","state.loadingShort":"正在读取…","state.failed":"读取失败","state.failedShort":"读取失败","state.enabled":"已启用","state.enabledShort":"已启用","state.disabled":"已停用","state.disabledShort":"已停用","state.unpatched":"未接管内置 websearch","state.unpatchedShort":"未接管","state.missingKey":"未配置 API Key","state.missingKeyShort":"缺 API Key","action.disable":"停用","action.enable":"启用","action.release":"交还","action.retry":"重试","panel.tooltip.refresh":"刷新用量","panel.loading":"正在读取用量…","panel.label.on":"停用(交还原生搜索)","panel.label.off":"启用(由 DeepSeek 接管)","panel.label.warn":"交还原生搜索","panel.label.error":"重试","panel.label.idle":"重试","command.open":"DeepSeek: 打开统计面板","command.toggle":"DeepSeek: 启用/停用搜索接管","menu.description":"搜索用量与接管开关","toast.openSessionFirst":"请先打开一个会话","toast.statusMalformed":"状态数据格式不符","toast.modeMalformed":"模式切换结果格式不符","dialog.title":"DeepSeek 搜索","dialog.enable":"启用","dialog.enableDesc":"接管内置 websearch","dialog.disable":"停用","dialog.disableDesc":"把内置 websearch 交还宿主","dialog.refresh":"刷新","dialog.refreshDesc":"重新读取状态与统计","mode.report":"当前搜索后端: {mode}(来源: {source}){suffix}","mode.source.override":"deepseek_mode 开关,已持久化","mode.source.config":"配置文件 options.mode","mode.suffix.patched":" · 内置 websearch 已由 DeepSeek 接管","mode.suffix.notPatched":" · 未找到内置 websearch 工具,实际未能接管","mode.suffix.native":" · 内置 websearch 保持原生","mode.notFound":'未找到内置 {tool} 工具,无法在运行时切换。请改用配置文件把 options.mode 设为 "{mode}"。',"mode.configCleared":"已清除持久化开关,回到配置文件设置({mode})。","mode.persisted":'已持久化:重启 OpenCode 后仍为 {mode}。要恢复配置文件设置,调用 deepseek_mode 并传 mode: "config"。',"mode.statusOnly":"status 只读取当前模式,不做切换。","mode.invalid":"mode 只能是 status / deepseek / native / config,收到:{raw}","progress.fetch":"DeepSeek Fetch 抓取中: {url}","fetch.source":"来源: {url}","usage.reset":"已清零本地统计。","usage.report.title":"DeepSeek 用量(本地统计,按成功请求估算;当前模式 {mode};单价 ¥{price}/1000 次)","usage.report.line":"{label}: 搜索 {search} · 抓取 {fetch} · 空/失 {skipped}(空结果 {empty} / 失败 {failed})· 估算 {cost}","usage.report.note":"说明:空结果与失败请求不计费;DeepSeek 以账号侧账单为准,本统计不含其他机器或其他工具的调用。","usage.label.today":"今日","usage.label.month":"本月","usage.label.total":"累计","usage.summary.line":"搜索 {search} · 抓取 {fetch} · 空/失 {skipped} · {cost}","balance.label":"余额","balance.breakdown":"赠金 {granted} · 充值 {topped}","error.missingKey":'未配置 DeepSeek API Key:请设置环境变量 DEEPSEEK_API_KEY(PowerShell: [Environment]::SetEnvironmentVariable("DEEPSEEK_API_KEY","sk-...","User")),或在插件 options 中写 "apiKey": "{env:DEEPSEEK_API_KEY}",然后重启 opencode。',"error.http.400":"DeepSeek 请求参数无效(400){upstream}。搜索词需 1~128 字符;Fetch 的 URL 需为合法的 http(s) 绝对地址。","error.http.401":"DeepSeek API Key 无效(401){upstream}。请检查 DEEPSEEK_API_KEY 环境变量或 options.apiKey。","error.http.402":"DeepSeek 账户余额不足(402),失败请求不计费。请充值后重试。","error.http.403":"DeepSeek Fetch 被目标站点安全策略拒绝(403){upstream}。{hint}","error.http.404":"DeepSeek 无法提取该链接的正文(404){upstream}。请确认链接可访问。{hint}","error.http.408":"DeepSeek 请求超时(408),已退避重试一次仍失败。请稍后重试。","error.http.413":"DeepSeek 请求体过大(413)。请缩短输入。","error.http.429":"DeepSeek 触发限速(429,账号级 Search 100 QPS / Fetch 25 QPS),已退避重试一次仍失败。请稍后再试。","error.http.5xx":'DeepSeek 服务端错误({status}),已退避重试一次仍失败。可把插件 options.mode 改为 "native" 暂用原生搜索。',"error.http.other":"DeepSeek 请求失败(HTTP {status}){upstream}","error.http.fetchHint":" 可改用内置 webfetch 工具抓取该页面。","error.parse":"DeepSeek 返回了无法解析的响应(HTTP {status})。请稍后重试。","error.aborted":"DeepSeek 请求已取消。","error.timeout":"DeepSeek 请求超时({seconds} 秒),请重试。","error.network":"无法连接 {base}(网络错误:{message})。请检查网络或代理设置。","error.validate.queryEmpty":"搜索词为空:DeepSeek Search 需要非空 query(1~128 字符)。","error.validate.queryLong":"搜索词过长({length} 字符,上限 {max}),请精简后重试。","error.validate.urlScheme":"无效 URL:「{url}」。DeepSeek Fetch 仅支持 http/https 绝对地址。","error.validate.urlLong":"URL 过长({length} > {max} 字符)。","error.validate.urlHost":"URL 缺少主机名:「{url}」。","error.validate.urlUserinfo":"URL 不能包含用户名/密码(userinfo)。","error.validate.urlParse":"无法解析 URL:「{url}」。","error.fetch.truncated":"[DeepSeek Fetch 截断提示] 原文共 {raw} 字符,超出上限 {max},此处仅保留前 {max} 字符。"},le=["{label} Search {search} Fetch {fetch} {cost}","{label} Search {search} Fetch {fetch} {cost}","{label} {search}S {fetch}F {cost}","{label} {search}S {fetch}F","{label} {search}S"],ce=["余额 {amount}"],de=["设置 DEEPSEEK_API_KEY 后重启","设置 API KEY 后重启","设置 API KEY","设置 KEY"],B={en:{messages:Z,usageForms:ee,hintForms:re,balanceForms:te},ja:{messages:oe,usageForms:ae,hintForms:se,balanceForms:ne},"zh-CN":{messages:ie,usageForms:le,hintForms:de,balanceForms:ce}};function T(e){return B[e]}function pe(e){return B[e].messages}function F(e,t={}){return e.replace(/\{(\w+)\}/g,(o,r)=>r in t?String(t[r]):o)}function c(e,t,o){const n=pe(e)[t];return n===void 0?t:F(n,o)}const ue="DeepSeek search/fetch",N="DeepSeek",he=2,fe=1,me=24,R=["today","month","total"],U={today:"usage.label.today",month:"usage.label.month",total:"usage.label.total"};function q(e){const t=e.codePointAt(0)??0;return t===0?0:t>=4352&&t<=4447||t>=11904&&t<=42191||t>=44032&&t<=55203||t>=63744&&t<=64255||t>=65040&&t<=65049||t>=65072&&t<=65135||t>=65280&&t<=65376||t>=65504&&t<=65510||t>=127744&&t<=129791||t>=131072&&t<=262141?2:1}function w(e){let t=0;for(const o of e)t+=q(o);return t}function _(e,t){if(t<1)return"";if(w(e)<=t)return e;let o="",r=0;for(const n of e){const s=q(n);if(r+s>t-1)return`${o}…`;o+=n,r+=s}return o}function ge(e,t,o){const r=e.replace(/\s+/g," ").trim();return _(r||c(o,"state.failed"),t)}function L(e){let o=(Number.isFinite(e)&&e>0?e/1e6:0).toFixed(3);for(;o.endsWith("0")&&o.length>4;)o=o.slice(0,-1);return`¥${o}`}function be(e){const t=R.map(o=>e[o]);return{search:Math.max(...t.map(o=>String(o.search).length)),fetch:Math.max(...t.map(o=>String(o.fetch).length)),cost:Math.max(...t.map(o=>L(o.costMicro).length))}}function ye(e,t,o,r,n){const s=c(n,U[e]),l=String(t.search).padStart(o.search," "),i=String(t.fetch).padStart(o.fetch," "),u=L(t.costMicro).padStart(o.cost," "),b={label:s,search:l,fetch:i,cost:u},m=T(n).usageForms;for(const p of m.slice(0,-1)){const S=F(p,b);if(w(S)<=r)return S}const f=`${s} ${u}`;if(w(f)<=r)return f;const h=m[m.length-1];if(h!==void 0){const p=F(h,b);if(w(p)<=r)return p}}function Se(e,t,o,r){const n=c(r,U.total),s=L(e.costMicro),l=[`${n} ${s.padStart(t.cost," ")}`,`${n} ${String(e.search).padStart(t.search," ")}S`,s];for(const i of l)if(w(i)<=o)return i;return _(s,o)}function ve(e,t,o){if(t===Number.POSITIVE_INFINITY)return[`${c(o,"usage.label.today")} ${e.today}`,`${c(o,"usage.label.month")} ${e.month}`,`${c(o,"usage.label.total")} ${e.total}`];const r=e.counts;if(r){const n=be(r),s=[];for(const l of R){const i=ye(l,r[l],n,t,o);if(i===void 0)return[Se(r.total,n,t,o)];s.push(i)}return s}return R.map(n=>Ee(n,e[n],t,o))}const O={CNY:"¥",USD:"$"};function ke(e,t,o){const r=e.isAvailable?"":"!",n=`${O[e.currency]??""}${e.totalBalance}`;for(const s of T(o).balanceForms){const l=`${r}${F(s,{amount:n})}`;if(w(l)<=t)return l}}function Ee(e,t,o,r){const n=c(r,U[e]),s=`${n} ${t}`;if(w(s)<=o)return s;const l=t.split(" · ").filter(u=>!u.startsWith("¥")&&!u.startsWith("空/失")),i=[`${n} ${l.join(" ")}`.trimEnd(),`${n} ${l[0]??""}`.trimEnd(),n];for(const u of i)if(w(u)<=o)return u;return _(n,o)}function we(e,t){const o=`[${e}]`;if(t===Number.POSITIVE_INFINITY||w(o)<=t)return o;const r=t-2;return r>=1?`[${_(e,r)}]`:_(o,t)}function H(e,t,o,r,n=!0){const s=(b,m)=>{if(o===Number.POSITIVE_INFINITY)return Number.POSITIVE_INFINITY;const f=b?he:0,h=r===void 0||m?0:r+fe;return Math.max(0,o-f-h)},l=[`${ue} · ${e}`,`${N} · ${e}`,`${N} · ${t}`,e,t],i=r===void 0||!n?[!1]:[!1,!0];for(const b of l)for(const m of i)for(const f of[!0,!1])if(w(b)<=s(f,m))return{title:b,glyph:f,actionBelow:m};const u=n&&r!==void 0;return{title:_(t,s(!1,u)),glyph:!1,actionBelow:u}}function De(e,t){const o=T(t).hintForms;if(e===Number.POSITIVE_INFINITY)return o[0]??"";for(const r of o)if(w(r)<=e)return r;return _(o[o.length-1]??"",e)}function x(e,t,o,r,n=!0){const s=we(o,r);return{...H(e,t,r,w(s),n),action:s}}function Pe(e){const{status:t,error:o,language:r}=e,n=e.maxColumns??me,s=t===void 0?[]:ve(t,n,r),l=t!==void 0&&(t.patched&&t.hasKey||t.mode==="native")&&t.balance!==void 0?ke(t.balance,n,r):void 0,i=l===void 0?s:[...s,l],u=l===void 0;if(t===void 0){const m=c(r,"action.retry");return o===void 0||o.trim()===""?{...x(c(r,"state.loading"),c(r,"state.loadingShort"),m,n),hint:void 0,clickable:!0,lines:i,tone:"idle"}:{...x(c(r,"state.failed"),c(r,"state.failedShort"),m,n),hint:void 0,clickable:!0,lines:[ge(o,n,r)],tone:"error"}}if(t.patched&&t.hasKey)return{...x(c(r,"state.enabled"),c(r,"state.enabledShort"),c(r,"action.disable"),n,u),hint:void 0,clickable:!0,lines:i,tone:"on"};if(!t.patched&&t.mode==="deepseek")return{...x(c(r,"state.unpatched"),c(r,"state.unpatchedShort"),c(r,"action.release"),n,u),hint:void 0,clickable:!0,lines:i,tone:"warn"};if(t.mode==="native")return{...x(c(r,"state.disabled"),c(r,"state.disabledShort"),c(r,"action.enable"),n,u),hint:void 0,clickable:!0,lines:i,tone:"off"};const b=De(n,r);return{...H(c(r,"state.missingKey"),c(r,"state.missingKeyShort"),n,void 0),action:b,hint:b,clickable:!1,lines:i,tone:"error"}}var M=a.template("<span>"),_e=a.template("<span style=opacity:0.6;font-weight:400>"),xe=a.template("<span style=opacity:0.3;font-weight:400> · "),Ie=a.template("<code style=font-size:0.94em>DEEPSEEK_API_KEY"),Ke=a.template('<p style="margin:16px 0 0;font-size:12px;line-height:1.6;opacity:0.68;overflow-wrap:anywhere">'),Fe=a.template("<div style=margin-top:18px>"),$e=a.template('<div style="display:flex;flex-direction:column;height:100%;min-width:0;box-sizing:border-box;padding:16px 16px 24px;overflow-y:auto;overflow-x:hidden"><header style=display:flex;align-items:flex-start;gap:8px><span aria-hidden=true style=flex-shrink:0;font-size:12px;line-height:20px;width:12px;text-align:center;font-weight:700></span><h2 aria-live=polite style=flex:1;min-width:0;margin:0;font-size:13px;font-weight:600;line-height:20px;overflow-wrap:anywhere>'),Ae=a.template('<p style="margin:14px 0 0;font-size:12px;line-height:1.6;opacity:0.7;overflow-wrap:anywhere">'),Re=a.template("<div style=display:flex;align-items:center;gap:8px;margin-top:14px;opacity:0.7><span style=font-size:12px>"),Te=a.template('<dl style="margin:16px 0 0">'),Ue=a.template('<div style="display:flex;align-items:baseline;gap:12px;padding:10px 0"><dt style=flex-shrink:0;font-size:11px;line-height:1.9;opacity:0.62></dt><dd style=margin:0;margin-left:auto;min-width:0;display:flex;flex-wrap:wrap;justify-content:flex-end;column-gap:10px;row-gap:2px;font-size:13px;line-height:1.55;font-variant-numeric:tabular-nums;text-align:right>'),Le=a.template("<span style=min-width:0;overflow-wrap:anywhere>"),Ye=a.template("<dd style=margin:0;margin-left:auto;font-size:14px;line-height:1.55;font-variant-numeric:tabular-nums;text-align:right;font-weight:500;flex-shrink:0>"),Ne=a.template('<div style="display:flex;align-items:baseline;gap:12px;padding:10px 0 4px"><dt style=flex-shrink:0;font-size:11px;line-height:1.9;opacity:0.62>');const Me={on:"●",off:"○",warn:"◐",error:"!",idle:"…"},Ce={dark:{on:"#63d19a",warn:"#e0ad4f",error:"#ee7b74",off:"#8b9198",idle:"#8b9198",rule:"rgba(255,255,255,0.14)"},light:{on:"#1e7a4c",warn:"#8a5f08",error:"#b03931",off:"#6b7076",idle:"#6b7076",rule:"rgba(0,0,0,0.13)"}},Be={on:"panel.label.on",off:"panel.label.off",warn:"panel.label.warn",error:"panel.label.error",idle:"panel.label.idle"},qe={on:"arrow-undo-down",off:"globe",warn:"arrow-undo-down",error:"refresh",idle:"refresh"};function Oe(e,t){const o=e.indexOf(" · ");return o<0?(()=>{var r=M();return a.setStyleProperty(r,"color",t),a.insert(r,e),r})():[(()=>{var r=_e();return a.insert(r,()=>e.slice(0,o)),r})(),xe(),(()=>{var r=M();return a.setStyleProperty(r,"color",t),a.insert(r,()=>e.slice(o+3)),r})()]}function He(e){const t=e.indexOf(" ");return t<0?{label:e,chunks:[]}:{label:e.slice(0,t),chunks:e.slice(t+1).split(" · ")}}function ze(e,t){const o="DEEPSEEK_API_KEY",r=e.indexOf(o);return r<0?e:[a.memo(()=>e.slice(0,r)),(()=>{var n=Ie();return a.setStyleProperty(n,"font-family",t),n})(),a.memo(()=>e.slice(r+o.length))]}function je(e,t){const o=Number.parseFloat(e.grantedBalance)>0,r=Number.parseFloat(e.toppedUpBalance)>0;if(!o||!r)return;const n=c(t,"balance.breakdown",{granted:e.grantedBalance,topped:e.toppedUpBalance});return e.currency==="USD"?`${n} (USD)`:n}function Qe(e){const t=I.usePlugin(),o=()=>e.status?.language??"en",r=k.createMemo(()=>Pe({status:e.status,error:e.error,maxColumns:1/0,language:o()})),n=k.createMemo(()=>Ce[t.appearance.mode()]),s=()=>t.appearance.font("mono"),l=k.createMemo(()=>{const i=e.status?.balance;if(!(i===void 0||r().tone!=="on"&&r().tone!=="off"))return i});return k.onMount(()=>{const i=setInterval(()=>e.onRefresh(),1e4);k.onCleanup(()=>clearInterval(i))}),(()=>{var i=$e(),u=i.firstChild,b=u.firstChild,m=b.nextSibling;return a.insert(b,()=>Me[r().tone]),a.insert(m,()=>Oe(r().title,n()[r().tone])),a.insert(u,a.createComponent(t.ui.Tooltip,{get value(){return c(o(),"panel.tooltip.refresh")},placement:"bottom",get children(){return a.createComponent(t.ui.IconButton,{icon:"refresh",get label(){return c(o(),"panel.tooltip.refresh")},size:"small",onClick:()=>e.onRefresh(!0)})}}),null),a.insert(i,(()=>{var f=a.memo(()=>e.status===void 0);return()=>f()?a.memo(()=>r().lines.length>0)()?(()=>{var h=Ae();return a.insert(h,()=>r().lines[0]),h})():(()=>{var h=Re(),p=h.firstChild;return a.insert(h,a.createComponent(t.ui.Loader,{size:"small"}),p),a.insert(p,()=>c(o(),"panel.loading")),h})():(()=>{var h=Te();return a.insert(h,a.createComponent(k.For,{get each(){return a.memo(()=>l()!==void 0)()?r().lines.slice(0,-1):r().lines},children:(p,S)=>{const d=He(p);return(()=>{var g=Ue(),v=g.firstChild,D=v.nextSibling;return a.insert(v,()=>d.label),a.insert(D,a.createComponent(k.For,{get each(){return d.chunks},children:y=>{const E=/^[¥$]/.test(y);return(()=>{var P=Le();return a.setStyleProperty(P,"font-weight",E?500:400),a.setStyleProperty(P,"opacity",E?1:.8),a.setStyleProperty(P,"flex-shrink",E?0:void 0),a.insert(P,y),P})()}})),a.effect(y=>{var E=S()>0?`1px solid ${n().rule}`:void 0,P=/^[\x20-\x7E]+$/.test(d.label)?"0.08em":void 0,Y=s();return E!==y.e&&a.setStyleProperty(g,"border-top",y.e=E),P!==y.t&&a.setStyleProperty(v,"letter-spacing",y.t=P),Y!==y.a&&a.setStyleProperty(D,"font-family",y.a=Y),y},{e:void 0,t:void 0,a:void 0}),g})()}}),null),a.insert(h,a.createComponent(k.Show,{get when(){return l()},keyed:!0,children:p=>{const S=c(o(),"balance.label"),d=je(p,o()),g=(()=>{var v=Ye();return a.insert(v,()=>O[p.currency]??"",null),a.insert(v,()=>p.totalBalance,null),a.effect(D=>{var y=s(),E=p.isAvailable?void 0:n().warn;return y!==D.e&&a.setStyleProperty(v,"font-family",D.e=y),E!==D.t&&a.setStyleProperty(v,"color",D.t=E),D},{e:void 0,t:void 0}),v})();return(()=>{var v=Ne(),D=v.firstChild;return a.insert(D,S),a.insert(v,d!==void 0?a.createComponent(t.ui.Tooltip,{value:d,placement:"bottom",children:g}):g,null),a.effect(y=>{var E=`1px solid ${n().rule}`,P=/^[\x20-\x7E]+$/.test(S)?"0.08em":void 0;return E!==y.e&&a.setStyleProperty(v,"border-top",y.e=E),P!==y.t&&a.setStyleProperty(D,"letter-spacing",y.t=P),y},{e:void 0,t:void 0}),v})()}}),null),a.effect(p=>{var S=`1px solid ${n().rule}`,d=`1px solid ${n().rule}`;return S!==p.e&&a.setStyleProperty(h,"border-top",p.e=S),d!==p.t&&a.setStyleProperty(h,"border-bottom",p.t=d),p},{e:void 0,t:void 0}),h})()})(),null),a.insert(i,a.createComponent(k.Show,{get when(){return!r().clickable},get children(){var f=Ke();return a.insert(f,()=>ze(r().action,s())),f}}),null),a.insert(i,a.createComponent(k.Show,{get when(){return a.memo(()=>!!r().clickable)()&&r().tone!=="idle"},get children(){var f=Fe();return a.insert(f,a.createComponent(t.ui.Button,{get variant(){return r().tone==="off"||r().tone==="error"?"contrast":"neutral"},get icon(){return qe[r().tone]},get disabled(){return e.busy},get busy(){return e.busy},get onClick(){return e.onToggle},get children(){return c(o(),Be[r().tone])}})),f}}),null),a.effect(f=>{var h=e.busy||void 0,p=n()[r().tone];return h!==f.e&&a.setAttribute(i,"aria-busy",f.e=h),p!==f.t&&a.setStyleProperty(b,"color",f.t=p),f},{e:void 0,t:void 0}),i})()}const C={id:"main",title:"DeepSeek"},Ve=80,Ge=I.Plugin.define({setup(e){const t=e.client.rpc(V),[o,r]=k.createSignal(),[n,s]=k.createSignal(),[l,i]=k.createSignal(!1),u=()=>o()?.language??"en",b=d=>(d instanceof Error?d.message:String(d)).slice(0,Ve);async function m(d=!1){try{const g=J(await t.status(d?{force:!0}:{}));g?(r(g),s(void 0)):s(c(u(),"toast.statusMalformed"))}catch(g){s(b(g))}}async function f(d){i(!0);try{const g=X(await t.setMode({mode:d}));g?g.ok||e.toast.show({title:"DeepSeek",description:g.message,variant:"error"}):e.toast.show({title:"DeepSeek",description:c(u(),"toast.modeMalformed"),variant:"error"})}catch(g){e.toast.show({title:"DeepSeek",description:b(g),variant:"error"})}finally{await m(),i(!1)}}function h(){const d=o();if(!d||n()){m(!0);return}f(d.mode==="deepseek"?"native":"deepseek")}e.add(I.Panel,{id:"main",region:"side",mobile:{title:"DeepSeek",order:50,kind:"menu",icon:"globe"},list:d=>d.open.includes(C.id)?[C]:[],render:()=>a.createComponent(Qe,{get status(){return o()},get error(){return n()},get busy(){return l()},onToggle:h,onRefresh:d=>{m(d)}})});const p=()=>{const d=e.sessions.current();if(!d){e.toast.show({title:"DeepSeek",description:c(u(),"toast.openSessionFirst")});return}e.layout.open(`${e.id}:main`,d)};e.add(I.Command,()=>({id:"open",title:c(u(),"command.open"),section:"session",run:p})),e.add(I.MenuItem,()=>{const d=e.sessions.current(),g=d!==void 0&&e.layout.stored(d).includes("main");return{menu:"session.panel",id:"open",title:"DeepSeek",description:c(u(),"menu.description"),icon:"globe",order:60,opened:g,run:p}});const S=t.events.on("changed",()=>{m()});return m(),S}});exports.default=Ge;
package/package.json CHANGED
@@ -1,88 +1,88 @@
1
1
  {
2
- "name": "@qthinker/opencode-deepseek-search",
3
- "version": "0.3.0",
4
- "description": "DeepSeek Search \u0026 Fetch for OpenCode V2: a switchable websearch backend, clean Markdown fetch, local usage/cost stats, and TUI + desktop sidebars",
5
- "type": "module",
6
- "license": "MIT",
7
- "author": "QThinker",
8
- "homepage": "https://github.com/QThinkerJR/opencode-deepseek-search#readme",
9
- "repository": {
10
- "type": "git",
11
- "url": "git+https://github.com/QThinkerJR/opencode-deepseek-search.git"
12
- },
13
- "bugs": {
14
- "url": "https://github.com/QThinkerJR/opencode-deepseek-search/issues"
15
- },
16
- "keywords": [
17
- "opencode",
18
- "opencode-plugin",
19
- "deepseek",
20
- "websearch",
21
- "search",
22
- "fetch",
23
- "usage",
24
- "cost",
25
- "tui",
26
- "desktop",
27
- "sidebar",
28
- "i18n"
29
- ],
30
- "files": [
31
- "index.ts",
32
- "tui.ts",
33
- "gui-experimental.cjs",
34
- "src",
35
- "README.md",
36
- "CHANGELOG.md",
37
- "LICENSE"
38
- ],
39
- "engines": {
40
- "node": "\u003e=22.18"
41
- },
42
- "scripts": {
43
- "typecheck": "tsc --noEmit \u0026\u0026 tsc --noEmit -p tsconfig.gui.json",
44
- "test": "node --test test/*.test.ts",
45
- "build:gui": "vite build",
46
- "smoke": "node --experimental-strip-types scripts/smoke.ts",
47
- "prepublishOnly": "npm run typecheck \u0026\u0026 npm test \u0026\u0026 npm run build:gui"
48
- },
49
- "peerDependencies": {
50
- "@opencode/plugin": "\u003e=2.2.5",
51
- "@opencode/theme": "\u003e=2.2.5",
52
- "@opentui/core": "\u003e=0.8.0",
53
- "@opentui/solid": "\u003e=0.8.0",
54
- "solid-js": "\u003e=1.9.0"
55
- },
56
- "peerDependenciesMeta": {
57
- "@opencode/plugin": {
58
- "optional": true
59
- },
60
- "@opencode/theme": {
61
- "optional": true
62
- },
63
- "@opentui/core": {
64
- "optional": true
65
- },
66
- "@opentui/solid": {
67
- "optional": true
68
- },
69
- "solid-js": {
70
- "optional": true
71
- }
72
- },
73
- "devDependencies": {
74
- "@opencode/plugin": "2.2.5",
75
- "@opencode/theme": "2.2.5",
76
- "@opentui/core": "^0.8.0",
77
- "@opentui/solid": "^0.8.0",
78
- "@types/node": "^24.0.0",
79
- "solid-js": "^1.9.0",
80
- "typescript": "^5.9.0",
81
- "vite": "^7.3.7",
82
- "vite-plugin-solid": "^2.11.10"
83
- },
84
- "main": "./index.ts",
85
- "publishConfig": {
86
- "access": "public"
87
- }
2
+ "name": "@qthinker/opencode-deepseek-search",
3
+ "version": "0.3.2",
4
+ "description": "DeepSeek Search & Fetch for OpenCode V2: a switchable websearch backend, clean Markdown fetch, local usage/cost stats, and TUI + desktop sidebars",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "QThinker",
8
+ "homepage": "https://github.com/QThinkerJR/opencode-deepseek-search#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/QThinkerJR/opencode-deepseek-search.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/QThinkerJR/opencode-deepseek-search/issues"
15
+ },
16
+ "keywords": [
17
+ "opencode",
18
+ "opencode-plugin",
19
+ "deepseek",
20
+ "websearch",
21
+ "search",
22
+ "fetch",
23
+ "usage",
24
+ "cost",
25
+ "tui",
26
+ "desktop",
27
+ "sidebar",
28
+ "i18n"
29
+ ],
30
+ "files": [
31
+ "index.ts",
32
+ "tui.ts",
33
+ "gui-experimental.cjs",
34
+ "src",
35
+ "README.md",
36
+ "CHANGELOG.md",
37
+ "LICENSE"
38
+ ],
39
+ "engines": {
40
+ "node": ">=22.18"
41
+ },
42
+ "scripts": {
43
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.gui.json",
44
+ "test": "node --test test/*.test.ts",
45
+ "build:gui": "vite build",
46
+ "smoke": "node --experimental-strip-types scripts/smoke.ts",
47
+ "prepublishOnly": "npm run typecheck && npm test && npm run build:gui"
48
+ },
49
+ "peerDependencies": {
50
+ "@opencode/plugin": ">=2.2.5",
51
+ "@opencode/theme": ">=2.2.5",
52
+ "@opentui/core": ">=0.8.0",
53
+ "@opentui/solid": ">=0.8.0",
54
+ "solid-js": ">=1.9.0"
55
+ },
56
+ "peerDependenciesMeta": {
57
+ "@opencode/plugin": {
58
+ "optional": true
59
+ },
60
+ "@opencode/theme": {
61
+ "optional": true
62
+ },
63
+ "@opentui/core": {
64
+ "optional": true
65
+ },
66
+ "@opentui/solid": {
67
+ "optional": true
68
+ },
69
+ "solid-js": {
70
+ "optional": true
71
+ }
72
+ },
73
+ "devDependencies": {
74
+ "@opencode/plugin": "2.2.5",
75
+ "@opencode/theme": "2.2.5",
76
+ "@opentui/core": "^0.8.0",
77
+ "@opentui/solid": "^0.8.0",
78
+ "@types/node": "^24.0.0",
79
+ "solid-js": "^1.9.0",
80
+ "typescript": "^5.9.0",
81
+ "vite": "^7.3.7",
82
+ "vite-plugin-solid": "^2.11.10"
83
+ },
84
+ "main": "./index.ts",
85
+ "publishConfig": {
86
+ "access": "public"
87
+ }
88
88
  }
package/src/deepseek.ts CHANGED
@@ -6,14 +6,18 @@
6
6
  // network access.
7
7
 
8
8
  import { t } from "./i18n/index.ts"
9
- import type { Language } from "./rpc.ts"
9
+ import type { BalanceRead, Language } from "./rpc.ts"
10
10
 
11
11
  export const API_BASE = "https://api.deepseek.com"
12
12
  export const SEARCH_PATH = "/v1/tools/search"
13
13
  export const FETCH_PATH = "/v1/tools/fetch"
14
+ // Account balance endpoint, per the official docs:
15
+ // https://api-docs.deepseek.com/zh-cn/api/get-user-balance
16
+ export const BALANCE_PATH = "/user/balance"
14
17
 
15
18
  export const SEARCH_TIMEOUT_MS = 15_000
16
19
  export const FETCH_TIMEOUT_MS = 30_000
20
+ export const BALANCE_TIMEOUT_MS = 10_000
17
21
  const RETRY_DELAY_MS = 1_000
18
22
  const RETRY_DELAY_MAX_MS = 5_000
19
23
  export const MAX_QUERY_CHARS = 128
@@ -128,7 +132,8 @@ function retryAfterMs(headers: Headers): number {
128
132
 
129
133
  // ---------- shared HTTP core: timeout, signal passthrough, single retry ----------
130
134
 
131
- export async function postJson(
135
+ async function requestJson(
136
+ method: "POST" | "GET",
132
137
  path: string,
133
138
  body: unknown,
134
139
  apiKey: string,
@@ -140,10 +145,15 @@ export async function postJson(
140
145
  for (let attempt = 1; ; attempt++) {
141
146
  const merged = mergeAbort(signal, timeoutMs)
142
147
  try {
148
+ // GET reads a resource, so it sends no body and no content-type header.
149
+ const headers: Record<string, string> =
150
+ method === "POST"
151
+ ? { "content-type": "application/json", authorization: `Bearer ${apiKey}` }
152
+ : { authorization: `Bearer ${apiKey}` }
143
153
  const res = await fetch(`${API_BASE}${path}`, {
144
- method: "POST",
145
- headers: { "content-type": "application/json", authorization: `Bearer ${apiKey}` },
146
- body: JSON.stringify(body),
154
+ method,
155
+ headers,
156
+ body: method === "POST" ? JSON.stringify(body) : undefined,
147
157
  signal: merged.signal,
148
158
  })
149
159
  if (res.ok) {
@@ -179,6 +189,27 @@ export async function postJson(
179
189
  }
180
190
  }
181
191
 
192
+ export function postJson(
193
+ path: string,
194
+ body: unknown,
195
+ apiKey: string,
196
+ timeoutMs: number,
197
+ language: Language,
198
+ signal?: AbortSignal,
199
+ ): Promise<unknown> {
200
+ return requestJson("POST", path, body, apiKey, timeoutMs, language, signal)
201
+ }
202
+
203
+ export function getJson(
204
+ path: string,
205
+ apiKey: string,
206
+ timeoutMs: number,
207
+ language: Language,
208
+ signal?: AbortSignal,
209
+ ): Promise<unknown> {
210
+ return requestJson("GET", path, undefined, apiKey, timeoutMs, language, signal)
211
+ }
212
+
182
213
  // ---------- DeepSeek Search -> WebSearch.Result[] ----------
183
214
 
184
215
  interface SearchRow {
@@ -286,6 +317,52 @@ export async function deepseekFetch(
286
317
  return mapFetchResponse(data, url, maxChars, language)
287
318
  }
288
319
 
320
+ // ---------- DeepSeek Balance -> BalanceRead ----------
321
+
322
+ interface BalanceInfo {
323
+ currency?: unknown
324
+ total_balance?: unknown
325
+ granted_balance?: unknown
326
+ topped_up_balance?: unknown
327
+ }
328
+
329
+ function balanceText(value: unknown): string | undefined {
330
+ return typeof value === "string" && value.length > 0 ? value : undefined
331
+ }
332
+
333
+ // `/user/balance` lists one entry per currency. The plugin bills in CNY, so that
334
+ // entry wins when present; anything else is accepted only as a fallback.
335
+ export function mapBalance(data: unknown): BalanceRead | undefined {
336
+ if (typeof data !== "object" || data === null) return undefined
337
+ const raw = data as { is_available?: unknown; balance_infos?: unknown }
338
+ const isAvailable = raw.is_available
339
+ if (typeof isAvailable !== "boolean") return undefined
340
+ const infos = raw.balance_infos
341
+ if (!Array.isArray(infos) || infos.length === 0) return undefined
342
+ const rows = infos as Array<BalanceInfo | null | undefined>
343
+ const entry = rows.find((row) => row?.currency === "CNY") ?? rows[0]
344
+ const currency = entry?.currency
345
+ if (currency !== "CNY" && currency !== "USD") return undefined
346
+ const totalBalance = balanceText(entry?.total_balance)
347
+ const grantedBalance = balanceText(entry?.granted_balance)
348
+ const toppedUpBalance = balanceText(entry?.topped_up_balance)
349
+ if (!totalBalance || !grantedBalance || !toppedUpBalance) return undefined
350
+ return { isAvailable, currency, totalBalance, grantedBalance, toppedUpBalance }
351
+ }
352
+
353
+ export async function deepseekBalance(
354
+ apiKey: string,
355
+ language: Language,
356
+ signal?: AbortSignal,
357
+ ): Promise<BalanceRead> {
358
+ const data = await getJson(BALANCE_PATH, apiKey, BALANCE_TIMEOUT_MS, language, signal)
359
+ const balance = mapBalance(data)
360
+ // No localizable message on purpose: the balance cache swallows this error, so
361
+ // it is a diagnostic string rather than user-facing copy.
362
+ if (!balance) throw new Error("DeepSeek balance response is missing required fields")
363
+ return balance
364
+ }
365
+
289
366
  export function validateQuery(query: string, language: Language): string | undefined {
290
367
  if (query.length === 0) return t(language, "error.validate.queryEmpty")
291
368
  if (query.length > MAX_QUERY_CHARS)