@virzz/dsh-plugin-deepseek-balance 1.1.0 → 1.2.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 +36 -3
- package/lib/client.js +74 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# @virzz/dsh-plugin-deepseek-balance
|
|
2
2
|
|
|
3
3
|
A [DSH](https://github.com/deepseek-ai/deepseek-harness) plugin that shows the DeepSeek
|
|
4
|
-
official account balance as a row in the sidebar
|
|
4
|
+
official account balance — and the current peak/off-peak tariff — as a row in the sidebar
|
|
5
|
+
footer, above **Settings**.
|
|
5
6
|
|
|
6
7
|
```
|
|
7
8
|
◆ Cordis Plugin 0 running
|
|
8
|
-
▤ DeepSeek 余额
|
|
9
|
+
▤ DeepSeek 余额 空闲 $1031.31
|
|
9
10
|
⚙ 设置
|
|
10
11
|
```
|
|
11
12
|
|
|
@@ -38,6 +39,30 @@ Design notes:
|
|
|
38
39
|
in a balance read-out, so it is left out of both the row and its tooltip. The tooltip
|
|
39
40
|
reports how many entries were hidden, and the row reads `0` when nothing is positive. The
|
|
40
41
|
host still returns every currency it receives — the rule is presentation, not data.
|
|
42
|
+
- **The tariff is live.** A chip next to the label shows which DeepSeek price window is in
|
|
43
|
+
force right now, and the tooltip counts down to the next switch. It is derived from the
|
|
44
|
+
clock on every tick, not from the balance fetch, so it stays right even when the API is
|
|
45
|
+
unreachable.
|
|
46
|
+
|
|
47
|
+
### Peak and off-peak windows
|
|
48
|
+
|
|
49
|
+
Off-peak costs half of peak. In Beijing time (UTC+8), peak is **Mon–Fri 09:00–12:00 and
|
|
50
|
+
14:00–18:00**; everything else is off-peak. DeepSeek's off-peak discount exists so that batch
|
|
51
|
+
work can be shifted out of the busy hours — the row's tooltip says as much in one line:
|
|
52
|
+
*空闲时段随便蹬* vs *高峰时段建议节约蹬*.
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
▤ DeepSeek 余额 空闲 $1031.31 ← Mon–Fri outside the windows, weekends, nights
|
|
56
|
+
▤ DeepSeek 余额 高峰 $1031.31 ← Mon–Fri 09:00–12:00, 14:00–18:00
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The chip carries the window name only; the price ratio and the countdown to the next switch
|
|
60
|
+
live in the tooltip.
|
|
61
|
+
|
|
62
|
+
The boundaries are half-open — 12:00 and 18:00 are already off-peak — and the calculation
|
|
63
|
+
applies a fixed +8h shift to epoch milliseconds rather than reading the browser's local time,
|
|
64
|
+
so it is correct wherever the page is opened. `test/period.test.mjs` pins the boundaries,
|
|
65
|
+
both weekend days, and the 63-hour Friday-evening-to-Monday-morning gap.
|
|
41
66
|
|
|
42
67
|
## Install into DSH
|
|
43
68
|
|
|
@@ -111,7 +136,7 @@ The row appears in the sidebar footer, directly above **Settings**:
|
|
|
111
136
|
|
|
112
137
|
```
|
|
113
138
|
◆ Cordis Plugin 0 running
|
|
114
|
-
▤ DeepSeek 余额
|
|
139
|
+
▤ DeepSeek 余额 空闲 $1058.69
|
|
115
140
|
⚙ 设置
|
|
116
141
|
```
|
|
117
142
|
|
|
@@ -215,3 +240,11 @@ lines, snapshot, and route JSON without a DSH process:
|
|
|
215
240
|
```sh
|
|
216
241
|
DEEPSEEK_API_KEY=... node test/host-smoke.mjs
|
|
217
242
|
```
|
|
243
|
+
|
|
244
|
+
`test/period.test.mjs` runs without any credential and is part of the publish workflow's
|
|
245
|
+
`check` job. It lifts the tariff helpers out of `lib/client.js` and asserts them, so the
|
|
246
|
+
boundaries cannot drift from the shipped code:
|
|
247
|
+
|
|
248
|
+
```sh
|
|
249
|
+
node test/period.test.mjs
|
|
250
|
+
```
|
package/lib/client.js
CHANGED
|
@@ -27,11 +27,16 @@ window.__ModuleLoader__.load({
|
|
|
27
27
|
".dsb-icon{flex:none;place-items:center;width:16px;height:16px;display:inline-flex;color:var(--dsw-alias-label-secondary)}",
|
|
28
28
|
".dsb-label{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}",
|
|
29
29
|
".dsb-value{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;white-space:nowrap;flex:none;margin-left:auto;font-size:12px;line-height:16px}",
|
|
30
|
+
".dsb-period{flex:none;display:inline-flex;align-items:center;height:18px;padding:0 6px;border:.5px solid currentColor;border-radius:6px;font-size:11px;line-height:1;letter-spacing:.02em;opacity:.85}",
|
|
31
|
+
'.dsb-row[data-period="off"] .dsb-period{color:var(--dsw-alias-state-success-primary)}',
|
|
32
|
+
'.dsb-row[data-period="peak"] .dsb-period{color:var(--dsw-alias-state-warn-primary)}',
|
|
30
33
|
'.dsb-row[data-tone="error"] .dsb-value{color:var(--dsw-alias-state-error-primary)}',
|
|
31
34
|
'.dsb-row[data-busy="true"] .dsb-value{opacity:.7}',
|
|
32
35
|
".dsb-row.dsb-rail{width:36px;height:36px;margin:0;padding:0;justify-content:center;gap:0;border-radius:50%}",
|
|
33
|
-
".dsb-row.dsb-rail .dsb-label,.dsb-row.dsb-rail .dsb-value{display:none}",
|
|
36
|
+
".dsb-row.dsb-rail .dsb-label,.dsb-row.dsb-rail .dsb-value,.dsb-row.dsb-rail .dsb-period{display:none}",
|
|
34
37
|
".dsb-railGlyph{color:var(--dsw-alias-label-secondary);font-size:13px;line-height:1}",
|
|
38
|
+
'.dsb-row.dsb-rail[data-period="off"] .dsb-railGlyph{color:var(--dsw-alias-state-success-primary)}',
|
|
39
|
+
'.dsb-row.dsb-rail[data-period="peak"] .dsb-railGlyph{color:var(--dsw-alias-state-warn-primary)}',
|
|
35
40
|
].join("");
|
|
36
41
|
|
|
37
42
|
function symbolOf(currency) {
|
|
@@ -45,6 +50,55 @@ window.__ModuleLoader__.load({
|
|
|
45
50
|
return symbolOf(entry.currency) + entry.total;
|
|
46
51
|
}
|
|
47
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Peak pricing windows as minutes after midnight, Beijing time. DeepSeek charges half
|
|
55
|
+
* price outside them. Beijing is UTC+8 with no daylight saving, so a fixed shift keeps
|
|
56
|
+
* this correct no matter which timezone the browser happens to be in.
|
|
57
|
+
*/
|
|
58
|
+
const BEIJING_SHIFT_MS = 8 * 60 * 60 * 1000;
|
|
59
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
60
|
+
const PEAK_WINDOWS = [
|
|
61
|
+
[9 * 60, 12 * 60],
|
|
62
|
+
[14 * 60, 18 * 60],
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Which tariff applies at an instant: "peak" inside a weekday peak window, else "off".
|
|
67
|
+
*
|
|
68
|
+
* @param ms - epoch milliseconds.
|
|
69
|
+
* @returns the tariff key.
|
|
70
|
+
*/
|
|
71
|
+
function periodOf(ms) {
|
|
72
|
+
const shifted = ms + BEIJING_SHIFT_MS;
|
|
73
|
+
const day = Math.floor(shifted / DAY_MS);
|
|
74
|
+
const minute = Math.floor((shifted - day * DAY_MS) / 60000);
|
|
75
|
+
const weekday = (day + 4) % 7; // 1970-01-01 was a Thursday, so 0 = Sunday here
|
|
76
|
+
if (weekday < 1 || weekday > 5) return "off";
|
|
77
|
+
for (let index = 0; index < PEAK_WINDOWS.length; index += 1) {
|
|
78
|
+
if (minute >= PEAK_WINDOWS[index][0] && minute < PEAK_WINDOWS[index][1]) return "peak";
|
|
79
|
+
}
|
|
80
|
+
return "off";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Whole minutes until the tariff flips. The longest gap is Fri 18:00 → Mon 09:00. */
|
|
84
|
+
function minutesUntilSwitch(ms) {
|
|
85
|
+
const current = periodOf(ms);
|
|
86
|
+
const limit = 4 * 24 * 60;
|
|
87
|
+
for (let step = 1; step <= limit; step += 1) {
|
|
88
|
+
if (periodOf(ms + step * 60000) !== current) return step;
|
|
89
|
+
}
|
|
90
|
+
return 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function humanMinutes(total) {
|
|
94
|
+
if (total <= 0) return "";
|
|
95
|
+
const hours = Math.floor(total / 60);
|
|
96
|
+
const minutes = total % 60;
|
|
97
|
+
if (hours === 0) return minutes + " 分钟";
|
|
98
|
+
if (minutes === 0) return hours + " 小时";
|
|
99
|
+
return hours + " 小时 " + minutes + " 分";
|
|
100
|
+
}
|
|
101
|
+
|
|
48
102
|
/**
|
|
49
103
|
* The balances worth showing. A currency whose total is zero or negative carries no
|
|
50
104
|
* information for a balance read-out, so it is left out of both the row and its
|
|
@@ -102,6 +156,7 @@ window.__ModuleLoader__.load({
|
|
|
102
156
|
balances: [],
|
|
103
157
|
fetchedAt: 0,
|
|
104
158
|
});
|
|
159
|
+
const [now, setNow] = React.useState(() => Date.now());
|
|
105
160
|
|
|
106
161
|
const load = () => {
|
|
107
162
|
fetch(ROUTE, { headers: { accept: "application/json" } })
|
|
@@ -131,13 +186,26 @@ window.__ModuleLoader__.load({
|
|
|
131
186
|
|
|
132
187
|
React.useEffect(() => {
|
|
133
188
|
load();
|
|
134
|
-
|
|
189
|
+
setNow(Date.now());
|
|
190
|
+
// The tariff flips on wall-clock time rather than on a fetch, so the tick drives
|
|
191
|
+
// both the clock the period is read from and the snapshot re-read.
|
|
192
|
+
const id = setInterval(() => {
|
|
193
|
+
setNow(Date.now());
|
|
194
|
+
load();
|
|
195
|
+
}, REFRESH_MS);
|
|
135
196
|
return () => clearInterval(id);
|
|
136
197
|
}, []);
|
|
137
198
|
|
|
138
199
|
const balances = state.balances;
|
|
139
200
|
const shown = positiveOf(balances);
|
|
140
201
|
const hidden = balances.length - shown.length;
|
|
202
|
+
const period = periodOf(now);
|
|
203
|
+
const periodShort = period === "peak" ? "高峰" : "空闲";
|
|
204
|
+
const periodPrice = period === "peak" ? "全价" : "½价";
|
|
205
|
+
const periodAdvice = period === "peak" ? "高峰时段建议节约蹬" : "空闲时段随便蹬";
|
|
206
|
+
const untilSwitch = minutesUntilSwitch(now);
|
|
207
|
+
const periodLine = periodShort + "时段 · " + periodPrice + " · " + periodAdvice
|
|
208
|
+
+ (untilSwitch > 0 ? "(距切换还有 " + humanMinutes(untilSwitch) + ")" : "");
|
|
141
209
|
let tone = "loading";
|
|
142
210
|
let label = "DeepSeek 余额";
|
|
143
211
|
let value = "…";
|
|
@@ -146,7 +214,7 @@ window.__ModuleLoader__.load({
|
|
|
146
214
|
if (state.phase === "ok") {
|
|
147
215
|
tone = state.available ? "ok" : "error";
|
|
148
216
|
value = shown.length > 0 ? shown.slice(0, 2).map(amountOf).join(" · ") : "0";
|
|
149
|
-
const lines = ["DeepSeek 余额(常驻 node 采集)"];
|
|
217
|
+
const lines = ["DeepSeek 余额(常驻 node 采集)", periodLine];
|
|
150
218
|
for (let index = 0; index < shown.length; index += 1) {
|
|
151
219
|
const entry = shown[index];
|
|
152
220
|
lines.push(entry.currency + " " + entry.total + "(赠金 " + entry.granted + " · 充值 " + entry.toppedUp + ")");
|
|
@@ -173,6 +241,7 @@ window.__ModuleLoader__.load({
|
|
|
173
241
|
type: "button",
|
|
174
242
|
className: "dsb-row dsb-rail",
|
|
175
243
|
"data-tone": tone,
|
|
244
|
+
"data-period": period,
|
|
176
245
|
"data-busy": state.phase === "loading" ? "true" : "false",
|
|
177
246
|
title: detail,
|
|
178
247
|
"aria-label": label + " " + value,
|
|
@@ -184,6 +253,7 @@ window.__ModuleLoader__.load({
|
|
|
184
253
|
type: "button",
|
|
185
254
|
className: "dsb-row",
|
|
186
255
|
"data-tone": tone,
|
|
256
|
+
"data-period": period,
|
|
187
257
|
"data-busy": state.phase === "loading" ? "true" : "false",
|
|
188
258
|
title: detail,
|
|
189
259
|
"aria-label": label + " " + value,
|
|
@@ -191,6 +261,7 @@ window.__ModuleLoader__.load({
|
|
|
191
261
|
}, [
|
|
192
262
|
React.createElement("span", { key: "icon", className: "dsb-icon" }, walletGlyph()),
|
|
193
263
|
React.createElement("span", { key: "label", className: "dsb-label" }, label),
|
|
264
|
+
React.createElement("span", { key: "period", className: "dsb-period" }, periodShort),
|
|
194
265
|
React.createElement("span", { key: "value", className: "dsb-value" }, value),
|
|
195
266
|
]);
|
|
196
267
|
}
|
package/package.json
CHANGED