@dougen/dsh-deepseek-usage 0.3.2 → 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 +62 -4
- package/lib/client.js +53 -11
- package/lib/index.js +42 -7
- package/lib/pricing.js +3 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
# @dougen/dsh-deepseek-usage
|
|
2
2
|
|
|
3
|
-
DeepSeek
|
|
3
|
+
DeepSeek 余额侧边栏插件(DeepSeek Harness Web UI)
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
在 DSH **网页界面**的侧边栏底部常驻显示你的 **DeepSeek 账号余额**(放大显示)与**当前模型档单价**卡片,并提供高峰/空闲计费时段指示灯。
|
|
5
|
+
在 DSH **网页界面**的侧边栏底部常驻显示你的 **DeepSeek 账号余额**(放大显示)与**当前模型档单价**,并提供高峰/空闲计费时段指示灯。界面语言跟随 DSH 语言设置(自动检测浏览器语言,中文界面显示中文,其他语言显示英文)。
|
|
8
6
|
|
|
9
7
|

|
|
10
8
|
|
|
@@ -14,8 +12,13 @@ DeepSeek 用量侧边栏插件(DeepSeek Harness Web UI)
|
|
|
14
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
|
+

|
|
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);
|
|
@@ -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
|
|
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:
|
|
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:
|
|
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), "
|
|
204
|
-
Price(h(IconCache, null), "
|
|
205
|
-
Price(h(IconOut, null), "
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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.
|
|
4
|
-
"description": "DeepSeek
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "DeepSeek 余额侧边栏插件:余额(官方 API)、当前档单价与峰谷时段指示灯,支持中/英文界面。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"files": [
|