@dougen/dsh-deepseek-usage 0.3.1 → 0.4.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/README.md CHANGED
@@ -1,21 +1,24 @@
1
1
  # @dougen/dsh-deepseek-usage
2
2
 
3
- DeepSeek 用量侧边栏插件(DeepSeek Harness Web UI)
3
+ DeepSeek 余额侧边栏插件(DeepSeek Harness Web UI)
4
4
 
5
- ## 简介
6
-
7
- 在 DSH **网页界面**的侧边栏底部常驻显示你的 **DeepSeek 账号余额**(放大显示)与**当前模型档单价**卡片,并提供高峰/空闲计费时段指示灯。
5
+ DSH **网页界面**的侧边栏底部常驻显示你的 **DeepSeek 账号余额**(放大显示)与**当前模型档单价**,并提供高峰/空闲计费时段指示灯。界面语言跟随 DSH 语言设置(自动检测浏览器语言,中文界面显示中文,其他语言显示英文)。
8
6
 
9
7
  ![DeepSeek 用量卡片](https://cdn.jsdelivr.net/npm/@dougen/dsh-deepseek-usage/screenshot.png)
10
8
 
11
9
  ## 功能
12
10
 
13
11
  - **余额**:`¥ 110.00` 大字显示(符号随账号币种自动切换:人民币 `¥` / 美元 `$`)
14
- - **档单价**:`→|1.5 ⚡0.05 |→4.5`(输入 / 输入缓存 / 输出,位于卡片右下角;随当前启用模型与峰谷时段自动切换)
12
+ - **档单价**:`→|1.5 ⚡0.05 |→4.5`(输入 / 输入缓存 / 输出,位于卡片右下角;随当前启用模型与峰谷时段自动切换,**切换模型时立即同步**)
15
13
  - **峰谷指示灯**:卡片右上角,高峰(9:00–12:00、14:00–18:00 北京时间)橙色,空闲绿色
16
14
  - **自动刷新**:每 5 分钟一次;点击卡片任意位置立即刷新
15
+ - **多语言**:界面文案跟随 DSH 语言(中文 / English)
17
16
  - 跟随应用明暗主题
18
17
 
18
+ ## 支持范围
19
+
20
+ > ⚠️ 本插件**仅支持 DeepSeek 官方平台**的余额显示:余额来自 DeepSeek 官方 API `GET https://api.deepseek.com/user/balance`(使用 DSH 中已配置的 `DEEPSEEK_API_KEY`)。通过 `DEEPSEEK_BASE_URL` 指向第三方中转/代理时,余额接口可能不可用或返回异常,**不保证兼容**,请使用 DeepSeek 官方平台账号。
21
+
19
22
  ## 安装
20
23
 
21
24
  需要 DSH CLI,从 npm 安装:
@@ -50,6 +53,61 @@ dsh plugin --profile web add @dougen/dsh-deepseek-usage
50
53
  dsh plugin --profile web remove @dougen/dsh-deepseek-usage
51
54
  ```
52
55
 
56
+ ---
57
+
58
+ # English
59
+
60
+ A sidebar plugin for **DeepSeek Harness Web UI** that shows your **DeepSeek account balance** (large display) and the **current model's unit pricing**, with a peak/off-peak billing indicator. UI language follows the DSH locale (Chinese UI shows Chinese, other languages show English).
61
+
62
+ ![DeepSeek usage card](https://cdn.jsdelivr.net/npm/@dougen/dsh-deepseek-usage/screenshot.png)
63
+
64
+ ## Features
65
+
66
+ - **Balance**: `¥ 110.00` large display (symbol follows account currency: `¥` CNY / `$` USD)
67
+ - **Unit pricing**: `→|1.5 ⚡0.05 |→4.5` (input / input cache / output, bottom-right of the card; switches with the active model and peak/off-peak window — **updates instantly when you switch models**)
68
+ - **Peak indicator**: top-right dot, orange during peak hours (9:00–12:00, 14:00–18:00 Beijing Time), green otherwise
69
+ - **Auto refresh**: every 5 minutes; click the card to refresh instantly
70
+ - **i18n**: UI copy follows the DSH locale (Chinese / English)
71
+ - Follows the app light/dark theme
72
+
73
+ ## Supported scope
74
+
75
+ > ⚠️ This plugin **only supports DeepSeek's official platform** balance display: balance comes from the official API `GET https://api.deepseek.com/user/balance` (using the `DEEPSEEK_API_KEY` configured in DSH). Pointing `DEEPSEEK_BASE_URL` at a third-party proxy/gateway may break or return unexpected results for the balance endpoint — **not guaranteed to be compatible**. Use an official DeepSeek platform account.
76
+
77
+ ## Install
78
+
79
+ Requires the DSH CLI. Install from npm:
80
+
81
+ ```sh
82
+ dsh plugin --profile web add @dougen/dsh-deepseek-usage
83
+ ```
84
+
85
+ Restart the web app (`dsh web`), open http://127.0.0.1:3080 , and the usage card appears at the bottom of the sidebar (below the Cordis panel button).
86
+
87
+ ## Data sources
88
+
89
+ | Data | Source |
90
+ |---|---|
91
+ | Balance | Official DeepSeek API `GET /user/balance` (uses `DEEPSEEK_API_KEY` configured in DSH) |
92
+ | Unit pricing / peak window | Built-in price table (DeepSeek official peak/off-peak pricing snapshot, effective 2026-08-17) |
93
+
94
+ Make sure `DEEPSEEK_API_KEY` is configured in DSH (Model settings page) before use.
95
+
96
+ ## FAQ
97
+
98
+ **Balance shows "—"?**
99
+ - `DEEPSEEK_API_KEY` is not configured — configure it in DSH settings and restart.
100
+ - Network or API error — the error message is shown below the card.
101
+
102
+ **Price differs from the official one?**
103
+ - The unit price switches with the **active model** and the **peak/off-peak window**; if the active model is not in the built-in price table, `deepseek-chat` pricing is shown with a notice.
104
+
105
+ ## Uninstall
106
+
107
+ ```sh
108
+ dsh plugin --profile web remove @dougen/dsh-deepseek-usage
109
+ ```
110
+
53
111
  ## License
54
112
 
55
113
  MIT
package/lib/client.js CHANGED
@@ -20,6 +20,23 @@ window.__ModuleLoader__.load({
20
20
  const POLL_MS = 5 * 60 * 1000;
21
21
  const USAGE_PATH = "/api/deepseek-usage";
22
22
 
23
+ // ---- i18n(DSH locale 服务;zh 为兜底)----
24
+ const NS = "dsh-deepseek-usage";
25
+ const ZH = {
26
+ "price.input": "输入",
27
+ "price.cache": "输入缓存",
28
+ "price.output": "输出",
29
+ "peak.peak": "高峰时段",
30
+ "peak.off": "空闲时段",
31
+ };
32
+ const EN = {
33
+ "price.input": "Input",
34
+ "price.cache": "Input cache",
35
+ "price.output": "Output",
36
+ "peak.peak": "Peak hours",
37
+ "peak.off": "Off-peak hours",
38
+ };
39
+
23
40
  // ---- helpers ---------------------------------------------------
24
41
  function money(value) {
25
42
  const n = Number(value);
@@ -71,14 +88,34 @@ window.__ModuleLoader__.load({
71
88
 
72
89
  // ---- the widget -------------------------------------------------
73
90
  function UsageCell(props) {
91
+ const ctx = props.ctx;
74
92
  const wide = !!props.wide;
75
93
  const [data, setData] = useState(null);
76
94
  const [error, setError] = useState("");
77
95
  const mounted = useRef(true);
96
+ const locale = ctx && ctx.locale;
97
+
98
+ // 订阅语言变化(DSH locale 服务;不可用时回退中文)。
99
+ const lcSnap = react.useSyncExternalStore(
100
+ (cb) => (locale && typeof locale.subscribe === "function" ? locale.subscribe(cb) : () => {}),
101
+ () => (locale && typeof locale.getSnapshot === "function" ? locale.getSnapshot() : { active: "zh", locales: [], revision: 0 }),
102
+ );
103
+ const lang = lcSnap && lcSnap.active === "en" ? "en" : "zh";
104
+ // locale.bind(ns) 返回绑定命名空间的翻译函数;不可用时回退中文词典。
105
+ const boundT = locale && typeof locale.bind === "function" ? locale.bind(NS) : null;
106
+ const t = (key) => {
107
+ try {
108
+ if (boundT) {
109
+ const v = boundT(key);
110
+ if (typeof v === "string" && v !== key) return v;
111
+ }
112
+ } catch (e) { /* 词典不可用,回退中文 */ }
113
+ return ZH[key] || key;
114
+ };
78
115
 
79
116
  const load = useCallback(async () => {
80
117
  try {
81
- const res = await fetch(USAGE_PATH, { cache: "no-store" });
118
+ const res = await fetch(USAGE_PATH + "?lang=" + lang, { cache: "no-store" });
82
119
  let body = null;
83
120
  try {
84
121
  body = await res.json();
@@ -93,17 +130,31 @@ window.__ModuleLoader__.load({
93
130
  if (!mounted.current) return;
94
131
  setError(e instanceof Error ? e.message : String(e));
95
132
  }
96
- }, []);
133
+ }, [lang]);
97
134
 
98
135
  useEffect(() => {
99
136
  mounted.current = true;
100
137
  load();
101
138
  const timer = setInterval(load, POLL_MS);
139
+ // 模型切换(写入 settings 触发 document-updated)时立即刷新,价格同步。
140
+ let off = null;
141
+ try {
142
+ if (ctx && ctx.remote && typeof ctx.remote.$on === "function") {
143
+ off = ctx.remote.$on("settings/document-updated", () => {
144
+ try { load(); } catch (e) { /* 忽略刷新错误 */ }
145
+ });
146
+ }
147
+ } catch (e) {
148
+ off = null;
149
+ }
102
150
  return () => {
103
151
  mounted.current = false;
104
152
  clearInterval(timer);
153
+ try {
154
+ if (typeof off === "function") off();
155
+ } catch (e) { /* 忽略退订错误 */ }
105
156
  };
106
- }, [load]);
157
+ }, [load, ctx]);
107
158
 
108
159
  const balance = data && data.balance ? data.balance : null;
109
160
  const tier = data && data.tier ? data.tier : null;
@@ -117,7 +168,9 @@ window.__ModuleLoader__.load({
117
168
  background: data && data.peak ? C.peak : C.idle,
118
169
  boxShadow: "0 0 4px rgba(0,0,0,.3)",
119
170
  };
120
- const title = "DeepSeek 用量 · 点击刷新" + (errs.length ? " · " + errs.join("; ") : "");
171
+ const peakTitle = data && data.peak
172
+ ? t("peak.peak") + (data.peakHours ? " " + data.peakHours : "")
173
+ : t("peak.off");
121
174
 
122
175
  if (!wide) {
123
176
  return h("div", {
@@ -136,13 +189,12 @@ window.__ModuleLoader__.load({
136
189
  gap: 5,
137
190
  userSelect: "none",
138
191
  },
139
- title,
140
192
  onClick: load,
141
193
  },
142
194
  h("span", { style: { display: "inline-flex", alignItems: "baseline", gap: 3 } },
143
195
  h("span", { style: { fontWeight: 700, fontSize: 13, color: C.label } }, symbol),
144
196
  h("span", { style: { fontWeight: 700, fontSize: 13, color: C.label } }, money(balance && balance.total))),
145
- h("span", { style: dot, title: data && data.peak ? "高峰时段" : "空闲时段" }));
197
+ h("span", { style: dot, title: peakTitle }));
146
198
  }
147
199
 
148
200
  const Price = (icon, label, value) =>
@@ -170,7 +222,6 @@ window.__ModuleLoader__.load({
170
222
  fontSize: "12px",
171
223
  lineHeight: "15px",
172
224
  },
173
- title,
174
225
  onClick: load,
175
226
  },
176
227
  // 第一行:余额(24px,符号与数字 3px 间距)
@@ -179,24 +230,30 @@ window.__ModuleLoader__.load({
179
230
  h("span", { style: { fontWeight: 700, fontSize: 24, color: C.label, lineHeight: "28px" } }, money(balance && balance.total))),
180
231
  h("span", {
181
232
  style: { position: "absolute", top: 8, right: 9, ...dot },
182
- title: data && data.peak ? "高峰时段 " + (data.peakHours || "") : "空闲时段",
233
+ title: peakTitle,
183
234
  }),
184
235
  // 当前档单价:卡片右下角
185
236
  h("span", {
186
237
  style: { position: "absolute", right: 9, bottom: 4, display: "inline-flex", alignItems: "center", gap: 5 },
187
238
  },
188
- Price(h(IconIn, null), "输入", tier && tier.input),
189
- Price(h(IconCache, null), "输入缓存", tier && tier.cacheRead),
190
- Price(h(IconOut, null), "输出", tier && tier.output)),
239
+ Price(h(IconIn, null), t("price.input"), tier && tier.input),
240
+ Price(h(IconCache, null), t("price.cache"), tier && tier.cacheRead),
241
+ Price(h(IconOut, null), t("price.output"), tier && tier.output)),
191
242
  errs.length
192
243
  ? h("div", { style: { color: C.err, fontSize: 10, whiteSpace: "pre-wrap" } }, errs.join(" | "))
193
244
  : null);
194
245
  }
195
246
 
196
247
  // ---- client plugin body -----------------------------------------
197
- const inject = ["slots"];
248
+ const inject = ["slots", "remote", "locale"];
198
249
 
199
250
  function apply(ctx) {
251
+ // 注册 i18n 词典(DSH locale 服务;不可用时组件回退中文)。
252
+ try {
253
+ if (ctx.locale && typeof ctx.locale.register === "function") {
254
+ ctx.locale.register(NS, { zh: ZH, en: EN });
255
+ }
256
+ } catch (e) { /* 词典注册失败不影响卡片 */ }
200
257
  // 注入本包样式:footerActions 换行(卡片独占一行)。
201
258
  const tagId = "dsh-deepseek-usage";
202
259
  if (typeof document !== "undefined" && document.querySelector(`style[data-plugin-css="${tagId}"]`) === null) {
@@ -208,7 +265,7 @@ window.__ModuleLoader__.load({
208
265
  }
209
266
  ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register(
210
267
  { name: "sidebar.footer.action", id: "dsh-usage", order: 100, label: "DeepSeek 用量" },
211
- (props) => h(UsageCell, { wide: props.wide }),
268
+ (props) => h(UsageCell, { wide: props.wide, ctx }),
212
269
  ));
213
270
  }
214
271
 
package/lib/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * 注册一个 HTTP 路由:
5
5
  *
6
- * GET /api/deepseek-usage
6
+ * GET /api/deepseek-usage[?lang=zh|en]
7
7
  *
8
8
  * 返回(JSON):
9
9
  * {
@@ -14,9 +14,13 @@
14
14
  * updatedAt, errors
15
15
  * }
16
16
  *
17
+ * `lang` 参数(zh 默认)控制错误消息与峰谷文案的语言;UI 其余文案由
18
+ * client 端(DSH locale 服务)本地化。
19
+ *
17
20
  * 数据源:余额来自 DeepSeek 官方 GET /user/balance(API Key 经凭证服务解析,
18
21
  * 与 llm-deepseek 同一引用,不出本机)。单价与峰谷时段来自内置价格表
19
- * (lib/pricing.js,DeepSeek 官方峰谷定价快照)。
22
+ * (lib/pricing.js,DeepSeek 官方峰谷定价快照)。本插件仅支持 DeepSeek
23
+ * 官方平台余额显示。
20
24
  *
21
25
  * 安全:路由仅接受本机 Host(127.0.0.1 / localhost / ::1),拦截 DNS
22
26
  * rebinding 与 0.0.0.0 绑定下的局域网访问。
@@ -25,6 +29,7 @@ import { credentialRef } from "@deepseek-ai/dsh-credentials";
25
29
  import {
26
30
  DEFAULT_MODEL,
27
31
  PEAK_HOURS_TEXT,
32
+ PEAK_HOURS_TEXT_EN,
28
33
  isPeak,
29
34
  unitAt,
30
35
  } from "./pricing.js";
@@ -45,6 +50,22 @@ const JSON_HEADERS = {
45
50
  "cache-control": "no-store",
46
51
  };
47
52
 
53
+ /** 错误消息字典(zh/en)。 */
54
+ const MESSAGES = {
55
+ zh: {
56
+ noKey: "未配置 DEEPSEEK_API_KEY",
57
+ balanceHttp: (status) => `余额接口 HTTP ${status}`,
58
+ balanceFail: (detail) => `余额获取失败: ${detail}`,
59
+ modelUnknown: (model) => `当前模型 ${model} 不在价格表,档单价按 deepseek-chat 展示`,
60
+ },
61
+ en: {
62
+ noKey: "DEEPSEEK_API_KEY is not configured",
63
+ balanceHttp: (status) => `Balance API HTTP ${status}`,
64
+ balanceFail: (detail) => `Failed to fetch balance: ${detail}`,
65
+ modelUnknown: (model) => `Model ${model} is not in the price table, showing deepseek-chat pricing`,
66
+ },
67
+ };
68
+
48
69
  function sendJson(res, status, body) {
49
70
  res.writeHead(status, JSON_HEADERS);
50
71
  res.end(JSON.stringify(body));
@@ -81,6 +102,17 @@ function isLocalHost(req) {
81
102
  return host === "127.0.0.1" || host === "localhost" || host === "::1";
82
103
  }
83
104
 
105
+ /** 解析 ?lang= 参数(zh 默认;仅识别 en 为英文,其余中文)。 */
106
+ function resolveLang(req) {
107
+ const url = req.url ?? "";
108
+ const q = url.indexOf("?");
109
+ if (q === -1) return "zh";
110
+ for (const kv of url.slice(q + 1).split("&")) {
111
+ if (kv === "lang=en") return "en";
112
+ }
113
+ return "zh";
114
+ }
115
+
84
116
  function apply(ctx) {
85
117
  ctx.effect(
86
118
  () => ctx.webServer.register({
@@ -93,6 +125,19 @@ function apply(ctx) {
93
125
  return sendJson(res, 403, { ok: false, error: "forbidden", message: "仅允许本机访问" });
94
126
  }
95
127
 
128
+ const lang = resolveLang(req);
129
+ const msg = MESSAGES[lang] || MESSAGES.zh;
130
+
131
+ // ---- 当前模型当前档单价(展示用)----
132
+ const now = Date.now();
133
+ let model = DEFAULT_MODEL;
134
+ try {
135
+ const adm = ctx.get("agentDefaultModel");
136
+ const selection = adm && typeof adm.currentSelection === "function" ? adm.currentSelection() : void 0;
137
+ if (selection && typeof selection.model === "string") model = selection.model;
138
+ } catch {}
139
+ const unit = unitAt(model, now);
140
+
96
141
  const errors = [];
97
142
 
98
143
  // ---- 余额(官方接口,API Key)----
@@ -100,7 +145,7 @@ function apply(ctx) {
100
145
  try {
101
146
  const hit = await ctx.credentials.resolve(API_KEY_REF);
102
147
  if (hit === void 0) {
103
- errors.push("未配置 DEEPSEEK_API_KEY");
148
+ errors.push(msg.noKey);
104
149
  } else {
105
150
  const response = await fetch(balanceUrl(), {
106
151
  headers: { Authorization: `Bearer ${hit.value}`, Accept: "application/json" },
@@ -108,7 +153,7 @@ function apply(ctx) {
108
153
  });
109
154
  const text = await response.text();
110
155
  if (!response.ok) {
111
- errors.push(`余额接口 HTTP ${response.status}`);
156
+ errors.push(msg.balanceHttp(response.status));
112
157
  } else {
113
158
  const body = JSON.parse(text);
114
159
  const infos = body && Array.isArray(body.balance_infos) ? body.balance_infos : [];
@@ -121,25 +166,17 @@ function apply(ctx) {
121
166
  }
122
167
  }
123
168
  } catch (error) {
124
- errors.push(`余额获取失败: ${error instanceof Error ? error.message : String(error)}`);
169
+ errors.push(msg.balanceFail(error instanceof Error ? error.message : String(error)));
125
170
  }
126
171
 
127
- // ---- 当前模型当前档单价 + 峰谷(展示用)----
128
- const now = Date.now();
129
- let model = DEFAULT_MODEL;
130
- try {
131
- const adm = ctx.get("agentDefaultModel");
132
- const selection = adm && typeof adm.currentSelection === "function" ? adm.currentSelection() : void 0;
133
- if (selection && typeof selection.model === "string") model = selection.model;
134
- } catch {}
135
- const unit = unitAt(model, now);
136
- if (!unit.known) errors.push(`当前模型 ${model} 不在价格表,档单价按 deepseek-chat 展示`);
172
+ // ---- 档单价提示(未知模型回退 chat 档)----
173
+ if (!unit.known) errors.push(msg.modelUnknown(model));
137
174
 
138
175
  sendJson(res, 200, {
139
176
  ok: true,
140
177
  balance,
141
178
  peak: isPeak(now),
142
- peakHours: PEAK_HOURS_TEXT,
179
+ peakHours: lang === "en" ? PEAK_HOURS_TEXT_EN : PEAK_HOURS_TEXT,
143
180
  tier: {
144
181
  input: unit.input,
145
182
  cacheRead: unit.cacheRead,
package/lib/pricing.js CHANGED
@@ -29,8 +29,10 @@ export const DEFAULT_MODEL = 'deepseek-v4-flash';
29
29
 
30
30
  /** 高峰时段窗口(北京时间,含起点不含终点)。 */
31
31
  export const PEAK_WINDOWS = [[9, 12], [14, 18]];
32
- /** 高峰时段展示文案。 */
32
+ /** 高峰时段展示文案(中文)。 */
33
33
  export const PEAK_HOURS_TEXT = '9:00–12:00、14:00–18:00(北京时间)';
34
+ /** 高峰时段展示文案(英文)。 */
35
+ export const PEAK_HOURS_TEXT_EN = '9:00–12:00, 14:00–18:00 (Beijing Time)';
34
36
 
35
37
  /** 某时刻的北京时间小时(0–23)。 */
36
38
  export function shanghaiHour(ms) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dougen/dsh-deepseek-usage",
3
- "version": "0.3.1",
4
- "description": "DeepSeek 用量侧边栏插件:余额(官方 API)、当前档单价与峰谷时段指示灯。",
3
+ "version": "0.4.0",
4
+ "description": "DeepSeek 余额侧边栏插件:余额(官方 API)、当前档单价与峰谷时段指示灯,支持中/英文界面。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "files": [