@lancecheney/dsh-deepseek-balance 0.1.0 → 0.1.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/lib/client.js +268 -245
- package/lib/index.js +422 -70
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -6,170 +6,86 @@ window.__ModuleLoader__.load({
|
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
7
|
let react = require("react");
|
|
8
8
|
|
|
9
|
-
const css = ".Dbg1_root{display:inline-flex;align-items:center;gap:6px;height:32px;border:1px solid var(--dsw-alias-border-l2);border-radius:18px;padding:0 12px;font-family:var(--dsw-font-family);font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);background:0 0;white-space:nowrap;
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const MODEL_META = {
|
|
21
|
-
"deepseek-v4-pro": { label: "DeepSeek-V4-Pro" },
|
|
22
|
-
"deepseek-v4-flash": { label: "DeepSeek-V4-Flash" }
|
|
9
|
+
const css = ".Dbg1_root{display:inline-flex;align-items:center;gap:6px;height:32px;border:1px solid var(--dsw-alias-border-l2);border-radius:18px;padding:0 12px;font-family:var(--dsw-font-family);font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);background:0 0;white-space:nowrap;cursor:pointer}.Dbg1_root:hover{background:var(--dsw-alias-interactive-bg-hover)}.Dbg1_num{color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;font-weight:500}.Dbg1_sep{color:var(--dsw-alias-label-dimmed)}.Dbg1_chip{border-radius:9px;padding:0 8px;font-size:11px;line-height:18px;font-weight:600}.Dbg1_peak{color:var(--dsw-alias-state-warn-primary)}.Dbg1_offPeak{color:var(--dsw-alias-state-success-primary)}.Dbg1_legacy{color:var(--dsw-alias-label-tertiary)}.Dbg1_dimmed{color:var(--dsw-alias-label-dimmed)}.Dbg1_plus{font-size:9px;line-height:1;vertical-align:super;color:var(--dsw-alias-state-warn-primary);font-weight:700}.Dbg1_unit{font-size:10px}";
|
|
10
|
+
const css2 = ".Dp1_backdrop{position:fixed;top:0;bottom:0;right:0;background:rgba(0,0,0,.32);z-index:60}.Dp1_panel{position:fixed;top:0;bottom:0;right:0;z-index:61;display:flex;flex-direction:column;background:var(--dsw-alias-bg-base);border-left:1px solid var(--dsw-alias-border-l1);box-shadow:var(--dsw-shadow-lv3);overflow-y:auto}.Dp1_head{display:flex;flex-direction:column;align-items:center;gap:8px;padding:32px 56px 22px;position:relative}.Dp1_avatar{width:64px;height:64px;border-radius:50%;background:var(--dsw-alias-fill-l2);color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:24px;flex:none;border:2px solid var(--dsw-alias-border-l2)}.Dp1_name{font-size:18px;font-weight:600;color:var(--dsw-alias-label-primary)}.Dp1_key{font-family:var(--dsw-font-mono);color:var(--dsw-alias-label-tertiary);font-size:13px}.Dp1_close{position:fixed;top:16px;right:16px;cursor:pointer;background:0 0;border:0;color:var(--dsw-alias-label-secondary);font-size:22px;line-height:1;z-index:62}.Dp1_body{padding:0 56px 36px;display:flex;flex-direction:column;gap:26px}.Dp1_stats{display:flex;align-items:center;justify-content:center;gap:56px;flex-wrap:wrap;padding:18px 0}.Dp1_statItem{display:flex;flex-direction:column;align-items:center;gap:4px}.Dp1_statValue{font-size:18px;font-weight:600;color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums}.Dp1_statLabel{font-size:12px;color:var(--dsw-alias-label-tertiary)}.Dp1_sepBar{color:var(--dsw-alias-label-dimmed);font-size:14px}.Dp1_balance{display:flex;flex-wrap:wrap;gap:16px;justify-content:center;margin:0 auto}.Dp1_balanceBlock{position:relative;box-sizing:border-box;width:200px;background:rgba(127,127,127,.10);border-radius:14px;padding:10px 14px;display:flex;flex-direction:column;align-items:flex-start;gap:2px;height:80px}.Dp1_balanceBlockLabel{font-size:12px;color:var(--dsw-alias-label-tertiary)}.Dp1_balanceBlockValue{font-size:28px;font-weight:700;color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;margin-top:auto}.Dp1_balanceBlock .Dp1_btn{position:absolute;right:12px;top:10px;height:22px;padding:0 8px;font-size:12px}.Dp1_btn{display:inline-flex;align-items:center;justify-content:center;height:30px;padding:0 12px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);background:0 0;color:var(--dsw-alias-label-primary);cursor:pointer;font-size:13px}.Dp1_btn:hover{background:var(--dsw-alias-interactive-bg-hover)}.Dp1_btnPrimary{background:#4d6bfe;color:#fff;border:none}.Dp1_section{padding:16px 0}.Dp1_title{font-size:13px;color:var(--dsw-alias-label-secondary);margin:0 0 12px;font-weight:600;letter-spacing:.02em}.Dp1_row{display:flex;align-items:center;gap:8px;padding:7px 0}.Dp1_twoCol{display:grid;grid-template-columns:1fr 1fr;gap:0 80px;align-items:start}.Dp1_rowTitle{flex:1;min-width:0;color:var(--dsw-alias-label-primary);font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.Dp1_rowTitleGray{color:var(--dsw-alias-label-dimmed)}.Dp1_rowVal{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;font-size:13px;flex:none}.Dp1_rank{color:var(--dsw-alias-label-tertiary);width:18px;text-align:right;flex:none;font-size:13px}.Dp1_monthNav{display:flex;align-items:center;justify-content:center;gap:16px;margin-bottom:14px}.Dp1_monthGrids{display:flex;flex-wrap:wrap;gap:20px}.Dp1_monthGrids .Dp1_monthGroup{flex:1 1 260px;min-width:0}.Dp1_monthGroup{display:flex;flex-direction:column;align-items:center;gap:8px}.Dp1_monthGroupTitle{font-size:12px;color:var(--dsw-alias-label-tertiary)}.Dp1_grid{display:grid;grid-template-columns:repeat(7,1fr);gap:5px;width:100%;max-width:170px}.Dp1_cell{aspect-ratio:1;border-radius:6px;background:rgba(127,127,127,.08);display:flex;align-items:center;justify-content:center;font-size:10px;color:var(--dsw-alias-label-tertiary)}.Dp1_cellFlat{background:#60a5fa;color:#fff}.Dp1_cellPeak{background:#f97316;color:#fff}.Dp1_cellOff{background:#22c55e;color:#fff}.Dp1_cellBlue{color:#fff}.Dp1_legend{display:flex;gap:12px;margin-top:10px;justify-content:center;font-size:11px;color:var(--dsw-alias-label-tertiary);flex-wrap:wrap}.Dp1_dot{display:inline-block;width:8px;height:8px;border-radius:2px;margin-right:4px;vertical-align:middle}.Dp1_nameRow{display:flex;align-items:center;gap:6px}.Dp1_edit{border:0;background:0 0;cursor:pointer;color:var(--dsw-alias-label-tertiary);font-size:14px;padding:2px;line-height:1}.Dp1_edit:hover{color:var(--dsw-alias-label-primary)}.Dp1_editRow{display:flex;align-items:center;gap:8px}.Dp1_editInput{height:30px;padding:0 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-fill-l1);color:var(--dsw-alias-label-primary);font-size:13px;width:180px}@media (max-width:640px){.Dbg1_root{margin-top:80px}.Dp1_head{padding:24px 16px 16px}.Dp1_body{padding:0 16px 24px}.Dp1_twoCol{grid-template-columns:1fr;gap:0}.Dp1_stats{gap:20px;padding:12px 0}.Dp1_close{right:12px;top:12px}}";
|
|
11
|
+
const injectCss = (text, tagId) => {
|
|
12
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
13
|
+
const tag = document.createElement("style");
|
|
14
|
+
tag.dataset.plugin = "@lancecheney/dsh-deepseek-balance";
|
|
15
|
+
tag.dataset.pluginCss = tagId;
|
|
16
|
+
tag.textContent = text;
|
|
17
|
+
document.head.appendChild(tag);
|
|
18
|
+
}
|
|
23
19
|
};
|
|
20
|
+
injectCss(css, "@lancecheney/dsh-deepseek-balance/BillingBadge.css");
|
|
21
|
+
injectCss(css2, "@lancecheney/dsh-deepseek-balance/UsagePanel.css");
|
|
22
|
+
const C = { root: "Dbg1_root", num: "Dbg1_num", sep: "Dbg1_sep", chip: "Dbg1_chip", peak: "Dbg1_peak", offPeak: "Dbg1_offPeak", legacy: "Dbg1_legacy", dimmed: "Dbg1_dimmed", plus: "Dbg1_plus", unit: "Dbg1_unit" };
|
|
23
|
+
const P = { backdrop: "Dp1_backdrop", panel: "Dp1_panel", head: "Dp1_head", avatar: "Dp1_avatar", name: "Dp1_name", key: "Dp1_key", close: "Dp1_close", body: "Dp1_body", stats: "Dp1_stats", statItem: "Dp1_statItem", statValue: "Dp1_statValue", statLabel: "Dp1_statLabel", sepBar: "Dp1_sepBar", balance: "Dp1_balance", balanceBlock: "Dp1_balanceBlock", balanceBlockLabel: "Dp1_balanceBlockLabel", balanceBlockValue: "Dp1_balanceBlockValue", btn: "Dp1_btn", btnPrimary: "Dp1_btnPrimary", section: "Dp1_section", title: "Dp1_title", row: "Dp1_row", twoCol: "Dp1_twoCol", rowTitle: "Dp1_rowTitle", rowTitleGray: "Dp1_rowTitleGray", rowVal: "Dp1_rowVal", rank: "Dp1_rank", monthNav: "Dp1_monthNav", monthGrids: "Dp1_monthGrids", monthGroup: "Dp1_monthGroup", monthGroupTitle: "Dp1_monthGroupTitle", grid: "Dp1_grid", cell: "Dp1_cell", cellFlat: "Dp1_cellFlat", cellPeak: "Dp1_cellPeak", cellOff: "Dp1_cellOff", cellBlue: "Dp1_cellBlue", legend: "Dp1_legend", dot: "Dp1_dot", nameRow: "Dp1_nameRow", edit: "Dp1_edit", editRow: "Dp1_editRow", editInput: "Dp1_editInput" };
|
|
24
24
|
|
|
25
|
+
const MODEL_META = { "deepseek-v4-pro": { label: "DeepSeek-V4-Pro" }, "deepseek-v4-flash": { label: "DeepSeek-V4-Flash" } };
|
|
25
26
|
const FALLBACK_PRICING = {
|
|
26
27
|
effectiveFrom: "2026-08-17T00:00:00+08:00",
|
|
27
28
|
currencies: {
|
|
28
|
-
CNY: {
|
|
29
|
-
|
|
30
|
-
models: {
|
|
31
|
-
"deepseek-v4-pro": {
|
|
32
|
-
legacy: { hit: 0.025, miss: 3.0, output: 6.0 },
|
|
33
|
-
peak: { hit: 0.30, miss: 9.0, output: 27.0 },
|
|
34
|
-
offPeak: { hit: 0.15, miss: 4.5, output: 13.5 }
|
|
35
|
-
},
|
|
36
|
-
"deepseek-v4-flash": {
|
|
37
|
-
legacy: { hit: 0.02, miss: 1.0, output: 2.0 },
|
|
38
|
-
peak: { hit: 0.10, miss: 3.0, output: 9.0 },
|
|
39
|
-
offPeak: { hit: 0.05, miss: 1.5, output: 4.5 }
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
},
|
|
43
|
-
USD: {
|
|
44
|
-
symbol: "$",
|
|
45
|
-
models: {
|
|
46
|
-
"deepseek-v4-pro": {
|
|
47
|
-
legacy: { hit: 0.003625, miss: 0.435, output: 0.87 },
|
|
48
|
-
peak: { hit: 0.044, miss: 1.32, output: 3.96 },
|
|
49
|
-
offPeak: { hit: 0.022, miss: 0.66, output: 1.98 }
|
|
50
|
-
},
|
|
51
|
-
"deepseek-v4-flash": {
|
|
52
|
-
legacy: { hit: 0.0028, miss: 0.14, output: 0.28 },
|
|
53
|
-
peak: { hit: 0.014, miss: 0.44, output: 1.32 },
|
|
54
|
-
offPeak: { hit: 0.007, miss: 0.22, output: 0.66 }
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
29
|
+
CNY: { symbol: "¥", models: { "deepseek-v4-pro": { legacy: { hit: 0.025, miss: 3.0, output: 6.0 }, peak: { hit: 0.30, miss: 9.0, output: 27.0 }, offPeak: { hit: 0.15, miss: 4.5, output: 13.5 } }, "deepseek-v4-flash": { legacy: { hit: 0.02, miss: 1.0, output: 2.0 }, peak: { hit: 0.10, miss: 3.0, output: 9.0 }, offPeak: { hit: 0.05, miss: 1.5, output: 4.5 } } } },
|
|
30
|
+
USD: { symbol: "$", models: { "deepseek-v4-pro": { legacy: { hit: 0.003625, miss: 0.435, output: 0.87 }, peak: { hit: 0.044, miss: 1.32, output: 3.96 }, offPeak: { hit: 0.022, miss: 0.66, output: 1.98 } }, "deepseek-v4-flash": { legacy: { hit: 0.0028, miss: 0.14, output: 0.28 }, peak: { hit: 0.014, miss: 0.44, output: 1.32 }, offPeak: { hit: 0.007, miss: 0.22, output: 0.66 } } } }
|
|
58
31
|
}
|
|
59
32
|
};
|
|
60
33
|
const DEFAULT_MODEL = "deepseek-v4-pro";
|
|
61
34
|
|
|
62
|
-
function beijingDecimalHour(now) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const h = beijingDecimalHour(now);
|
|
87
|
-
return (h >= 9 && h < 12) || (h >= 14 && h < 18) ? "peak" : "offPeak";
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function firstBalance(data) {
|
|
91
|
-
const infos = data && data.balance_infos;
|
|
92
|
-
if (!Array.isArray(infos) || infos.length === 0) return null;
|
|
93
|
-
return infos.find((row) => row.currency === "CNY") || infos[0];
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function fmtPrice(value) {
|
|
97
|
-
const n = Number(value);
|
|
98
|
-
return Number.isFinite(n) ? String(n) : String(value);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function fmtMoney(value) {
|
|
102
|
-
const n = Number(value);
|
|
103
|
-
if (!Number.isFinite(n)) return "—";
|
|
104
|
-
if (n <= 0) return "0.00";
|
|
105
|
-
if (n < 0.01) return n.toFixed(4);
|
|
106
|
-
if (n < 1) return n.toFixed(3);
|
|
107
|
-
return n.toFixed(2);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function costOf(usage, p) {
|
|
111
|
-
const miss = (usage.uncachedInputTokens ?? 0) + (usage.cacheWriteTokens ?? 0);
|
|
112
|
-
const hit = usage.cacheReadTokens ?? 0;
|
|
113
|
-
const output = usage.outputTokens ?? 0;
|
|
114
|
-
return (miss * p.miss + hit * p.hit + output * p.output) / 1e6;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function sumBuckets(a, b) {
|
|
118
|
-
return {
|
|
119
|
-
uncachedInputTokens: (a.uncachedInputTokens ?? 0) + (b.uncachedInputTokens ?? 0),
|
|
120
|
-
outputTokens: (a.outputTokens ?? 0) + (b.outputTokens ?? 0),
|
|
121
|
-
cacheReadTokens: (a.cacheReadTokens ?? 0) + (b.cacheReadTokens ?? 0),
|
|
122
|
-
cacheWriteTokens: (a.cacheWriteTokens ?? 0) + (b.cacheWriteTokens ?? 0)
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function translate(dict, key, params) {
|
|
127
|
-
let s = dict[key];
|
|
128
|
-
if (s === void 0) return key;
|
|
129
|
-
if (params) {
|
|
130
|
-
for (const [k, v] of Object.entries(params)) s = s.split(`{${k}}`).join(String(v));
|
|
35
|
+
function beijingDecimalHour(now) { let h = 0, m = 0; try { const p = new Intl.DateTimeFormat("en-GB", { timeZone: "Asia/Shanghai", hour12: false, hour: "2-digit", minute: "2-digit" }).formatToParts(now); for (const x of p) { if (x.type === "hour") h = Number(x.value); if (x.type === "minute") m = Number(x.value); } } catch { h = now.getHours(); m = now.getMinutes(); } return h + m / 60; }
|
|
36
|
+
function periodFor(now, effectiveFrom) { const eff = Date.parse(effectiveFrom); if (Number.isFinite(eff) && now.getTime() < eff) return "legacy"; const h = beijingDecimalHour(now); return (h >= 9 && h < 12) || (h >= 14 && h < 18) ? "peak" : "offPeak"; }
|
|
37
|
+
function firstBalance(data) { const infos = data && data.balance_infos; if (!Array.isArray(infos) || infos.length === 0) return null; return infos.find((r) => r.currency === "CNY") || infos[0]; }
|
|
38
|
+
function fmtPrice(v) { const n = Number(v); return Number.isFinite(n) ? String(n) : String(v); }
|
|
39
|
+
function fmtMoney(v) { const n = Number(v); if (!Number.isFinite(n)) return "—"; if (n <= 0) return "0.00"; if (n < 0.01) return n.toFixed(4); if (n < 1) return n.toFixed(3); return n.toFixed(2); }
|
|
40
|
+
function translate(dict, key, params) { let s = dict[key]; if (s === void 0) return key; if (params) for (const [k, v] of Object.entries(params)) s = s.split(`{${k}}`).join(String(v)); return s; }
|
|
41
|
+
function fmtTokens(n) { if (!Number.isFinite(n)) return "—"; if (n < 1000) return String(Math.round(n)); const scaled = (v) => v >= 100 ? String(Math.round(v)) : String(Math.round(v * 10) / 10); if (n < 1e6) return scaled(n / 1e3) + "K"; return scaled(n / 1e6) + "M"; }
|
|
42
|
+
function fmtDuration(ms) { if (!Number.isFinite(ms) || ms <= 0) return "—"; const s = ms / 1e3; if (s < 60) return Math.round(s) + "s"; const w = Math.round(s); const h = Math.floor(w / 3600); const m = Math.floor((w % 3600) / 60); if (h > 0) return h + "h" + m + "m"; return m + "m" + (w % 60) + "s"; }
|
|
43
|
+
function beijingDayKey(ms) { return new Date(ms + 8 * 3600 * 1000).toISOString().slice(0, 10); }
|
|
44
|
+
function beijingMonthKey(ms) { return new Date(ms + 8 * 3600 * 1000).toISOString().slice(0, 7); }
|
|
45
|
+
function findFrame() { if (typeof document === "undefined") return null; const overlay = document.querySelector("[data-shell-overlay]"); return overlay && overlay.parentElement ? overlay.parentElement : null; }
|
|
46
|
+
function sidebarWidthOf(frame) { const col = frame && frame.firstElementChild; return col ? col.getBoundingClientRect().width : null; }
|
|
47
|
+
const noopSubscribe = () => () => {};
|
|
48
|
+
function dayColor(d) { if (!d || d.tokens <= 0) return "none"; if (d.flat >= d.peak && d.flat >= d.offPeak) return "flat"; return d.peak >= d.offPeak ? "peak" : "off"; }
|
|
49
|
+
function tokenBlue(tokens, max) { if (!tokens || tokens <= 0 || max <= 0) return null; const r = tokens / max; if (r < 0.2) return "#93c5fd"; if (r < 0.4) return "#60a5fa"; if (r < 0.6) return "#3b82f6"; if (r < 0.8) return "#2563eb"; return "#1e40af"; }
|
|
50
|
+
function monthGrid(year, month, daysData) {
|
|
51
|
+
const first = new Date(Date.UTC(year, month, 1));
|
|
52
|
+
const daysInMonth = new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
|
|
53
|
+
const lead = (first.getUTCDay() + 6) % 7;
|
|
54
|
+
const cells = [];
|
|
55
|
+
for (let i = 0; i < lead; i++) cells.push(null);
|
|
56
|
+
for (let d = 1; d <= daysInMonth; d++) {
|
|
57
|
+
const key = `${year}-${String(month + 1).padStart(2, "0")}-${String(d).padStart(2, "0")}`;
|
|
58
|
+
cells.push({ day: d, key, data: daysData[key] });
|
|
131
59
|
}
|
|
132
|
-
return
|
|
60
|
+
return cells;
|
|
133
61
|
}
|
|
134
62
|
|
|
135
|
-
const noopSubscribe = () => () => {};
|
|
136
|
-
|
|
137
63
|
function BillingBadge(props) {
|
|
138
|
-
const useProjection = props.useProjection;
|
|
139
64
|
const directory = props.directory;
|
|
140
|
-
const
|
|
141
|
-
const
|
|
65
|
+
const sessionId = props.sessionId;
|
|
66
|
+
const [open, setOpen] = react.useState(false);
|
|
142
67
|
const [state, setState] = react.useState({ phase: "loading" });
|
|
143
68
|
|
|
144
69
|
react.useEffect(() => {
|
|
145
|
-
let alive = true;
|
|
146
|
-
let timer;
|
|
70
|
+
let alive = true; let timer;
|
|
147
71
|
const load = async () => {
|
|
148
72
|
try {
|
|
149
|
-
const
|
|
73
|
+
const usageUrl = sessionId ? `/api/deepseek-usage?session=${encodeURIComponent(sessionId)}` : "/api/deepseek-usage";
|
|
74
|
+
const [balanceRes, pricingRes, usageRes] = await Promise.all([
|
|
150
75
|
fetch("/api/deepseek-balance", { headers: { accept: "application/json" }, cache: "no-store" }),
|
|
151
|
-
fetch("/api/deepseek-pricing", { headers: { accept: "application/json" }, cache: "no-store" })
|
|
76
|
+
fetch("/api/deepseek-pricing", { headers: { accept: "application/json" }, cache: "no-store" }),
|
|
77
|
+
fetch(usageUrl, { headers: { accept: "application/json" }, cache: "no-store" })
|
|
152
78
|
]);
|
|
153
79
|
const balanceData = await balanceRes.json().catch(() => ({}));
|
|
154
80
|
const pricingData = await pricingRes.json().catch(() => null);
|
|
81
|
+
const usageData = await usageRes.json().catch(() => null);
|
|
155
82
|
if (!alive) return;
|
|
156
|
-
setState({
|
|
157
|
-
|
|
158
|
-
balance: balanceRes.ok ? balanceData : null,
|
|
159
|
-
balanceError: balanceRes.ok ? null : (balanceData.error || `HTTP ${balanceRes.status}`),
|
|
160
|
-
pricing: pricingData && pricingRes.ok && pricingData.currencies ? pricingData : FALLBACK_PRICING
|
|
161
|
-
});
|
|
162
|
-
} catch (error) {
|
|
163
|
-
if (alive) setState({ phase: "error", message: error instanceof Error ? error.message : String(error) });
|
|
164
|
-
}
|
|
165
|
-
};
|
|
166
|
-
load();
|
|
167
|
-
timer = setInterval(load, 60000);
|
|
168
|
-
return () => {
|
|
169
|
-
alive = false;
|
|
170
|
-
clearInterval(timer);
|
|
83
|
+
setState({ phase: "ok", balance: balanceRes.ok ? balanceData : null, pricing: pricingData && pricingRes.ok && pricingData.currencies ? pricingData : FALLBACK_PRICING, usage: usageData && usageData.spend ? usageData : null });
|
|
84
|
+
} catch (e) { if (alive) setState({ phase: "error" }); }
|
|
171
85
|
};
|
|
172
|
-
|
|
86
|
+
load(); timer = setInterval(load, 30000);
|
|
87
|
+
return () => { alive = false; clearInterval(timer); };
|
|
88
|
+
}, [sessionId]);
|
|
173
89
|
|
|
174
90
|
const pricing = state.pricing || FALLBACK_PRICING;
|
|
175
91
|
const effectiveFrom = pricing.effectiveFrom || FALLBACK_PRICING.effectiveFrom;
|
|
@@ -181,10 +97,7 @@ window.__ModuleLoader__.load({
|
|
|
181
97
|
const symbol = pricingSet.symbol || "¥";
|
|
182
98
|
const models = pricingSet.models || FALLBACK_PRICING.currencies.CNY.models;
|
|
183
99
|
|
|
184
|
-
const modelState = react.useSyncExternalStore(
|
|
185
|
-
directory ? (fn) => directory.subscribe(fn) : noopSubscribe,
|
|
186
|
-
() => (directory ? directory.getSnapshot() : null)
|
|
187
|
-
);
|
|
100
|
+
const modelState = react.useSyncExternalStore(directory ? (fn) => directory.subscribe(fn) : noopSubscribe, () => (directory ? directory.getSnapshot() : null));
|
|
188
101
|
const currentSelection = modelState && modelState.current ? modelState.current : null;
|
|
189
102
|
const modelId = currentSelection && currentSelection.model ? currentSelection.model : DEFAULT_MODEL;
|
|
190
103
|
const reasoningEffort = currentSelection ? currentSelection.reasoningEffort : void 0;
|
|
@@ -195,118 +108,232 @@ window.__ModuleLoader__.load({
|
|
|
195
108
|
const model = models[modelId] || models[DEFAULT_MODEL] || FALLBACK_PRICING.currencies.CNY.models[DEFAULT_MODEL];
|
|
196
109
|
const p = (period === "peak" ? model.peak : period === "offPeak" ? model.offPeak : model.legacy) || model.peak;
|
|
197
110
|
const outputPrice = fmtPrice(p.output);
|
|
198
|
-
const cost =
|
|
199
|
-
if (byPeriod && byPeriod.peak && byPeriod.offPeak) {
|
|
200
|
-
if (isLegacy) {
|
|
201
|
-
return model.legacy ? costOf(sumBuckets(byPeriod.peak, byPeriod.offPeak), model.legacy) : null;
|
|
202
|
-
}
|
|
203
|
-
return costOf(byPeriod.peak, model.peak) + costOf(byPeriod.offPeak, model.offPeak);
|
|
204
|
-
}
|
|
205
|
-
return usage !== undefined && usage !== null ? costOf(usage, p) : null;
|
|
206
|
-
})();
|
|
207
|
-
|
|
208
|
-
// off -> base price; high (default) -> one +; max -> two +.
|
|
111
|
+
const cost = state.usage && state.usage.spend ? state.usage.spend.total : null;
|
|
209
112
|
const plus = reasoningEffort === "max" ? "++" : reasoningEffort === "off" ? "" : "+";
|
|
210
|
-
|
|
211
|
-
const periodClass = isLegacy ? cssModule.legacy : isPeak ? cssModule.peak : cssModule.offPeak;
|
|
113
|
+
const periodClass = isLegacy ? C.legacy : isPeak ? C.peak : C.offPeak;
|
|
212
114
|
const periodLabel = isLegacy ? tr("period.legacy") : isPeak ? tr("period.peak") : tr("period.offPeak");
|
|
213
115
|
|
|
214
|
-
const
|
|
215
|
-
isLegacy ? tr("period.legacyNote") : tr("period.peakHours"),
|
|
216
|
-
isLegacy ? "" : tr("period.offPeakNote"),
|
|
217
|
-
"",
|
|
218
|
-
`${tr("label.model")}: ${(MODEL_META[modelId] || {}).label || modelId}${reasoningEffort ? ` · ${reasoningEffort}` : ""}`,
|
|
219
|
-
tr("reasoning.note")
|
|
220
|
-
].filter((line) => line !== "");
|
|
221
|
-
if (cost !== null && usage) {
|
|
222
|
-
tooltipParts.push(
|
|
223
|
-
"",
|
|
224
|
-
`${tr("label.spent")}: ${symbol}${fmtMoney(cost)} (${tr("cost.estimate")})`,
|
|
225
|
-
tr("cost.breakdown", {
|
|
226
|
-
miss: (usage.uncachedInputTokens ?? 0) + (usage.cacheWriteTokens ?? 0),
|
|
227
|
-
hit: usage.cacheReadTokens ?? 0,
|
|
228
|
-
output: usage.outputTokens ?? 0
|
|
229
|
-
})
|
|
230
|
-
);
|
|
231
|
-
}
|
|
232
|
-
tooltipParts.push("");
|
|
233
|
-
for (const [id, info] of Object.entries(models)) {
|
|
234
|
-
const meta = MODEL_META[id] || {};
|
|
235
|
-
const row = tr("price.row", { sym: symbol, hit: fmtPrice(info.peak.hit), miss: fmtPrice(info.peak.miss), output: fmtPrice(info.peak.output) });
|
|
236
|
-
const off = tr("price.row", { sym: symbol, hit: fmtPrice(info.offPeak.hit), miss: fmtPrice(info.offPeak.miss), output: fmtPrice(info.offPeak.output) });
|
|
237
|
-
const legacy = info.legacy ? tr("price.row", { sym: symbol, hit: fmtPrice(info.legacy.hit), miss: fmtPrice(info.legacy.miss), output: fmtPrice(info.legacy.output) }) : null;
|
|
238
|
-
tooltipParts.push(
|
|
239
|
-
`${meta.label || id} — ${tr("period.peak")}: ${row} / ${tr("period.offPeak")}: ${off}${legacy ? ` / ${tr("period.legacy")}: ${legacy}` : ""}`
|
|
240
|
-
);
|
|
241
|
-
}
|
|
242
|
-
const tooltip = tooltipParts.join("\n");
|
|
243
|
-
|
|
244
|
-
const sep = (key) => react.createElement("span", { key, className: cssModule.sep, "aria-hidden": true }, "|");
|
|
245
|
-
|
|
116
|
+
const sep = (key) => react.createElement("span", { key, className: C.sep, "aria-hidden": true }, "|");
|
|
246
117
|
const segs = [];
|
|
247
|
-
segs.push(react.createElement("span", { key: "spent" },
|
|
248
|
-
tr("label.spent"), " ",
|
|
249
|
-
cost === null
|
|
250
|
-
? react.createElement("span", { className: cssModule.num }, "—")
|
|
251
|
-
: react.createElement("span", { className: cssModule.num },
|
|
252
|
-
react.createElement("span", { className: cssModule.unit }, symbol),
|
|
253
|
-
fmtMoney(cost))));
|
|
118
|
+
segs.push(react.createElement("span", { key: "spent" }, tr("label.spent"), " ", react.createElement("span", { className: C.num }, cost === null ? "—" : `${symbol}${fmtMoney(cost)}`)));
|
|
254
119
|
segs.push(sep("sep1"));
|
|
255
|
-
segs.push(react.createElement("span", { key: "balance" },
|
|
256
|
-
tr("label.balance"), " ",
|
|
257
|
-
balanceRow === null
|
|
258
|
-
? react.createElement("span", { className: cssModule.num }, state.phase === "loading" ? "…" : tr("balance.unavailable"))
|
|
259
|
-
: react.createElement("span", { className: cssModule.num },
|
|
260
|
-
react.createElement("span", { className: cssModule.unit }, symbol),
|
|
261
|
-
fmtMoney(Number(balanceRow.total_balance)))));
|
|
120
|
+
segs.push(react.createElement("span", { key: "balance" }, tr("label.balance"), " ", react.createElement("span", { className: C.num }, balanceRow !== null ? `${symbol}${fmtMoney(Number(balanceRow.total_balance))}` : (state.phase === "loading" ? "…" : tr("balance.unavailable")))));
|
|
262
121
|
segs.push(sep("sep2"));
|
|
263
|
-
segs.push(react.createElement("span", { key: "period", className: `${
|
|
122
|
+
segs.push(react.createElement("span", { key: "period", className: `${C.chip} ${periodClass}` }, periodLabel));
|
|
264
123
|
segs.push(sep("sep3"));
|
|
265
|
-
segs.push(react.createElement("span", { key: "price" },
|
|
266
|
-
react.createElement("span", { className: cssModule.unit }, symbol),
|
|
267
|
-
outputPrice,
|
|
268
|
-
react.createElement("span", { className: cssModule.unit }, tr("price.perM")),
|
|
269
|
-
plus !== "" ? react.createElement("span", { className: cssModule.plus }, plus) : null));
|
|
124
|
+
segs.push(react.createElement("span", { key: "price" }, react.createElement("span", { className: C.unit }, symbol), outputPrice, react.createElement("span", { className: C.unit }, "/M"), plus !== "" ? react.createElement("span", { className: C.plus }, plus) : null));
|
|
270
125
|
|
|
271
|
-
return react.createElement(
|
|
126
|
+
return react.createElement(react.Fragment, null,
|
|
127
|
+
react.createElement("button", { type: "button", className: C.root, onClick: () => setOpen(true) }, segs),
|
|
128
|
+
open ? react.createElement(UsagePanel, { sessionId: props.sessionId, currency, symbol, pricing, effectiveFrom, tr, onClose: () => setOpen(false) }) : null
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function UsagePanel({ currency, symbol, pricing, tr, onClose }) {
|
|
133
|
+
const [usageData, setUsageData] = react.useState(null);
|
|
134
|
+
const [balance, setBalance] = react.useState(null);
|
|
135
|
+
const [sidebarW, setSidebarW] = react.useState(280);
|
|
136
|
+
const [monthIdxPeriod, setMonthIdxPeriod] = react.useState(0);
|
|
137
|
+
const [monthIdxTokens, setMonthIdxTokens] = react.useState(0);
|
|
138
|
+
const [editing, setEditing] = react.useState(false);
|
|
139
|
+
const [draft, setDraft] = react.useState("");
|
|
140
|
+
|
|
141
|
+
react.useEffect(() => {
|
|
142
|
+
let alive = true;
|
|
143
|
+
(async () => {
|
|
144
|
+
try {
|
|
145
|
+
const [u, b] = await Promise.all([
|
|
146
|
+
fetch("/api/deepseek-usage", { headers: { accept: "application/json" }, cache: "no-store" }).then((r) => r.json()),
|
|
147
|
+
fetch("/api/deepseek-balance", { headers: { accept: "application/json" }, cache: "no-store" }).then((r) => r.json())
|
|
148
|
+
]);
|
|
149
|
+
if (!alive) return;
|
|
150
|
+
setUsageData(u); setBalance(firstBalance(b));
|
|
151
|
+
} catch (e) {}
|
|
152
|
+
})();
|
|
153
|
+
return () => { alive = false; };
|
|
154
|
+
}, []);
|
|
155
|
+
|
|
156
|
+
react.useEffect(() => {
|
|
157
|
+
const frame = findFrame();
|
|
158
|
+
const col = frame && frame.firstElementChild;
|
|
159
|
+
if (!col) return;
|
|
160
|
+
const apply = () => setSidebarW(col.getBoundingClientRect().width);
|
|
161
|
+
apply();
|
|
162
|
+
const ro = new ResizeObserver(apply);
|
|
163
|
+
ro.observe(col);
|
|
164
|
+
return () => ro.disconnect();
|
|
165
|
+
}, []);
|
|
166
|
+
|
|
167
|
+
const months = react.useMemo(() => {
|
|
168
|
+
const set = new Set();
|
|
169
|
+
const cur = beijingMonthKey(Date.now());
|
|
170
|
+
if (usageData && usageData.days) for (const k of Object.keys(usageData.days)) set.add(k.slice(0, 7));
|
|
171
|
+
set.add(cur);
|
|
172
|
+
return [...set].sort();
|
|
173
|
+
}, [usageData]);
|
|
174
|
+
const monthKeyPeriod = months[months.length - 1 - monthIdxPeriod] || beijingMonthKey(Date.now());
|
|
175
|
+
const [yP, mP] = monthKeyPeriod.split("-").map(Number);
|
|
176
|
+
const cellsPeriod = react.useMemo(() => monthGrid(yP, mP - 1, usageData ? usageData.days : {}), [yP, mP, usageData]);
|
|
177
|
+
const monthKeyTokens = months[months.length - 1 - monthIdxTokens] || beijingMonthKey(Date.now());
|
|
178
|
+
const [yT, mT] = monthKeyTokens.split("-").map(Number);
|
|
179
|
+
const cellsTokens = react.useMemo(() => monthGrid(yT, mT - 1, usageData ? usageData.days : {}), [yT, mT, usageData]);
|
|
180
|
+
const maxDayTokens = react.useMemo(() => {
|
|
181
|
+
if (!usageData || !usageData.days) return 0;
|
|
182
|
+
return Object.values(usageData.days).reduce((m, d) => Math.max(m, d.tokens || 0), 0);
|
|
183
|
+
}, [usageData]);
|
|
184
|
+
const pricingSet = (pricing.currencies && pricing.currencies[currency]) || pricing.currencies.CNY;
|
|
185
|
+
const sym = pricingSet.symbol || "¥";
|
|
186
|
+
|
|
187
|
+
const totalSpend = usageData && usageData.spend ? usageData.spend.total : null;
|
|
188
|
+
const todaySpend = usageData && usageData.spend ? usageData.spend.today : null;
|
|
189
|
+
|
|
190
|
+
const modelEffortList = react.useMemo(() => {
|
|
191
|
+
if (!usageData || !usageData.modelEffort) return [];
|
|
192
|
+
return Object.entries(usageData.modelEffort).map(([k, v]) => ({ key: k, tokens: v.tokens })).sort((a, b) => b.tokens - a.tokens);
|
|
193
|
+
}, [usageData]);
|
|
194
|
+
|
|
195
|
+
const cellFor = (cell) => {
|
|
196
|
+
if (!cell) return react.createElement("div", { key: "b" + Math.random(), className: P.cell });
|
|
197
|
+
const color = dayColor(cell.data);
|
|
198
|
+
const cls = color === "flat" ? P.cell + " " + P.cellFlat : color === "peak" ? P.cell + " " + P.cellPeak : color === "off" ? P.cell + " " + P.cellOff : P.cell;
|
|
199
|
+
return react.createElement("div", { key: cell.key, className: cls, title: cell.key }, cell.day);
|
|
200
|
+
};
|
|
201
|
+
const tokenCellFor = (cell) => {
|
|
202
|
+
if (!cell) return react.createElement("div", { key: "t" + Math.random(), className: P.cell });
|
|
203
|
+
const tokens = cell.data ? cell.data.tokens : 0;
|
|
204
|
+
const color = tokenBlue(tokens, maxDayTokens);
|
|
205
|
+
return react.createElement("div", { key: cell.key, className: P.cell + (color ? " " + P.cellBlue : ""), style: color ? { background: color } : void 0, title: cell.key }, cell.day);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const modelLabel = (k) => { const [m, e] = k.split("|"); return `${(MODEL_META[m] || {}).label || m}${e ? " · " + e : ""}`; };
|
|
209
|
+
const stat = (label, value) => react.createElement("div", { className: P.statItem }, react.createElement("div", { className: P.statValue }, value), react.createElement("div", { className: P.statLabel }, label));
|
|
210
|
+
const sepBar = (key) => react.createElement("span", { key, className: P.sepBar }, "|");
|
|
211
|
+
const tokenName = usageData && usageData.tokenName ? usageData.tokenName : "API key";
|
|
212
|
+
const saveName = async () => {
|
|
213
|
+
const name = draft.trim() || "API key";
|
|
214
|
+
try {
|
|
215
|
+
const r = await fetch("/api/deepseek-balance/token-name", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ tokenName: name }) });
|
|
216
|
+
const d = await r.json();
|
|
217
|
+
setUsageData((u) => (u ? { ...u, tokenName: d.tokenName } : u));
|
|
218
|
+
} catch {}
|
|
219
|
+
setEditing(false);
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
return react.createElement(react.Fragment, null,
|
|
223
|
+
react.createElement("div", { className: P.backdrop, style: { left: sidebarW + "px" }, onClick: onClose }),
|
|
224
|
+
react.createElement("div", { className: P.panel, style: { left: sidebarW + "px" } },
|
|
225
|
+
react.createElement("div", { className: P.head },
|
|
226
|
+
react.createElement("div", { className: P.avatar }, tokenName.slice(0, 2).toUpperCase()),
|
|
227
|
+
editing ? react.createElement("div", { className: P.editRow }, react.createElement("input", { className: P.editInput, value: draft, onChange: (e) => setDraft(e.target.value), autoFocus: true }), react.createElement("button", { className: P.btn + " " + P.btnPrimary, onClick: saveName }, tr("panel.save"))) : react.createElement("div", { className: P.nameRow }, react.createElement("span", { className: P.name }, tokenName), react.createElement("button", { className: P.edit, onClick: () => { setDraft(tokenName); setEditing(true); } }, "✎")),
|
|
228
|
+
react.createElement("div", { className: P.key }, usageData && usageData.apiKeyPreview ? usageData.apiKeyPreview : tr("balance.unavailable")),
|
|
229
|
+
react.createElement("button", { className: P.close, onClick: onClose }, "×")
|
|
230
|
+
),
|
|
231
|
+
react.createElement("div", { className: P.body },
|
|
232
|
+
react.createElement("div", { className: P.stats },
|
|
233
|
+
stat(tr("stat.total"), usageData ? fmtTokens(usageData.totalTokens) : "…"), sepBar("s1"),
|
|
234
|
+
stat(tr("stat.dailyPeak"), usageData ? fmtTokens(usageData.dailyPeakTokens) : "…"), sepBar("s2"),
|
|
235
|
+
stat(tr("stat.longest"), usageData ? fmtDuration(usageData.longestChatMs) : "…"), sepBar("s3"),
|
|
236
|
+
stat(tr("stat.curStreak"), usageData ? usageData.currentStreak + tr("unit.day") : "…"), sepBar("s4"),
|
|
237
|
+
stat(tr("stat.longStreak"), usageData ? usageData.longestStreak + tr("unit.day") : "…")
|
|
238
|
+
),
|
|
239
|
+
react.createElement("div", { className: P.balance },
|
|
240
|
+
react.createElement("div", { className: P.balanceBlock },
|
|
241
|
+
react.createElement("div", { className: P.balanceBlockLabel }, tr("stat.today")),
|
|
242
|
+
react.createElement("div", { className: P.balanceBlockValue }, todaySpend !== null ? `${sym}${fmtMoney(todaySpend)}` : "…")
|
|
243
|
+
),
|
|
244
|
+
react.createElement("div", { className: P.balanceBlock },
|
|
245
|
+
react.createElement("div", { className: P.balanceBlockLabel }, tr("stat.cumulative")),
|
|
246
|
+
react.createElement("div", { className: P.balanceBlockValue }, totalSpend !== null ? `${sym}${fmtMoney(totalSpend)}` : "…")
|
|
247
|
+
),
|
|
248
|
+
react.createElement("div", { className: P.balanceBlock },
|
|
249
|
+
react.createElement("div", { className: P.balanceBlockLabel }, tr("label.balance")),
|
|
250
|
+
react.createElement("div", { className: P.balanceBlockValue }, balance ? `${sym}${fmtMoney(Number(balance.total_balance))}` : "…"),
|
|
251
|
+
react.createElement("button", { className: P.btn + " " + P.btnPrimary, onClick: () => window.open("https://platform.deepseek.com/usage", "_blank", "noopener") }, tr("panel.topup"))
|
|
252
|
+
)
|
|
253
|
+
),
|
|
254
|
+
react.createElement("div", { className: P.section },
|
|
255
|
+
react.createElement("h3", { className: P.title }, tr("panel.month")),
|
|
256
|
+
react.createElement("div", { className: P.monthGrids },
|
|
257
|
+
react.createElement("div", { className: P.monthGroup },
|
|
258
|
+
react.createElement("div", { className: P.monthNav },
|
|
259
|
+
react.createElement("button", { className: P.btn, disabled: monthIdxPeriod >= months.length - 1, onClick: () => setMonthIdxPeriod(monthIdxPeriod + 1) }, "‹"),
|
|
260
|
+
react.createElement("span", { className: P.rowVal }, tr("panel.monthPeriod") + " · " + monthKeyPeriod),
|
|
261
|
+
react.createElement("button", { className: P.btn, disabled: monthIdxPeriod <= 0, onClick: () => setMonthIdxPeriod(monthIdxPeriod - 1) }, "›")
|
|
262
|
+
),
|
|
263
|
+
react.createElement("div", { className: P.grid }, cellsPeriod.map(cellFor)),
|
|
264
|
+
react.createElement("div", { className: P.legend },
|
|
265
|
+
react.createElement("span", null, react.createElement("span", { className: P.dot, style: { background: "#60a5fa" } }), tr("legend.flat")),
|
|
266
|
+
react.createElement("span", null, react.createElement("span", { className: P.dot, style: { background: "#f97316" } }), tr("legend.peak")),
|
|
267
|
+
react.createElement("span", null, react.createElement("span", { className: P.dot, style: { background: "#22c55e" } }), tr("legend.off")),
|
|
268
|
+
react.createElement("span", null, react.createElement("span", { className: P.dot, style: { background: "rgba(127,127,127,.15)" } }), tr("legend.none"))
|
|
269
|
+
)
|
|
270
|
+
),
|
|
271
|
+
react.createElement("div", { className: P.monthGroup },
|
|
272
|
+
react.createElement("div", { className: P.monthNav },
|
|
273
|
+
react.createElement("button", { className: P.btn, disabled: monthIdxTokens >= months.length - 1, onClick: () => setMonthIdxTokens(monthIdxTokens + 1) }, "‹"),
|
|
274
|
+
react.createElement("span", { className: P.rowVal }, tr("panel.monthTokens") + " · " + monthKeyTokens),
|
|
275
|
+
react.createElement("button", { className: P.btn, disabled: monthIdxTokens <= 0, onClick: () => setMonthIdxTokens(monthIdxTokens - 1) }, "›")
|
|
276
|
+
),
|
|
277
|
+
react.createElement("div", { className: P.grid }, cellsTokens.map(tokenCellFor)),
|
|
278
|
+
react.createElement("div", { className: P.legend },
|
|
279
|
+
react.createElement("span", null, react.createElement("span", { className: P.dot, style: { background: "#93c5fd" } }), tr("legend.less")),
|
|
280
|
+
react.createElement("span", null, react.createElement("span", { className: P.dot, style: { background: "#1e40af" } }), tr("legend.more"))
|
|
281
|
+
)
|
|
282
|
+
)
|
|
283
|
+
)
|
|
284
|
+
),
|
|
285
|
+
react.createElement("div", { className: P.twoCol },
|
|
286
|
+
react.createElement("div", { className: P.section },
|
|
287
|
+
react.createElement("h3", { className: P.title }, tr("panel.topSessions")),
|
|
288
|
+
(usageData ? (usageData.sessions || []).slice(0, 8) : []).map((s, i) => react.createElement("div", { key: s.id, className: P.row },
|
|
289
|
+
react.createElement("span", { className: P.rank }, String(i + 1)),
|
|
290
|
+
react.createElement("span", { className: P.rowTitle + (s.archived ? " " + P.rowTitleGray : "") }, s.title || s.id),
|
|
291
|
+
react.createElement("span", { className: P.rowVal }, fmtTokens(s.tokens))
|
|
292
|
+
))
|
|
293
|
+
),
|
|
294
|
+
react.createElement("div", { className: P.section },
|
|
295
|
+
react.createElement("h3", { className: P.title }, tr("panel.peakSessions")),
|
|
296
|
+
(usageData ? [...(usageData.sessions || [])].sort((a, b) => b.peakTokens - a.peakTokens).slice(0, 5) : []).map((s, i) => react.createElement("div", { key: s.id, className: P.row },
|
|
297
|
+
react.createElement("span", { className: P.rank }, String(i + 1)),
|
|
298
|
+
react.createElement("span", { className: P.rowTitle }, s.title || s.id),
|
|
299
|
+
react.createElement("span", { className: P.rowVal }, fmtTokens(s.peakTokens))
|
|
300
|
+
))
|
|
301
|
+
)
|
|
302
|
+
),
|
|
303
|
+
react.createElement("div", { className: P.twoCol },
|
|
304
|
+
react.createElement("div", { className: P.section },
|
|
305
|
+
react.createElement("h3", { className: P.title }, tr("panel.modelEffort")),
|
|
306
|
+
modelEffortList.map((m) => react.createElement("div", { key: m.key, className: P.row },
|
|
307
|
+
react.createElement("span", { className: P.rowTitle }, modelLabel(m.key)),
|
|
308
|
+
react.createElement("span", { className: P.rowVal }, fmtTokens(m.tokens))
|
|
309
|
+
))
|
|
310
|
+
)
|
|
311
|
+
)
|
|
312
|
+
)
|
|
313
|
+
)
|
|
314
|
+
);
|
|
272
315
|
}
|
|
273
316
|
|
|
274
317
|
const NS = "deepseek-balance";
|
|
275
318
|
const zh = {
|
|
276
|
-
"label.spent": "消耗",
|
|
277
|
-
"
|
|
278
|
-
"label.model": "模型",
|
|
279
|
-
"period.peak": "高峰",
|
|
280
|
-
"period.offPeak": "空闲",
|
|
281
|
-
"period.legacy": "平价",
|
|
282
|
-
"period.legacyNote": "当前为调价前平价(8/17 前)",
|
|
283
|
-
"period.peakHours": "高峰:09:00–12:00、14:00–18:00(北京时间)",
|
|
284
|
-
"period.offPeakNote": "空闲:其余时段",
|
|
319
|
+
"label.spent": "消耗", "label.balance": "余额", "label.model": "模型",
|
|
320
|
+
"period.peak": "高峰", "period.offPeak": "空闲", "period.legacy": "平价",
|
|
285
321
|
"balance.unavailable": "余额不可用",
|
|
286
|
-
"
|
|
287
|
-
"
|
|
288
|
-
"
|
|
289
|
-
"
|
|
290
|
-
"
|
|
291
|
-
"reasoning.note": "思考链按输出 token 计费;+ / ++ 表示 high / max 思考会额外增加输出量"
|
|
322
|
+
"panel.stats": "统计", "panel.balance": "余额与消耗", "panel.topup": "跳转API官网", "panel.save": "保存",
|
|
323
|
+
"panel.topSessions": "消耗最多的对话", "panel.month": "每日分布", "panel.monthPeriod": "每日时段分布", "panel.monthTokens": "每日 Token 消耗", "panel.peakSessions": "高峰消耗排行", "panel.modelEffort": "模型与思考强度",
|
|
324
|
+
"stat.total": "累计 Token", "stat.dailyPeak": "每日峰值", "stat.longest": "最长聊天", "stat.curStreak": "当前连续", "stat.longStreak": "最长连续", "stat.today": "今日消耗", "stat.cumulative": "累计消耗",
|
|
325
|
+
"unit.day": " 天",
|
|
326
|
+
"legend.flat": "平价", "legend.peak": "高峰", "legend.off": "空闲", "legend.none": "无数据", "legend.less": "少", "legend.more": "多"
|
|
292
327
|
};
|
|
293
328
|
const en = {
|
|
294
|
-
"label.spent": "Spent",
|
|
295
|
-
"
|
|
296
|
-
"label.model": "Model",
|
|
297
|
-
"period.peak": "Peak",
|
|
298
|
-
"period.offPeak": "Off-peak",
|
|
299
|
-
"period.legacy": "Flat",
|
|
300
|
-
"period.legacyNote": "Pre-change flat pricing (before Aug 17)",
|
|
301
|
-
"period.peakHours": "Peak: 01:00–04:00, 06:00–10:00 UTC",
|
|
302
|
-
"period.offPeakNote": "Off-peak: all other hours",
|
|
329
|
+
"label.spent": "Spent", "label.balance": "Balance", "label.model": "Model",
|
|
330
|
+
"period.peak": "Peak", "period.offPeak": "Off-peak", "period.legacy": "Flat",
|
|
303
331
|
"balance.unavailable": "balance unavailable",
|
|
304
|
-
"
|
|
305
|
-
"
|
|
306
|
-
"
|
|
307
|
-
"
|
|
308
|
-
"
|
|
309
|
-
"reasoning.note": "Reasoning tokens are billed as output; + / ++ mark high / max effort which adds extra output"
|
|
332
|
+
"panel.stats": "Stats", "panel.balance": "Balance & Spend", "panel.topup": "API site", "panel.save": "Save",
|
|
333
|
+
"panel.topSessions": "Top conversations", "panel.month": "Daily", "panel.monthPeriod": "Daily period", "panel.monthTokens": "Daily token usage", "panel.peakSessions": "Peak-hour ranking", "panel.modelEffort": "Model & effort",
|
|
334
|
+
"stat.total": "Total tokens", "stat.dailyPeak": "Daily peak", "stat.longest": "Longest chat", "stat.curStreak": "Current streak", "stat.longStreak": "Longest streak", "stat.today": "Today", "stat.cumulative": "Total",
|
|
335
|
+
"unit.day": "d",
|
|
336
|
+
"legend.flat": "Flat", "legend.peak": "Peak", "legend.off": "Off-peak", "legend.none": "None", "legend.less": "Less", "legend.more": "More"
|
|
310
337
|
};
|
|
311
338
|
|
|
312
339
|
const inject = ["slots", "locale"];
|
|
@@ -319,12 +346,8 @@ window.__ModuleLoader__.load({
|
|
|
319
346
|
inject: (sessionId) => {
|
|
320
347
|
const models = ctx.get("modelDirectories");
|
|
321
348
|
let directory;
|
|
322
|
-
if (models && typeof models.directoryFor === "function") {
|
|
323
|
-
|
|
324
|
-
directory = models.directoryFor(sessionId);
|
|
325
|
-
} catch {}
|
|
326
|
-
}
|
|
327
|
-
return { directory: directory ? directory.store : void 0 };
|
|
349
|
+
if (models && typeof models.directoryFor === "function") { try { directory = models.directoryFor(sessionId); } catch {} }
|
|
350
|
+
return { directory: directory ? directory.store : void 0, sessionId };
|
|
328
351
|
}
|
|
329
352
|
}, BillingBadge));
|
|
330
353
|
}
|
package/lib/index.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
2
2
|
import { settingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { zstdDecompressSync } from "node:zlib";
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
10
|
* @lancecheney/dsh-deepseek-balance — host half.
|
|
@@ -11,6 +15,9 @@ import { z } from "zod";
|
|
|
11
15
|
|
|
12
16
|
const name = "deepseek-balance";
|
|
13
17
|
const inject = ["webServer", "credentials", "settings"];
|
|
18
|
+
const Config = z.object({
|
|
19
|
+
tokenName: z.string().default("API key")
|
|
20
|
+
});
|
|
14
21
|
|
|
15
22
|
const DEFAULT_BASE_URL = "https://api.deepseek.com";
|
|
16
23
|
const DEFAULT_KEY_ENV = "DEEPSEEK_API_KEY";
|
|
@@ -268,27 +275,6 @@ function msUntilNextHourBeijing(hour) {
|
|
|
268
275
|
return deltaHours * 3600 * 1000 - bMinute * 60 * 1000 - bSecond * 1000 - bMs;
|
|
269
276
|
}
|
|
270
277
|
|
|
271
|
-
/** Beijing hour of a timestamp (UTC+8, no DST). */
|
|
272
|
-
function beijingHourOf(time) {
|
|
273
|
-
return new Date(time + 8 * 3600 * 1000).getUTCHours();
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function isPeakHour(hour) {
|
|
277
|
-
return (hour >= 9 && hour < 12) || (hour >= 14 && hour < 18);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const bucketSchema = z.object({
|
|
281
|
-
uncachedInputTokens: z.number().int().nonnegative(),
|
|
282
|
-
outputTokens: z.number().int().nonnegative(),
|
|
283
|
-
cacheReadTokens: z.number().int().nonnegative(),
|
|
284
|
-
cacheWriteTokens: z.number().int().nonnegative()
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
const tokenUsageByPeriodSchema = z.object({
|
|
288
|
-
peak: bucketSchema,
|
|
289
|
-
offPeak: bucketSchema
|
|
290
|
-
});
|
|
291
|
-
|
|
292
278
|
const zeroBucket = () => ({ uncachedInputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 });
|
|
293
279
|
|
|
294
280
|
const bucketsFrom = (usage) => ({
|
|
@@ -298,12 +284,6 @@ const bucketsFrom = (usage) => ({
|
|
|
298
284
|
cacheWriteTokens: usage.cacheWriteTokens ?? 0
|
|
299
285
|
});
|
|
300
286
|
|
|
301
|
-
const bucketsEqual = (a, b) =>
|
|
302
|
-
a.uncachedInputTokens === b.uncachedInputTokens &&
|
|
303
|
-
a.outputTokens === b.outputTokens &&
|
|
304
|
-
a.cacheReadTokens === b.cacheReadTokens &&
|
|
305
|
-
a.cacheWriteTokens === b.cacheWriteTokens;
|
|
306
|
-
|
|
307
287
|
const addBucket = (total, delta) => ({
|
|
308
288
|
uncachedInputTokens: total.uncachedInputTokens + delta.uncachedInputTokens,
|
|
309
289
|
outputTokens: total.outputTokens + delta.outputTokens,
|
|
@@ -311,53 +291,326 @@ const addBucket = (total, delta) => ({
|
|
|
311
291
|
cacheWriteTokens: total.cacheWriteTokens + delta.cacheWriteTokens
|
|
312
292
|
});
|
|
313
293
|
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
294
|
+
const ZSTD_MAGIC = 4247762216;
|
|
295
|
+
|
|
296
|
+
function scanZstdFrames(buffer) {
|
|
297
|
+
const frames = [];
|
|
298
|
+
let offset = 0;
|
|
299
|
+
while (offset < buffer.length) {
|
|
300
|
+
const start = offset;
|
|
301
|
+
if (buffer.length - offset < 4) break;
|
|
302
|
+
if (buffer.readUInt32LE(offset) !== ZSTD_MAGIC) break;
|
|
303
|
+
offset += 4;
|
|
304
|
+
const descriptor = buffer.readUInt8(offset);
|
|
305
|
+
offset += 1;
|
|
306
|
+
const contentSizeFlag = descriptor >>> 6;
|
|
307
|
+
const singleSegment = (descriptor & 32) !== 0;
|
|
308
|
+
const checksum = (descriptor & 4) !== 0;
|
|
309
|
+
const dictionaryFlag = descriptor & 3;
|
|
310
|
+
const dictionaryBytes = dictionaryFlag === 3 ? 4 : dictionaryFlag;
|
|
311
|
+
const contentSizeBytes = contentSizeFlag === 0 ? (singleSegment ? 1 : 0) : (1 << contentSizeFlag);
|
|
312
|
+
offset += (singleSegment ? 0 : 1) + dictionaryBytes + contentSizeBytes;
|
|
313
|
+
for (;;) {
|
|
314
|
+
const blockHeader = buffer.readUIntLE(offset, 3);
|
|
315
|
+
offset += 3;
|
|
316
|
+
const lastBlock = (blockHeader & 1) !== 0;
|
|
317
|
+
const blockType = (blockHeader >>> 1) & 3;
|
|
318
|
+
const blockSize = blockHeader >>> 3;
|
|
319
|
+
offset += blockType === 1 ? 1 : blockSize;
|
|
320
|
+
if (lastBlock) break;
|
|
321
|
+
}
|
|
322
|
+
if (checksum) offset += 4;
|
|
323
|
+
frames.push({ start, end: offset });
|
|
324
|
+
}
|
|
325
|
+
return frames;
|
|
326
|
+
}
|
|
320
327
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
328
|
+
function dshHome() {
|
|
329
|
+
return process.env.DSH_HOME || join(homedir(), ".dsh");
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function beijingDayKey(ms) {
|
|
333
|
+
return new Date(ms + 8 * 3600 * 1000).toISOString().slice(0, 10);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function periodOfUsage(time, effectiveFromMs) {
|
|
337
|
+
if (time < effectiveFromMs) return "flat";
|
|
338
|
+
const hour = new Date(time + 8 * 3600 * 1000).getUTCHours();
|
|
339
|
+
return (hour >= 9 && hour < 12) || (hour >= 14 && hour < 18) ? "peak" : "offPeak";
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function readSessionRecords(path) {
|
|
343
|
+
const buf = readFileSync(path);
|
|
344
|
+
let text;
|
|
345
|
+
if (path.endsWith(".zstd")) {
|
|
346
|
+
const parts = [];
|
|
347
|
+
for (const frame of scanZstdFrames(buf)) parts.push(zstdDecompressSync(buf.subarray(frame.start, frame.end)).toString("utf8"));
|
|
348
|
+
text = parts.join("");
|
|
349
|
+
} else {
|
|
350
|
+
text = buf.toString("utf8");
|
|
351
|
+
}
|
|
352
|
+
const records = [];
|
|
353
|
+
for (const line of text.split("\n")) {
|
|
354
|
+
const t = line.trim();
|
|
355
|
+
if (t === "") continue;
|
|
356
|
+
try { records.push(JSON.parse(t)); } catch {}
|
|
357
|
+
}
|
|
358
|
+
return records;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function aggregateSessions(archivedIds = []) {
|
|
362
|
+
const archivedSet = new Set(archivedIds);
|
|
363
|
+
const root = join(dshHome(), "sessions");
|
|
364
|
+
const summary = {
|
|
365
|
+
totalTokens: 0,
|
|
366
|
+
peakTokens: 0,
|
|
367
|
+
offPeakTokens: 0,
|
|
368
|
+
flatTokens: 0,
|
|
369
|
+
dailyPeakTokens: 0,
|
|
370
|
+
longestChatMs: 0,
|
|
371
|
+
currentStreak: 0,
|
|
372
|
+
longestStreak: 0,
|
|
373
|
+
days: {},
|
|
374
|
+
sessions: [],
|
|
375
|
+
modelEffort: {},
|
|
376
|
+
byModel: {}
|
|
377
|
+
};
|
|
378
|
+
if (!existsSync(root)) return summary;
|
|
379
|
+
const effectiveFromMs = Date.parse(pricing.data.effectiveFrom || FALLBACK_PRICING.effectiveFrom);
|
|
380
|
+
if (!Number.isFinite(effectiveFromMs)) return summary;
|
|
381
|
+
|
|
382
|
+
let wsDirs = [];
|
|
383
|
+
try { wsDirs = readdirSync(root); } catch { return summary; }
|
|
384
|
+
for (const wsDir of wsDirs) {
|
|
385
|
+
const wsPath = join(root, wsDir);
|
|
386
|
+
let sDirs = [];
|
|
387
|
+
try { sDirs = readdirSync(wsPath); } catch { continue; }
|
|
388
|
+
for (const sDir of sDirs) {
|
|
389
|
+
const sPath = join(wsPath, sDir);
|
|
390
|
+
const zstd = join(sPath, "session.jsonl.zstd");
|
|
391
|
+
const plain = join(sPath, "session.jsonl");
|
|
392
|
+
const path = existsSync(zstd) ? zstd : existsSync(plain) ? plain : null;
|
|
393
|
+
if (path === null) continue;
|
|
394
|
+
let records;
|
|
395
|
+
try { records = readSessionRecords(path); } catch { continue; }
|
|
396
|
+
let header = null;
|
|
397
|
+
let title = null;
|
|
398
|
+
let model = null;
|
|
399
|
+
let effort = null;
|
|
400
|
+
let firstTime = null;
|
|
401
|
+
let lastTime = null;
|
|
402
|
+
let tokens = 0;
|
|
403
|
+
let peakTokens = 0;
|
|
404
|
+
let offPeakTokens = 0;
|
|
405
|
+
let flatTokens = 0;
|
|
406
|
+
const dayTokens = {};
|
|
407
|
+
const sessionByModel = {};
|
|
408
|
+
const dedupe = new Set();
|
|
409
|
+
{
|
|
410
|
+
const last = new Map();
|
|
411
|
+
for (const r of records) {
|
|
412
|
+
let key = null;
|
|
413
|
+
if (r.type === "assistant/message" && r.data?.usage) {
|
|
414
|
+
const t = r.data?.turn;
|
|
415
|
+
const s = r.data?.step;
|
|
416
|
+
if (t !== void 0 && s !== void 0) key = t + "|" + s;
|
|
417
|
+
} else if (r.type === "assistant/chunk" && r.data?.chunk?.type === "usage") {
|
|
418
|
+
const t = r.data?.turn;
|
|
419
|
+
const s = r.data?.step;
|
|
420
|
+
if (t !== void 0 && s !== void 0) key = t + "|" + s;
|
|
421
|
+
}
|
|
422
|
+
if (key !== null) last.set(key, r);
|
|
423
|
+
}
|
|
424
|
+
for (const r of last.values()) dedupe.add(r);
|
|
425
|
+
}
|
|
426
|
+
for (const r of records) {
|
|
427
|
+
if (r.type === "session" && header === null) { header = r; continue; }
|
|
428
|
+
if (r.type === "session/title" && r.data?.title) title = r.data.title;
|
|
429
|
+
if (r.type === "request/header") {
|
|
430
|
+
model = r.data?.header?.config?.model ?? model;
|
|
431
|
+
effort = r.data?.header?.config?.reasoningEffort ?? effort;
|
|
432
|
+
}
|
|
433
|
+
const time = typeof r.time === "number" ? r.time : null;
|
|
434
|
+
if (time !== null) {
|
|
435
|
+
if (firstTime === null || time < firstTime) firstTime = time;
|
|
436
|
+
if (lastTime === null || time > lastTime) lastTime = time;
|
|
437
|
+
}
|
|
438
|
+
let usage = null;
|
|
439
|
+
if (r.type === "assistant/message" && r.data?.usage) usage = r.data.usage;
|
|
440
|
+
else if (r.type === "assistant/chunk" && r.data?.chunk?.type === "usage") usage = r.data.chunk.usage;
|
|
441
|
+
if (usage && time !== null && dedupe.has(r)) {
|
|
442
|
+
const t = (usage.inputTokens ?? 0) + (usage.outputTokens ?? 0) + (usage.cacheReadTokens ?? 0);
|
|
443
|
+
const period = periodOfUsage(time, effectiveFromMs);
|
|
444
|
+
const dayKey = beijingDayKey(time);
|
|
445
|
+
tokens += t;
|
|
446
|
+
if (period === "peak") peakTokens += t;
|
|
447
|
+
else if (period === "offPeak") offPeakTokens += t;
|
|
448
|
+
else flatTokens += t;
|
|
449
|
+
const mkModel = model ?? "";
|
|
450
|
+
const b = bucketsFrom(usage);
|
|
451
|
+
dayTokens[dayKey] = dayTokens[dayKey] || {};
|
|
452
|
+
dayTokens[dayKey][mkModel] = dayTokens[dayKey][mkModel] || { flat: zeroBucket(), peak: zeroBucket(), offPeak: zeroBucket() };
|
|
453
|
+
dayTokens[dayKey][mkModel][period] = addBucket(dayTokens[dayKey][mkModel][period], b);
|
|
454
|
+
summary.byModel[mkModel] = summary.byModel[mkModel] || { flat: zeroBucket(), peak: zeroBucket(), offPeak: zeroBucket() };
|
|
455
|
+
summary.byModel[mkModel][period] = addBucket(summary.byModel[mkModel][period], b);
|
|
456
|
+
sessionByModel[mkModel] = sessionByModel[mkModel] || { flat: zeroBucket(), peak: zeroBucket(), offPeak: zeroBucket() };
|
|
457
|
+
sessionByModel[mkModel][period] = addBucket(sessionByModel[mkModel][period], b);
|
|
458
|
+
const mk = `${model ?? ""}|${effort ?? ""}`;
|
|
459
|
+
summary.modelEffort[mk] = summary.modelEffort[mk] || { tokens: 0 };
|
|
460
|
+
summary.modelEffort[mk].tokens += t;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
summary.totalTokens += tokens;
|
|
464
|
+
summary.peakTokens += peakTokens;
|
|
465
|
+
summary.offPeakTokens += offPeakTokens;
|
|
466
|
+
summary.flatTokens += flatTokens;
|
|
467
|
+
for (const [dayKey, d] of Object.entries(dayTokens)) {
|
|
468
|
+
summary.days[dayKey] = summary.days[dayKey] || { tokens: 0, peak: 0, offPeak: 0, flat: 0, byModel: {} };
|
|
469
|
+
const day = summary.days[dayKey];
|
|
470
|
+
const byModel = day.byModel;
|
|
471
|
+
const bucketTotal = (bucket) => bucket.uncachedInputTokens + bucket.outputTokens + bucket.cacheReadTokens;
|
|
472
|
+
let dayTotal = 0;
|
|
473
|
+
let dayPeak = 0;
|
|
474
|
+
let dayOff = 0;
|
|
475
|
+
let dayFlat = 0;
|
|
476
|
+
for (const [m, b] of Object.entries(d)) {
|
|
477
|
+
byModel[m] = byModel[m] || { flat: zeroBucket(), peak: zeroBucket(), offPeak: zeroBucket() };
|
|
478
|
+
byModel[m].flat = addBucket(byModel[m].flat, b.flat);
|
|
479
|
+
byModel[m].peak = addBucket(byModel[m].peak, b.peak);
|
|
480
|
+
byModel[m].offPeak = addBucket(byModel[m].offPeak, b.offPeak);
|
|
481
|
+
dayPeak += bucketTotal(b.peak);
|
|
482
|
+
dayOff += bucketTotal(b.offPeak);
|
|
483
|
+
dayFlat += bucketTotal(b.flat);
|
|
484
|
+
dayTotal += bucketTotal(b.peak) + bucketTotal(b.offPeak) + bucketTotal(b.flat);
|
|
485
|
+
}
|
|
486
|
+
day.tokens += dayTotal;
|
|
487
|
+
day.peak += dayPeak;
|
|
488
|
+
day.offPeak += dayOff;
|
|
489
|
+
day.flat += dayFlat;
|
|
490
|
+
if (day.tokens > summary.dailyPeakTokens) summary.dailyPeakTokens = day.tokens;
|
|
491
|
+
}
|
|
492
|
+
const duration = (lastTime ?? 0) - (firstTime ?? 0);
|
|
493
|
+
if (duration > summary.longestChatMs) summary.longestChatMs = duration;
|
|
494
|
+
|
|
495
|
+
const sid = header?.id ?? sDir.replace(/^session-/, "");
|
|
496
|
+
summary.sessions.push({
|
|
497
|
+
id: sid,
|
|
498
|
+
title: title ?? header?.id ?? sDir,
|
|
499
|
+
cwd: header?.cwd ?? wsDir,
|
|
500
|
+
tokens,
|
|
501
|
+
peakTokens,
|
|
502
|
+
byModel: sessionByModel,
|
|
503
|
+
createdAt: header?.createdAt ?? firstTime,
|
|
504
|
+
endedAt: lastTime,
|
|
505
|
+
archived: archivedSet.has(sid)
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
summary.sessions.sort((a, b) => b.tokens - a.tokens);
|
|
511
|
+
|
|
512
|
+
const dayKeys = Object.keys(summary.days).sort();
|
|
513
|
+
let longest = 0;
|
|
514
|
+
let run = 0;
|
|
515
|
+
let prev = null;
|
|
516
|
+
for (const d of dayKeys) {
|
|
517
|
+
if (prev === null) run = 1;
|
|
518
|
+
else {
|
|
519
|
+
const diff = Math.round((Date.parse(d) - Date.parse(prev)) / 86400000);
|
|
520
|
+
run = diff === 1 ? run + 1 : 1;
|
|
521
|
+
}
|
|
522
|
+
if (run > longest) longest = run;
|
|
523
|
+
prev = d;
|
|
524
|
+
}
|
|
525
|
+
summary.longestStreak = longest;
|
|
526
|
+
|
|
527
|
+
const daySet = new Set(dayKeys);
|
|
528
|
+
let cur = 0;
|
|
529
|
+
let cursor = Date.parse(beijingDayKey(Date.now()));
|
|
530
|
+
while (daySet.has(beijingDayKey(cursor))) {
|
|
531
|
+
cur++;
|
|
532
|
+
cursor -= 86400000;
|
|
533
|
+
}
|
|
534
|
+
summary.currentStreak = cur;
|
|
535
|
+
|
|
536
|
+
return summary;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const DEFAULT_MODEL = "deepseek-v4-pro";
|
|
540
|
+
|
|
541
|
+
/** Compute spend (CNY/USD) from an aggregated usage summary, using the live pricing table. */
|
|
542
|
+
function computeSpend(summary, currency = "CNY") {
|
|
543
|
+
const pricingSet =
|
|
544
|
+
(pricing.data.currencies && pricing.data.currencies[currency]) ||
|
|
545
|
+
pricing.data.currencies?.CNY ||
|
|
546
|
+
FALLBACK_PRICING.currencies.CNY;
|
|
547
|
+
const models = pricingSet.models || {};
|
|
548
|
+
const defaultModel = models[DEFAULT_MODEL] || { legacy: null, peak: { miss: 0, hit: 0, output: 0 }, offPeak: { miss: 0, hit: 0, output: 0 } };
|
|
549
|
+
const priceOf = (tokens, price) => (tokens || 0) * (price || 0) / 1e6;
|
|
550
|
+
const bucketCost = (b, mm) => {
|
|
551
|
+
if (!mm) return 0;
|
|
552
|
+
const cost = (bucket, price) => {
|
|
553
|
+
if (!price) return 0;
|
|
554
|
+
return priceOf(bucket.uncachedInputTokens, price.miss)
|
|
555
|
+
+ priceOf(bucket.cacheWriteTokens, price.miss)
|
|
556
|
+
+ priceOf(bucket.cacheReadTokens, price.hit)
|
|
557
|
+
+ priceOf(bucket.outputTokens, price.output);
|
|
558
|
+
};
|
|
559
|
+
return (mm.legacy ? cost(b.flat, mm.legacy) : 0) + cost(b.peak, mm.peak) + cost(b.offPeak, mm.offPeak);
|
|
560
|
+
};
|
|
561
|
+
let total = 0;
|
|
562
|
+
for (const [k, b] of Object.entries(summary.byModel || {})) {
|
|
563
|
+
const [m] = k.split("|");
|
|
564
|
+
total += bucketCost(b, models[m] || defaultModel);
|
|
565
|
+
}
|
|
566
|
+
const day = (summary.days || {})[beijingDayKey(Date.now())];
|
|
567
|
+
let today = null;
|
|
568
|
+
if (day) {
|
|
569
|
+
if (day.byModel) {
|
|
570
|
+
today = 0;
|
|
571
|
+
for (const [m, b] of Object.entries(day.byModel)) today += bucketCost(b, models[m] || defaultModel);
|
|
340
572
|
} else {
|
|
341
|
-
|
|
573
|
+
today = bucketCost(day, defaultModel);
|
|
342
574
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
if (previous !== void 0 && previous.period === period && bucketsEqual(previous.buckets, buckets)) return state;
|
|
347
|
-
const totals = { peak: state.totals.peak, offPeak: state.totals.offPeak };
|
|
348
|
-
if (previous !== void 0) totals[previous.period] = subtractBucket(totals[previous.period], previous.buckets);
|
|
349
|
-
totals[period] = addBucket(totals[period], buckets);
|
|
350
|
-
return { totals, last: { turn, step, period, buckets } };
|
|
351
|
-
},
|
|
352
|
-
view: (state) => state.totals,
|
|
353
|
-
stateVersion: 1
|
|
354
|
-
};
|
|
575
|
+
}
|
|
576
|
+
return { total, today, currency };
|
|
577
|
+
}
|
|
355
578
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
579
|
+
const usageCache = { data: null, at: 0, currency: null };
|
|
580
|
+
const USAGE_CACHE_TTL_MS = 30000;
|
|
581
|
+
|
|
582
|
+
function tokenNameFile() {
|
|
583
|
+
return join(dshHome(), "deepseek-balance.json");
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function readTokenName() {
|
|
587
|
+
try {
|
|
588
|
+
const p = tokenNameFile();
|
|
589
|
+
if (!existsSync(p)) return null;
|
|
590
|
+
const d = JSON.parse(readFileSync(p, "utf8"));
|
|
591
|
+
return typeof d.tokenName === "string" && d.tokenName.trim() ? d.tokenName.trim() : null;
|
|
592
|
+
} catch { return null; }
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function writeTokenName(name) {
|
|
596
|
+
try { mkdirSync(dshHome(), { recursive: true }); } catch {}
|
|
597
|
+
writeFileSync(tokenNameFile(), JSON.stringify({ tokenName: name }), "utf8");
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function resolveTokenName(config) {
|
|
601
|
+
return readTokenName() || config.tokenName || "API key";
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async function maskApiKey(ctx) {
|
|
605
|
+
try {
|
|
606
|
+
const { apiKey } = await resolveDeepSeekFacts(ctx);
|
|
607
|
+
return apiKey.length > 12 ? `${apiKey.slice(0, 8)}*****${apiKey.slice(-4)}` : `${apiKey.slice(0, 8)}****`;
|
|
608
|
+
} catch {
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
360
612
|
|
|
613
|
+
function apply(ctx, config = {}) {
|
|
361
614
|
const balanceHandler = async (req, res) => {
|
|
362
615
|
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
363
616
|
json(res, 405, { error: "method not allowed" });
|
|
@@ -383,6 +636,7 @@ function apply(ctx) {
|
|
|
383
636
|
json(res, upstream.status === 401 || upstream.status === 403 ? 401 : 502, { error: message });
|
|
384
637
|
return;
|
|
385
638
|
}
|
|
639
|
+
if (typeof body?.currency === "string" && body.currency.length > 0) usageCache.currency = body.currency;
|
|
386
640
|
json(res, 200, body);
|
|
387
641
|
} catch (error) {
|
|
388
642
|
json(res, 502, { error: error instanceof Error ? error.message : String(error) });
|
|
@@ -413,6 +667,104 @@ function apply(ctx) {
|
|
|
413
667
|
ctx.effect(() => ctx.webServer.register({ kind: "exact", path: "/api/deepseek-balance", handler: balanceHandler }), "deepseek-balance: /api/deepseek-balance route");
|
|
414
668
|
ctx.effect(() => ctx.webServer.register({ kind: "exact", path: "/api/deepseek-pricing", handler: pricingHandler }), "deepseek-balance: /api/deepseek-pricing route");
|
|
415
669
|
|
|
670
|
+
const usageHandler = async (req, res) => {
|
|
671
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
672
|
+
json(res, 405, { error: "method not allowed" });
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
if (!isTrustedRead(req)) {
|
|
676
|
+
json(res, 403, { error: "forbidden" });
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
if (usageCache.at === 0 || Date.now() - usageCache.at > USAGE_CACHE_TTL_MS) {
|
|
680
|
+
try {
|
|
681
|
+
const ws = ctx.get("workspaceRegistry");
|
|
682
|
+
usageCache.data = aggregateSessions(ws?.archivedSessionIds ?? []);
|
|
683
|
+
usageCache.data.spend = computeSpend(usageCache.data, usageCache.currency || "CNY");
|
|
684
|
+
usageCache.at = Date.now();
|
|
685
|
+
} catch (error) {
|
|
686
|
+
if (ctx?.logger?.warn) ctx.logger.warn(`deepseek-balance: usage aggregation failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
const key = await maskApiKey(ctx);
|
|
690
|
+
let payload = usageCache.data;
|
|
691
|
+
if (!payload) {
|
|
692
|
+
payload = aggregateSessions();
|
|
693
|
+
payload.spend = computeSpend(payload, usageCache.currency || "CNY");
|
|
694
|
+
}
|
|
695
|
+
let query = null;
|
|
696
|
+
try { query = new URL(req.url, "http://dsh.local").searchParams; } catch {}
|
|
697
|
+
const sessionId = query ? query.get("session") : null;
|
|
698
|
+
if (sessionId && sessionId.length > 0) {
|
|
699
|
+
const s = (payload.sessions || []).find((x) => x.id === sessionId);
|
|
700
|
+
if (s) {
|
|
701
|
+
json(res, 200, {
|
|
702
|
+
id: s.id,
|
|
703
|
+
title: s.title,
|
|
704
|
+
tokens: s.tokens,
|
|
705
|
+
spend: computeSpend({ byModel: s.byModel || {} }, usageCache.currency || "CNY"),
|
|
706
|
+
apiKeyPreview: key,
|
|
707
|
+
tokenName: resolveTokenName(config),
|
|
708
|
+
effectiveFrom: pricing.data.effectiveFrom,
|
|
709
|
+
fetchedAt: usageCache.at,
|
|
710
|
+
source: pricing.source
|
|
711
|
+
});
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
json(res, 200, {
|
|
715
|
+
id: sessionId,
|
|
716
|
+
title: sessionId,
|
|
717
|
+
tokens: 0,
|
|
718
|
+
spend: { total: 0, today: null, currency: usageCache.currency || "CNY" },
|
|
719
|
+
apiKeyPreview: key,
|
|
720
|
+
tokenName: resolveTokenName(config),
|
|
721
|
+
effectiveFrom: pricing.data.effectiveFrom,
|
|
722
|
+
fetchedAt: usageCache.at,
|
|
723
|
+
source: pricing.source
|
|
724
|
+
});
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
json(res, 200, {
|
|
728
|
+
...payload,
|
|
729
|
+
apiKeyPreview: key,
|
|
730
|
+
tokenName: resolveTokenName(config),
|
|
731
|
+
effectiveFrom: pricing.data.effectiveFrom,
|
|
732
|
+
fetchedAt: usageCache.at,
|
|
733
|
+
source: pricing.source
|
|
734
|
+
});
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
ctx.effect(() => ctx.webServer.register({ kind: "exact", path: "/api/deepseek-usage", handler: usageHandler }), "deepseek-balance: /api/deepseek-usage route");
|
|
738
|
+
|
|
739
|
+
const tokenNameHandler = async (req, res) => {
|
|
740
|
+
if (!isTrustedRead(req)) {
|
|
741
|
+
json(res, 403, { error: "forbidden" });
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
if (req.method === "GET" || req.method === "HEAD") {
|
|
745
|
+
json(res, 200, { tokenName: resolveTokenName(config) });
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
if (req.method === "POST") {
|
|
749
|
+
let raw = "";
|
|
750
|
+
try {
|
|
751
|
+
const chunks = [];
|
|
752
|
+
for await (const c of req) chunks.push(c);
|
|
753
|
+
raw = Buffer.concat(chunks).toString("utf8");
|
|
754
|
+
const body = raw ? JSON.parse(raw) : {};
|
|
755
|
+
const name = typeof body.tokenName === "string" && body.tokenName.trim() ? body.tokenName.trim().slice(0, 64) : "API key";
|
|
756
|
+
writeTokenName(name);
|
|
757
|
+
json(res, 200, { tokenName: name });
|
|
758
|
+
} catch (error) {
|
|
759
|
+
json(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
760
|
+
}
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
json(res, 405, { error: "method not allowed" });
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
ctx.effect(() => ctx.webServer.register({ kind: "exact", path: "/api/deepseek-balance/token-name", handler: tokenNameHandler }), "deepseek-balance: /api/deepseek-balance/token-name route");
|
|
767
|
+
|
|
416
768
|
ctx.effect(() => {
|
|
417
769
|
refreshPricing(ctx).catch(() => {});
|
|
418
770
|
let timer;
|
|
@@ -428,4 +780,4 @@ function apply(ctx) {
|
|
|
428
780
|
}, "deepseek-balance: daily 01:00 Beijing pricing refresh");
|
|
429
781
|
}
|
|
430
782
|
|
|
431
|
-
export { apply, inject, name };
|
|
783
|
+
export { Config, apply, inject, name };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lancecheney/dsh-deepseek-balance",
|
|
3
3
|
"description": "Session-header DeepSeek API balance, per-conversation spend, and peak/off-peak pricing indicator",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|