@qthinker/opencode-deepseek-search 0.2.1 → 0.3.0
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 +18 -0
- package/README.md +26 -10
- package/gui-experimental.cjs +1 -1
- package/package.json +3 -2
- package/src/deepseek.ts +65 -47
- package/src/gui/index.tsx +14 -7
- package/src/gui/panel.tsx +13 -8
- package/src/i18n/en.ts +151 -0
- package/src/i18n/index.ts +87 -0
- package/src/i18n/ja.ts +138 -0
- package/src/i18n/zh.ts +133 -0
- package/src/index.ts +58 -41
- package/src/rpc.ts +34 -2
- package/src/status-view.ts +59 -51
- package/src/tui/card.ts +4 -1
- package/src/tui/main.ts +16 -8
- package/src/usage.ts +32 -9
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,24 @@
|
|
|
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.0] - 2026-11-02
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **UI languages `en` / `ja` / `zh-CN`**: the server resolves the language once (`options.language` > `LC_ALL`/`LANG` >
|
|
11
|
+
English) and carries it to both sidebars over RPC, so tool output, the TUI card, the desktop panel, commands, menus
|
|
12
|
+
and dialogs always speak the same language. Every language owns its narrow-width ladder; the Japanese copy is new and
|
|
13
|
+
the Chinese copy keeps the previous wording.
|
|
14
|
+
- `options.language` (`"en" | "ja" | "zh-CN"`, unset = auto). An unknown value logs a warning and falls back to
|
|
15
|
+
auto-detection instead of blocking the plugin.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **The default UI language is now English.** To keep the previous Chinese interface, set `"language": "zh-CN"` in the
|
|
20
|
+
plugin options; the sidebars pick it up on their next status read (a restart always works).
|
|
21
|
+
- Console diagnostics are English in every language, and the `deepseek_fetch` Markdown header follows the UI language
|
|
22
|
+
instead of a hardcoded Chinese `来源:`.
|
|
23
|
+
|
|
6
24
|
## [0.2.1] - 2026-11-02
|
|
7
25
|
|
|
8
26
|
### Changed
|
package/README.md
CHANGED
|
@@ -45,7 +45,11 @@ Then add the plugin to `opencode.jsonc`:
|
|
|
45
45
|
}
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
> **
|
|
48
|
+
> **One entry is enough.** A plugin listed in `opencode.json(c)` is enabled for the server **and** for both sidebars:
|
|
49
|
+
> the TUI and the desktop load the corresponding half of every active plugin that ships one. Verified: with the plugin
|
|
50
|
+
> listed only in the global `~/.config/opencode/opencode.jsonc`, both the TUI card and the desktop panel appear.
|
|
51
|
+
> `cli.json` (`"plugins": [...]`) is only for **TUI-only** plugins — packages without a server half, such as
|
|
52
|
+
> `opencode-go-usage-tui`. Keep the entry in the **global** `opencode.jsonc` so the desktop shows it in every window.
|
|
49
53
|
|
|
50
54
|
With options (recommended — this is where `mode` and friends live):
|
|
51
55
|
|
|
@@ -56,6 +60,8 @@ With options (recommended — this is where `mode` and friends live):
|
|
|
56
60
|
"package": "@qthinker/opencode-deepseek-search",
|
|
57
61
|
"options": {
|
|
58
62
|
"mode": "deepseek",
|
|
63
|
+
// "en" | "ja" | "zh-CN"; omit it to follow LC_ALL/LANG, then English
|
|
64
|
+
"language": "en",
|
|
59
65
|
"count": 5,
|
|
60
66
|
"apiKey": "{env:DEEPSEEK_API_KEY}"
|
|
61
67
|
}
|
|
@@ -86,11 +92,21 @@ The other two halves are clients: restart the **TUI**, and reload the **desktop
|
|
|
86
92
|
| Option | Type | Default | Meaning |
|
|
87
93
|
|---|---|---|---|
|
|
88
94
|
| `mode` | `"deepseek" \| "native"` | `deepseek` | Default backend for `websearch`; a persisted `deepseek_mode` switch overrides it |
|
|
95
|
+
| `language` | `"en" \| "ja" \| "zh-CN"` | auto | UI language for the sidebars, the desktop panel, `deepseek_*` tool output and errors. Auto reads `LC_ALL`, then `LANG`, then English |
|
|
89
96
|
| `apiKey` | string | `DEEPSEEK_API_KEY` | Usually `"{env:DEEPSEEK_API_KEY}"`; unset env becomes `""` |
|
|
90
97
|
| `count` | 1–50 | `5` | Results per search (does not change the price) |
|
|
91
98
|
| `fetchMaxChars` | 500–200000 | `30000` | Truncation limit for `deepseek_fetch`, with an explicit notice |
|
|
92
99
|
| `pricePerThousand` | 0–10000 | `5` | CNY per 1000 requests (integer), used only for the local cost estimate |
|
|
93
100
|
|
|
101
|
+
**UI languages.** The plugin ships `en`, `ja` and `zh-CN`; English is the default and the fallback. The server resolves
|
|
102
|
+
the language once — `options.language`, else the `LC_ALL`/`LANG` prefix (`ja*` → `ja`, `zh*` → `zh-CN`), else English —
|
|
103
|
+
and the same value drives the tool output and both sidebars, so they can never disagree. An unknown value logs a
|
|
104
|
+
warning and falls back to auto-detection instead of failing. The change reaches the sidebars with their next status
|
|
105
|
+
read (saving the config re-runs the server half; a restart always works). Model-facing text stays English in every
|
|
106
|
+
language — tool descriptions, the `## [title](url)` result format, `Published:` and the empty-result notice — so the
|
|
107
|
+
rerouted `websearch` keeps the built-in tool's shape. Docs stay English and Chinese: the Japanese UI does not add a
|
|
108
|
+
Japanese README.
|
|
109
|
+
|
|
94
110
|
## Tools
|
|
95
111
|
|
|
96
112
|
| Tool | What it does |
|
|
@@ -110,7 +126,7 @@ and `need_content` is never requested.
|
|
|
110
126
|
|
|
111
127
|
| Way | How | Persisted | Use it for |
|
|
112
128
|
|---|---|---|---|
|
|
113
|
-
| Sidebar | Click the action on the TUI card (`[停用]` / `[启用]`), or the panel's button | Yes (plugin storage `mode/v1`) | While you are working |
|
|
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 |
|
|
114
130
|
| Config | Edit `options.mode`; applies on save | Yes (in the config file) | The long-term default |
|
|
115
131
|
| Tool | Ask the model to call `deepseek_mode`, or call it yourself | Yes (plugin storage `mode/v1`) | Switching mid-session without touching files |
|
|
116
132
|
| Script | `.\tools\switch-mode.ps1 native` | Yes (the script edits the config) | Quick switch from a terminal, no editor |
|
|
@@ -155,8 +171,8 @@ The plugin also shows up where you work, and can be switched without the model o
|
|
|
155
171
|
|
|
156
172
|
| Surface | What you get | How to switch |
|
|
157
173
|
|---|---|---|
|
|
158
|
-
| TUI | A DeepSeek block appended to the session sidebar: state, then
|
|
159
|
-
| Desktop | A **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:
|
|
174
|
+
| TUI | A DeepSeek block appended to the session sidebar: state, then Today / Month / Total rows (`Search` · `Fetch` · `¥` estimate) in the resolved UI language; it degrades to abbreviations and then to fewer columns as the sidebar narrows | Click the action at the right of the title, or run the `/deepseek` slash command for a menu |
|
|
175
|
+
| Desktop | A **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: Open usage panel` command (localized) | The panel's button, plus a refresh control |
|
|
160
176
|
|
|
161
177
|
- Both surfaces read the same ledger and the same stored switch as `deepseek_usage` / `deepseek_mode`: a single
|
|
162
178
|
server-side source, reached over the plugin's own RPC, so the numbers never diverge between TUI and desktop.
|
|
@@ -164,8 +180,8 @@ The plugin also shows up where you work, and can be switched without the model o
|
|
|
164
180
|
It is not an uninstall: `deepseek_fetch`, `deepseek_usage` and `deepseek_mode` stay registered, and the state is
|
|
165
181
|
persisted in `mode/v1` like any other switch.
|
|
166
182
|
- The action row always says what a click does. When no API key is configured there is no action to take, so it shows
|
|
167
|
-
the fix instead (`DEEPSEEK_API_KEY`); when the built-in tool could not be patched it reports
|
|
168
|
-
pretending to be enabled.
|
|
183
|
+
the fix instead (`DEEPSEEK_API_KEY`); when the built-in tool could not be patched it reports *not taken over* rather
|
|
184
|
+
than pretending to be enabled.
|
|
169
185
|
- Stats refresh on a `changed` event emitted after every tally and every switch, with a 10-second reread as a
|
|
170
186
|
fallback in both surfaces.
|
|
171
187
|
|
|
@@ -212,15 +228,15 @@ contain text that tries to steer the model.
|
|
|
212
228
|
|---|---|
|
|
213
229
|
| Unsure which backend search uses | Call `deepseek_mode` — it reports the backend **and** its source |
|
|
214
230
|
| Config edit seems ignored | A persisted switch is present (the source says so); clear it with `deepseek_mode({"mode": "config"})` |
|
|
215
|
-
| `未配置 DeepSeek API Key` (missing
|
|
231
|
+
| `未配置 DeepSeek API Key` (zh-CN UI; `API key missing` elsewhere) | Set `DEEPSEEK_API_KEY`, restart the OpenCode service once so the new process inherits it |
|
|
216
232
|
| `401` invalid key | Check the key and that `apiKey`/env points at it |
|
|
217
233
|
| `402` insufficient balance | Top up; failed requests are not billed |
|
|
218
234
|
| `429` rate limited | Backed off once already; retry later |
|
|
219
235
|
| `403` / `404` on fetch | The site blocks extraction or the page has no readable text; try the built-in `webfetch` |
|
|
220
236
|
| Search still hits the native backend | `mode` or the persisted switch is `native`; if both are `deepseek`, the built-in `websearch` tool was not found — use config `"websearch": { "provider": "deepseek" }` |
|
|
221
|
-
| TUI sidebar has no DeepSeek card |
|
|
237
|
+
| TUI sidebar has no DeepSeek card | Check `opencode plugin list`: the card comes from the **active server plugin**, so `opencode.json(c)` is what matters — `cli.json` does not need it. Restart the TUI after installing |
|
|
222
238
|
| Desktop has no DeepSeek entry in the *New tab* list | The 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 |
|
|
223
|
-
| 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 |
|
|
239
|
+
| Sidebar says `读取失败` (zh-CN UI; `Read failed` elsewhere) | 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 |
|
|
224
240
|
|
|
225
241
|
## Local development
|
|
226
242
|
|
|
@@ -261,7 +277,7 @@ Layout:
|
|
|
261
277
|
index.ts # directory-plugin entry, re-exports src/index.ts
|
|
262
278
|
tui.ts # TUI entry, re-exports src/tui/main.ts
|
|
263
279
|
src/index.ts # plugin: options, registration, websearch patch, three tools
|
|
264
|
-
src/deepseek.ts # HTTP core: timeout/cancel, single backoff retry,
|
|
280
|
+
src/deepseek.ts # HTTP core: timeout/cancel, single backoff retry, localized errors, mapping
|
|
265
281
|
src/usage.ts # pure ledger: micro-CNY pricing, 60-day prune, defensive parse, formatting
|
|
266
282
|
src/rpc.ts # RPC contract shared by the server, TUI and desktop halves
|
|
267
283
|
src/status-view.ts # pure view model both sidebars derive their copy from
|
package/gui-experimental.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const o=require("solid-js/web"),_=require("@opencode/plugin/gui"),y=require("solid-js"),Y="deepseek-search",k={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},j={id:Y,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"},counts:{type:"object",properties:{today:k,month:k,total:k},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 E(t){return typeof t=="object"&&t!==null&&!Array.isArray(t)}function z(t){return t==="deepseek"||t==="native"}function x(t){if(!E(t))return;const{search:e,fetch:r,empty:n,failed:i,costMicro:s}=t;for(const d of[e,r,n,i])if(typeof d!="number"||!Number.isInteger(d)||d<0)return;if(!(typeof s!="number"||!Number.isFinite(s)||s<0))return{search:e,fetch:r,empty:n,failed:i,costMicro:s}}function B(t){if(!E(t))return;const{mode:e,source:r,patched:n,hasKey:i,pricePerThousand:s,today:d,month:u,total:p,counts:a}=t;if(!z(e)||r!=="override"&&r!=="config"||typeof n!="boolean"||typeof i!="boolean"||typeof s!="number"||!Number.isFinite(s)||typeof d!="string"||typeof u!="string"||typeof p!="string")return;const c={mode:e,source:r,patched:n,hasKey:i,pricePerThousand:s,today:d,month:u,total:p};if(E(a)){const f=x(a.today),m=x(a.month),h=x(a.total);f&&m&&h&&(c.counts={today:f,month:m,total:h})}return c}function q(t){if(!E(t))return;const{ok:e,mode:r,message:n}=t;if(!(typeof e!="boolean"||typeof r!="string"||typeof n!="string"))return{ok:e,mode:r,message:n}}const U="DeepSeek search/fetch",D="DeepSeek",H=2,V=1,W=24,N=["today","month","total"],O={today:"今日",month:"本月",total:"累计"};function R(t){const e=t.codePointAt(0)??0;return e===0?0:e>=4352&&e<=4447||e>=11904&&e<=42191||e>=44032&&e<=55203||e>=63744&&e<=64255||e>=65040&&e<=65049||e>=65072&&e<=65135||e>=65280&&e<=65376||e>=65504&&e<=65510||e>=127744&&e<=129791||e>=131072&&e<=262141?2:1}function b(t){let e=0;for(const r of t)e+=R(r);return e}function S(t,e){if(e<1)return"";if(b(t)<=e)return t;let r="",n=0;for(const i of t){const s=R(i);if(n+s>e-1)return`${r}…`;r+=i,n+=s}return r}function G(t,e){const r=t.replace(/\s+/g," ").trim();return S(r||"读取失败",e)}function L(t){let r=(Number.isFinite(t)&&t>0?t/1e6:0).toFixed(3);for(;r.endsWith("0")&&r.length>4;)r=r.slice(0,-1);return`¥${r}`}function J(t){const e=N.map(r=>t[r]);return{search:Math.max(...e.map(r=>String(r.search).length)),fetch:Math.max(...e.map(r=>String(r.fetch).length)),cost:Math.max(...e.map(r=>L(r.costMicro).length))}}function X(t,e,r,n){const i=O[t],s=String(e.search).padStart(r.search," "),d=String(e.fetch).padStart(r.fetch," "),u=L(e.costMicro).padStart(r.cost," "),p=[`${i} Search ${s} Fetch ${d} ${u}`,`${i} Search ${s} Fetch ${d} ${u}`,`${i} Search ${s} Fetch ${d}`,`${i} Search ${s} Fetch ${d}`,`${i} ${s}S ${d}F ${u}`,`${i} ${s}S ${d}F`,`${i} ${s}S`];for(const a of p)if(b(a)<=n)return a;return b(i)<=n?i:S(i,n)}function Q(t,e,r){const n=O[t],i=`${n} ${e}`;if(b(i)<=r)return i;const s=e.split(" · ").filter(u=>!u.startsWith("¥")&&!u.startsWith("空/失")),d=[`${n} ${s.join(" ")}`.trimEnd(),`${n} ${s[0]??""}`.trimEnd(),n];for(const u of d)if(b(u)<=r)return u;return S(n,r)}function Z(t,e){if(e===Number.POSITIVE_INFINITY)return[`今日 ${t.today}`,`本月 ${t.month}`,`累计 ${t.total}`];const r=t.counts;if(r){const n=J(r);return N.map(i=>X(i,r[i],n,e))}return N.map(n=>Q(n,t[n],e))}function ee(t,e){const r=`[${t}]`;if(e===Number.POSITIVE_INFINITY||b(r)<=e)return r;const n=e-2;return n>=1?`[${S(t,n)}]`:S(r,e)}function C(t,e,r,n){const i=(p,a)=>{if(r===Number.POSITIVE_INFINITY)return Number.POSITIVE_INFINITY;const c=p?H:0,f=n===void 0||a?0:n+V;return Math.max(0,r-c-f)},s=[`${U} · ${t}`,`${D} · ${t}`,`${D} · ${e}`,t,e],d=n===void 0?[!1]:[!1,!0];for(const p of s)for(const a of d)for(const c of[!0,!1])if(b(p)<=i(c,a))return{title:p,glyph:c,actionBelow:a};const u=n!==void 0;return{title:S(e,i(!1,u)),glyph:!1,actionBelow:u}}function te(t){const e=["设置 DEEPSEEK_API_KEY 后重启","设置 API KEY 后重启","设置 API KEY","设置 KEY"];if(t===Number.POSITIVE_INFINITY)return e[0];for(const r of e)if(b(r)<=t)return r;return S(e[e.length-1],t)}function P(t,e,r,n){const i=ee(r,n);return{...C(t,e,n,b(i)),action:i}}function re(t){const{status:e,error:r}=t,n=t.maxColumns??W,i=e===void 0?[]:Z(e,n);if(e===void 0)return r===void 0||r.trim()===""?{...P("正在读取…","正在读取…","重试",n),hint:void 0,clickable:!0,lines:i,tone:"idle"}:{...P("读取失败","读取失败","重试",n),hint:void 0,clickable:!0,lines:[G(r,n)],tone:"error"};if(e.patched&&e.hasKey)return{...P("已启用","已启用","停用",n),hint:void 0,clickable:!0,lines:i,tone:"on"};if(!e.patched&&e.mode==="deepseek")return{...P("未接管内置 websearch","未接管","交还",n),hint:void 0,clickable:!0,lines:i,tone:"warn"};if(e.mode==="native")return{...P("已停用","已停用","启用",n),hint:void 0,clickable:!0,lines:i,tone:"off"};const s=te(n);return{...C("未配置 API Key","缺 API Key",n,void 0),action:s,hint:s,clickable:!1,lines:i,tone:"error"}}var F=o.template("<span>"),ne=o.template("<span style=opacity:0.6;font-weight:400>"),oe=o.template("<span style=opacity:0.3;font-weight:400> · "),ie=o.template("<code style=font-size:0.94em>DEEPSEEK_API_KEY"),se=o.template('<p style="margin:16px 0 0;font-size:12px;line-height:1.6;opacity:0.68;overflow-wrap:anywhere">'),ae=o.template("<div style=margin-top:18px>"),le=o.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>'),ce=o.template('<p style="margin:14px 0 0;font-size:12px;line-height:1.6;opacity:0.7;overflow-wrap:anywhere">'),de=o.template("<div style=display:flex;align-items:center;gap:8px;margin-top:14px;opacity:0.7><span style=font-size:12px>正在读取用量…"),ue=o.template('<dl style="margin:16px 0 0">'),fe=o.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>'),pe=o.template("<span style=min-width:0;overflow-wrap:anywhere>");const he={on:"●",off:"○",warn:"◐",error:"!",idle:"…"},ye={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)"}},me={on:"停用(交还原生搜索)",off:"启用(由 DeepSeek 接管)",warn:"交还原生搜索",error:"重试",idle:"重试"},ge={on:"arrow-undo-down",off:"globe",warn:"arrow-undo-down",error:"refresh",idle:"refresh"};function ve(t,e){const r=t.indexOf(" · ");return r<0?(()=>{var n=F();return o.setStyleProperty(n,"color",e),o.insert(n,t),n})():[(()=>{var n=ne();return o.insert(n,()=>t.slice(0,r)),n})(),oe(),(()=>{var n=F();return o.setStyleProperty(n,"color",e),o.insert(n,()=>t.slice(r+3)),n})()]}function be(t){const e=t.indexOf(" ");return e<0?{label:t,chunks:[]}:{label:t.slice(0,e),chunks:t.slice(e+1).split(" · ")}}function $e(t,e){const r="DEEPSEEK_API_KEY",n=t.indexOf(r);return n<0?t:[o.memo(()=>t.slice(0,n)),(()=>{var i=ie();return o.setStyleProperty(i,"font-family",e),i})(),o.memo(()=>t.slice(n+r.length))]}function Se(t){const e=_.usePlugin(),r=y.createMemo(()=>re({status:t.status,error:t.error,maxColumns:1/0})),n=y.createMemo(()=>ye[e.appearance.mode()]),i=()=>e.appearance.font("mono");return y.onMount(()=>{const s=setInterval(()=>t.onRefresh(),1e4);y.onCleanup(()=>clearInterval(s))}),(()=>{var s=le(),d=s.firstChild,u=d.firstChild,p=u.nextSibling;return o.insert(u,()=>he[r().tone]),o.insert(p,()=>ve(r().title,n()[r().tone])),o.insert(d,o.createComponent(e.ui.Tooltip,{value:"刷新用量",placement:"bottom",get children(){return o.createComponent(e.ui.IconButton,{icon:"refresh",label:"刷新用量",size:"small",get onClick(){return t.onRefresh}})}}),null),o.insert(s,(()=>{var a=o.memo(()=>t.status===void 0);return()=>a()?o.memo(()=>r().lines.length>0)()?(()=>{var c=ce();return o.insert(c,()=>r().lines[0]),c})():(()=>{var c=de(),f=c.firstChild;return o.insert(c,o.createComponent(e.ui.Loader,{size:"small"}),f),c})():(()=>{var c=ue();return o.insert(c,o.createComponent(y.For,{get each(){return r().lines},children:(f,m)=>{const h=be(f);return(()=>{var l=fe(),g=l.firstChild,T=g.nextSibling;return o.insert(g,()=>h.label),o.insert(T,o.createComponent(y.For,{get each(){return h.chunks},children:(v,I)=>(()=>{var $=pe();return o.insert($,v,null),o.insert($,()=>I()<h.chunks.length-1?" ·":"",null),o.effect(w=>{var M=v.startsWith("¥")?500:400,A=v.startsWith("¥")?1:.8;return M!==w.e&&o.setStyleProperty($,"font-weight",w.e=M),A!==w.t&&o.setStyleProperty($,"opacity",w.t=A),w},{e:void 0,t:void 0}),$})()})),o.effect(v=>{var I=m()>0?`1px solid ${n().rule}`:void 0,$=i();return I!==v.e&&o.setStyleProperty(l,"border-top",v.e=I),$!==v.t&&o.setStyleProperty(T,"font-family",v.t=$),v},{e:void 0,t:void 0}),l})()}})),o.effect(f=>{var m=`1px solid ${n().rule}`,h=`1px solid ${n().rule}`;return m!==f.e&&o.setStyleProperty(c,"border-top",f.e=m),h!==f.t&&o.setStyleProperty(c,"border-bottom",f.t=h),f},{e:void 0,t:void 0}),c})()})(),null),o.insert(s,o.createComponent(y.Show,{get when(){return!r().clickable},get children(){var a=se();return o.insert(a,()=>$e(r().action,i())),a}}),null),o.insert(s,o.createComponent(y.Show,{get when(){return o.memo(()=>!!r().clickable)()&&r().tone!=="idle"},get children(){var a=ae();return o.insert(a,o.createComponent(e.ui.Button,{get variant(){return r().tone==="off"||r().tone==="error"?"contrast":"neutral"},get icon(){return ge[r().tone]},get disabled(){return t.busy},get busy(){return t.busy},get onClick(){return t.onToggle},get children(){return me[r().tone]}})),a}}),null),o.effect(a=>{var c=t.busy||void 0,f=n()[r().tone];return c!==a.e&&o.setAttribute(s,"aria-busy",a.e=c),f!==a.t&&o.setStyleProperty(u,"color",a.t=f),a},{e:void 0,t:void 0}),s})()}const K={id:"main",title:"DeepSeek"},we=80,Pe=_.Plugin.define({setup(t){const e=t.client.rpc(j),[r,n]=y.createSignal(),[i,s]=y.createSignal(),[d,u]=y.createSignal(!1),p=l=>(l instanceof Error?l.message:String(l)).slice(0,we);async function a(){try{const l=B(await e.status({}));l?(n(l),s(void 0)):s("状态数据格式不符")}catch(l){s(p(l))}}async function c(l){u(!0);try{const g=q(await e.setMode({mode:l}));g?g.ok||t.toast.show({title:"DeepSeek",description:g.message,variant:"error"}):t.toast.show({title:"DeepSeek",description:"模式切换结果格式不符",variant:"error"})}catch(g){t.toast.show({title:"DeepSeek",description:p(g),variant:"error"})}finally{await a(),u(!1)}}function f(){const l=r();if(!l||i()){a();return}c(l.mode==="deepseek"?"native":"deepseek")}t.add(_.Panel,{id:"main",region:"side",mobile:{title:"DeepSeek",order:50,kind:"menu",icon:"globe"},list:l=>l.open.includes(K.id)?[K]:[],render:()=>o.createComponent(Se,{get status(){return r()},get error(){return i()},get busy(){return d()},onToggle:f,onRefresh:()=>{a()}})});const m=()=>{const l=t.sessions.current();if(!l){t.toast.show({title:"DeepSeek",description:"请先打开一个会话"});return}t.layout.open(`${t.id}:main`,l)};t.add(_.Command,{id:"open",title:"DeepSeek: 打开统计面板",section:"session",run:m}),t.add(_.MenuItem,()=>{const l=t.sessions.current();return{menu:"session.panel",id:"open",title:"DeepSeek",description:"搜索用量与接管开关",icon:"globe",order:60,opened:l!==void 0&&t.layout.stored(l).includes("main"),run:m}});const h=e.events.on("changed",()=>{a()});return a(),h}});exports.default=Pe;
|
|
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;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qthinker/opencode-deepseek-search",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
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
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"cost",
|
|
25
25
|
"tui",
|
|
26
26
|
"desktop",
|
|
27
|
-
"sidebar"
|
|
27
|
+
"sidebar",
|
|
28
|
+
"i18n"
|
|
28
29
|
],
|
|
29
30
|
"files": [
|
|
30
31
|
"index.ts",
|
package/src/deepseek.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
// DeepSeek Search / Fetch HTTP core.
|
|
2
2
|
//
|
|
3
3
|
// Responsibilities: request shaping, retry policy, response mapping and
|
|
4
|
-
// user-facing
|
|
5
|
-
// so every mapper below is unit-testable without
|
|
4
|
+
// user-facing error messages (rendered through the i18n tables). No plugin
|
|
5
|
+
// context and no storage here, so every mapper below is unit-testable without
|
|
6
|
+
// network access.
|
|
7
|
+
|
|
8
|
+
import { t } from "./i18n/index.ts"
|
|
9
|
+
import type { Language } from "./rpc.ts"
|
|
6
10
|
|
|
7
11
|
export const API_BASE = "https://api.deepseek.com"
|
|
8
12
|
export const SEARCH_PATH = "/v1/tools/search"
|
|
@@ -32,12 +36,8 @@ export interface FetchResult {
|
|
|
32
36
|
truncated: boolean
|
|
33
37
|
}
|
|
34
38
|
|
|
35
|
-
export function missingKeyMessage(): string {
|
|
36
|
-
return (
|
|
37
|
-
"未配置 DeepSeek API Key:请设置环境变量 DEEPSEEK_API_KEY" +
|
|
38
|
-
'(PowerShell: [Environment]::SetEnvironmentVariable("DEEPSEEK_API_KEY","sk-...","User")),' +
|
|
39
|
-
'或在插件 options 中写 "apiKey": "{env:DEEPSEEK_API_KEY}",然后重启 opencode。'
|
|
40
|
-
)
|
|
39
|
+
export function missingKeyMessage(language: Language): string {
|
|
40
|
+
return t(language, "error.missingKey")
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
// ---------- abort plumbing: combine the caller signal with a timeout ----------
|
|
@@ -80,42 +80,43 @@ function sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
|
80
80
|
|
|
81
81
|
// ---------- error mapping ----------
|
|
82
82
|
|
|
83
|
-
function upstreamMessage(body: string): string {
|
|
83
|
+
function upstreamMessage(body: string, language: Language): string {
|
|
84
84
|
try {
|
|
85
85
|
const parsed = JSON.parse(body) as { error?: { message?: unknown } }
|
|
86
86
|
const message = parsed?.error?.message
|
|
87
|
-
|
|
87
|
+
if (typeof message !== "string" || !message.trim()) return ""
|
|
88
|
+
// The provider text is kept verbatim; only the separator is localized.
|
|
89
|
+
const separator = language === "en" ? ": " : ":"
|
|
90
|
+
return `${separator}${message.trim()}`
|
|
88
91
|
} catch {
|
|
89
92
|
return ""
|
|
90
93
|
}
|
|
91
94
|
}
|
|
92
95
|
|
|
93
|
-
export function httpError(status: number, body: string, isFetch: boolean): Error {
|
|
94
|
-
const upstream = upstreamMessage(body)
|
|
95
|
-
const
|
|
96
|
+
export function httpError(status: number, body: string, isFetch: boolean, language: Language): Error {
|
|
97
|
+
const upstream = upstreamMessage(body, language)
|
|
98
|
+
const hint = isFetch ? t(language, "error.http.fetchHint") : ""
|
|
99
|
+
const params = { upstream, hint, status }
|
|
96
100
|
switch (status) {
|
|
97
101
|
case 400:
|
|
98
|
-
return new Error(
|
|
102
|
+
return new Error(t(language, "error.http.400", params))
|
|
99
103
|
case 401:
|
|
100
|
-
return new Error(
|
|
104
|
+
return new Error(t(language, "error.http.401", params))
|
|
101
105
|
case 402:
|
|
102
|
-
return new Error("
|
|
106
|
+
return new Error(t(language, "error.http.402", params))
|
|
103
107
|
case 403:
|
|
104
|
-
return new Error(
|
|
108
|
+
return new Error(t(language, "error.http.403", params))
|
|
105
109
|
case 404:
|
|
106
|
-
return new Error(
|
|
110
|
+
return new Error(t(language, "error.http.404", params))
|
|
107
111
|
case 408:
|
|
108
|
-
return new Error("
|
|
112
|
+
return new Error(t(language, "error.http.408", params))
|
|
109
113
|
case 413:
|
|
110
|
-
return new Error("
|
|
114
|
+
return new Error(t(language, "error.http.413", params))
|
|
111
115
|
case 429:
|
|
112
|
-
return new Error("
|
|
116
|
+
return new Error(t(language, "error.http.429", params))
|
|
113
117
|
default:
|
|
114
|
-
if (status >= 500)
|
|
115
|
-
|
|
116
|
-
`DeepSeek 服务端错误(${status}),已退避重试一次仍失败。可把插件 options.mode 改为 "native" 暂用原生搜索。`,
|
|
117
|
-
)
|
|
118
|
-
return new Error(`DeepSeek 请求失败(HTTP ${status})${upstream}`)
|
|
118
|
+
if (status >= 500) return new Error(t(language, "error.http.5xx", params))
|
|
119
|
+
return new Error(t(language, "error.http.other", params))
|
|
119
120
|
}
|
|
120
121
|
}
|
|
121
122
|
|
|
@@ -132,9 +133,10 @@ export async function postJson(
|
|
|
132
133
|
body: unknown,
|
|
133
134
|
apiKey: string,
|
|
134
135
|
timeoutMs: number,
|
|
136
|
+
language: Language,
|
|
135
137
|
signal?: AbortSignal,
|
|
136
138
|
): Promise<unknown> {
|
|
137
|
-
if (!apiKey) throw new Error(missingKeyMessage())
|
|
139
|
+
if (!apiKey) throw new Error(missingKeyMessage(language))
|
|
138
140
|
for (let attempt = 1; ; attempt++) {
|
|
139
141
|
const merged = mergeAbort(signal, timeoutMs)
|
|
140
142
|
try {
|
|
@@ -149,7 +151,7 @@ export async function postJson(
|
|
|
149
151
|
try {
|
|
150
152
|
return JSON.parse(text) as unknown
|
|
151
153
|
} catch {
|
|
152
|
-
throw new Error(
|
|
154
|
+
throw new Error(t(language, "error.parse", { status: res.status }))
|
|
153
155
|
}
|
|
154
156
|
}
|
|
155
157
|
const text = await res.text().catch(() => "")
|
|
@@ -157,18 +159,18 @@ export async function postJson(
|
|
|
157
159
|
await sleep(retryAfterMs(res.headers), signal)
|
|
158
160
|
continue
|
|
159
161
|
}
|
|
160
|
-
throw httpError(res.status, text, path === FETCH_PATH)
|
|
162
|
+
throw httpError(res.status, text, path === FETCH_PATH, language)
|
|
161
163
|
} catch (e) {
|
|
162
164
|
if (e instanceof Error && e.name === "AbortError") {
|
|
163
165
|
throw new Error(
|
|
164
166
|
signal?.aborted
|
|
165
|
-
?
|
|
166
|
-
:
|
|
167
|
+
? t(language, "error.aborted")
|
|
168
|
+
: t(language, "error.timeout", { seconds: Math.round(timeoutMs / 1000) }),
|
|
167
169
|
)
|
|
168
170
|
}
|
|
169
171
|
// fetch() rejects with TypeError on DNS/TLS/proxy failures.
|
|
170
172
|
if (e instanceof TypeError) {
|
|
171
|
-
throw new Error(
|
|
173
|
+
throw new Error(t(language, "error.network", { base: API_BASE, message: e.message }))
|
|
172
174
|
}
|
|
173
175
|
throw e
|
|
174
176
|
} finally {
|
|
@@ -222,39 +224,53 @@ export async function deepseekSearch(
|
|
|
222
224
|
query: string,
|
|
223
225
|
apiKey: string,
|
|
224
226
|
count: number,
|
|
227
|
+
language: Language,
|
|
225
228
|
signal?: AbortSignal,
|
|
226
229
|
): Promise<SearchResult[]> {
|
|
227
|
-
const data = await postJson(
|
|
230
|
+
const data = await postJson(
|
|
231
|
+
SEARCH_PATH,
|
|
232
|
+
buildSearchBody(query, count),
|
|
233
|
+
apiKey,
|
|
234
|
+
SEARCH_TIMEOUT_MS,
|
|
235
|
+
language,
|
|
236
|
+
signal,
|
|
237
|
+
)
|
|
228
238
|
return mapSearchResults(data)
|
|
229
239
|
}
|
|
230
240
|
|
|
231
241
|
// ---------- DeepSeek Fetch -> Markdown with truncation ----------
|
|
232
242
|
|
|
233
|
-
export function mapFetchResponse(
|
|
243
|
+
export function mapFetchResponse(
|
|
244
|
+
data: unknown,
|
|
245
|
+
url: string,
|
|
246
|
+
maxChars: number,
|
|
247
|
+
language: Language,
|
|
248
|
+
): FetchResult {
|
|
234
249
|
const title = typeof (data as { title?: unknown })?.title === "string" ? (data as { title: string }).title : ""
|
|
235
250
|
const raw = typeof (data as { content?: unknown })?.content === "string" ? (data as { content: string }).content : ""
|
|
236
251
|
const truncated = raw.length > maxChars
|
|
237
252
|
let body = truncated ? raw.slice(0, maxChars) : raw
|
|
238
253
|
if (truncated) {
|
|
239
|
-
body += `\n\n
|
|
254
|
+
body += `\n\n${t(language, "error.fetch.truncated", { raw: raw.length, max: maxChars })}`
|
|
240
255
|
}
|
|
241
256
|
return {
|
|
242
|
-
markdown: `# ${title || url}\n
|
|
257
|
+
markdown: `# ${title || url}\n${t(language, "fetch.source", { url })}\n\n${body}`,
|
|
243
258
|
chars: raw.length,
|
|
244
259
|
empty: raw.length === 0,
|
|
245
260
|
truncated,
|
|
246
261
|
}
|
|
247
262
|
}
|
|
248
263
|
|
|
249
|
-
export function validateUrl(url: string): string | undefined {
|
|
250
|
-
if (!/^https?:\/\//i.test(url)) return
|
|
251
|
-
if (url.length > MAX_URL_CHARS)
|
|
264
|
+
export function validateUrl(url: string, language: Language): string | undefined {
|
|
265
|
+
if (!/^https?:\/\//i.test(url)) return t(language, "error.validate.urlScheme", { url })
|
|
266
|
+
if (url.length > MAX_URL_CHARS)
|
|
267
|
+
return t(language, "error.validate.urlLong", { length: url.length, max: MAX_URL_CHARS })
|
|
252
268
|
try {
|
|
253
269
|
const parsed = new URL(url)
|
|
254
|
-
if (!parsed.host) return
|
|
255
|
-
if (parsed.username || parsed.password) return
|
|
270
|
+
if (!parsed.host) return t(language, "error.validate.urlHost", { url })
|
|
271
|
+
if (parsed.username || parsed.password) return t(language, "error.validate.urlUserinfo")
|
|
256
272
|
} catch {
|
|
257
|
-
return
|
|
273
|
+
return t(language, "error.validate.urlParse", { url })
|
|
258
274
|
}
|
|
259
275
|
return undefined
|
|
260
276
|
}
|
|
@@ -263,15 +279,17 @@ export async function deepseekFetch(
|
|
|
263
279
|
url: string,
|
|
264
280
|
apiKey: string,
|
|
265
281
|
maxChars: number,
|
|
282
|
+
language: Language,
|
|
266
283
|
signal?: AbortSignal,
|
|
267
284
|
): Promise<FetchResult> {
|
|
268
|
-
const data = await postJson(FETCH_PATH, { url }, apiKey, FETCH_TIMEOUT_MS, signal)
|
|
269
|
-
return mapFetchResponse(data, url, maxChars)
|
|
285
|
+
const data = await postJson(FETCH_PATH, { url }, apiKey, FETCH_TIMEOUT_MS, language, signal)
|
|
286
|
+
return mapFetchResponse(data, url, maxChars, language)
|
|
270
287
|
}
|
|
271
288
|
|
|
272
|
-
export function validateQuery(query: string): string | undefined {
|
|
273
|
-
if (query.length === 0) return
|
|
274
|
-
if (query.length > MAX_QUERY_CHARS)
|
|
289
|
+
export function validateQuery(query: string, language: Language): string | undefined {
|
|
290
|
+
if (query.length === 0) return t(language, "error.validate.queryEmpty")
|
|
291
|
+
if (query.length > MAX_QUERY_CHARS)
|
|
292
|
+
return t(language, "error.validate.queryLong", { length: query.length, max: MAX_QUERY_CHARS })
|
|
275
293
|
return undefined
|
|
276
294
|
}
|
|
277
295
|
|
package/src/gui/index.tsx
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
type SessionPanelMenuItem,
|
|
15
15
|
} from "@opencode/plugin/gui"
|
|
16
16
|
import { createSignal } from "solid-js"
|
|
17
|
+
import { t } from "../i18n/index.ts"
|
|
17
18
|
import { DeepSeekRpc, readModeResult, readStatus, type SearchStatus } from "../rpc.ts"
|
|
18
19
|
import { DeepSeekPanel } from "./panel.tsx"
|
|
19
20
|
|
|
@@ -30,6 +31,10 @@ export default Plugin.define({
|
|
|
30
31
|
const [error, setError] = createSignal<string | undefined>()
|
|
31
32
|
const [busy, setBusy] = createSignal(false)
|
|
32
33
|
|
|
34
|
+
// The desktop side has no forced locale: the server resolves the language
|
|
35
|
+
// and carries it in `status`, so every label follows that signal.
|
|
36
|
+
const language = () => status()?.language ?? "en"
|
|
37
|
+
|
|
33
38
|
const describeError = (e: unknown) => (e instanceof Error ? e.message : String(e)).slice(0, ERROR_MAX)
|
|
34
39
|
|
|
35
40
|
// `status` takes a required first argument: plain JSON Schema degrades the
|
|
@@ -42,7 +47,7 @@ export default Plugin.define({
|
|
|
42
47
|
setStatus(next)
|
|
43
48
|
setError(undefined)
|
|
44
49
|
} else {
|
|
45
|
-
setError("
|
|
50
|
+
setError(t(language(), "toast.statusMalformed"))
|
|
46
51
|
}
|
|
47
52
|
} catch (e) {
|
|
48
53
|
setError(describeError(e))
|
|
@@ -53,7 +58,7 @@ export default Plugin.define({
|
|
|
53
58
|
setBusy(true)
|
|
54
59
|
try {
|
|
55
60
|
const result = readModeResult(await rpc.setMode({ mode: next }))
|
|
56
|
-
if (!result) ctx.toast.show({ title: "DeepSeek", description: "
|
|
61
|
+
if (!result) ctx.toast.show({ title: "DeepSeek", description: t(language(), "toast.modeMalformed"), variant: "error" })
|
|
57
62
|
else if (!result.ok) ctx.toast.show({ title: "DeepSeek", description: result.message, variant: "error" })
|
|
58
63
|
} catch (e) {
|
|
59
64
|
ctx.toast.show({ title: "DeepSeek", description: describeError(e), variant: "error" })
|
|
@@ -96,18 +101,20 @@ export default Plugin.define({
|
|
|
96
101
|
const openPanel = () => {
|
|
97
102
|
const session = ctx.sessions.current()
|
|
98
103
|
if (!session) {
|
|
99
|
-
ctx.toast.show({ title: "DeepSeek", description: "
|
|
104
|
+
ctx.toast.show({ title: "DeepSeek", description: t(language(), "toast.openSessionFirst") })
|
|
100
105
|
return
|
|
101
106
|
}
|
|
102
107
|
ctx.layout.open(`${ctx.id}:main`, session)
|
|
103
108
|
}
|
|
104
109
|
|
|
105
|
-
|
|
110
|
+
// A factory, because the palette label has to follow the language that
|
|
111
|
+
// arrives with the status signal.
|
|
112
|
+
ctx.add(Command, (): Command => ({
|
|
106
113
|
id: "open",
|
|
107
|
-
title:
|
|
114
|
+
title: t(language(), "command.open"),
|
|
108
115
|
section: "session",
|
|
109
116
|
run: openPanel,
|
|
110
|
-
})
|
|
117
|
+
}))
|
|
111
118
|
|
|
112
119
|
// The new-tab launcher and the + menu list `session.panel` items, not panel
|
|
113
120
|
// tabs, so without this the panel is reachable only by typing the command.
|
|
@@ -119,7 +126,7 @@ export default Plugin.define({
|
|
|
119
126
|
menu: "session.panel",
|
|
120
127
|
id: "open",
|
|
121
128
|
title: "DeepSeek",
|
|
122
|
-
description: "
|
|
129
|
+
description: t(language(), "menu.description"),
|
|
123
130
|
icon: "globe",
|
|
124
131
|
order: 60,
|
|
125
132
|
opened,
|