@ychris12138/dsh-usage-stats 0.2.10 → 0.3.1
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 +97 -20
- package/SECURITY.md +3 -1
- package/docs/release-checklist.md +111 -0
- package/docs/release-notes-v0.3.0.md +25 -0
- package/docs/release-notes-v0.3.1.md +40 -0
- package/lib/accounts.js +421 -172
- package/lib/balance.js +116 -15
- package/lib/billing.js +319 -0
- package/lib/client.js +481 -86
- package/lib/export.js +227 -0
- package/lib/index.js +506 -64
- package/lib/network.js +65 -0
- package/lib/orcarouter.js +79 -0
- package/lib/pricing.js +391 -0
- package/lib/provider-identity.js +129 -0
- package/lib/usage.js +190 -13
- package/package.json +17 -5
package/lib/client.js
CHANGED
|
@@ -59,12 +59,16 @@ window.__ModuleLoader__.load({
|
|
|
59
59
|
".usg_providerPickerLabel{color:var(--dsw-alias-label-tertiary);flex:none}",
|
|
60
60
|
".usg_providerSelect{box-sizing:border-box;min-width:0;flex:1;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-overlay,var(--dsw-alias-bg-base));border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:4px 6px;font:inherit;font-size:12px;line-height:18px}",
|
|
61
61
|
".usg_accountGrid{flex-direction:column;gap:8px;display:flex}",
|
|
62
|
+
".usg_exportLinks{display:flex;flex-wrap:wrap;gap:6px}",
|
|
63
|
+
".usg_exportLink{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-fill-l1,transparent);border:1px solid var(--dsw-alias-border-l2);border-radius:7px;padding:4px 7px;font-size:11px;line-height:16px;text-decoration:none}",
|
|
64
|
+
".usg_exportLink:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}",
|
|
62
65
|
".usg_accountCard{--usg-providerAccent:#1f6feb;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:linear-gradient(135deg,color-mix(in srgb,var(--usg-providerAccent) 8%,transparent),transparent 42%);border-radius:12px;padding:10px 11px;display:flex;flex-direction:column;gap:9px}",
|
|
63
66
|
".usg_accountCard[data-provider=deepseek],.usg_accountCard[data-provider=deepseek-official]{--usg-providerAccent:#1f6feb}",
|
|
64
67
|
".usg_accountCard[data-provider=opencode-go]{--usg-providerAccent:#00a67d}",
|
|
65
68
|
".usg_accountCard[data-provider=ollama],.usg_accountCard[data-adapter=ollama]{--usg-providerAccent:#d97706}",
|
|
66
69
|
".usg_accountCard[data-provider=zai],.usg_accountCard[data-provider=zai-coding-cn]{--usg-providerAccent:#7656e8}",
|
|
67
70
|
".usg_accountCard[data-provider=openrouter]{--usg-providerAccent:#6366f1}",
|
|
71
|
+
".usg_accountCard[data-provider=orcarouter]{--usg-providerAccent:#0891b2}",
|
|
68
72
|
".usg_accountCard[data-provider=moonshotai],.usg_accountCard[data-provider=moonshotai-cn],.usg_accountCard[data-provider=kimi],.usg_accountCard[data-provider=kimi-coding]{--usg-providerAccent:#e07a1f}",
|
|
69
73
|
".usg_accountHead{align-items:center;gap:8px;display:flex}",
|
|
70
74
|
".usg_accountMark{width:24px;height:24px;color:#fff;background:var(--usg-providerAccent);border-radius:7px;justify-content:center;align-items:center;font-size:10px;font-weight:700;display:flex;box-shadow:0 4px 12px color-mix(in srgb,var(--usg-providerAccent) 25%,transparent)}",
|
|
@@ -73,6 +77,9 @@ window.__ModuleLoader__.load({
|
|
|
73
77
|
".usg_accountPlan{color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;font-size:10px;line-height:14px;overflow:hidden}",
|
|
74
78
|
".usg_accountStatus{color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-fill-l2);border-radius:999px;padding:2px 7px;font-size:10px;line-height:16px;white-space:nowrap}",
|
|
75
79
|
".usg_accountStatus[data-status=ok]{color:var(--usg-providerAccent);background:color-mix(in srgb,var(--usg-providerAccent) 12%,transparent)}",
|
|
80
|
+
".usg_accountHealth{border-top:1px solid var(--dsw-alias-border-l1);grid-template-columns:auto minmax(0,1fr);gap:3px 10px;margin:2px 0 0;padding-top:7px;display:grid}",
|
|
81
|
+
".usg_accountHealth dt{color:var(--dsw-alias-label-caption);font-size:10px;line-height:15px}",
|
|
82
|
+
".usg_accountHealth dd{color:var(--dsw-alias-label-secondary);min-width:0;margin:0;font-size:10px;line-height:15px;text-align:right;overflow-wrap:anywhere}",
|
|
76
83
|
".usg_quotaList{flex-direction:column;gap:8px;display:flex}",
|
|
77
84
|
".usg_quotaRow{display:flex;flex-direction:column;gap:4px}",
|
|
78
85
|
".usg_quotaMeta{align-items:baseline;gap:8px;display:flex}",
|
|
@@ -116,8 +123,8 @@ window.__ModuleLoader__.load({
|
|
|
116
123
|
".usg_day{width:100%;min-height:30px;align-items:center;gap:8px;border:0;background:0 0;border-bottom:1px solid var(--dsw-alias-border-l1);padding:5px 0;font:inherit;text-align:left;cursor:pointer;display:flex}",
|
|
117
124
|
".usg_day:last-child{border-bottom:0}",
|
|
118
125
|
".usg_day:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
119
|
-
".usg_dayDate{color:var(--dsw-alias-label-secondary);flex:
|
|
120
|
-
".usg_dayTokens{color:var(--dsw-alias-label-primary);flex:none;font-size:12px;line-height:18px;font-variant-numeric:tabular-nums}",
|
|
126
|
+
".usg_dayDate{color:var(--dsw-alias-label-secondary);flex:0 1 104px;min-width:0;text-overflow:ellipsis;white-space:nowrap;font-size:12px;line-height:18px;font-variant-numeric:tabular-nums;overflow:hidden}",
|
|
127
|
+
".usg_dayTokens{color:var(--dsw-alias-label-primary);flex:none;min-width:84px;text-align:right;font-size:12px;line-height:18px;font-variant-numeric:tabular-nums}",
|
|
121
128
|
".usg_dayHit{color:var(--dsw-alias-label-tertiary);flex:none;width:52px;font-size:11px;line-height:18px;font-variant-numeric:tabular-nums;text-align:right}",
|
|
122
129
|
".usg_dayBar{background:var(--usg-blue);border-radius:2px;height:6px;flex:1;min-width:4px;opacity:.65}",
|
|
123
130
|
".usg_detailHeader{align-items:center;gap:8px;display:flex}",
|
|
@@ -171,6 +178,8 @@ window.__ModuleLoader__.load({
|
|
|
171
178
|
providerPickerLabel: "usg_providerPickerLabel",
|
|
172
179
|
providerSelect: "usg_providerSelect",
|
|
173
180
|
accountGrid: "usg_accountGrid",
|
|
181
|
+
exportLinks: "usg_exportLinks",
|
|
182
|
+
exportLink: "usg_exportLink",
|
|
174
183
|
accountCard: "usg_accountCard",
|
|
175
184
|
accountHead: "usg_accountHead",
|
|
176
185
|
accountMark: "usg_accountMark",
|
|
@@ -178,6 +187,7 @@ window.__ModuleLoader__.load({
|
|
|
178
187
|
accountName: "usg_accountName",
|
|
179
188
|
accountPlan: "usg_accountPlan",
|
|
180
189
|
accountStatus: "usg_accountStatus",
|
|
190
|
+
accountHealth: "usg_accountHealth",
|
|
181
191
|
quotaList: "usg_quotaList",
|
|
182
192
|
quotaRow: "usg_quotaRow",
|
|
183
193
|
quotaMeta: "usg_quotaMeta",
|
|
@@ -301,6 +311,21 @@ window.__ModuleLoader__.load({
|
|
|
301
311
|
}
|
|
302
312
|
}
|
|
303
313
|
|
|
314
|
+
function budgetWindowText(period, window, translate) {
|
|
315
|
+
if (window === null || typeof window !== "object" || window.limit === null || window.limit === void 0) return null;
|
|
316
|
+
const spent = window.costComplete === true ? fmtCurrency(window.estimatedSpend, window.currency) : "—";
|
|
317
|
+
const limit = fmtCurrency(window.limit, window.currency);
|
|
318
|
+
const level = ["normal", "warning", "critical"].includes(window.level)
|
|
319
|
+
? translate(`budget.level.${window.level}`)
|
|
320
|
+
: translate("budget.level.unknown");
|
|
321
|
+
return translate("budget.summary", {
|
|
322
|
+
period: translate(`budget.period.${period}`),
|
|
323
|
+
spent,
|
|
324
|
+
limit,
|
|
325
|
+
level
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
304
329
|
/**
|
|
305
330
|
* Collapsed-badge account value, derived from the unified v0.2.0 account
|
|
306
331
|
* snapshot. Balance providers show a real-currency amount; subscription /
|
|
@@ -367,13 +392,25 @@ window.__ModuleLoader__.load({
|
|
|
367
392
|
return !inside(layer) && !inside(panel);
|
|
368
393
|
}
|
|
369
394
|
|
|
395
|
+
const SAFE_DIAGNOSTIC_REASONS = new Set([
|
|
396
|
+
"dns-resolution-failed",
|
|
397
|
+
"timeout",
|
|
398
|
+
"rate-limited",
|
|
399
|
+
"unauthorized",
|
|
400
|
+
"upstream-invalid-json",
|
|
401
|
+
"upstream-not-json",
|
|
402
|
+
"upstream-too-large",
|
|
403
|
+
"upstream-invalid-response",
|
|
404
|
+
"blocked-network",
|
|
405
|
+
"all-addresses-unreachable",
|
|
406
|
+
"no-validated-address",
|
|
407
|
+
"sub2api-balance-shape-unrecognized",
|
|
408
|
+
"unknown"
|
|
409
|
+
]);
|
|
410
|
+
|
|
370
411
|
/** Defense-in-depth filter for server-provided, secret-free diagnostic reasons. */
|
|
371
412
|
function safeDiagnosticReason(reason) {
|
|
372
|
-
|
|
373
|
-
const value = reason.trim();
|
|
374
|
-
if (value === "" || value.length > 160) return null;
|
|
375
|
-
if (/(authorization|bearer\s|api[_-]?key|cookie|token\s*[=:]|sk-[a-z0-9])/i.test(value)) return null;
|
|
376
|
-
return value;
|
|
413
|
+
return typeof reason === "string" && SAFE_DIAGNOSTIC_REASONS.has(reason) ? reason : null;
|
|
377
414
|
}
|
|
378
415
|
|
|
379
416
|
function diagnosticReasonText(reason, translate) {
|
|
@@ -383,8 +420,15 @@ window.__ModuleLoader__.load({
|
|
|
383
420
|
if (value === "all-addresses-unreachable") return translate("account.reason.allAddressesUnreachable");
|
|
384
421
|
if (value === "upstream-not-json") return translate("account.reason.upstreamNotJson");
|
|
385
422
|
if (value === "upstream-invalid-json") return translate("account.reason.upstreamInvalidJson");
|
|
423
|
+
if (value === "upstream-too-large") return translate("account.reason.upstreamTooLarge");
|
|
424
|
+
if (value === "upstream-invalid-response") return translate("account.reason.upstreamInvalidResponse");
|
|
425
|
+
if (value === "timeout") return translate("account.reason.timeout");
|
|
426
|
+
if (value === "rate-limited") return translate("account.reason.rateLimited");
|
|
427
|
+
if (value === "unauthorized") return translate("account.reason.unauthorized");
|
|
428
|
+
if (value === "blocked-network") return translate("account.reason.blockedNetwork");
|
|
429
|
+
if (value === "no-validated-address") return translate("account.reason.noValidatedAddress");
|
|
386
430
|
if (value === "sub2api-balance-shape-unrecognized") return translate("account.reason.sub2apiBalanceShapeUnrecognized");
|
|
387
|
-
return
|
|
431
|
+
return translate("account.reason.unknown");
|
|
388
432
|
}
|
|
389
433
|
|
|
390
434
|
/**
|
|
@@ -398,20 +442,80 @@ window.__ModuleLoader__.load({
|
|
|
398
442
|
})) : [];
|
|
399
443
|
}
|
|
400
444
|
|
|
445
|
+
const ORCAROUTER_ADD_SENTINEL = "__add_orcarouter__";
|
|
446
|
+
const ORCAROUTER_INTEGRATION_PATH = "/api/usage-stats/integrations/orcarouter";
|
|
447
|
+
|
|
448
|
+
/** Append the explicit setup action without changing the real provider list. */
|
|
449
|
+
function buildProviderPickerChoices(providers, integration) {
|
|
450
|
+
if (!Array.isArray(providers)) return [];
|
|
451
|
+
if (providers.some((provider) => provider?.id === "orcarouter")) return providers;
|
|
452
|
+
if (integration?.available !== true || integration?.installed !== false) return providers;
|
|
453
|
+
return [...providers, {
|
|
454
|
+
id: ORCAROUTER_ADD_SENTINEL,
|
|
455
|
+
displayName: "OrcaRouter",
|
|
456
|
+
accountMode: "balance",
|
|
457
|
+
synthetic: true
|
|
458
|
+
}];
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** Localized selector label; sponsorship is a small disclosure, not a card. */
|
|
462
|
+
function providerChoiceLabel(provider, translate) {
|
|
463
|
+
const label = typeof provider?.displayName === "string" && provider.displayName.trim() !== ""
|
|
464
|
+
? provider.displayName
|
|
465
|
+
: String(provider?.id ?? "");
|
|
466
|
+
if (provider?.id !== "orcarouter" && provider?.id !== ORCAROUTER_ADD_SENTINEL) return label;
|
|
467
|
+
const suffix = typeof translate === "function" ? translate("orcarouter.choiceSuffix") : "";
|
|
468
|
+
return typeof suffix === "string" && suffix !== "" ? `${label}${suffix}` : label;
|
|
469
|
+
}
|
|
470
|
+
|
|
401
471
|
/** Locale-safe template interpolation: `t("key", {a})` replaces `{a}`. */
|
|
402
472
|
function interpolate(template, params) {
|
|
403
473
|
if (params === void 0) return template;
|
|
404
474
|
return template.replace(/\{(\w+)\}/g, (match, key) => (Object.hasOwn(params, key) ? String(params[key]) : match));
|
|
405
475
|
}
|
|
406
476
|
|
|
407
|
-
async function fetchJson(path) {
|
|
408
|
-
const response = await fetch(path, {
|
|
477
|
+
async function fetchJson(path, options = {}) {
|
|
478
|
+
const response = await fetch(path, {
|
|
479
|
+
...options,
|
|
480
|
+
headers: { accept: "application/json", ...options.headers }
|
|
481
|
+
});
|
|
409
482
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
410
483
|
const payload = await response.json();
|
|
411
484
|
if (payload === null || typeof payload !== "object") throw new Error("unexpected response");
|
|
412
485
|
return payload;
|
|
413
486
|
}
|
|
414
487
|
|
|
488
|
+
async function loadOrcaRouterIntegration(request = fetchJson) {
|
|
489
|
+
const payload = await request(ORCAROUTER_INTEGRATION_PATH);
|
|
490
|
+
if (payload?.ok !== true || payload.integration === null || typeof payload.integration !== "object") throw new Error("integration status unavailable");
|
|
491
|
+
return payload.integration;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async function addOrcaRouterIntegration(request = fetchJson) {
|
|
495
|
+
const payload = await request(ORCAROUTER_INTEGRATION_PATH, {
|
|
496
|
+
method: "POST",
|
|
497
|
+
headers: {
|
|
498
|
+
"content-type": "application/json",
|
|
499
|
+
"x-dsh-usage-stats-action": "add-orcarouter"
|
|
500
|
+
},
|
|
501
|
+
body: "{}"
|
|
502
|
+
});
|
|
503
|
+
if (payload?.ok !== true || payload.integration?.installed !== true) throw new Error("provider preset was not added");
|
|
504
|
+
return payload.integration;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function enableFooterActionWrapping(host) {
|
|
508
|
+
const hostStyle = window.getComputedStyle(host);
|
|
509
|
+
if (!hostStyle.display.includes("flex")
|
|
510
|
+
|| hostStyle.flexDirection.startsWith("column")
|
|
511
|
+
|| hostStyle.flexWrap !== "nowrap") return void 0;
|
|
512
|
+
const previous = host.style.flexWrap;
|
|
513
|
+
host.style.flexWrap = "wrap";
|
|
514
|
+
return () => {
|
|
515
|
+
host.style.flexWrap = previous;
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
415
519
|
/**
|
|
416
520
|
* Build one month's calendar heatmap: weeks as rows (Mon-first), only
|
|
417
521
|
* the month's own days, padded with null placeholders. Cell tokens come
|
|
@@ -473,6 +577,68 @@ window.__ModuleLoader__.load({
|
|
|
473
577
|
//#endregion
|
|
474
578
|
|
|
475
579
|
//#region UsageStatsPanel
|
|
580
|
+
const SELECTED_PROVIDER_STORAGE_KEY = "@ychris12138/dsh-usage-stats:selected-provider:v1";
|
|
581
|
+
|
|
582
|
+
function browserStorage() {
|
|
583
|
+
try {
|
|
584
|
+
return typeof window === "object" ? window.localStorage ?? null : null;
|
|
585
|
+
} catch {
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function validStoredProvider(value) {
|
|
591
|
+
return typeof value === "string"
|
|
592
|
+
&& value !== ORCAROUTER_ADD_SENTINEL
|
|
593
|
+
&& value.length > 0
|
|
594
|
+
&& value.length <= 256
|
|
595
|
+
&& !value.includes("\0") ? value : null;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function readSelectedProvider(storage = browserStorage()) {
|
|
599
|
+
try {
|
|
600
|
+
const stored = storage?.getItem?.(SELECTED_PROVIDER_STORAGE_KEY) ?? null;
|
|
601
|
+
const normalized = validStoredProvider(stored);
|
|
602
|
+
if (stored !== null && normalized === null) storage?.removeItem?.(SELECTED_PROVIDER_STORAGE_KEY);
|
|
603
|
+
return normalized;
|
|
604
|
+
} catch {
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function writeSelectedProvider(providerId, storage = browserStorage()) {
|
|
610
|
+
try {
|
|
611
|
+
// The synthetic dropdown entry is an action, never provider state. A
|
|
612
|
+
// defensive no-op also preserves the previous real selection if this
|
|
613
|
+
// helper is called directly by a future UI refactor.
|
|
614
|
+
if (providerId === ORCAROUTER_ADD_SENTINEL) return;
|
|
615
|
+
const normalized = validStoredProvider(providerId);
|
|
616
|
+
if (normalized === null) storage?.removeItem?.(SELECTED_PROVIDER_STORAGE_KEY);
|
|
617
|
+
else storage?.setItem?.(SELECTED_PROVIDER_STORAGE_KEY, normalized);
|
|
618
|
+
} catch {
|
|
619
|
+
// Storage may be disabled or quota-limited. Selection remains usable
|
|
620
|
+
// for the current mount and silently falls back on the next one.
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function reconcileSelectedProvider(current, providers, storage = browserStorage()) {
|
|
625
|
+
// An empty response can be a transient provider-service refresh. Keep the
|
|
626
|
+
// current choice until a non-empty authoritative list can prove removal.
|
|
627
|
+
if (!Array.isArray(providers) || providers.length === 0) return current;
|
|
628
|
+
if (Array.isArray(providers) && providers.some((provider) => provider?.id === current)) return current;
|
|
629
|
+
if (current !== null) writeSelectedProvider(null, storage);
|
|
630
|
+
if (!Array.isArray(providers) || providers.length === 0) return null;
|
|
631
|
+
return providers.find((provider) => provider.id === "deepseek-official" && provider.configured)?.id
|
|
632
|
+
?? providers.find((provider) => provider.id === "deepseek")?.id
|
|
633
|
+
?? providers.find((provider) => provider.configured)?.id
|
|
634
|
+
?? providers[0].id;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function authoritativeProviderList(payload) {
|
|
638
|
+
if (payload?.ok !== true) return null;
|
|
639
|
+
return Array.isArray(payload.providers) ? payload.providers : [];
|
|
640
|
+
}
|
|
641
|
+
|
|
476
642
|
/**
|
|
477
643
|
* Sidebar footer action: badge + floating panel with balance and usage.
|
|
478
644
|
* @param props - `wide` from the sidebar shell, `t` bound by the slot runtime.
|
|
@@ -485,13 +651,18 @@ window.__ModuleLoader__.load({
|
|
|
485
651
|
const [selectedDay, setSelectedDay] = react.useState(null);
|
|
486
652
|
const [viewMonth, setViewMonth] = react.useState(() => currentMonthKey());
|
|
487
653
|
const [providers, setProviders] = react.useState([]);
|
|
488
|
-
const [
|
|
489
|
-
const
|
|
654
|
+
const [providersAuthoritative, setProvidersAuthoritative] = react.useState(false);
|
|
655
|
+
const storageRef = react.useRef(browserStorage());
|
|
656
|
+
const [selectedProvider, setSelectedProvider] = react.useState(() => readSelectedProvider(storageRef.current));
|
|
490
657
|
const [account, setAccount] = react.useState(null);
|
|
491
658
|
const [accountLoading, setAccountLoading] = react.useState(false);
|
|
492
659
|
const [accountError, setAccountError] = react.useState(null);
|
|
660
|
+
const [orcaRouterIntegration, setOrcaRouterIntegration] = react.useState(null);
|
|
661
|
+
const [orcaRouterPending, setOrcaRouterPending] = react.useState(false);
|
|
662
|
+
const [orcaRouterError, setOrcaRouterError] = react.useState(false);
|
|
493
663
|
const [refreshedAt, setRefreshedAt] = react.useState(null);
|
|
494
664
|
const mountedRef = react.useRef(true);
|
|
665
|
+
const orcaRouterMutationRef = react.useRef(false);
|
|
495
666
|
const usageLoaderRef = react.useRef(null);
|
|
496
667
|
const accountLoaderRef = react.useRef(null);
|
|
497
668
|
const layerRef = react.useRef(null);
|
|
@@ -499,8 +670,24 @@ window.__ModuleLoader__.load({
|
|
|
499
670
|
if (usageLoaderRef.current === null) usageLoaderRef.current = createLoader();
|
|
500
671
|
if (accountLoaderRef.current === null) accountLoaderRef.current = createLoader();
|
|
501
672
|
const providerChoices = react.useMemo(() => buildProviderChoices(providers), [providers]);
|
|
673
|
+
const providerPickerChoices = react.useMemo(
|
|
674
|
+
() => buildProviderPickerChoices(providerChoices, orcaRouterIntegration),
|
|
675
|
+
[providerChoices, orcaRouterIntegration]
|
|
676
|
+
);
|
|
502
677
|
const selectedProviderInfo = providerChoices.find((provider) => provider.id === selectedProvider) ?? null;
|
|
503
|
-
|
|
678
|
+
const selectProvider = react.useCallback((providerId) => {
|
|
679
|
+
if (providerId === ORCAROUTER_ADD_SENTINEL) return;
|
|
680
|
+
const normalized = validStoredProvider(providerId);
|
|
681
|
+
setSelectedProvider(normalized);
|
|
682
|
+
writeSelectedProvider(normalized, storageRef.current);
|
|
683
|
+
}, []);
|
|
684
|
+
const closePanel = react.useCallback(() => {
|
|
685
|
+
// Re-assert the last visible choice at the explicit close boundary. This
|
|
686
|
+
// keeps the X action stable even when storage was temporarily unavailable
|
|
687
|
+
// during an earlier provider-selection event.
|
|
688
|
+
if (selectedProvider !== null) writeSelectedProvider(selectedProvider, storageRef.current);
|
|
689
|
+
setOpen(false);
|
|
690
|
+
}, [selectedProvider]);
|
|
504
691
|
const loadUsage = react.useCallback(() => {
|
|
505
692
|
const seq = usageLoaderRef.current.start();
|
|
506
693
|
setUsageError(null);
|
|
@@ -518,20 +705,60 @@ window.__ModuleLoader__.load({
|
|
|
518
705
|
});
|
|
519
706
|
}, []);
|
|
520
707
|
|
|
521
|
-
const loadProviders = react.useCallback(() => {
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
if (
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
}
|
|
528
|
-
const list = Array.isArray(payload.providers) ? payload.providers : [];
|
|
708
|
+
const loadProviders = react.useCallback(async () => {
|
|
709
|
+
try {
|
|
710
|
+
const payload = await fetchJson("/api/usage-stats/providers");
|
|
711
|
+
if (!mountedRef.current) return null;
|
|
712
|
+
const list = authoritativeProviderList(payload);
|
|
713
|
+
if (list === null) return null;
|
|
529
714
|
setProviders(list);
|
|
530
|
-
|
|
531
|
-
|
|
715
|
+
setProvidersAuthoritative(true);
|
|
716
|
+
return list;
|
|
717
|
+
} catch {
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
720
|
+
}, []);
|
|
721
|
+
|
|
722
|
+
const loadOrcaRouterStatus = react.useCallback(async () => {
|
|
723
|
+
if (mountedRef.current) setOrcaRouterError(false);
|
|
724
|
+
try {
|
|
725
|
+
const integration = await loadOrcaRouterIntegration();
|
|
726
|
+
if (mountedRef.current) setOrcaRouterIntegration(integration);
|
|
727
|
+
return integration;
|
|
728
|
+
} catch {
|
|
729
|
+
if (mountedRef.current) setOrcaRouterIntegration(null);
|
|
730
|
+
return null;
|
|
731
|
+
}
|
|
532
732
|
}, []);
|
|
533
733
|
|
|
534
|
-
const
|
|
734
|
+
const selectProviderChoice = react.useCallback(async (providerId) => {
|
|
735
|
+
if (providerId !== ORCAROUTER_ADD_SENTINEL) {
|
|
736
|
+
setOrcaRouterError(false);
|
|
737
|
+
selectProvider(providerId);
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
if (orcaRouterMutationRef.current) return;
|
|
741
|
+
orcaRouterMutationRef.current = true;
|
|
742
|
+
setOrcaRouterPending(true);
|
|
743
|
+
setOrcaRouterError(false);
|
|
744
|
+
try {
|
|
745
|
+
const integration = await addOrcaRouterIntegration();
|
|
746
|
+
const refreshedProviders = await loadProviders();
|
|
747
|
+
if (!mountedRef.current) return;
|
|
748
|
+
if (!Array.isArray(refreshedProviders) || !refreshedProviders.some((provider) => provider?.id === "orcarouter")) {
|
|
749
|
+
throw new Error("installed provider was not returned");
|
|
750
|
+
}
|
|
751
|
+
setOrcaRouterIntegration(integration);
|
|
752
|
+
selectProvider("orcarouter");
|
|
753
|
+
} catch {
|
|
754
|
+
if (mountedRef.current) setOrcaRouterError(true);
|
|
755
|
+
} finally {
|
|
756
|
+
orcaRouterMutationRef.current = false;
|
|
757
|
+
if (mountedRef.current) setOrcaRouterPending(false);
|
|
758
|
+
}
|
|
759
|
+
}, [loadProviders, selectProvider]);
|
|
760
|
+
|
|
761
|
+
const loadAccount = react.useCallback((providerId, force = false, activity = null) => {
|
|
535
762
|
const seq = accountLoaderRef.current.start();
|
|
536
763
|
setAccountLoading(true);
|
|
537
764
|
setAccountError(null);
|
|
@@ -541,7 +768,8 @@ window.__ModuleLoader__.load({
|
|
|
541
768
|
setAccountError("no providers");
|
|
542
769
|
return;
|
|
543
770
|
}
|
|
544
|
-
const
|
|
771
|
+
const activityQuery = activity === "detail" ? "&activity=detail" : "";
|
|
772
|
+
const query = `?provider=${encodeURIComponent(target)}${force ? "&refresh=1" : ""}${activityQuery}`;
|
|
545
773
|
fetchJson(`/api/usage-stats/account${query}`).then((payload) => {
|
|
546
774
|
if (!mountedRef.current || !accountLoaderRef.current.isCurrent(seq)) return;
|
|
547
775
|
if (payload.ok !== true) {
|
|
@@ -565,13 +793,12 @@ window.__ModuleLoader__.load({
|
|
|
565
793
|
};
|
|
566
794
|
}, []);
|
|
567
795
|
|
|
568
|
-
// The
|
|
569
|
-
//
|
|
570
|
-
//
|
|
571
|
-
//
|
|
572
|
-
//
|
|
573
|
-
//
|
|
574
|
-
// and a container that is already columnar is left untouched.
|
|
796
|
+
// The shared footer-actions host lays full-width registrations out in a
|
|
797
|
+
// row. Let those actions wrap onto separate lines for #21 without
|
|
798
|
+
// changing the row main axis used by sibling plugins (#82). Walking up
|
|
799
|
+
// avoids depending on the host's hashed CSS-module class. Existing
|
|
800
|
+
// column/wrap layouts are left untouched, and our inline wrap override
|
|
801
|
+
// is restored on unmount.
|
|
575
802
|
react.useEffect(() => {
|
|
576
803
|
let host = layerRef.current?.parentElement ?? null;
|
|
577
804
|
for (let depth = 0; host !== null && depth < 3; depth += 1) {
|
|
@@ -579,13 +806,7 @@ window.__ModuleLoader__.load({
|
|
|
579
806
|
host = host.parentElement;
|
|
580
807
|
}
|
|
581
808
|
if (host === null) return void 0;
|
|
582
|
-
|
|
583
|
-
if (!hostStyle.display.includes("flex") || hostStyle.flexDirection === "column") return void 0;
|
|
584
|
-
const previous = host.style.flexDirection;
|
|
585
|
-
host.style.flexDirection = "column";
|
|
586
|
-
return () => {
|
|
587
|
-
host.style.flexDirection = previous;
|
|
588
|
-
};
|
|
809
|
+
return enableFooterActionWrapping(host);
|
|
589
810
|
}, []);
|
|
590
811
|
|
|
591
812
|
|
|
@@ -611,41 +832,36 @@ window.__ModuleLoader__.load({
|
|
|
611
832
|
// Keep exactly one valid provider selected across independent provider
|
|
612
833
|
// and subscription responses. DeepSeek remains the initial preference.
|
|
613
834
|
react.useEffect(() => {
|
|
614
|
-
if (!
|
|
615
|
-
setSelectedProvider((current) =>
|
|
616
|
-
|
|
617
|
-
return providerChoices.find((provider) => provider.id === "deepseek-official" && provider.configured)?.id
|
|
618
|
-
?? providerChoices.find((provider) => provider.id === "deepseek")?.id
|
|
619
|
-
?? providerChoices.find((provider) => provider.configured)?.id
|
|
620
|
-
?? providerChoices[0].id;
|
|
621
|
-
});
|
|
622
|
-
}, [providerChoices, providersLoaded]);
|
|
835
|
+
if (!providersAuthoritative) return;
|
|
836
|
+
setSelectedProvider((current) => reconcileSelectedProvider(current, providerChoices, storageRef.current));
|
|
837
|
+
}, [providerChoices, providersAuthoritative]);
|
|
623
838
|
|
|
624
839
|
react.useEffect(() => {
|
|
625
840
|
if (!open) return;
|
|
626
841
|
loadUsage();
|
|
627
842
|
loadProviders();
|
|
843
|
+
loadOrcaRouterStatus();
|
|
628
844
|
const usageTimer = window.setInterval(loadUsage, 60000);
|
|
629
845
|
const providerTimer = window.setInterval(loadProviders, 300000);
|
|
630
846
|
return () => {
|
|
631
847
|
window.clearInterval(usageTimer);
|
|
632
848
|
window.clearInterval(providerTimer);
|
|
633
849
|
};
|
|
634
|
-
}, [open, loadUsage, loadProviders]);
|
|
850
|
+
}, [open, loadUsage, loadProviders, loadOrcaRouterStatus]);
|
|
635
851
|
|
|
636
852
|
// Fetch the selected account for BOTH the panel and the collapsed
|
|
637
|
-
// sidebar badge. The server
|
|
638
|
-
//
|
|
639
|
-
// minutes (no refresh=1
|
|
640
|
-
// the badge/panel never stays on a stale balance or quota value.
|
|
853
|
+
// sidebar badge. The server owns the adaptive upstream schedule; this
|
|
854
|
+
// pre-existing shared panel path only re-reads that cache every five
|
|
855
|
+
// minutes (no refresh=1), and marks an open detail view as active.
|
|
641
856
|
react.useEffect(() => {
|
|
642
857
|
if (selectedProvider === null) return;
|
|
643
|
-
|
|
644
|
-
|
|
858
|
+
const activity = open ? "detail" : null;
|
|
859
|
+
loadAccount(selectedProvider, false, activity);
|
|
860
|
+
const cacheTimer = window.setInterval(() => loadAccount(selectedProvider, false, activity), 300000);
|
|
645
861
|
return () => {
|
|
646
862
|
window.clearInterval(cacheTimer);
|
|
647
863
|
};
|
|
648
|
-
}, [selectedProvider, loadAccount]);
|
|
864
|
+
}, [selectedProvider, loadAccount, open]);
|
|
649
865
|
|
|
650
866
|
const dayMap = react.useMemo(() => {
|
|
651
867
|
const map = new Map();
|
|
@@ -702,6 +918,10 @@ window.__ModuleLoader__.load({
|
|
|
702
918
|
|
|
703
919
|
const selectedEntry = selectedDay !== null ? dayMap.get(selectedDay) ?? null : null;
|
|
704
920
|
const badgeCount = stats !== null ? fmtCompact(stats.dayTokens) : null;
|
|
921
|
+
const budgetRows = usage?.budgets === null || usage?.budgets === void 0 ? [] : [
|
|
922
|
+
["daily", usage.budgets.daily],
|
|
923
|
+
["monthly", usage.budgets.monthly]
|
|
924
|
+
].map(([period, window]) => ({ period, window, text: budgetWindowText(period, window, translate) })).filter((entry) => entry.text !== null);
|
|
705
925
|
|
|
706
926
|
// Collapsed-badge account value, derived from the unified v0.2.0
|
|
707
927
|
// account snapshot. Balance providers show a real-currency amount;
|
|
@@ -723,7 +943,7 @@ window.__ModuleLoader__.load({
|
|
|
723
943
|
const retry = () => {
|
|
724
944
|
loadUsage();
|
|
725
945
|
loadProviders();
|
|
726
|
-
if (selectedProvider !== null) loadAccount(selectedProvider, true);
|
|
946
|
+
if (selectedProvider !== null) loadAccount(selectedProvider, true, open ? "detail" : null);
|
|
727
947
|
};
|
|
728
948
|
|
|
729
949
|
const updatedLabel = refreshedAt === null ? "" : translate("panel.updatedAt", {
|
|
@@ -777,7 +997,7 @@ window.__ModuleLoader__.load({
|
|
|
777
997
|
type: "button",
|
|
778
998
|
className: S.iconButton,
|
|
779
999
|
"aria-label": translate("action.close"),
|
|
780
|
-
onClick:
|
|
1000
|
+
onClick: closePanel,
|
|
781
1001
|
children: react_jsx_runtime.jsx(primitives.IconCloseOutline16, { size: 14 })
|
|
782
1002
|
})
|
|
783
1003
|
})
|
|
@@ -799,20 +1019,27 @@ window.__ModuleLoader__.load({
|
|
|
799
1019
|
children: react_jsx_runtime.jsx("h3", { className: S.sectionTitle, children: translate("account.title") })
|
|
800
1020
|
}),
|
|
801
1021
|
react_jsx_runtime.jsx(ProviderPicker, {
|
|
802
|
-
providers:
|
|
1022
|
+
providers: providerPickerChoices,
|
|
803
1023
|
selectedProvider,
|
|
804
|
-
onSelect:
|
|
1024
|
+
onSelect: selectProviderChoice,
|
|
1025
|
+
disabled: orcaRouterPending,
|
|
805
1026
|
translate
|
|
806
1027
|
}),
|
|
1028
|
+
orcaRouterError ? react_jsx_runtime.jsx("p", {
|
|
1029
|
+
className: S.note,
|
|
1030
|
+
role: "status",
|
|
1031
|
+
"data-orcarouter-action-error": true,
|
|
1032
|
+
children: translate("orcarouter.addError")
|
|
1033
|
+
}) : null,
|
|
807
1034
|
selectedProviderInfo === null ? react_jsx_runtime.jsx("p", { className: S.note, children: translate("account.loading") }) : react_jsx_runtime.jsx("div", {
|
|
808
1035
|
className: S.accountGrid,
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
1036
|
+
children: react_jsx_runtime.jsx(ProviderAccountCard, {
|
|
1037
|
+
provider: selectedProviderInfo,
|
|
1038
|
+
account: account?.id === selectedProvider ? account : null,
|
|
1039
|
+
accountLoading,
|
|
1040
|
+
accountError,
|
|
1041
|
+
translate,
|
|
1042
|
+
onRetry: () => loadAccount(selectedProvider, true, "detail")
|
|
816
1043
|
}, selectedProviderInfo.id)
|
|
817
1044
|
}),
|
|
818
1045
|
react_jsx_runtime.jsx("section", {
|
|
@@ -837,9 +1064,9 @@ window.__ModuleLoader__.load({
|
|
|
837
1064
|
react_jsx_runtime.jsxs("div", {
|
|
838
1065
|
className: S.statsRow,
|
|
839
1066
|
children: [
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
1067
|
+
react_jsx_runtime.jsxs("div", { className: S.stat, children: [react_jsx_runtime.jsx("span", { className: S.statValue, children: fmt(stats.dayTokens) }), react_jsx_runtime.jsx("span", { className: S.statLabel, children: translate("usage.today") })] }),
|
|
1068
|
+
react_jsx_runtime.jsxs("div", { className: S.stat, children: [react_jsx_runtime.jsx("span", { className: S.statValue, children: fmt(stats.monthTokens) }), react_jsx_runtime.jsx("span", { className: S.statLabel, children: translate("usage.month") })] }),
|
|
1069
|
+
react_jsx_runtime.jsxs("div", { className: S.stat, children: [react_jsx_runtime.jsx("span", { className: S.statValue, children: fmt(stats.total) }), react_jsx_runtime.jsx("span", { className: S.statLabel, children: translate("usage.total") })] })
|
|
843
1070
|
]
|
|
844
1071
|
}),
|
|
845
1072
|
react_jsx_runtime.jsx("p", {
|
|
@@ -849,10 +1076,16 @@ window.__ModuleLoader__.load({
|
|
|
849
1076
|
translate("usage.hit.today"),
|
|
850
1077
|
": ",
|
|
851
1078
|
react_jsx_runtime.jsx("b", { children: fmtHit(stats.todayHit) })
|
|
852
|
-
|
|
853
|
-
})
|
|
1079
|
+
]
|
|
854
1080
|
})
|
|
855
|
-
|
|
1081
|
+
}),
|
|
1082
|
+
...budgetRows.map((entry) => react_jsx_runtime.jsx("p", {
|
|
1083
|
+
className: S.hitCaption,
|
|
1084
|
+
"data-budget-period": entry.period,
|
|
1085
|
+
"data-budget-level": entry.window.level,
|
|
1086
|
+
children: entry.text
|
|
1087
|
+
}, entry.period))
|
|
1088
|
+
]
|
|
856
1089
|
}),
|
|
857
1090
|
usage !== null && usageError === null && react_jsx_runtime.jsxs("section", {
|
|
858
1091
|
className: S.section,
|
|
@@ -898,7 +1131,7 @@ window.__ModuleLoader__.load({
|
|
|
898
1131
|
})
|
|
899
1132
|
]
|
|
900
1133
|
}),
|
|
901
|
-
|
|
1134
|
+
recent.length > 0 && react_jsx_runtime.jsxs("section", {
|
|
902
1135
|
className: S.section,
|
|
903
1136
|
children: [
|
|
904
1137
|
react_jsx_runtime.jsx("h3", { className: S.sectionTitle, children: translate("usage.recent") }),
|
|
@@ -923,8 +1156,22 @@ window.__ModuleLoader__.load({
|
|
|
923
1156
|
})
|
|
924
1157
|
})
|
|
925
1158
|
]
|
|
926
|
-
|
|
927
|
-
|
|
1159
|
+
}),
|
|
1160
|
+
react_jsx_runtime.jsxs("section", {
|
|
1161
|
+
className: S.section,
|
|
1162
|
+
children: [
|
|
1163
|
+
react_jsx_runtime.jsx("h3", { className: S.sectionTitle, children: translate("export.title") }),
|
|
1164
|
+
react_jsx_runtime.jsxs("div", {
|
|
1165
|
+
className: S.exportLinks,
|
|
1166
|
+
children: [
|
|
1167
|
+
react_jsx_runtime.jsx("a", { className: S.exportLink, href: "/api/usage-stats/export/daily.csv", download: "dsh-usage-daily.csv", "data-export-format": "daily-csv", children: translate("export.dailyCsv") }),
|
|
1168
|
+
react_jsx_runtime.jsx("a", { className: S.exportLink, href: "/api/usage-stats/export/sessions.csv", download: "dsh-usage-sessions.csv", "data-export-format": "sessions-csv", children: translate("export.sessionsCsv") }),
|
|
1169
|
+
react_jsx_runtime.jsx("a", { className: S.exportLink, href: "/api/usage-stats/export.json", download: "dsh-usage-stats.json", "data-export-format": "json", children: translate("export.json") })
|
|
1170
|
+
]
|
|
1171
|
+
})
|
|
1172
|
+
]
|
|
1173
|
+
}),
|
|
1174
|
+
updatedLabel !== "" && react_jsx_runtime.jsx("p", { className: S.footerNote, children: updatedLabel })
|
|
928
1175
|
]
|
|
929
1176
|
})
|
|
930
1177
|
]
|
|
@@ -963,6 +1210,7 @@ window.__ModuleLoader__.load({
|
|
|
963
1210
|
"opencode-go": "GO",
|
|
964
1211
|
ollama: "OL",
|
|
965
1212
|
openrouter: "OR",
|
|
1213
|
+
orcarouter: "OC",
|
|
966
1214
|
moonshotai: "K",
|
|
967
1215
|
"moonshotai-cn": "K",
|
|
968
1216
|
kimi: "K",
|
|
@@ -1015,7 +1263,7 @@ window.__ModuleLoader__.load({
|
|
|
1015
1263
|
}
|
|
1016
1264
|
|
|
1017
1265
|
/** Provider selector shared by monetary and subscription account modes. */
|
|
1018
|
-
function ProviderPicker({ providers, selectedProvider, onSelect, translate }) {
|
|
1266
|
+
function ProviderPicker({ providers, selectedProvider, onSelect, disabled = false, translate }) {
|
|
1019
1267
|
if (providers.length === 0) return null;
|
|
1020
1268
|
return react_jsx_runtime.jsxs("label", {
|
|
1021
1269
|
className: S.providerPicker,
|
|
@@ -1024,11 +1272,12 @@ window.__ModuleLoader__.load({
|
|
|
1024
1272
|
react_jsx_runtime.jsx("select", {
|
|
1025
1273
|
className: S.providerSelect,
|
|
1026
1274
|
value: selectedProvider ?? "",
|
|
1275
|
+
disabled,
|
|
1027
1276
|
"aria-label": translate("account.provider"),
|
|
1028
1277
|
onChange: (event) => onSelect(event.target.value),
|
|
1029
1278
|
children: providers.map((provider) => react_jsx_runtime.jsx("option", {
|
|
1030
1279
|
value: provider.id,
|
|
1031
|
-
children: provider
|
|
1280
|
+
children: providerChoiceLabel(provider, translate)
|
|
1032
1281
|
}, provider.id))
|
|
1033
1282
|
})
|
|
1034
1283
|
]
|
|
@@ -1043,6 +1292,7 @@ window.__ModuleLoader__.load({
|
|
|
1043
1292
|
if (status === "invalid-response") return translate("account.status.invalidResponse");
|
|
1044
1293
|
if (status === "blocked") return translate("account.status.blocked");
|
|
1045
1294
|
if (status === "unsupported") return translate("account.status.unsupported");
|
|
1295
|
+
if (status === "unknown") return translate("account.status.unknown");
|
|
1046
1296
|
return translate("subscription.status.unavailable");
|
|
1047
1297
|
}
|
|
1048
1298
|
|
|
@@ -1056,12 +1306,69 @@ window.__ModuleLoader__.load({
|
|
|
1056
1306
|
return kind;
|
|
1057
1307
|
}
|
|
1058
1308
|
|
|
1059
|
-
function
|
|
1309
|
+
function durationLabel(totalMinutes, translate) {
|
|
1310
|
+
if (totalMinutes >= 1440) {
|
|
1311
|
+
const days = Math.floor(totalMinutes / 1440);
|
|
1312
|
+
const hours = Math.floor(totalMinutes % 1440 / 60);
|
|
1313
|
+
return translate("duration.daysHours", { days, hours });
|
|
1314
|
+
}
|
|
1315
|
+
if (totalMinutes >= 60) {
|
|
1316
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
1317
|
+
const minutes = totalMinutes % 60;
|
|
1318
|
+
return translate("duration.hoursMinutes", { hours, minutes });
|
|
1319
|
+
}
|
|
1320
|
+
return translate("duration.minutes", { minutes: totalMinutes });
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
function formatResetCountdown(resetsAt, now, translate) {
|
|
1060
1324
|
if (typeof resetsAt !== "string") return "";
|
|
1061
1325
|
const date = new Date(resetsAt);
|
|
1062
1326
|
if (Number.isNaN(date.getTime())) return "";
|
|
1063
|
-
|
|
1064
|
-
|
|
1327
|
+
const diffMs = date.getTime() - now;
|
|
1328
|
+
if (diffMs <= 0) return translate("subscription.resetDue");
|
|
1329
|
+
const totalMinutes = Math.max(1, Math.ceil(diffMs / 60000));
|
|
1330
|
+
return translate("subscription.resets", { time: durationLabel(totalMinutes, translate) });
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
function resetLabel(resetsAt, translate, now = Date.now()) {
|
|
1334
|
+
return formatResetCountdown(resetsAt, now, translate);
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
function provenanceLabel(provenance, translate) {
|
|
1338
|
+
if (provenance === "official") return translate("account.provenance.official");
|
|
1339
|
+
if (provenance === "provider") return translate("account.provenance.provider");
|
|
1340
|
+
if (provenance === "configured") return translate("account.provenance.configured");
|
|
1341
|
+
if (provenance === "experimental") return translate("account.provenance.experimental");
|
|
1342
|
+
return translate("account.provenance.unknown");
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
function healthTimeLabel(value) {
|
|
1346
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return "";
|
|
1347
|
+
const date = new Date(value);
|
|
1348
|
+
return Number.isNaN(date.getTime()) ? "" : date.toLocaleString([], { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
function healthAgeLabel(value, translate) {
|
|
1352
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return "";
|
|
1353
|
+
return durationLabel(Math.floor(value / 60000), translate);
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
function AccountHealth({ account, translate }) {
|
|
1357
|
+
if (account === null || account === void 0 || typeof account !== "object") return null;
|
|
1358
|
+
const rows = [];
|
|
1359
|
+
const attempted = healthTimeLabel(account.lastAttemptAt);
|
|
1360
|
+
const succeeded = healthTimeLabel(account.lastSuccessAt);
|
|
1361
|
+
const age = healthAgeLabel(account.ageMs, translate);
|
|
1362
|
+
if (attempted !== "") rows.push(["account.health.lastAttempt", attempted]);
|
|
1363
|
+
if (succeeded !== "") rows.push(["account.health.lastSuccess", succeeded]);
|
|
1364
|
+
if (age !== "") rows.push(["account.health.age", age]);
|
|
1365
|
+
rows.push(["account.health.provenance", provenanceLabel(account.provenance, translate)]);
|
|
1366
|
+
return react_jsx_runtime.jsx("dl", {
|
|
1367
|
+
className: S.accountHealth,
|
|
1368
|
+
children: rows.flatMap(([label, value]) => [
|
|
1369
|
+
react_jsx_runtime.jsx("dt", { children: translate(label) }, `${label}-term`),
|
|
1370
|
+
react_jsx_runtime.jsx("dd", { children: value }, `${label}-value`)
|
|
1371
|
+
])
|
|
1065
1372
|
});
|
|
1066
1373
|
}
|
|
1067
1374
|
|
|
@@ -1115,7 +1422,8 @@ window.__ModuleLoader__.load({
|
|
|
1115
1422
|
const mode = account?.mode ?? provider.accountMode ?? "balance";
|
|
1116
1423
|
const subscriptionMode = mode === "subscription";
|
|
1117
1424
|
const status = accountLoading && account === null ? "loading" : account?.status ?? "unavailable";
|
|
1118
|
-
const statusText =
|
|
1425
|
+
const statusText = account?.stale === true ? translate("account.status.stale")
|
|
1426
|
+
: status === "loading" ? translate("account.status.loading")
|
|
1119
1427
|
: status === "blocked" ? translate("account.status.blocked")
|
|
1120
1428
|
: status === "unsupported" ? translate("account.status.unsupported")
|
|
1121
1429
|
: subscriptionStatusLabel(status, translate);
|
|
@@ -1133,6 +1441,7 @@ window.__ModuleLoader__.load({
|
|
|
1133
1441
|
"data-provider": provider.id,
|
|
1134
1442
|
"data-adapter": provider.adapter ?? provider.accountMode ?? "",
|
|
1135
1443
|
"data-account-mode": mode,
|
|
1444
|
+
"data-account-stale": account?.stale === true,
|
|
1136
1445
|
children: [
|
|
1137
1446
|
react_jsx_runtime.jsxs("div", {
|
|
1138
1447
|
className: S.accountHead,
|
|
@@ -1159,7 +1468,8 @@ window.__ModuleLoader__.load({
|
|
|
1159
1468
|
? react_jsx_runtime.jsx("p", { className: S.quotaEmpty, children: translate("subscription.loading") })
|
|
1160
1469
|
: react_jsx_runtime.jsx(SubscriptionContent, { provider: account ?? { status: "unavailable", windows: [] }, translate })
|
|
1161
1470
|
: react_jsx_runtime.jsx(BalanceContent, { balance: account?.balance ?? null, state: balanceState, message: balanceMessage, translate, onRetry }),
|
|
1162
|
-
|
|
1471
|
+
react_jsx_runtime.jsx(AccountHealth, { account, translate }),
|
|
1472
|
+
reasonText !== null && status !== "ok" ? react_jsx_runtime.jsx("p", { className: S.note, children: reasonText }) : null
|
|
1163
1473
|
]
|
|
1164
1474
|
});
|
|
1165
1475
|
}
|
|
@@ -1318,20 +1628,41 @@ window.__ModuleLoader__.load({
|
|
|
1318
1628
|
const zh = {
|
|
1319
1629
|
"panel.title": "用量与余额",
|
|
1320
1630
|
"panel.badge": "用量/余额",
|
|
1631
|
+
"orcarouter.choiceSuffix": "(赞助集成)",
|
|
1632
|
+
"orcarouter.addError": "未能添加 OrcaRouter,请稍后重试。",
|
|
1321
1633
|
"account.title": "供应商账户",
|
|
1322
1634
|
"account.provider": "当前供应商",
|
|
1323
1635
|
"account.balanceMode": "API 余额",
|
|
1324
1636
|
"account.loading": "正在加载供应商…",
|
|
1325
1637
|
"account.status.loading": "查询中",
|
|
1638
|
+
"account.status.stale": "数据已过期",
|
|
1326
1639
|
"account.status.blocked": "已阻止",
|
|
1327
1640
|
"account.status.unsupported": "不支持余额",
|
|
1328
1641
|
"account.status.invalidResponse": "响应异常",
|
|
1642
|
+
"account.status.unknown": "未知",
|
|
1329
1643
|
"account.invalidResponse": "供应商返回了无法识别的额度数据。",
|
|
1330
1644
|
"account.reason.dnsResolutionFailed": "无法解析供应商域名。",
|
|
1331
1645
|
"account.reason.allAddressesUnreachable": "已验证的网络地址均无法连接。",
|
|
1332
1646
|
"account.reason.upstreamNotJson": "上游返回的不是 JSON",
|
|
1333
1647
|
"account.reason.upstreamInvalidJson": "上游返回了无法解析的 JSON",
|
|
1334
1648
|
"account.reason.sub2apiBalanceShapeUnrecognized": "Sub2API 余额接口返回了未识别的数据结构。",
|
|
1649
|
+
"account.reason.timeout": "账户请求超时。",
|
|
1650
|
+
"account.reason.rateLimited": "供应商限制了账户查询频率。",
|
|
1651
|
+
"account.reason.unauthorized": "账户凭据未通过验证。",
|
|
1652
|
+
"account.reason.upstreamTooLarge": "上游响应超过安全大小限制。",
|
|
1653
|
+
"account.reason.upstreamInvalidResponse": "上游返回了无法识别的响应。",
|
|
1654
|
+
"account.reason.blockedNetwork": "账户请求被本地网络安全策略阻止。",
|
|
1655
|
+
"account.reason.noValidatedAddress": "没有可安全连接的供应商地址。",
|
|
1656
|
+
"account.reason.unknown": "账户查询失败,未提供可安全展示的诊断信息。",
|
|
1657
|
+
"account.health.lastAttempt": "上次尝试",
|
|
1658
|
+
"account.health.lastSuccess": "上次成功",
|
|
1659
|
+
"account.health.age": "数据年龄",
|
|
1660
|
+
"account.health.provenance": "数据来源",
|
|
1661
|
+
"account.provenance.official": "官方接口",
|
|
1662
|
+
"account.provenance.provider": "供应商接口",
|
|
1663
|
+
"account.provenance.configured": "自定义配置",
|
|
1664
|
+
"account.provenance.experimental": "实验性探测",
|
|
1665
|
+
"account.provenance.unknown": "未知",
|
|
1335
1666
|
"account.blocked": "账户查询被本地安全策略阻止,请检查 HTTPS、同源和私网访问设置。",
|
|
1336
1667
|
"balance.title": "账户余额",
|
|
1337
1668
|
"balance.provider": "供应商",
|
|
@@ -1362,7 +1693,11 @@ window.__ModuleLoader__.load({
|
|
|
1362
1693
|
"subscription.window.quota": "总额度",
|
|
1363
1694
|
"subscription.window.mcp": "MCP 月度额度",
|
|
1364
1695
|
"subscription.used": "已用 {value}%",
|
|
1365
|
-
"subscription.resets": "{time}
|
|
1696
|
+
"subscription.resets": "{time}后重置",
|
|
1697
|
+
"subscription.resetDue": "已到重置时间",
|
|
1698
|
+
"duration.minutes": "{minutes} 分钟",
|
|
1699
|
+
"duration.hoursMinutes": "{hours} 小时 {minutes} 分钟",
|
|
1700
|
+
"duration.daysHours": "{days} 天 {hours} 小时",
|
|
1366
1701
|
"subscription.notConfigured": "配置 {refs} 后显示真实订阅比例。",
|
|
1367
1702
|
"subscription.unauthorized": "凭据已失效,请更新后重试。",
|
|
1368
1703
|
"subscription.rateLimited": "供应商暂时限制查询,请稍后重试。",
|
|
@@ -1386,6 +1721,17 @@ window.__ModuleLoader__.load({
|
|
|
1386
1721
|
"usage.cacheRead": "缓存读",
|
|
1387
1722
|
"usage.unknownModel": "未知模型",
|
|
1388
1723
|
"usage.noModels": "这一天没有分模型数据。",
|
|
1724
|
+
"export.title": "安全导出",
|
|
1725
|
+
"export.dailyCsv": "每日 CSV",
|
|
1726
|
+
"export.sessionsCsv": "会话 CSV",
|
|
1727
|
+
"export.json": "完整 JSON",
|
|
1728
|
+
"budget.period.daily": "今日估算",
|
|
1729
|
+
"budget.period.monthly": "本月估算",
|
|
1730
|
+
"budget.level.normal": "正常",
|
|
1731
|
+
"budget.level.warning": "接近预算",
|
|
1732
|
+
"budget.level.critical": "已超预算",
|
|
1733
|
+
"budget.level.unknown": "费用不完整",
|
|
1734
|
+
"budget.summary": "{period}:{spent} / {limit} · {level}",
|
|
1389
1735
|
"month.year": "{year}年{month}",
|
|
1390
1736
|
"action.refresh": "刷新",
|
|
1391
1737
|
"action.retry": "重试",
|
|
@@ -1406,20 +1752,41 @@ window.__ModuleLoader__.load({
|
|
|
1406
1752
|
const en = {
|
|
1407
1753
|
"panel.title": "Usage & Balance",
|
|
1408
1754
|
"panel.badge": "Usage/Balance",
|
|
1755
|
+
"orcarouter.choiceSuffix": " (Sponsored integration)",
|
|
1756
|
+
"orcarouter.addError": "Could not add OrcaRouter. Please try again.",
|
|
1409
1757
|
"account.title": "Provider account",
|
|
1410
1758
|
"account.provider": "Current provider",
|
|
1411
1759
|
"account.balanceMode": "API balance",
|
|
1412
1760
|
"account.loading": "Loading providers…",
|
|
1413
1761
|
"account.status.loading": "Loading",
|
|
1762
|
+
"account.status.stale": "Stale data",
|
|
1414
1763
|
"account.status.blocked": "Blocked",
|
|
1415
1764
|
"account.status.unsupported": "Balance unsupported",
|
|
1416
1765
|
"account.status.invalidResponse": "Invalid response",
|
|
1766
|
+
"account.status.unknown": "Unknown",
|
|
1417
1767
|
"account.invalidResponse": "The provider returned unrecognized quota data.",
|
|
1418
1768
|
"account.reason.dnsResolutionFailed": "The provider hostname could not be resolved.",
|
|
1419
1769
|
"account.reason.allAddressesUnreachable": "None of the validated network addresses could be reached.",
|
|
1420
1770
|
"account.reason.upstreamNotJson": "The upstream response was not JSON",
|
|
1421
1771
|
"account.reason.upstreamInvalidJson": "The upstream returned unparsable JSON",
|
|
1422
1772
|
"account.reason.sub2apiBalanceShapeUnrecognized": "The Sub2API balance endpoint returned an unrecognized data shape.",
|
|
1773
|
+
"account.reason.timeout": "The account request timed out.",
|
|
1774
|
+
"account.reason.rateLimited": "The provider rate limited account checks.",
|
|
1775
|
+
"account.reason.unauthorized": "The account credential was rejected.",
|
|
1776
|
+
"account.reason.upstreamTooLarge": "The upstream response exceeded the safety size limit.",
|
|
1777
|
+
"account.reason.upstreamInvalidResponse": "The upstream returned an unrecognized response.",
|
|
1778
|
+
"account.reason.blockedNetwork": "The account request was blocked by the local network policy.",
|
|
1779
|
+
"account.reason.noValidatedAddress": "No validated provider address was available.",
|
|
1780
|
+
"account.reason.unknown": "The account check failed without a safe diagnostic detail.",
|
|
1781
|
+
"account.health.lastAttempt": "Last attempt",
|
|
1782
|
+
"account.health.lastSuccess": "Last success",
|
|
1783
|
+
"account.health.age": "Data age",
|
|
1784
|
+
"account.health.provenance": "Source",
|
|
1785
|
+
"account.provenance.official": "Official API",
|
|
1786
|
+
"account.provenance.provider": "Provider API",
|
|
1787
|
+
"account.provenance.configured": "Configured",
|
|
1788
|
+
"account.provenance.experimental": "Experimental",
|
|
1789
|
+
"account.provenance.unknown": "Unknown",
|
|
1423
1790
|
"account.blocked": "The account query was blocked by the local security policy. Check HTTPS, same-origin, and private-network settings.",
|
|
1424
1791
|
"balance.title": "Account balance",
|
|
1425
1792
|
"balance.provider": "Provider",
|
|
@@ -1450,7 +1817,11 @@ window.__ModuleLoader__.load({
|
|
|
1450
1817
|
"subscription.window.quota": "Total quota",
|
|
1451
1818
|
"subscription.window.mcp": "Monthly MCP quota",
|
|
1452
1819
|
"subscription.used": "{value}% used",
|
|
1453
|
-
"subscription.resets": "Resets {time}",
|
|
1820
|
+
"subscription.resets": "Resets in {time}",
|
|
1821
|
+
"subscription.resetDue": "Reset due",
|
|
1822
|
+
"duration.minutes": "{minutes}m",
|
|
1823
|
+
"duration.hoursMinutes": "{hours}h {minutes}m",
|
|
1824
|
+
"duration.daysHours": "{days}d {hours}h",
|
|
1454
1825
|
"subscription.notConfigured": "Configure {refs} to show live subscription usage.",
|
|
1455
1826
|
"subscription.unauthorized": "The credential has expired; update it and retry.",
|
|
1456
1827
|
"subscription.rateLimited": "The provider is rate limiting checks; retry later.",
|
|
@@ -1474,6 +1845,17 @@ window.__ModuleLoader__.load({
|
|
|
1474
1845
|
"usage.cacheRead": "Cache read",
|
|
1475
1846
|
"usage.unknownModel": "Unknown model",
|
|
1476
1847
|
"usage.noModels": "No per-model data for this day.",
|
|
1848
|
+
"export.title": "Secret-free export",
|
|
1849
|
+
"export.dailyCsv": "Daily CSV",
|
|
1850
|
+
"export.sessionsCsv": "Sessions CSV",
|
|
1851
|
+
"export.json": "Full JSON",
|
|
1852
|
+
"budget.period.daily": "Today estimated",
|
|
1853
|
+
"budget.period.monthly": "This month estimated",
|
|
1854
|
+
"budget.level.normal": "Normal",
|
|
1855
|
+
"budget.level.warning": "Near budget",
|
|
1856
|
+
"budget.level.critical": "Over budget",
|
|
1857
|
+
"budget.level.unknown": "Incomplete cost",
|
|
1858
|
+
"budget.summary": "{period}: {spent} / {limit} · {level}",
|
|
1477
1859
|
"month.year": "{month} {year}",
|
|
1478
1860
|
"action.refresh": "Refresh",
|
|
1479
1861
|
"action.retry": "Retry",
|
|
@@ -1498,7 +1880,7 @@ window.__ModuleLoader__.load({
|
|
|
1498
1880
|
const inject = ["slots", "locale"];
|
|
1499
1881
|
|
|
1500
1882
|
/**
|
|
1501
|
-
* Client plugin body: register
|
|
1883
|
+
* Client plugin body: register dictionaries and the sidebar account panel.
|
|
1502
1884
|
* @param ctx - client root context.
|
|
1503
1885
|
*/
|
|
1504
1886
|
function apply(ctx) {
|
|
@@ -1522,13 +1904,26 @@ window.__ModuleLoader__.load({
|
|
|
1522
1904
|
exports.cellColor = cellColor;
|
|
1523
1905
|
exports.createLoader = createLoader;
|
|
1524
1906
|
exports.buildProviderChoices = buildProviderChoices;
|
|
1907
|
+
exports.buildProviderPickerChoices = buildProviderPickerChoices;
|
|
1908
|
+
exports.loadOrcaRouterIntegration = loadOrcaRouterIntegration;
|
|
1909
|
+
exports.addOrcaRouterIntegration = addOrcaRouterIntegration;
|
|
1525
1910
|
exports.modelLabelOf = modelLabelOf;
|
|
1526
1911
|
exports.fmt = fmt;
|
|
1527
1912
|
exports.fmtCurrency = fmtCurrency;
|
|
1913
|
+
exports.budgetWindowText = budgetWindowText;
|
|
1528
1914
|
exports.badgeAccountValue = badgeAccountValue;
|
|
1529
1915
|
exports.badgeWarnOf = badgeWarnOf;
|
|
1530
1916
|
exports.shouldDismissPanel = shouldDismissPanel;
|
|
1531
1917
|
exports.safeDiagnosticReason = safeDiagnosticReason;
|
|
1918
|
+
exports.formatResetCountdown = formatResetCountdown;
|
|
1919
|
+
exports.enableFooterActionWrapping = enableFooterActionWrapping;
|
|
1920
|
+
exports.providerChoiceLabel = providerChoiceLabel;
|
|
1921
|
+
exports.ORCAROUTER_ADD_SENTINEL = ORCAROUTER_ADD_SENTINEL;
|
|
1922
|
+
exports.SELECTED_PROVIDER_STORAGE_KEY = SELECTED_PROVIDER_STORAGE_KEY;
|
|
1923
|
+
exports.authoritativeProviderList = authoritativeProviderList;
|
|
1924
|
+
exports.readSelectedProvider = readSelectedProvider;
|
|
1925
|
+
exports.reconcileSelectedProvider = reconcileSelectedProvider;
|
|
1926
|
+
exports.writeSelectedProvider = writeSelectedProvider;
|
|
1532
1927
|
return module.exports;
|
|
1533
1928
|
}
|
|
1534
1929
|
});
|