@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.
Files changed (3) hide show
  1. package/README.md +36 -3
  2. package/lib/client.js +74 -3
  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
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 余额 $1058.69
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
- 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);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@virzz/dsh-plugin-deepseek-balance",
3
- "version": "1.1.0",
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",