balance-dsh 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.
- package/lib/client.js +280 -66
- package/lib/index.js +83 -9
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -47,24 +47,41 @@ window.__ModuleLoader__.load({
|
|
|
47
47
|
.balance-stats-anchor--wide{width:100%}
|
|
48
48
|
.balance-stats-anchor--compact{position:fixed;left:68px;bottom:12px;z-index:10000}
|
|
49
49
|
.balance-stats-anchor--compact .balance-fab{position:static}
|
|
50
|
-
.balance-stats-
|
|
50
|
+
.balance-stats-layer{box-sizing:border-box;position:fixed;inset:0;z-index:9999;display:flex;align-items:center;justify-content:center;padding:24px;background:var(--dsw-alias-bg-mask-1,rgba(0,0,0,.24));backdrop-filter:var(--dsw-mask-blur,blur(2px));-webkit-backdrop-filter:var(--dsw-mask-blur,blur(2px));animation:balance-stats-backdrop-in .16s var(--ds-ease-in-out,ease-out) both}
|
|
51
|
+
.balance-stats-layer--closing{pointer-events:none;animation:balance-stats-backdrop-out .14s var(--ds-ease-in-out,ease-in) both}
|
|
52
|
+
.balance-stats-panel{box-sizing:border-box;position:relative;z-index:10000;width:min(800px,100%);max-height:100%;overflow-x:hidden;overflow-y:auto;scrollbar-gutter:stable;padding:0 0 24px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-base,#fff));border:0;border-radius:24px;box-shadow:var(--dsw-elevation-prominent,0 14px 36px rgba(0,0,0,.24));transform-origin:center center;will-change:transform,opacity;contain:layout style;animation:balance-stats-panel-in .26s var(--ds-ease-in-out,ease-out) .04s both;--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2,rgba(127,127,127,.35));--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2,rgba(127,127,127,.55))}
|
|
53
|
+
/* 滚动条对齐 DSH 设置窗口:宽度 12px,滑块靠 transparent 边框内缩到 4px(永远不贴 24px 圆角),
|
|
54
|
+
轨道上下各留 10px,颜色沿用 DSH 自己的 --dsh-scrollbar-thumb 变量。 */
|
|
55
|
+
.balance-stats-panel::-webkit-scrollbar{width:12px;height:12px}
|
|
56
|
+
.balance-stats-panel::-webkit-scrollbar-track{background:transparent;margin:10px}
|
|
57
|
+
.balance-stats-panel::-webkit-scrollbar-thumb{border:4px solid transparent;background-clip:padding-box;border-radius:6px;background-color:var(--dsh-scrollbar-thumb)}
|
|
58
|
+
.balance-stats-panel::-webkit-scrollbar-thumb:hover{background-color:var(--dsh-scrollbar-thumb-hover)}
|
|
59
|
+
.balance-stats-panel::-webkit-scrollbar-corner{background:transparent}
|
|
60
|
+
@keyframes balance-stats-backdrop-in{from{opacity:0}to{opacity:1}}
|
|
61
|
+
@keyframes balance-stats-backdrop-out{from{opacity:1}to{opacity:0}}
|
|
51
62
|
.balance-stats-panel--closing{pointer-events:none;animation:balance-stats-panel-out .14s var(--ds-ease-in-out,ease-in) both}
|
|
52
|
-
@keyframes balance-stats-panel-in{from{opacity:0;transform:translateY(
|
|
63
|
+
@keyframes balance-stats-panel-in{from{opacity:0;transform:translateY(18px) scale(.955)}to{opacity:1;transform:translateY(0) scale(1)}}
|
|
53
64
|
@keyframes balance-stats-panel-out{from{opacity:1;transform:translateY(0) scale(1)}to{opacity:0;transform:translateY(6px) scale(.985)}}
|
|
54
|
-
.balance-stats-header{display:flex;align-items:
|
|
55
|
-
.balance-stats-
|
|
65
|
+
.balance-stats-header{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:22px 14px 12px 24px}
|
|
66
|
+
.balance-stats-body{display:flex;flex-direction:column;min-width:0;padding:0 24px}
|
|
67
|
+
.balance-stats-title{margin:0;font-size:16px;line-height:24px;font-weight:500}
|
|
56
68
|
.balance-stats-subtitle{margin:4px 0 0;color:var(--dsw-alias-label-secondary);font-size:12px}
|
|
57
|
-
.balance-stats-close{width:
|
|
69
|
+
.balance-stats-close{flex:none;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border:0;border-radius:8px;color:var(--dsw-alias-label-secondary);background:transparent;font-size:22px;line-height:1;cursor:pointer}
|
|
58
70
|
.balance-stats-close:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
|
|
59
71
|
.balance-stats-error{padding:12px;border-radius:10px;color:var(--dsw-alias-state-error-primary,#e5484d);background:rgba(229,72,77,.10)}
|
|
60
72
|
.balance-stats-loading{padding:48px 12px;text-align:center;color:var(--dsw-alias-label-secondary)}
|
|
61
|
-
.balance-stats-
|
|
62
|
-
.balance-stats-
|
|
73
|
+
.balance-stats-top{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:8px;margin-bottom:8px}
|
|
74
|
+
.balance-stats-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin-bottom:8px}
|
|
75
|
+
/* 分组数为奇数时(「按模型」「时段拆分」条件渲染可能缺席),最后一个横跨整行,避免半空行 */
|
|
76
|
+
.balance-stats-grid>:last-child:nth-child(odd){grid-column:1/-1}
|
|
77
|
+
.balance-stats-cards{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));grid-auto-rows:1fr;gap:8px;min-width:0}
|
|
78
|
+
.balance-stats-card{display:flex;flex-direction:column;justify-content:center;min-width:0;padding:11px;border:1px solid var(--dsw-alias-border-l2,#d9d9df);border-radius:10px;background:var(--dsw-alias-bg-module-platform,rgba(127,127,127,.04));animation:balance-stats-card-in .2s var(--ds-ease-in-out,ease-out) both}
|
|
63
79
|
.balance-stats-card:nth-child(2){animation-delay:.025s}.balance-stats-card:nth-child(3){animation-delay:.05s}.balance-stats-card:nth-child(4){animation-delay:.075s}
|
|
64
80
|
@keyframes balance-stats-card-in{from{opacity:0;transform:translateY(5px)}to{opacity:1;transform:translateY(0)}}
|
|
65
81
|
.balance-stats-card__label{display:block;margin-bottom:6px;color:var(--dsw-alias-label-secondary);font-size:12px}
|
|
66
82
|
.balance-stats-card__value{display:block;overflow:hidden;color:var(--dsw-alias-label-primary);font-size:16px;font-weight:700;font-variant-numeric:tabular-nums;text-overflow:ellipsis;white-space:nowrap}
|
|
67
|
-
.balance-stats-
|
|
83
|
+
.balance-stats-heatmap-card{box-sizing:border-box;min-width:0;padding:11px;border:1px solid var(--dsw-alias-border-l2,#d9d9df);border-radius:10px;background:var(--dsw-alias-bg-module-platform,rgba(127,127,127,.04));animation:balance-stats-card-in .22s var(--ds-ease-in-out,ease-out) .08s both}
|
|
84
|
+
.balance-stats-section-title{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:0 0 8px;font-size:12px;font-weight:600}
|
|
68
85
|
.balance-stats-legend{display:flex;align-items:center;gap:4px;color:var(--dsw-alias-label-secondary);font-size:11px;font-weight:400}
|
|
69
86
|
.balance-heatmap-wrap{min-width:0;overflow:hidden;padding:4px 2px 8px}
|
|
70
87
|
.balance-heatmap{display:grid;grid-template-rows:repeat(7,14px);grid-auto-flow:column;grid-auto-columns:14px;gap:4px;width:max-content;min-width:100%}
|
|
@@ -84,8 +101,16 @@ window.__ModuleLoader__.load({
|
|
|
84
101
|
.balance-stats-footer{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:14px;color:var(--dsw-alias-label-secondary);font-size:11px}
|
|
85
102
|
.balance-stats-link{padding:7px 12px;border:1px solid var(--dsw-alias-border-l2,#d9d9df);border-radius:8px;color:var(--dsw-alias-label-primary);text-decoration:none;background:var(--dsw-alias-bg-module-platform,rgba(127,127,127,.04))}
|
|
86
103
|
.balance-stats-link:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
|
|
87
|
-
|
|
88
|
-
|
|
104
|
+
.balance-stats-group{box-sizing:border-box;margin-bottom:0;padding:11px;border:1px solid var(--dsw-alias-border-l2,#d9d9df);border-radius:10px;background:var(--dsw-alias-bg-module-platform,rgba(127,127,127,.04));animation:balance-stats-card-in .22s var(--ds-ease-in-out,ease-out) both}
|
|
105
|
+
.balance-stats-group__title{display:flex;align-items:baseline;justify-content:space-between;gap:8px;margin:0 0 6px;color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600}
|
|
106
|
+
.balance-stats-group__hint{color:var(--dsw-alias-label-secondary);font-weight:400;font-variant-numeric:tabular-nums}
|
|
107
|
+
.balance-stats-row{display:flex;align-items:baseline;justify-content:space-between;gap:12px;min-width:0;padding:6px 0;font-size:12px}
|
|
108
|
+
.balance-stats-row+.balance-stats-row{border-top:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.16))}
|
|
109
|
+
.balance-stats-row__name{min-width:0;overflow:hidden;color:var(--dsw-alias-label-primary);text-overflow:ellipsis;white-space:nowrap}
|
|
110
|
+
.balance-stats-row__value{flex:none;text-align:right;color:var(--dsw-alias-label-primary);font-weight:600;font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
111
|
+
@media(max-width:900px){.balance-stats-top{grid-template-columns:minmax(0,1fr)}.balance-stats-grid{grid-template-columns:minmax(0,1fr)}}
|
|
112
|
+
@media(max-width:760px){.balance-stats-layer{padding:10px}}
|
|
113
|
+
@media(prefers-reduced-motion:reduce){.balance-stats-panel,.balance-stats-panel--closing,.balance-stats-layer,.balance-stats-layer--closing,.balance-stats-card,.balance-stats-group,.balance-stats-heatmap-card{animation:none}.balance-heatmap-cell{transition:none}}
|
|
89
114
|
/* ── 展开态底部一行:[设置齿轮][余额胶囊],余额在设置右边 ──
|
|
90
115
|
全部使用稳定锚点,不依赖 CSS Modules hash 类名(DSH 升级后仍有效):
|
|
91
116
|
- wide 态特征:底部存在 .balance-footer / .balance-topup(收起态渲染 position:fixed 的 .balance-fab,不影响)
|
|
@@ -389,17 +414,71 @@ window.__ModuleLoader__.load({
|
|
|
389
414
|
return usageStatsInFlight;
|
|
390
415
|
}
|
|
391
416
|
|
|
417
|
+
/** 定价表内部键 -> 展示名 */
|
|
418
|
+
function modelLabel(key) {
|
|
419
|
+
if (key === "deepseek-v4-flash") return "Flash";
|
|
420
|
+
if (key === "deepseek-v4-pro") return "Pro";
|
|
421
|
+
if (key === "deepseek-chat") return "Chat";
|
|
422
|
+
if (key === "deepseek-reasoner") return "Reasoner";
|
|
423
|
+
return String(key || "未知");
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** token 桶求和(服务端统计返回的 peak / offpeak 桶) */
|
|
427
|
+
function bucketTokens(bucket) {
|
|
428
|
+
if (!bucket) return 0;
|
|
429
|
+
return (
|
|
430
|
+
Number(bucket.inputTokens || 0) +
|
|
431
|
+
Number(bucket.outputTokens || 0) +
|
|
432
|
+
Number(bucket.cacheReadTokens || 0) +
|
|
433
|
+
Number(bucket.cacheWriteTokens || 0)
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** 概览卡片。数值已完整展示,**不挂原生 `title`**——浏览器系统灰框与 DSH 主题不一致。 */
|
|
392
438
|
const StatsCard = react.memo(function StatsCard({ label, value }) {
|
|
393
439
|
return jsx.jsxs("div", {
|
|
394
440
|
className: "balance-stats-card",
|
|
395
441
|
children: [
|
|
396
442
|
jsx.jsx("span", { className: "balance-stats-card__label", children: label }),
|
|
397
|
-
jsx.jsx("span", { className: "balance-stats-card__value",
|
|
443
|
+
jsx.jsx("span", { className: "balance-stats-card__value", children: value }),
|
|
444
|
+
],
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
/** 明细行:左侧名称、右侧数值(两端对齐)。
|
|
449
|
+
* 曾经画过占比条,但各分组的基准并不统一(Token 构成按 token、按模型/时段按费用、
|
|
450
|
+
* 近期消耗按 90 天总额),同一屏三种尺子无法横向比较,而数值本身已表达量级,
|
|
451
|
+
* 条只会让人猜「这到底占谁的比例」,故去掉。 */
|
|
452
|
+
const StatsRow = react.memo(function StatsRow({ name, value }) {
|
|
453
|
+
return jsx.jsxs("div", {
|
|
454
|
+
className: "balance-stats-row",
|
|
455
|
+
children: [
|
|
456
|
+
jsx.jsx("span", { className: "balance-stats-row__name", children: name }),
|
|
457
|
+
jsx.jsx("span", { className: "balance-stats-row__value", children: value }),
|
|
398
458
|
],
|
|
399
459
|
});
|
|
400
460
|
});
|
|
401
461
|
|
|
402
|
-
|
|
462
|
+
/** 明细分组:小标题(可带右侧提示) + 若干明细行。
|
|
463
|
+
* `index` 只用于卡片逐个浮现的阶梯动画延迟——面板先浮现、各卡片随后依次入场。 */
|
|
464
|
+
function StatsGroup({ title, hint, children, index = 0 }) {
|
|
465
|
+
return jsx.jsxs("div", {
|
|
466
|
+
className: "balance-stats-group",
|
|
467
|
+
style: { animationDelay: (0.08 + index * 0.035).toFixed(3) + "s" },
|
|
468
|
+
children: [
|
|
469
|
+
jsx.jsxs("div", {
|
|
470
|
+
className: "balance-stats-group__title",
|
|
471
|
+
children: [
|
|
472
|
+
jsx.jsx("span", { children: title }),
|
|
473
|
+
hint ? jsx.jsx("span", { className: "balance-stats-group__hint", children: hint }) : null,
|
|
474
|
+
],
|
|
475
|
+
}),
|
|
476
|
+
children,
|
|
477
|
+
],
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function UsageStatsPanel({ onClose, panelRef, closing }) {
|
|
403
482
|
const liveBalance = useBalance();
|
|
404
483
|
const [heatmapTooltip, setHeatmapTooltip] = react.useState(null);
|
|
405
484
|
const [state, setState] = react.useState(() => {
|
|
@@ -477,6 +556,58 @@ window.__ModuleLoader__.load({
|
|
|
477
556
|
const balanceText = balance
|
|
478
557
|
? String(balance.total_balance ?? "0.00") + " " + String(balance.currency || "CNY")
|
|
479
558
|
: "不可用";
|
|
559
|
+
|
|
560
|
+
// —— 派生统计:全部对旧缓存 / 缺失字段容错(数值缺失按 0、数组缺失按空) ——
|
|
561
|
+
const num = (v) => Number(v || 0);
|
|
562
|
+
const derived = react.useMemo(() => {
|
|
563
|
+
const t = total || {};
|
|
564
|
+
const dayList = Array.isArray(days) ? days : [];
|
|
565
|
+
const activeDays = dayList.filter((d) => num(d.totalTokens) > 0);
|
|
566
|
+
const sumRange = (n) =>
|
|
567
|
+
dayList.slice(-n).reduce(
|
|
568
|
+
(acc, d) => {
|
|
569
|
+
acc.tokens += num(d.totalTokens);
|
|
570
|
+
acc.cost += num(d.cost);
|
|
571
|
+
return acc;
|
|
572
|
+
},
|
|
573
|
+
{ tokens: 0, cost: 0 },
|
|
574
|
+
);
|
|
575
|
+
const today = dayList.length > 0 ? dayList[dayList.length - 1] : null;
|
|
576
|
+
const peakDay = activeDays.reduce(
|
|
577
|
+
(best, d) => (best == null || num(d.totalTokens) > num(best.totalTokens) ? d : best),
|
|
578
|
+
null,
|
|
579
|
+
);
|
|
580
|
+
const inputTokens = num(t.inputTokens);
|
|
581
|
+
const cacheReadTokens = num(t.cacheReadTokens);
|
|
582
|
+
const cacheWriteTokens = num(t.cacheWriteTokens);
|
|
583
|
+
const outputTokens = num(t.outputTokens);
|
|
584
|
+
const billedInput = inputTokens + cacheReadTokens + cacheWriteTokens;
|
|
585
|
+
const activeCount = activeDays.length;
|
|
586
|
+
return {
|
|
587
|
+
activeCount,
|
|
588
|
+
todayTokens: num(today && today.totalTokens),
|
|
589
|
+
todayCost: num(today && today.cost),
|
|
590
|
+
week: sumRange(7),
|
|
591
|
+
month: sumRange(30),
|
|
592
|
+
avgTokens: activeCount > 0 ? num(t.totalTokens) / activeCount : 0,
|
|
593
|
+
avgCost: activeCount > 0 ? num(t.cost) / activeCount : 0,
|
|
594
|
+
peakDay,
|
|
595
|
+
inputTokens,
|
|
596
|
+
cacheReadTokens,
|
|
597
|
+
cacheWriteTokens,
|
|
598
|
+
outputTokens,
|
|
599
|
+
hitRate: billedInput > 0 ? cacheReadTokens / billedInput : 0,
|
|
600
|
+
byModel: Array.isArray(t.byModel) ? t.byModel.filter((m) => m && num(m.totalTokens) > 0) : [],
|
|
601
|
+
peakTokens: bucketTokens(t.peak),
|
|
602
|
+
offpeakTokens: bucketTokens(t.offpeak),
|
|
603
|
+
peakCost: num(t.peakCost),
|
|
604
|
+
offpeakCost: num(t.offpeakCost),
|
|
605
|
+
};
|
|
606
|
+
}, [days, total]);
|
|
607
|
+
const fmtCost = (v) => "¥" + num(v).toFixed(4);
|
|
608
|
+
const denomCost = num(total && total.cost) > 0 ? num(total.cost) : 1;
|
|
609
|
+
const rangeDays = Number(stats && stats.rangeDays) || 90;
|
|
610
|
+
|
|
480
611
|
const body = state.loading
|
|
481
612
|
? jsx.jsx("div", { className: "balance-stats-loading", children: "正在汇总会话记录…" })
|
|
482
613
|
: state.error
|
|
@@ -485,34 +616,127 @@ window.__ModuleLoader__.load({
|
|
|
485
616
|
children: [
|
|
486
617
|
!liveBalance.balance && data.balanceError ? jsx.jsx("div", { className: "balance-stats-error", children: "余额:" + data.balanceError }) : null,
|
|
487
618
|
jsx.jsxs("div", {
|
|
488
|
-
className: "balance-stats-
|
|
619
|
+
className: "balance-stats-top",
|
|
489
620
|
children: [
|
|
490
|
-
jsx.
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
621
|
+
jsx.jsxs("div", {
|
|
622
|
+
className: "balance-stats-cards",
|
|
623
|
+
children: [
|
|
624
|
+
jsx.jsx(StatsCard, { label: "账户余额", value: balanceText }),
|
|
625
|
+
jsx.jsx(StatsCard, { label: "90 天估算费用", value: fmtCost(total && total.cost) }),
|
|
626
|
+
jsx.jsx(StatsCard, { label: "90 天计费 Token", value: formatTokens(Number(total?.totalTokens || 0)) }),
|
|
627
|
+
jsx.jsx(StatsCard, { label: "活跃天数", value: derived.activeCount + " / " + rangeDays + " 天" }),
|
|
628
|
+
],
|
|
629
|
+
}),
|
|
630
|
+
jsx.jsxs("div", {
|
|
631
|
+
className: "balance-stats-heatmap-card",
|
|
632
|
+
children: [
|
|
633
|
+
jsx.jsxs("div", {
|
|
634
|
+
className: "balance-stats-section-title",
|
|
635
|
+
children: [
|
|
636
|
+
jsx.jsx("span", { children: "近 90 天使用热力图" }),
|
|
637
|
+
jsx.jsxs("span", {
|
|
638
|
+
className: "balance-stats-legend",
|
|
639
|
+
children: ["少", 0, 1, 2, 3, 4].map((item, index) => index === 0
|
|
640
|
+
? item
|
|
641
|
+
: jsx.jsx("i", { className: "balance-heatmap-cell balance-heatmap-cell--" + item }, "legend-" + item)).concat(["多"]),
|
|
642
|
+
}),
|
|
643
|
+
],
|
|
644
|
+
}),
|
|
645
|
+
jsx.jsx("div", { className: "balance-heatmap-wrap", children: jsx.jsx("div", { className: "balance-heatmap", children: heatmap }) }),
|
|
646
|
+
],
|
|
647
|
+
}),
|
|
494
648
|
],
|
|
495
649
|
}),
|
|
496
650
|
jsx.jsxs("div", {
|
|
497
|
-
className: "balance-stats-
|
|
651
|
+
className: "balance-stats-grid",
|
|
498
652
|
children: [
|
|
499
|
-
jsx.
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
653
|
+
jsx.jsxs(StatsGroup, {
|
|
654
|
+
index: 0,
|
|
655
|
+
title: "Token 构成",
|
|
656
|
+
hint: "缓存命中率 " + (derived.hitRate * 100).toFixed(1) + "%",
|
|
657
|
+
children: [
|
|
658
|
+
jsx.jsx(StatsRow, { name: "未缓存输入", value: formatTokens(derived.inputTokens)}),
|
|
659
|
+
jsx.jsx(StatsRow, { name: "缓存读取", value: formatTokens(derived.cacheReadTokens)}),
|
|
660
|
+
jsx.jsx(StatsRow, { name: "缓存写入", value: formatTokens(derived.cacheWriteTokens)}),
|
|
661
|
+
jsx.jsx(StatsRow, { name: "输出", value: formatTokens(derived.outputTokens)}),
|
|
662
|
+
],
|
|
505
663
|
}),
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
664
|
+
derived.byModel.length > 0
|
|
665
|
+
? jsx.jsxs(StatsGroup, {
|
|
666
|
+
index: 1,
|
|
667
|
+
title: "按模型",
|
|
668
|
+
children: derived.byModel.map((item) =>
|
|
669
|
+
jsx.jsx(
|
|
670
|
+
StatsRow,
|
|
671
|
+
{
|
|
672
|
+
name: modelLabel(item.model),
|
|
673
|
+
value: fmtCost(item.cost) + " · " + formatTokens(Number(item.totalTokens || 0)),
|
|
674
|
+
},
|
|
675
|
+
"model-" + item.model,
|
|
676
|
+
)),
|
|
677
|
+
})
|
|
678
|
+
: null,
|
|
679
|
+
derived.peakTokens > 0 || derived.offpeakTokens > 0
|
|
680
|
+
? jsx.jsxs(StatsGroup, {
|
|
681
|
+
index: 2,
|
|
682
|
+
title: "时段拆分",
|
|
683
|
+
hint: "高峰占 " + ((derived.peakCost / denomCost) * 100).toFixed(0) + "%",
|
|
684
|
+
children: [
|
|
685
|
+
jsx.jsx(StatsRow, {
|
|
686
|
+
name: "高峰(×2)",
|
|
687
|
+
value: fmtCost(derived.peakCost) + " · " + formatTokens(derived.peakTokens),
|
|
688
|
+
}),
|
|
689
|
+
jsx.jsx(StatsRow, {
|
|
690
|
+
name: "空闲",
|
|
691
|
+
value: fmtCost(derived.offpeakCost) + " · " + formatTokens(derived.offpeakTokens),
|
|
692
|
+
}),
|
|
693
|
+
],
|
|
694
|
+
})
|
|
695
|
+
: null,
|
|
696
|
+
jsx.jsxs(StatsGroup, {
|
|
697
|
+
index: 3,
|
|
698
|
+
title: "近期消耗",
|
|
699
|
+
children: [
|
|
700
|
+
jsx.jsx(StatsRow, {
|
|
701
|
+
name: "今日",
|
|
702
|
+
value: formatTokens(derived.todayTokens) + " · " + fmtCost(derived.todayCost),
|
|
703
|
+
}),
|
|
704
|
+
jsx.jsx(StatsRow, {
|
|
705
|
+
name: "近 7 天",
|
|
706
|
+
value: formatTokens(derived.week.tokens) + " · " + fmtCost(derived.week.cost),
|
|
707
|
+
}),
|
|
708
|
+
jsx.jsx(StatsRow, {
|
|
709
|
+
name: "近 30 天",
|
|
710
|
+
value: formatTokens(derived.month.tokens) + " · " + fmtCost(derived.month.cost),
|
|
711
|
+
}),
|
|
712
|
+
],
|
|
713
|
+
}),
|
|
714
|
+
jsx.jsxs(StatsGroup, {
|
|
715
|
+
index: 4,
|
|
716
|
+
title: "日均与峰值",
|
|
717
|
+
children: [
|
|
718
|
+
// 日均 = 总额 / 活跃天,没有「占比」语义,因此不画条(否则是条空轨道)
|
|
719
|
+
jsx.jsx(StatsRow, {
|
|
720
|
+
name: "日均(按活跃天)",
|
|
721
|
+
value: formatTokens(derived.avgTokens) + " · " + fmtCost(derived.avgCost),
|
|
722
|
+
}),
|
|
723
|
+
jsx.jsx(StatsRow, {
|
|
724
|
+
name: derived.peakDay ? "峰值日 " + derived.peakDay.date : "峰值日",
|
|
725
|
+
value: derived.peakDay
|
|
726
|
+
? formatTokens(Number(derived.peakDay.totalTokens || 0)) + " · " + fmtCost(derived.peakDay.cost)
|
|
727
|
+
: "—",
|
|
728
|
+
}),
|
|
729
|
+
],
|
|
730
|
+
}),
|
|
731
|
+
] }),
|
|
509
732
|
jsx.jsxs("div", {
|
|
510
733
|
className: "balance-stats-footer",
|
|
511
734
|
children: [
|
|
512
735
|
jsx.jsx("span", {
|
|
513
|
-
children:
|
|
514
|
-
|
|
515
|
-
|
|
736
|
+
children:
|
|
737
|
+
"已扫描 " + String(stats?.scannedSessions || 0) + " 个会话" +
|
|
738
|
+
(stats?.limited ? "(为保证性能仅取最近 200 个)" : "") +
|
|
739
|
+
";按北京时间和实际模型峰谷价格估算,结果缓存 5 分钟。",
|
|
516
740
|
}),
|
|
517
741
|
jsx.jsx("a", {
|
|
518
742
|
className: "balance-stats-link",
|
|
@@ -527,7 +751,6 @@ window.__ModuleLoader__.load({
|
|
|
527
751
|
});
|
|
528
752
|
return jsx.jsxs("section", {
|
|
529
753
|
ref: panelRef,
|
|
530
|
-
style,
|
|
531
754
|
className: "balance-stats-panel" + (closing ? " balance-stats-panel--closing" : ""),
|
|
532
755
|
role: "dialog",
|
|
533
756
|
"aria-label": "余额与使用统计",
|
|
@@ -544,7 +767,7 @@ window.__ModuleLoader__.load({
|
|
|
544
767
|
jsx.jsx("button", { className: "balance-stats-close", type: "button", onClick: onClose, "aria-label": "关闭", children: "×" }),
|
|
545
768
|
],
|
|
546
769
|
}),
|
|
547
|
-
body,
|
|
770
|
+
jsx.jsx("div", { className: "balance-stats-body", children: body }),
|
|
548
771
|
heatmapTooltip ? reactDom.createPortal(jsx.jsx("div", {
|
|
549
772
|
className: "balance-heatmap-tooltip balance-heatmap-tooltip--" + heatmapTooltip.align,
|
|
550
773
|
style: heatmapTooltip.style,
|
|
@@ -580,10 +803,10 @@ window.__ModuleLoader__.load({
|
|
|
580
803
|
const state = useBalance();
|
|
581
804
|
const [statsOpen, setStatsOpen] = react.useState(false);
|
|
582
805
|
const [statsClosing, setStatsClosing] = react.useState(false);
|
|
583
|
-
const [statsPosition, setStatsPosition] = react.useState({ left: 12, bottom: 56, width: 460 });
|
|
584
806
|
const clickTimer = react.useRef(null);
|
|
585
807
|
const closeTimer = react.useRef(null);
|
|
586
808
|
const anchorRef = react.useRef(null);
|
|
809
|
+
const layerRef = react.useRef(null);
|
|
587
810
|
const panelRef = react.useRef(null);
|
|
588
811
|
react.useEffect(() => () => {
|
|
589
812
|
if (clickTimer.current !== null) clearTimeout(clickTimer.current);
|
|
@@ -599,38 +822,17 @@ window.__ModuleLoader__.load({
|
|
|
599
822
|
setStatsClosing(false);
|
|
600
823
|
}, 140);
|
|
601
824
|
}, [statsOpen, statsClosing]);
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
closeStats();
|
|
608
|
-
};
|
|
609
|
-
const closeOnResize = () => closeStats();
|
|
610
|
-
document.addEventListener("mousedown", closeOnOutsideClick);
|
|
611
|
-
window.addEventListener("resize", closeOnResize);
|
|
612
|
-
return () => {
|
|
613
|
-
document.removeEventListener("mousedown", closeOnOutsideClick);
|
|
614
|
-
window.removeEventListener("resize", closeOnResize);
|
|
615
|
-
};
|
|
616
|
-
}, [statsOpen, closeStats]);
|
|
825
|
+
// 点击遮罩(即 layer 自身)关闭;点击面板内部不关闭。
|
|
826
|
+
// 窗口缩放不再关闭面板(居中布局会自适应,旧实现是锚定按钮需要重算位置)。
|
|
827
|
+
const handleBackdropClick = (event) => {
|
|
828
|
+
if (event.target === layerRef.current) closeStats();
|
|
829
|
+
};
|
|
617
830
|
const openStats = (event) => {
|
|
618
831
|
if (event && typeof event.preventDefault === "function") event.preventDefault();
|
|
619
832
|
if (clickTimer.current !== null) clearTimeout(clickTimer.current);
|
|
620
833
|
if (closeTimer.current !== null) clearTimeout(closeTimer.current);
|
|
621
834
|
closeTimer.current = null;
|
|
622
835
|
setStatsClosing(false);
|
|
623
|
-
const trigger = event && event.currentTarget;
|
|
624
|
-
if (trigger && typeof trigger.getBoundingClientRect === "function") {
|
|
625
|
-
const rect = trigger.getBoundingClientRect();
|
|
626
|
-
const width = Math.max(1, Math.min(460, window.innerWidth - 24));
|
|
627
|
-
const desiredLeft = wide ? rect.right + 8 : rect.left;
|
|
628
|
-
const left = Math.max(12, Math.min(desiredLeft, window.innerWidth - width - 12));
|
|
629
|
-
const bottom = wide
|
|
630
|
-
? Math.max(12, window.innerHeight - rect.bottom)
|
|
631
|
-
: Math.max(12, window.innerHeight - rect.top + 8);
|
|
632
|
-
setStatsPosition({ left, bottom, width });
|
|
633
|
-
}
|
|
634
836
|
clickTimer.current = setTimeout(() => {
|
|
635
837
|
clickTimer.current = null;
|
|
636
838
|
setStatsClosing(false);
|
|
@@ -647,22 +849,34 @@ window.__ModuleLoader__.load({
|
|
|
647
849
|
setStatsOpen(false);
|
|
648
850
|
window.open("https://platform.deepseek.com/usage", "_blank", "noopener");
|
|
649
851
|
};
|
|
852
|
+
// 余额元素的交互。悬停提示不用原生 title,统一走主题化浮层(见 withPanel)。
|
|
650
853
|
const interactions = {
|
|
651
854
|
onClick: openStats,
|
|
652
855
|
onDoubleClick: openUsagePage,
|
|
653
856
|
onContextMenu: onBalanceContextMenu,
|
|
654
|
-
title: "单击查看余额与使用统计,双击打开 DeepSeek API 用量页,右键刷新",
|
|
655
857
|
};
|
|
656
858
|
const withPanel = (node) => jsx.jsxs("div", {
|
|
657
859
|
ref: anchorRef,
|
|
658
860
|
className: "balance-stats-anchor balance-stats-anchor--" + (wide ? "wide" : "compact"),
|
|
659
861
|
children: [
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
862
|
+
// 悬停提示走 DSH 主题化浮层:primitives.Tooltip 内部是 cloneElement(children, {ref}),
|
|
863
|
+
// 不会插入额外 DOM 层,因此不破坏余额行在 flex 容器里的 42px 布局(浮层本身 portal 到 body)。
|
|
864
|
+
jsx.jsx(primitives.Tooltip, {
|
|
865
|
+
label: "单击查看余额与使用统计,双击打开 DeepSeek API 用量页,右键刷新",
|
|
866
|
+
side: "top",
|
|
867
|
+
delayMs: 400,
|
|
868
|
+
children: node,
|
|
869
|
+
}),
|
|
870
|
+
// 统计面板:居中弹层 + 半透明遮罩(点击遮罩 / Esc / 关闭按钮均可关闭)
|
|
871
|
+
statsOpen ? reactDom.createPortal(jsx.jsx("div", {
|
|
872
|
+
ref: layerRef,
|
|
873
|
+
className: "balance-stats-layer" + (statsClosing ? " balance-stats-layer--closing" : ""),
|
|
874
|
+
onMouseDown: handleBackdropClick,
|
|
875
|
+
children: jsx.jsx(UsageStatsPanel, {
|
|
876
|
+
onClose: closeStats,
|
|
877
|
+
panelRef,
|
|
878
|
+
closing: statsClosing,
|
|
879
|
+
}),
|
|
666
880
|
}), document.body) : null,
|
|
667
881
|
],
|
|
668
882
|
});
|
package/lib/index.js
CHANGED
|
@@ -406,21 +406,54 @@ function collectDailyUsage(events, config, fallbackModel, cutoffMs, days, sessio
|
|
|
406
406
|
const key = beijingDayKey(time);
|
|
407
407
|
let day = days.get(key);
|
|
408
408
|
if (day == null) {
|
|
409
|
-
day = {
|
|
409
|
+
day = {
|
|
410
|
+
date: key,
|
|
411
|
+
...emptyTokenBucket(),
|
|
412
|
+
totalTokens: 0,
|
|
413
|
+
cost: 0,
|
|
414
|
+
sessionIds: new Set(),
|
|
415
|
+
// —— 以下为细分维度,供统计面板展示;不改动上方总额口径 ——
|
|
416
|
+
byModel: new Map(), // modelKey -> { model, tokens..., totalTokens, cost }
|
|
417
|
+
peak: emptyTokenBucket(),
|
|
418
|
+
offpeak: emptyTokenBucket(),
|
|
419
|
+
peakCost: 0,
|
|
420
|
+
offpeakCost: 0,
|
|
421
|
+
};
|
|
410
422
|
days.set(key, day);
|
|
411
423
|
}
|
|
424
|
+
const model = modelAtTime(switches, time) || fallback;
|
|
425
|
+
const isPeak = isPeakAt(time, config);
|
|
426
|
+
// 分时段精确计费:高峰桶按高峰价(= 目录价 ×2)、空闲桶按目录价
|
|
427
|
+
const bucket = { peak: emptyTokenBucket(), offpeak: emptyTokenBucket() };
|
|
428
|
+
addTokenUsage(isPeak ? bucket.peak : bucket.offpeak, usage);
|
|
429
|
+
const stepCost = estimateCost(bucket, resolvePricing(config, model, pricingEraAt(time)));
|
|
430
|
+
|
|
412
431
|
addTokenUsage(day, usage);
|
|
413
|
-
|
|
432
|
+
const tokens = tokenTotal({
|
|
414
433
|
inputTokens: usage.inputTokens ?? 0,
|
|
415
434
|
outputTokens: usage.outputTokens ?? 0,
|
|
416
435
|
cacheReadTokens: usage.cacheReadTokens ?? 0,
|
|
417
436
|
cacheWriteTokens: usage.cacheWriteTokens ?? 0,
|
|
418
437
|
});
|
|
438
|
+
day.totalTokens += tokens;
|
|
439
|
+
day.cost += stepCost;
|
|
419
440
|
day.sessionIds.add(sessionId);
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
addTokenUsage(
|
|
423
|
-
day.
|
|
441
|
+
|
|
442
|
+
// 高峰 / 空闲拆分(token 与费用)
|
|
443
|
+
addTokenUsage(isPeak ? day.peak : day.offpeak, usage);
|
|
444
|
+
if (isPeak) day.peakCost += stepCost;
|
|
445
|
+
else day.offpeakCost += stepCost;
|
|
446
|
+
|
|
447
|
+
// 按模型拆分:复用同一步的 stepCost,保证各模型费用之和 = day.cost
|
|
448
|
+
const mk = modelKey(model) || "deepseek-v4-flash";
|
|
449
|
+
let entry = day.byModel.get(mk);
|
|
450
|
+
if (entry == null) {
|
|
451
|
+
entry = { model: mk, ...emptyTokenBucket(), totalTokens: 0, cost: 0 };
|
|
452
|
+
day.byModel.set(mk, entry);
|
|
453
|
+
}
|
|
454
|
+
addTokenUsage(entry, usage);
|
|
455
|
+
entry.totalTokens += tokens;
|
|
456
|
+
entry.cost += stepCost;
|
|
424
457
|
}
|
|
425
458
|
}
|
|
426
459
|
|
|
@@ -494,7 +527,18 @@ async function buildUsageStats(ctx, config) {
|
|
|
494
527
|
const rows = recentDayKeys(STATS_DAYS, now).map((date) => {
|
|
495
528
|
const day = daily.get(date);
|
|
496
529
|
if (day == null) {
|
|
497
|
-
return {
|
|
530
|
+
return {
|
|
531
|
+
date,
|
|
532
|
+
...emptyTokenBucket(),
|
|
533
|
+
totalTokens: 0,
|
|
534
|
+
cost: 0,
|
|
535
|
+
sessions: 0,
|
|
536
|
+
byModel: [],
|
|
537
|
+
peak: emptyTokenBucket(),
|
|
538
|
+
offpeak: emptyTokenBucket(),
|
|
539
|
+
peakCost: 0,
|
|
540
|
+
offpeakCost: 0,
|
|
541
|
+
};
|
|
498
542
|
}
|
|
499
543
|
return {
|
|
500
544
|
date,
|
|
@@ -505,6 +549,11 @@ async function buildUsageStats(ctx, config) {
|
|
|
505
549
|
totalTokens: day.totalTokens,
|
|
506
550
|
cost: day.cost,
|
|
507
551
|
sessions: day.sessionIds.size,
|
|
552
|
+
byModel: [...day.byModel.values()],
|
|
553
|
+
peak: day.peak,
|
|
554
|
+
offpeak: day.offpeak,
|
|
555
|
+
peakCost: day.peakCost,
|
|
556
|
+
offpeakCost: day.offpeakCost,
|
|
508
557
|
};
|
|
509
558
|
});
|
|
510
559
|
const total = rows.reduce((acc, day) => {
|
|
@@ -514,11 +563,36 @@ async function buildUsageStats(ctx, config) {
|
|
|
514
563
|
acc.cacheWriteTokens += day.cacheWriteTokens;
|
|
515
564
|
acc.totalTokens += day.totalTokens;
|
|
516
565
|
acc.cost += day.cost;
|
|
566
|
+
// —— 细分维度:与上方总额同口径,只做拆分,不改变金额 ——
|
|
567
|
+
addTokenUsage(acc.peak, day.peak);
|
|
568
|
+
addTokenUsage(acc.offpeak, day.offpeak);
|
|
569
|
+
acc.peakCost += day.peakCost;
|
|
570
|
+
acc.offpeakCost += day.offpeakCost;
|
|
571
|
+
for (const item of day.byModel ?? []) {
|
|
572
|
+
let entry = acc.byModel.get(item.model);
|
|
573
|
+
if (entry == null) {
|
|
574
|
+
entry = { model: item.model, ...emptyTokenBucket(), totalTokens: 0, cost: 0 };
|
|
575
|
+
acc.byModel.set(item.model, entry);
|
|
576
|
+
}
|
|
577
|
+
addTokenUsage(entry, item);
|
|
578
|
+
entry.totalTokens += item.totalTokens;
|
|
579
|
+
entry.cost += item.cost;
|
|
580
|
+
}
|
|
517
581
|
return acc;
|
|
518
|
-
}, {
|
|
582
|
+
}, {
|
|
583
|
+
...emptyTokenBucket(),
|
|
584
|
+
totalTokens: 0,
|
|
585
|
+
cost: 0,
|
|
586
|
+
byModel: new Map(),
|
|
587
|
+
peak: emptyTokenBucket(),
|
|
588
|
+
offpeak: emptyTokenBucket(),
|
|
589
|
+
peakCost: 0,
|
|
590
|
+
offpeakCost: 0,
|
|
591
|
+
});
|
|
519
592
|
return {
|
|
520
593
|
days: rows,
|
|
521
|
-
|
|
594
|
+
// byModel 内部用 Map 聚合,返回前转成可直接 JSON 序列化的数组
|
|
595
|
+
total: { ...total, byModel: [...total.byModel.values()] },
|
|
522
596
|
rangeDays: STATS_DAYS,
|
|
523
597
|
scannedSessions,
|
|
524
598
|
failedSessions,
|