@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.
Files changed (3) hide show
  1. package/lib/client.js +268 -245
  2. package/lib/index.js +422 -70
  3. 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;text-decoration:none;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 tagId = "@lancecheney/dsh-deepseek-balance/BillingBadge.css";
11
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
12
- const tag = document.createElement("style");
13
- tag.dataset.plugin = "@lancecheney/dsh-deepseek-balance";
14
- tag.dataset.pluginCss = tagId;
15
- tag.textContent = css;
16
- document.head.appendChild(tag);
17
- }
18
- const cssModule = { 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" };
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
- symbol: "¥",
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
- let hour = 0;
64
- let minute = 0;
65
- try {
66
- const parts = new Intl.DateTimeFormat("en-GB", {
67
- timeZone: "Asia/Shanghai",
68
- hour12: false,
69
- hour: "2-digit",
70
- minute: "2-digit"
71
- }).formatToParts(now);
72
- for (const part of parts) {
73
- if (part.type === "hour") hour = Number(part.value);
74
- if (part.type === "minute") minute = Number(part.value);
75
- }
76
- } catch {
77
- hour = now.getHours();
78
- minute = now.getMinutes();
79
- }
80
- return hour + minute / 60;
81
- }
82
-
83
- function periodFor(now, effectiveFrom) {
84
- const eff = Date.parse(effectiveFrom);
85
- if (Number.isFinite(eff) && now.getTime() < eff) return "legacy";
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 s;
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 usage = useProjection("tokenUsage");
141
- const byPeriod = useProjection("tokenUsageByPeriod");
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 [balanceRes, pricingRes] = await Promise.all([
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
- phase: "ok",
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 tooltipParts = [
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: `${cssModule.chip} ${periodClass}` }, periodLabel));
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("a", { className: cssModule.root, title: tooltip, href: "https://platform.deepseek.com/usage", target: "_blank", rel: "noreferrer noopener" }, segs);
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
- "label.balance": "余额",
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
- "price.perM": "/M",
287
- "price.unit": "输出",
288
- "price.row": "命中{sym}{hit}/未命中{sym}{miss}/输出{sym}{output}",
289
- "cost.estimate": "按当前时段价格估算",
290
- "cost.breakdown": "输入(未命中) {miss} · 输入(命中) {hit} · 输出 {output} tokens",
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
- "label.balance": "Balance",
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
- "price.perM": "/M",
305
- "price.unit": "output",
306
- "price.row": "hit {sym}{hit}/miss {sym}{miss}/output {sym}{output}",
307
- "cost.estimate": "estimated at the current period's price",
308
- "cost.breakdown": "input(miss) {miss} · input(hit) {hit} · output {output} tokens",
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
- try {
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 subtractBucket = (total, delta) => ({
315
- uncachedInputTokens: total.uncachedInputTokens - delta.uncachedInputTokens,
316
- outputTokens: total.outputTokens - delta.outputTokens,
317
- cacheReadTokens: total.cacheReadTokens - delta.cacheReadTokens,
318
- cacheWriteTokens: total.cacheWriteTokens - delta.cacheWriteTokens
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
- * Whole-log token usage bucketed by Beijing-time peak/off-peak hour, so the
323
- * client can price each hour at its own rate. Replacement semantics mirror the
324
- * token-meter `tokenUsage` projection: an assistant/message finalizes the usage
325
- * for its turn/step, replacing an earlier usage chunk instead of double counting.
326
- */
327
- const tokenUsageByPeriodProjection = {
328
- key: "tokenUsageByPeriod",
329
- schema: tokenUsageByPeriodSchema,
330
- init: () => ({ totals: { peak: zeroBucket(), offPeak: zeroBucket() }, last: null }),
331
- apply: (state, event) => {
332
- let turn;
333
- let step;
334
- let usage;
335
- if (event.type === "assistant/chunk" && event.data.chunk.type === "usage") {
336
- ({ turn, step } = event.data);
337
- usage = event.data.chunk.usage;
338
- } else if (event.type === "assistant/message" && event.data.usage !== void 0) {
339
- ({ turn, step, usage } = event.data);
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
- return state;
573
+ today = bucketCost(day, defaultModel);
342
574
  }
343
- const period = isPeakHour(beijingHourOf(event.time)) ? "peak" : "offPeak";
344
- const buckets = bucketsFrom(usage);
345
- const previous = state.last !== null && state.last.turn === turn && state.last.step === step ? state.last : void 0;
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
- function apply(ctx) {
357
- ctx.inject(["sessionProjections"], (projectionCtx) => {
358
- projectionCtx.sessionProjections.register(tokenUsageByPeriodProjection);
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.0",
4
+ "version": "0.1.1",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {