@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.
Files changed (3) hide show
  1. package/README.md +40 -3
  2. package/lib/client.js +94 -12
  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 footer, above **Settings**.
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 余额 $1058.69 · ¥-0.01
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 · ¥-0.01
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
- /** The entry whose figure stands for the account: the first non-zero one, else the first. */
49
- function primaryOf(balances) {
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 !== 0) return balances[index];
111
+ if (Number.isFinite(value) && value > 0) shown.push(balances[index]);
53
112
  }
54
- return balances.length > 0 ? balances[0] : undefined;
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
- const id = setInterval(load, REFRESH_MS);
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 = balances.slice(0, 2).map(amountOf).join(" · ");
142
- const lines = ["DeepSeek 余额(常驻 node 采集)"];
143
- for (let index = 0; index < balances.length; index += 1) {
144
- const entry = balances[index];
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 = primaryOf(balances);
160
- const glyph = state.phase === "ok" ? symbolOf(primary === undefined ? "" : primary.currency) : state.phase === "error" ? "!" : "…";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@virzz/dsh-plugin-deepseek-balance",
3
- "version": "1.0.1",
3
+ "version": "1.2.0",
4
4
  "description": "DeepSeek official account balance as a DSH sidebar status row, collected by one resident node process",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",