@virzz/dsh-plugin-deepseek-balance 1.0.1 → 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 +40 -3
- package/lib/client.js +94 -12
- 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
|
|
|
@@ -34,6 +35,34 @@ Design notes:
|
|
|
34
35
|
the browser. The read route returns only balance figures.
|
|
35
36
|
- **Two cadences.** The child collects every 60s; the row re-reads the host's cached
|
|
36
37
|
snapshot every 15s, so no polling happens from the browser.
|
|
38
|
+
- **Positive balances only.** A currency whose total is zero or negative says nothing useful
|
|
39
|
+
in a balance read-out, so it is left out of both the row and its tooltip. The tooltip
|
|
40
|
+
reports how many entries were hidden, and the row reads `0` when nothing is positive. The
|
|
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.
|
|
37
66
|
|
|
38
67
|
## Install into DSH
|
|
39
68
|
|
|
@@ -107,7 +136,7 @@ The row appears in the sidebar footer, directly above **Settings**:
|
|
|
107
136
|
|
|
108
137
|
```
|
|
109
138
|
◆ Cordis Plugin 0 running
|
|
110
|
-
▤ DeepSeek 余额 $1058.69
|
|
139
|
+
▤ DeepSeek 余额 空闲 $1058.69
|
|
111
140
|
⚙ 设置
|
|
112
141
|
```
|
|
113
142
|
|
|
@@ -211,3 +240,11 @@ lines, snapshot, and route JSON without a DSH process:
|
|
|
211
240
|
```sh
|
|
212
241
|
DEEPSEEK_API_KEY=... node test/host-smoke.mjs
|
|
213
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,13 +50,67 @@ window.__ModuleLoader__.load({
|
|
|
45
50
|
return symbolOf(entry.currency) + entry.total;
|
|
46
51
|
}
|
|
47
52
|
|
|
48
|
-
/**
|
|
49
|
-
|
|
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
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The balances worth showing. A currency whose total is zero or negative carries no
|
|
104
|
+
* information for a balance read-out, so it is left out of both the row and its
|
|
105
|
+
* tooltip — and only counted, so the tooltip can say that something was hidden.
|
|
106
|
+
*/
|
|
107
|
+
function positiveOf(balances) {
|
|
108
|
+
const shown = [];
|
|
50
109
|
for (let index = 0; index < balances.length; index += 1) {
|
|
51
110
|
const value = Number(balances[index].total);
|
|
52
|
-
if (Number.isFinite(value) && value
|
|
111
|
+
if (Number.isFinite(value) && value > 0) shown.push(balances[index]);
|
|
53
112
|
}
|
|
54
|
-
return
|
|
113
|
+
return shown;
|
|
55
114
|
}
|
|
56
115
|
|
|
57
116
|
function timeText(ms) {
|
|
@@ -97,6 +156,7 @@ window.__ModuleLoader__.load({
|
|
|
97
156
|
balances: [],
|
|
98
157
|
fetchedAt: 0,
|
|
99
158
|
});
|
|
159
|
+
const [now, setNow] = React.useState(() => Date.now());
|
|
100
160
|
|
|
101
161
|
const load = () => {
|
|
102
162
|
fetch(ROUTE, { headers: { accept: "application/json" } })
|
|
@@ -126,11 +186,26 @@ window.__ModuleLoader__.load({
|
|
|
126
186
|
|
|
127
187
|
React.useEffect(() => {
|
|
128
188
|
load();
|
|
129
|
-
|
|
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);
|
|
130
196
|
return () => clearInterval(id);
|
|
131
197
|
}, []);
|
|
132
198
|
|
|
133
199
|
const balances = state.balances;
|
|
200
|
+
const shown = positiveOf(balances);
|
|
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) + ")" : "");
|
|
134
209
|
let tone = "loading";
|
|
135
210
|
let label = "DeepSeek 余额";
|
|
136
211
|
let value = "…";
|
|
@@ -138,12 +213,14 @@ window.__ModuleLoader__.load({
|
|
|
138
213
|
|
|
139
214
|
if (state.phase === "ok") {
|
|
140
215
|
tone = state.available ? "ok" : "error";
|
|
141
|
-
value =
|
|
142
|
-
const lines = ["DeepSeek 余额(常驻 node 采集)"];
|
|
143
|
-
for (let index = 0; index <
|
|
144
|
-
const entry =
|
|
216
|
+
value = shown.length > 0 ? shown.slice(0, 2).map(amountOf).join(" · ") : "0";
|
|
217
|
+
const lines = ["DeepSeek 余额(常驻 node 采集)", periodLine];
|
|
218
|
+
for (let index = 0; index < shown.length; index += 1) {
|
|
219
|
+
const entry = shown[index];
|
|
145
220
|
lines.push(entry.currency + " " + entry.total + "(赠金 " + entry.granted + " · 充值 " + entry.toppedUp + ")");
|
|
146
221
|
}
|
|
222
|
+
if (shown.length === 0) lines.push("所有币种余额均不大于 0")
|
|
223
|
+
else if (hidden > 0) lines.push("已隐藏 " + hidden + " 项非正余额")
|
|
147
224
|
if (state.available !== true) lines.push("该账户当前不可调用");
|
|
148
225
|
const updated = timeText(state.fetchedAt);
|
|
149
226
|
if (updated !== "") lines.push("采集于 " + updated);
|
|
@@ -156,12 +233,15 @@ window.__ModuleLoader__.load({
|
|
|
156
233
|
}
|
|
157
234
|
|
|
158
235
|
if (wide !== true) {
|
|
159
|
-
const primary =
|
|
160
|
-
const glyph = state.phase === "ok"
|
|
236
|
+
const primary = shown.length > 0 ? shown[0] : undefined;
|
|
237
|
+
const glyph = state.phase === "ok"
|
|
238
|
+
? (primary === undefined ? "0" : symbolOf(primary.currency))
|
|
239
|
+
: state.phase === "error" ? "!" : "…";
|
|
161
240
|
return React.createElement("button", {
|
|
162
241
|
type: "button",
|
|
163
242
|
className: "dsb-row dsb-rail",
|
|
164
243
|
"data-tone": tone,
|
|
244
|
+
"data-period": period,
|
|
165
245
|
"data-busy": state.phase === "loading" ? "true" : "false",
|
|
166
246
|
title: detail,
|
|
167
247
|
"aria-label": label + " " + value,
|
|
@@ -173,6 +253,7 @@ window.__ModuleLoader__.load({
|
|
|
173
253
|
type: "button",
|
|
174
254
|
className: "dsb-row",
|
|
175
255
|
"data-tone": tone,
|
|
256
|
+
"data-period": period,
|
|
176
257
|
"data-busy": state.phase === "loading" ? "true" : "false",
|
|
177
258
|
title: detail,
|
|
178
259
|
"aria-label": label + " " + value,
|
|
@@ -180,6 +261,7 @@ window.__ModuleLoader__.load({
|
|
|
180
261
|
}, [
|
|
181
262
|
React.createElement("span", { key: "icon", className: "dsb-icon" }, walletGlyph()),
|
|
182
263
|
React.createElement("span", { key: "label", className: "dsb-label" }, label),
|
|
264
|
+
React.createElement("span", { key: "period", className: "dsb-period" }, periodShort),
|
|
183
265
|
React.createElement("span", { key: "value", className: "dsb-value" }, value),
|
|
184
266
|
]);
|
|
185
267
|
}
|
package/package.json
CHANGED