@dougen/dsh-deepseek-usage 0.3.2 → 0.4.1

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,22 +1,36 @@
1
1
  # @dougen/dsh-deepseek-usage
2
2
 
3
- DeepSeek 用量侧边栏插件(DeepSeek Harness Web UI
3
+ 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).
4
4
 
5
- ## 简介
5
+ DeepSeek **余额侧边栏插件**(DeepSeek Harness Web UI):在 DSH **网页界面**的侧边栏底部常驻显示你的 **DeepSeek 账号余额**(放大显示)与**当前模型档单价**,并提供高峰/空闲计费时段指示灯。界面语言跟随 DSH 语言设置(中文界面显示中文,其他语言显示英文)。
6
6
 
7
- DSH **网页界面**的侧边栏底部常驻显示你的 **DeepSeek 账号余额**(放大显示)与**当前模型档单价**卡片,并提供高峰/空闲计费时段指示灯。
7
+ ![DeepSeek usage card / 用量卡片](https://cdn.jsdelivr.net/npm/@dougen/dsh-deepseek-usage/screenshot.png)
8
8
 
9
- ![DeepSeek 用量卡片](https://cdn.jsdelivr.net/npm/@dougen/dsh-deepseek-usage/screenshot.png)
9
+ ## Features / 功能
10
10
 
11
- ## 功能
11
+ - **Balance**: `¥ 110.00` large display (symbol follows account currency: `¥` CNY / `$` USD)
12
+ - **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**)
13
+ - **Peak indicator**: top-right dot, orange during peak hours (9:00–12:00, 14:00–18:00 Beijing Time), green otherwise
14
+ - **Auto refresh**: every 5 minutes; click the card to refresh instantly
15
+ - **i18n**: UI copy follows the DSH locale (Chinese / English)
16
+ - Follows the app light/dark theme
12
17
 
13
18
  - **余额**:`¥ 110.00` 大字显示(符号随账号币种自动切换:人民币 `¥` / 美元 `$`)
14
19
  - **档单价**:`→|1.5 ⚡0.05 |→4.5`(输入 / 输入缓存 / 输出,位于卡片右下角;随当前启用模型与峰谷时段自动切换,**切换模型时立即同步**)
15
20
  - **峰谷指示灯**:卡片右上角,高峰(9:00–12:00、14:00–18:00 北京时间)橙色,空闲绿色
16
21
  - **自动刷新**:每 5 分钟一次;点击卡片任意位置立即刷新
22
+ - **多语言**:界面文案跟随 DSH 语言(中文 / English)
17
23
  - 跟随应用明暗主题
18
24
 
19
- ## 安装
25
+ ## Supported scope / 支持范围
26
+
27
+ > ⚠️ 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.
28
+
29
+ > ⚠️ 本插件**仅支持 DeepSeek 官方平台**的余额显示:余额来自 DeepSeek 官方 API `GET https://api.deepseek.com/user/balance`(使用 DSH 中已配置的 `DEEPSEEK_API_KEY`)。通过 `DEEPSEEK_BASE_URL` 指向第三方中转/代理时,余额接口可能不可用或返回异常,**不保证兼容**,请使用 DeepSeek 官方平台账号。
30
+
31
+ ## Install / 安装
32
+
33
+ Requires the DSH CLI. Install from npm:
20
34
 
21
35
  需要 DSH CLI,从 npm 安装:
22
36
 
@@ -24,27 +38,43 @@ DeepSeek 用量侧边栏插件(DeepSeek Harness Web UI)
24
38
  dsh plugin --profile web add @dougen/dsh-deepseek-usage
25
39
  ```
26
40
 
41
+ 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).
42
+
27
43
  安装后重启网页应用:`dsh web`,打开 http://127.0.0.1:3080 ,侧边栏底部(Cordis 面板按钮下方)即出现用量卡片。
28
44
 
29
- ## 数据来源
45
+ ## Data sources / 数据来源
46
+
47
+ | Data | Source |
48
+ |---|---|
49
+ | Balance | Official DeepSeek API `GET /user/balance` (uses `DEEPSEEK_API_KEY` configured in DSH) |
50
+ | Unit pricing / peak window | Built-in price table (DeepSeek official peak/off-peak pricing snapshot, effective 2026-08-17) |
30
51
 
31
52
  | 数据 | 来源 |
32
53
  |---|---|
33
54
  | 余额 | DeepSeek 官方 API `GET /user/balance`(使用 DSH 中已配置的 `DEEPSEEK_API_KEY`) |
34
55
  | 档单价 / 峰谷时段 | 内置价格表(DeepSeek 官方峰谷定价快照,2026-08-17 起) |
35
56
 
57
+ Make sure `DEEPSEEK_API_KEY` is configured in DSH (Model settings page) before use.
58
+
36
59
  使用前请确保 DSH 已配置 `DEEPSEEK_API_KEY`(可在 DSH 设置中的模型页面配置)。
37
60
 
38
- ## 常见问题
61
+ ## FAQ / 常见问题
62
+
63
+ **Balance shows "—"? / 余额显示"—"?**
64
+
65
+ - `DEEPSEEK_API_KEY` is not configured — configure it in DSH settings and restart.
66
+ - Network or API error — the error message is shown below the card.
39
67
 
40
- **余额显示"—"?**
41
68
  - 未配置 `DEEPSEEK_API_KEY`:在 DSH 设置中配置后重启。
42
69
  - 网络或接口异常:错误信息会显示在卡片下方。
43
70
 
44
- **单价与官方价格不一致?**
71
+ **Price differs from the official one? / 单价与官方价格不一致?**
72
+
73
+ - 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.
74
+
45
75
  - 单价按**当前启用模型**与**峰谷时段**自动切换;若当前模型不在内置价格表,会按 `deepseek-chat` 档展示并给出提示。
46
76
 
47
- ## 卸载
77
+ ## Uninstall / 卸载
48
78
 
49
79
  ```sh
50
80
  dsh plugin --profile web remove @dougen/dsh-deepseek-usage
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);
@@ -76,10 +93,29 @@ window.__ModuleLoader__.load({
76
93
  const [data, setData] = useState(null);
77
94
  const [error, setError] = useState("");
78
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
+ };
79
115
 
80
116
  const load = useCallback(async () => {
81
117
  try {
82
- const res = await fetch(USAGE_PATH, { cache: "no-store" });
118
+ const res = await fetch(USAGE_PATH + "?lang=" + lang, { cache: "no-store" });
83
119
  let body = null;
84
120
  try {
85
121
  body = await res.json();
@@ -94,7 +130,7 @@ window.__ModuleLoader__.load({
94
130
  if (!mounted.current) return;
95
131
  setError(e instanceof Error ? e.message : String(e));
96
132
  }
97
- }, []);
133
+ }, [lang]);
98
134
 
99
135
  useEffect(() => {
100
136
  mounted.current = true;
@@ -132,7 +168,9 @@ window.__ModuleLoader__.load({
132
168
  background: data && data.peak ? C.peak : C.idle,
133
169
  boxShadow: "0 0 4px rgba(0,0,0,.3)",
134
170
  };
135
- 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");
136
174
 
137
175
  if (!wide) {
138
176
  return h("div", {
@@ -151,13 +189,12 @@ window.__ModuleLoader__.load({
151
189
  gap: 5,
152
190
  userSelect: "none",
153
191
  },
154
- title,
155
192
  onClick: load,
156
193
  },
157
194
  h("span", { style: { display: "inline-flex", alignItems: "baseline", gap: 3 } },
158
195
  h("span", { style: { fontWeight: 700, fontSize: 13, color: C.label } }, symbol),
159
196
  h("span", { style: { fontWeight: 700, fontSize: 13, color: C.label } }, money(balance && balance.total))),
160
- h("span", { style: dot, title: data && data.peak ? "高峰时段" : "空闲时段" }));
197
+ h("span", { style: dot, title: peakTitle }));
161
198
  }
162
199
 
163
200
  const Price = (icon, label, value) =>
@@ -185,7 +222,6 @@ window.__ModuleLoader__.load({
185
222
  fontSize: "12px",
186
223
  lineHeight: "15px",
187
224
  },
188
- title,
189
225
  onClick: load,
190
226
  },
191
227
  // 第一行:余额(24px,符号与数字 3px 间距)
@@ -194,24 +230,30 @@ window.__ModuleLoader__.load({
194
230
  h("span", { style: { fontWeight: 700, fontSize: 24, color: C.label, lineHeight: "28px" } }, money(balance && balance.total))),
195
231
  h("span", {
196
232
  style: { position: "absolute", top: 8, right: 9, ...dot },
197
- title: data && data.peak ? "高峰时段 " + (data.peakHours || "") : "空闲时段",
233
+ title: peakTitle,
198
234
  }),
199
235
  // 当前档单价:卡片右下角
200
236
  h("span", {
201
237
  style: { position: "absolute", right: 9, bottom: 4, display: "inline-flex", alignItems: "center", gap: 5 },
202
238
  },
203
- Price(h(IconIn, null), "输入", tier && tier.input),
204
- Price(h(IconCache, null), "输入缓存", tier && tier.cacheRead),
205
- 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)),
206
242
  errs.length
207
243
  ? h("div", { style: { color: C.err, fontSize: 10, whiteSpace: "pre-wrap" } }, errs.join(" | "))
208
244
  : null);
209
245
  }
210
246
 
211
247
  // ---- client plugin body -----------------------------------------
212
- const inject = ["slots", "remote"];
248
+ const inject = ["slots", "remote", "locale"];
213
249
 
214
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) { /* 词典注册失败不影响卡片 */ }
215
257
  // 注入本包样式:footerActions 换行(卡片独占一行)。
216
258
  const tagId = "dsh-deepseek-usage";
217
259
  if (typeof document !== "undefined" && document.querySelector(`style[data-plugin-css="${tagId}"]`) === null) {
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,9 @@ 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
+
96
131
  // ---- 当前模型当前档单价(展示用)----
97
132
  const now = Date.now();
98
133
  let model = DEFAULT_MODEL;
@@ -110,7 +145,7 @@ function apply(ctx) {
110
145
  try {
111
146
  const hit = await ctx.credentials.resolve(API_KEY_REF);
112
147
  if (hit === void 0) {
113
- errors.push("未配置 DEEPSEEK_API_KEY");
148
+ errors.push(msg.noKey);
114
149
  } else {
115
150
  const response = await fetch(balanceUrl(), {
116
151
  headers: { Authorization: `Bearer ${hit.value}`, Accept: "application/json" },
@@ -118,7 +153,7 @@ function apply(ctx) {
118
153
  });
119
154
  const text = await response.text();
120
155
  if (!response.ok) {
121
- errors.push(`余额接口 HTTP ${response.status}`);
156
+ errors.push(msg.balanceHttp(response.status));
122
157
  } else {
123
158
  const body = JSON.parse(text);
124
159
  const infos = body && Array.isArray(body.balance_infos) ? body.balance_infos : [];
@@ -131,17 +166,17 @@ function apply(ctx) {
131
166
  }
132
167
  }
133
168
  } catch (error) {
134
- errors.push(`余额获取失败: ${error instanceof Error ? error.message : String(error)}`);
169
+ errors.push(msg.balanceFail(error instanceof Error ? error.message : String(error)));
135
170
  }
136
171
 
137
172
  // ---- 档单价提示(未知模型回退 chat 档)----
138
- if (!unit.known) errors.push(`当前模型 ${model} 不在价格表,档单价按 deepseek-chat 展示`);
173
+ if (!unit.known) errors.push(msg.modelUnknown(model));
139
174
 
140
175
  sendJson(res, 200, {
141
176
  ok: true,
142
177
  balance,
143
178
  peak: isPeak(now),
144
- peakHours: PEAK_HOURS_TEXT,
179
+ peakHours: lang === "en" ? PEAK_HOURS_TEXT_EN : PEAK_HOURS_TEXT,
145
180
  tier: {
146
181
  input: unit.input,
147
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.2",
4
- "description": "DeepSeek 用量侧边栏插件:余额(官方 API)、当前档单价与峰谷时段指示灯。",
3
+ "version": "0.4.1",
4
+ "description": "DeepSeek 余额侧边栏插件:余额(官方 API)、当前档单价与峰谷时段指示灯,支持中/英文界面。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "files": [