@hystudio/dsh-quota-pill 0.2.5 → 0.2.7
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/client.js +39 -6
- package/package.json +1 -1
- package/src/adapters-apikey.js +37 -5
- package/src/adapters-cliproxy.js +13 -2
package/client.js
CHANGED
|
@@ -513,6 +513,30 @@ window.__ModuleLoader__.load({
|
|
|
513
513
|
credits: null,
|
|
514
514
|
}];
|
|
515
515
|
}
|
|
516
|
+
// Prepaid wallets (official DeepSeek): a balance, plus optional bonus
|
|
517
|
+
// credit. Both are money, not percentages, so each needs its own row —
|
|
518
|
+
// collapsing them into one label silently loses the bonus.
|
|
519
|
+
if (v.kind === 'payg' && v.credits && typeof v.credits.remain === 'number') {
|
|
520
|
+
const rows = [{
|
|
521
|
+
key: 'balance',
|
|
522
|
+
label: pick(lang, 'balanceLeft'),
|
|
523
|
+
pct: null,
|
|
524
|
+
reset: '',
|
|
525
|
+
credits: { remain: v.credits.remain, unit: v.credits.unit },
|
|
526
|
+
money: true,
|
|
527
|
+
}];
|
|
528
|
+
if (v.bonus && typeof v.bonus.amount === 'number') {
|
|
529
|
+
rows.push({
|
|
530
|
+
key: 'bonus',
|
|
531
|
+
label: pick(lang, 'bonusBalance'),
|
|
532
|
+
pct: null,
|
|
533
|
+
reset: '',
|
|
534
|
+
credits: { remain: v.bonus.amount, unit: v.bonus.unit },
|
|
535
|
+
money: true,
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
return rows;
|
|
539
|
+
}
|
|
516
540
|
if (v.kind !== 'subscription-quota' || v.remainingPct === null || v.remainingPct === undefined) {
|
|
517
541
|
return [{ key: 'kind', label: describeQuota(v, lang).text, pct: null, reset: '', credits: null }];
|
|
518
542
|
}
|
|
@@ -1250,7 +1274,7 @@ window.__ModuleLoader__.load({
|
|
|
1250
1274
|
}
|
|
1251
1275
|
|
|
1252
1276
|
function WindowRow(props) {
|
|
1253
|
-
const { label, pct, reset, credits, lang, stale } = props;
|
|
1277
|
+
const { label, pct, reset, credits, lang, stale, money: isMoney } = props;
|
|
1254
1278
|
const isNum = typeof pct === 'number' && Number.isFinite(pct);
|
|
1255
1279
|
const tweened = useTween(isNum ? pct : null, 450);
|
|
1256
1280
|
// 首帧 tween 还没拿到起点时 shown 是 null;这时按目标值渲染静态读数,
|
|
@@ -1271,7 +1295,14 @@ window.__ModuleLoader__.load({
|
|
|
1271
1295
|
h('div', { className: 'quota-pill-win-main' },
|
|
1272
1296
|
h('div', { className: 'quota-pill-win-text' },
|
|
1273
1297
|
h('span', { className: 'quota-pill-win-label', title: label }, label),
|
|
1274
|
-
|
|
1298
|
+
// Reserve the reset line even when there is nothing to show.
|
|
1299
|
+
// Conditionally omitting it made rows with a reset taller than those
|
|
1300
|
+
// without one, so the column of numbers did not line up.
|
|
1301
|
+
h('span', {
|
|
1302
|
+
className: 'quota-pill-win-reset',
|
|
1303
|
+
title: reset || undefined,
|
|
1304
|
+
style: reset ? undefined : { visibility: 'hidden' },
|
|
1305
|
+
}, reset || '\u00a0'),
|
|
1275
1306
|
),
|
|
1276
1307
|
h('div', { className: 'quota-pill-win-gauge' },
|
|
1277
1308
|
showCredits
|
|
@@ -1281,7 +1312,7 @@ window.__ModuleLoader__.load({
|
|
|
1281
1312
|
? `${fmtAmount(credits.remain)} / ${fmtAmount(credits.total)}`
|
|
1282
1313
|
+ (credits.unit ? ' ' + String(credits.unit) : '')
|
|
1283
1314
|
: undefined,
|
|
1284
|
-
}, fmtCompact(credits.remain))
|
|
1315
|
+
}, isMoney ? money(credits.remain, credits.unit) : fmtCompact(credits.remain))
|
|
1285
1316
|
: isNum
|
|
1286
1317
|
? h('span', {
|
|
1287
1318
|
className: 'quota-pill-win-pct',
|
|
@@ -1293,7 +1324,7 @@ window.__ModuleLoader__.load({
|
|
|
1293
1324
|
style: { color: remainingColor(shown) },
|
|
1294
1325
|
}, typeof shown === 'number' ? `${shown.toFixed(shown < 10 ? 1 : 0)}%` : '')
|
|
1295
1326
|
: null,
|
|
1296
|
-
showCredits && unitLabel(credits.unit, lang)
|
|
1327
|
+
showCredits && !isMoney && unitLabel(credits.unit, lang)
|
|
1297
1328
|
? h('span', { className: 'quota-pill-win-unit' }, unitLabel(credits.unit, lang))
|
|
1298
1329
|
: null,
|
|
1299
1330
|
|
|
@@ -1313,7 +1344,7 @@ window.__ModuleLoader__.load({
|
|
|
1313
1344
|
h('div', { className: 'quota-pill-group-head' }, name),
|
|
1314
1345
|
// 组内行 key 带组名,避免不同 provider 的同名窗口(都是 5h/weekly)
|
|
1315
1346
|
// 被 React 当成同一个组件实例,导致 tween 跨 provider 插值。
|
|
1316
|
-
entries.map((e) => h(WindowRow, { key: `${name}|${e.key}`, label: e.label, pct: e.pct, reset: e.reset, credits: e.credits, lang, stale })),
|
|
1347
|
+
entries.map((e) => h(WindowRow, { key: `${name}|${e.key}`, label: e.label, pct: e.pct, reset: e.reset, credits: e.credits, lang, stale, money: e.money })),
|
|
1317
1348
|
);
|
|
1318
1349
|
}
|
|
1319
1350
|
|
|
@@ -1388,7 +1419,7 @@ window.__ModuleLoader__.load({
|
|
|
1388
1419
|
// key 带 provider:换 provider 时强制重挂载,让 tween 从「空」开始,
|
|
1389
1420
|
// 而不是把 A 的 5h 平滑滑到 B 的 5h——那是两个不同账户的额度,
|
|
1390
1421
|
// 中间的数字不代表任何真实状态。
|
|
1391
|
-
entries.map((e) => h(WindowRow, { key: `${providerName}|${e.key}`, label: e.label, pct: e.pct, reset: e.reset, credits: e.credits, lang, stale })),
|
|
1422
|
+
entries.map((e) => h(WindowRow, { key: `${providerName}|${e.key}`, label: e.label, pct: e.pct, reset: e.reset, credits: e.credits, lang, stale, money: e.money })),
|
|
1392
1423
|
),
|
|
1393
1424
|
h('dl', { className: 'quota-pill-stats' },
|
|
1394
1425
|
data.account ? statRow(pick(lang, 'account'), String(data.account)) : null,
|
|
@@ -1504,6 +1535,8 @@ window.__ModuleLoader__.load({
|
|
|
1504
1535
|
'.quota-pill-win-unit{flex:none;margin-left:3px;color:var(--dsw-alias-label-tertiary)}',
|
|
1505
1536
|
|
|
1506
1537
|
'.quota-pill-win-ring{flex:none;display:inline-flex}',
|
|
1538
|
+
// 固定行高:没有重置时间的行也要占住这一行,数字才对得齐
|
|
1539
|
+
'.quota-pill-win-reset{min-height:14px}',
|
|
1507
1540
|
// Sidecar not running: show the fix instead of a bare dash.
|
|
1508
1541
|
'.quota-pill-offline{display:flex;flex-direction:column;gap:6px;padding:10px 2px}',
|
|
1509
1542
|
'.quota-pill-offline-title{font-weight:600;color:var(--dsw-alias-label-primary,inherit)}',
|
package/package.json
CHANGED
package/src/adapters-apikey.js
CHANGED
|
@@ -11,6 +11,27 @@ import { getJson } from './http.js';
|
|
|
11
11
|
const clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, n));
|
|
12
12
|
const round2 = (n) => Math.round(n * 100) / 100;
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Normalise a reset timestamp to ISO, or null when there is none.
|
|
16
|
+
*
|
|
17
|
+
* Upstreams are inconsistent: some send ISO strings, some epoch milliseconds,
|
|
18
|
+
* and some use `0` to mean "this window has no reset pending" (Command Code's
|
|
19
|
+
* five-hour window does exactly that). Treating 0 as a date would render 1970.
|
|
20
|
+
*/
|
|
21
|
+
function isoReset(v) {
|
|
22
|
+
if (v == null || v === '' || v === 0) return null;
|
|
23
|
+
if (typeof v === 'string') {
|
|
24
|
+
// Already ISO-ish: keep as-is so the client formats it consistently.
|
|
25
|
+
const t = Date.parse(v);
|
|
26
|
+
return Number.isFinite(t) ? new Date(t).toISOString() : null;
|
|
27
|
+
}
|
|
28
|
+
const n = Number(v);
|
|
29
|
+
if (!Number.isFinite(n) || n <= 0) return null;
|
|
30
|
+
// Seconds vs milliseconds: anything below ~year 33658 in ms is really seconds.
|
|
31
|
+
const ms = n < 1e11 ? n * 1000 : n;
|
|
32
|
+
return new Date(ms).toISOString();
|
|
33
|
+
}
|
|
34
|
+
|
|
14
35
|
/** Expand `products` (window → used%) into the `windows` rows the pill renders. */
|
|
15
36
|
function toWindows(products, resetByKey = {}) {
|
|
16
37
|
const out = [];
|
|
@@ -217,7 +238,7 @@ export async function commandcode() {
|
|
|
217
238
|
const pct = round2((Number(used) / Number(cap)) * 100);
|
|
218
239
|
products[label] = pct;
|
|
219
240
|
ratios.push(100 - pct);
|
|
220
|
-
if (tight === null || pct > tight[1]) tight = [key, pct, w.resetAt];
|
|
241
|
+
if (tight === null || pct > tight[1]) tight = [key, pct, isoReset(w.resetAt)];
|
|
221
242
|
}
|
|
222
243
|
|
|
223
244
|
const monthlyLeft = cred.monthlyCredits;
|
|
@@ -237,14 +258,21 @@ export async function commandcode() {
|
|
|
237
258
|
|
|
238
259
|
// Plan identity (GOAT / Pro / Max) lives on a sibling endpoint; failure there
|
|
239
260
|
// must not lose the quota we already have.
|
|
261
|
+
// Plan identity and the monthly period live on a sibling endpoint, nested under
|
|
262
|
+
// `data`. Getting this wrong is silent: the quota still renders, just with a
|
|
263
|
+
// null plan and a monthly window that cannot show when it renews.
|
|
240
264
|
let plan = null;
|
|
265
|
+
let monthlyReset = null;
|
|
241
266
|
try {
|
|
242
267
|
const sub = await getJson('https://api.commandcode.ai/alpha/billing/subscriptions', { token });
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
268
|
+
const d = sub.ok && sub.data ? (sub.data.data || {}) : {};
|
|
269
|
+
plan = d.planId || null;
|
|
270
|
+
// currentPeriodEnd is the real monthly renewal — better than deriving it
|
|
271
|
+
// from the weekly window, which was my earlier guess.
|
|
272
|
+
monthlyReset = d.currentPeriodEnd || null;
|
|
246
273
|
} catch {
|
|
247
274
|
plan = null;
|
|
275
|
+
monthlyReset = null;
|
|
248
276
|
}
|
|
249
277
|
|
|
250
278
|
const remaining = round2(Math.min(...ratios));
|
|
@@ -261,8 +289,12 @@ export async function commandcode() {
|
|
|
261
289
|
const r = {};
|
|
262
290
|
for (const [label, key] of [['5h', 'fiveHour'], ['weekly', 'weekly']]) {
|
|
263
291
|
const w = win[key] || {};
|
|
264
|
-
|
|
292
|
+
const iso = isoReset(w.resetAt);
|
|
293
|
+
if (iso) r[label] = iso;
|
|
265
294
|
}
|
|
295
|
+
// The monthly ceiling is derived, so its reset must come from the
|
|
296
|
+
// subscription period; the credits endpoint does not carry one.
|
|
297
|
+
if (monthlyReset) r.monthly = monthlyReset;
|
|
266
298
|
return r;
|
|
267
299
|
})()),
|
|
268
300
|
raw: { source: 'api.commandcode.ai/alpha/billing/credits', plan },
|
package/src/adapters-cliproxy.js
CHANGED
|
@@ -72,6 +72,14 @@ function isoFromEpoch(sec) {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
|
|
75
|
+
/** Same reset for every product row — xAI reports one billing period for all. */
|
|
76
|
+
function fillReset(products, resetAt) {
|
|
77
|
+
const out = {};
|
|
78
|
+
if (!resetAt) return out;
|
|
79
|
+
for (const key of Object.keys(products || {})) out[key] = resetAt;
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
75
83
|
/**
|
|
76
84
|
* Expand `products` (window → used%) into the `windows` array the client
|
|
77
85
|
* renders. The client shows one row per window with its own reset time, so an
|
|
@@ -142,7 +150,10 @@ export async function xai(model) {
|
|
|
142
150
|
window: period.type || null,
|
|
143
151
|
resetAt: period.end || cfg.billingPeriodEnd || null,
|
|
144
152
|
products,
|
|
145
|
-
|
|
153
|
+
// Every per-product row shares the account's billing period, so they all
|
|
154
|
+
// carry the same reset. Leaving it empty made those rows look like they
|
|
155
|
+
// never expire while the summary right above them showed a date.
|
|
156
|
+
windows: toWindows(products, fillReset(products, period.end || cfg.billingPeriodEnd)),
|
|
146
157
|
raw: { source: 'billing?format=credits', config: cfg },
|
|
147
158
|
};
|
|
148
159
|
}
|
|
@@ -175,7 +186,7 @@ export async function xai(model) {
|
|
|
175
186
|
window: period.type || null,
|
|
176
187
|
resetAt: period.end || cfg.billingPeriodEnd || null,
|
|
177
188
|
products,
|
|
178
|
-
windows: toWindows(products,
|
|
189
|
+
windows: toWindows(products, fillReset(products, period.end || cfg.billingPeriodEnd)),
|
|
179
190
|
detail: {
|
|
180
191
|
tokens: `${rl.remaining_tokens}/${rl.limit_tokens}`,
|
|
181
192
|
requests: `${rl.remaining_requests}/${rl.limit_requests}`,
|