@feiyang666/dsh-usage-plugin 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js ADDED
@@ -0,0 +1,1247 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@feiyang666/dsh-usage-plugin",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ var React = require("react");
8
+ var el = React.createElement;
9
+
10
+ // ── inline styles (replaces a stylesheet; portable across client plugins) ──
11
+ // 字号用 em 表示,随宿主“显示大小”设置的字体基准自动缩放(继承应用字号);
12
+ // 间距保持 px,仅微调以适配更大的字号。标注的 px 是默认 13px 基准下的设计值。
13
+ var BASE_FS = 13;
14
+ var fs = function (n) { return (Math.round(n / BASE_FS * 100) / 100).toFixed(2) + "em"; };
15
+ var st = {
16
+ root: { display: "flex", flexDirection: "column", gap: 14, padding: "4px 0", width: "100%", maxWidth: "100%", minWidth: 0, boxSizing: "border-box" },
17
+ tab: { padding: "16px 20px", maxWidth: "min(1200px, calc(100vw - 24px))", margin: "0 auto", width: "100%", minWidth: 0, boxSizing: "border-box" },
18
+ head: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12, flexWrap: "wrap" },
19
+ headleft: { display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" },
20
+ title: { fontSize: fs(15), fontWeight: 600 },
21
+ sub: { fontSize: fs(11), opacity: 0.55 },
22
+ actions: { display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" },
23
+ btn: { border: "1px solid rgba(128,128,128,.35)", background: "transparent", borderRadius: 6, padding: "4px 10px", fontSize: fs(12), cursor: "pointer", color: "inherit" },
24
+ btnPrimary: { border: "1px solid rgba(90,140,255,.6)", background: "rgba(90,140,255,.22)", borderRadius: 6, padding: "4px 10px", fontSize: fs(12), cursor: "pointer", color: "inherit" },
25
+ btnDisabled: { border: "1px solid rgba(128,128,128,.35)", background: "transparent", borderRadius: 6, padding: "4px 10px", fontSize: fs(12), cursor: "default", color: "inherit", opacity: 0.5 },
26
+ input: { border: "1px solid rgba(128,128,128,.35)", background: "transparent", borderRadius: 6, padding: "4px 10px", fontSize: fs(12), color: "inherit", minWidth: 280 },
27
+ dateInput: { border: "1px solid rgba(128,128,128,.35)", background: "transparent", borderRadius: 6, padding: "3px 8px", fontSize: fs(12), color: "inherit" },
28
+ seg: { display: "inline-flex", border: "1px solid rgba(128,128,128,.35)", borderRadius: 6, overflow: "hidden", flexWrap: "wrap" },
29
+ segBtn: { border: 0, background: "transparent", padding: "4px 10px", fontSize: fs(12), cursor: "pointer", color: "inherit" },
30
+ segBtnOn: { border: 0, background: "rgba(90,140,255,.22)", padding: "4px 10px", fontSize: fs(12), cursor: "pointer", color: "inherit", fontWeight: 600 },
31
+ cards: { display: "grid", gridTemplateColumns: "repeat(auto-fill,minmax(170px,1fr))", gap: 10 },
32
+ card: { border: "1px solid rgba(128,128,128,.35)", borderRadius: 8, padding: "10px 12px", minWidth: 0 },
33
+ cardL: { fontSize: fs(11), opacity: 0.6, marginBottom: 4 },
34
+ cardV: { fontSize: fs(18), fontWeight: 600, overflowWrap: "anywhere" },
35
+ cardH: { fontSize: fs(10), opacity: 0.5, marginTop: 2, overflowWrap: "anywhere" },
36
+ sec: { fontSize: fs(13), fontWeight: 600, marginTop: 6 },
37
+ subtabBar: { display: "flex", gap: 6, borderBottom: "1px solid rgba(128,128,128,.2)", paddingBottom: 8, flexWrap: "wrap" },
38
+ subtab: { border: "1px solid rgba(128,128,128,.35)", background: "transparent", borderRadius: 6, padding: "5px 14px", fontSize: fs(12), cursor: "pointer", color: "inherit" },
39
+ subtabOn: { border: "1px solid rgba(90,140,255,.5)", background: "rgba(90,140,255,.22)", borderRadius: 6, padding: "5px 14px", fontSize: fs(12), cursor: "pointer", color: "inherit", fontWeight: 600 },
40
+ scroll: { overflowX: "auto", overflowY: "hidden", width: "100%", maxWidth: "100%", minWidth: 0 },
41
+ // 表格宽度用 max-content:内容多宽表就有多宽,容器窄时出现横向滚动条而不是裁剪右侧列
42
+ tbl: { width: "max-content", minWidth: "100%", borderCollapse: "collapse", fontSize: fs(12) },
43
+ th: { textAlign: "right", padding: "7px 10px", borderBottom: "1px solid rgba(128,128,128,.2)", whiteSpace: "nowrap", opacity: 0.55, fontWeight: 500 },
44
+ thFirst: { textAlign: "left", padding: "7px 10px", borderBottom: "1px solid rgba(128,128,128,.2)", whiteSpace: "nowrap", opacity: 0.55, fontWeight: 500 },
45
+ td: { textAlign: "right", padding: "7px 10px", borderBottom: "1px solid rgba(128,128,128,.2)", whiteSpace: "nowrap" },
46
+ tdFirst: { textAlign: "left", padding: "7px 10px", borderBottom: "1px solid rgba(128,128,128,.2)", whiteSpace: "nowrap" },
47
+ tdWrap: { textAlign: "left", padding: "7px 10px", borderBottom: "1px solid rgba(128,128,128,.2)", whiteSpace: "normal", wordBreak: "break-word", minWidth: 120, maxWidth: 220 },
48
+ tdTotal: { textAlign: "right", padding: "7px 10px", fontWeight: 600, borderTop: "1px solid rgba(128,128,128,.35)" },
49
+ tdTotalFirst: { textAlign: "left", padding: "7px 10px", fontWeight: 600, borderTop: "1px solid rgba(128,128,128,.35)" },
50
+ tdClick: { textAlign: "left", padding: "7px 10px", borderBottom: "1px solid rgba(128,128,128,.2)", whiteSpace: "nowrap", cursor: "pointer" },
51
+ tdClickSel: { textAlign: "left", padding: "7px 10px", borderBottom: "1px solid rgba(128,128,128,.2)", whiteSpace: "nowrap", cursor: "pointer", color: "#5a8cff", fontWeight: 600 },
52
+ tdGroup: { textAlign: "left", padding: "7px 10px", fontWeight: 600, background: "rgba(128,128,128,.09)", borderBottom: "1px solid rgba(128,128,128,.2)", whiteSpace: "nowrap" },
53
+ tdGroupR: { textAlign: "right", padding: "7px 10px", fontWeight: 600, background: "rgba(128,128,128,.09)", borderBottom: "1px solid rgba(128,128,128,.2)", whiteSpace: "nowrap" },
54
+ badgeHit: { display: "inline-block", padding: "2px 8px", borderRadius: 999, fontSize: fs(11), fontWeight: 500, background: "rgba(46,204,113,.18)", color: "#2ecc71", whiteSpace: "nowrap" },
55
+ badgePeak: { display: "inline-block", padding: "2px 8px", borderRadius: 999, fontSize: fs(11), fontWeight: 500, background: "rgba(255,152,0,.18)", color: "#ff9800", whiteSpace: "nowrap" },
56
+ badgeValley: { display: "inline-block", padding: "2px 8px", borderRadius: 999, fontSize: fs(11), fontWeight: 500, background: "rgba(90,140,255,.18)", color: "#5a8cff", whiteSpace: "nowrap" },
57
+ costPeak: { color: "#e08700", fontWeight: 600, whiteSpace: "nowrap" },
58
+ costOff: { color: "#3d6bd6", fontWeight: 600, whiteSpace: "nowrap" },
59
+ err: { color: "#ff6b6b", fontSize: fs(12) },
60
+ empty: { fontSize: fs(12), opacity: 0.6, padding: "16px 0" },
61
+ note: { fontSize: fs(11), opacity: 0.55 },
62
+ errbox: { border: "1px solid rgba(255,107,107,.4)", background: "rgba(255,107,107,.08)", borderRadius: 8, padding: 12, fontSize: fs(12) },
63
+ errboxTitle: { fontWeight: 600, marginBottom: 4, color: "#ff6b6b" },
64
+ hero: { borderRadius: 12, padding: 20, color: "#fff", background: "linear-gradient(135deg,#3a7bd5,#00d2ff)", display: "flex", flexDirection: "column", gap: 6 },
65
+ heroLabel: { fontSize: fs(12), opacity: 0.85 },
66
+ heroValue: { fontSize: fs(32), fontWeight: 700, letterSpacing: 0.5 },
67
+ heroCurrency: { fontSize: fs(12), opacity: 0.9 },
68
+ calBar: { display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap", marginTop: 10 },
69
+ calGrid: { display: "grid", gridTemplateColumns: "repeat(7,1fr)", gap: 4, marginTop: 6 },
70
+ calWk: { textAlign: "center", fontSize: fs(11), opacity: 0.5, padding: "2px 0" },
71
+ calCell: { border: "1px solid rgba(128,128,128,.15)", borderRadius: 6, padding: "6px 2px", textAlign: "center", fontSize: fs(12), cursor: "pointer", minHeight: 40, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 2 },
72
+ calCellBlank: { border: "1px solid transparent", padding: "6px 2px", minHeight: 40 },
73
+ calCellOn: { outline: "2px solid #5a8cff", outlineOffset: -2 },
74
+ calCellVal: { fontSize: fs(10), opacity: 0.8, maxWidth: "100%", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
75
+ legend: { display: "flex", alignItems: "center", gap: 6, fontSize: fs(11), opacity: 0.65, marginTop: 8, flexWrap: "wrap" },
76
+ legendBar: { display: "inline-block", width: 100, height: 8, borderRadius: 4, background: "linear-gradient(90deg,rgba(128,128,128,.12),rgba(46,134,222,.3),rgba(46,134,222,.85))" },
77
+ numInput: { border: "1px solid rgba(128,128,128,.35)", background: "transparent", borderRadius: 4, padding: "3px 6px", fontSize: fs(12), color: "inherit", width: 78, textAlign: "right" },
78
+ selDayTitle: { fontSize: fs(13), fontWeight: 600, marginTop: 14 }
79
+ };
80
+
81
+ // ── helpers ──
82
+ var pad2 = function (n) { return (n < 10 ? "0" : "") + n; };
83
+ var fmtInt = function (n) { return String(Math.round(n || 0)).replace(/\B(?=(\d{3})+(?!\d))/g, ","); };
84
+ var fmtTime = function (ts) { var d = new Date(ts + 8 * 3600 * 1000); return pad2(d.getUTCMonth() + 1) + "-" + pad2(d.getUTCDate()) + " " + pad2(d.getUTCHours()) + ":" + pad2(d.getUTCMinutes()) + ":" + pad2(d.getUTCSeconds()); };
85
+ var fmtMoney = function (n) { if (!n) return "¥0.0000"; if (n < 0.0001) return "¥" + n.toExponential(2); if (n < 1) return "¥" + n.toFixed(4); return "¥" + n.toFixed(2); };
86
+ var fmtPrice = function (n) { if (n == null || isNaN(n)) return "—"; return String(parseFloat(Number(n).toFixed(4))); };
87
+ var modelLabel = function (mk) { if (mk === "deepseek-v4-flash") return "deepseek-v4-flash"; if (mk === "deepseek-v4-pro") return "deepseek-v4-pro"; return "未知模型"; };
88
+ // 模型显示名:以请求参数里的真实模型名为准(非 DeepSeek 模型也如实显示),
89
+ // 空模型名时退回已知档位名 / provider / 未知。
90
+ var modelName = function (r) {
91
+ var m = String((r && r.model) || "").trim();
92
+ if (m) return m;
93
+ var mv = r && r.modelKey;
94
+ if (mv && mv !== "unknown") return modelLabel(mv);
95
+ var p = String((r && r.provider) || "").trim();
96
+ return p ? p : "未知模型";
97
+ };
98
+ // 模型分组键:真实模型名小写(用于把同名的记录归为一组展示)
99
+ var modelGroupKey = function (r) {
100
+ var m = String((r && r.model) || "").trim();
101
+ if (m) return m.toLowerCase();
102
+ var mv = r && r.modelKey;
103
+ if (mv && mv !== "unknown") return mv;
104
+ var p = String((r && r.provider) || "").trim();
105
+ return p ? p.toLowerCase() : "unknown";
106
+ };
107
+ // API 服务商显示名 / 分组键
108
+ var providerName = function (p) {
109
+ var s = String(p == null ? "" : p).trim();
110
+ return s || "未知服务商";
111
+ };
112
+ var providerGroupKey = function (p) {
113
+ var s = String(p == null ? "" : p).trim();
114
+ return s ? s.toLowerCase() : "unknown";
115
+ };
116
+ var finishLabel = function (f) { if (f === "stop") return "完成"; if (f === "tool-calls") return "工具调用"; if (f === "max-tokens") return "超长"; if (f === "error") return "错误"; return f || "—"; };
117
+ var pct = function (a, b) { return b > 0 ? (a / b * 100).toFixed(1) + "%" : "—"; };
118
+ var isPeakNow = function (ts) { var d = new Date(ts + 8 * 3600 * 1000); var t = d.getUTCHours() * 60 + d.getUTCMinutes(); return (t >= 9 * 60 && t < 12 * 60) || (t >= 14 * 60 && t < 18 * 60); };
119
+ // 计费档位选择:auto 用 autoCost(按生效日期自动切换),base 用 baseCost,其余用 peakValleyCost
120
+ // 计费档位显示文案
121
+ var regimeLabel = function (regime) {
122
+ if (regime === "auto") return "自动(生效前基础价 · 生效后峰谷价)";
123
+ if (regime === "base") return "基础价格";
124
+ return "峰谷价格";
125
+ };
126
+ var costHint = function (regime) {
127
+ if (regime === "auto") return "自动";
128
+ if (regime === "base") return "基础价格";
129
+ return "峰谷价格";
130
+ };
131
+ var costOf = function (r, regime) {
132
+ if (!r) return 0;
133
+ if (regime === "base") return r.baseCost || 0;
134
+ if (regime === "auto") return r.autoCost != null ? r.autoCost : (r.baseCost || 0);
135
+ return r.peakValleyCost || 0;
136
+ };
137
+ // 一组记录的消耗按高峰/空闲时段拆分(regime 决定用哪个档位的 cost)
138
+ var splitTotals = function (list, regime) {
139
+ var peak = 0, off = 0;
140
+ for (var i = 0; i < list.length; i++) {
141
+ var c = costOf(list[i], regime);
142
+ if (list[i].peak) peak += c; else off += c;
143
+ }
144
+ return { peak: peak, off: off };
145
+ };
146
+ // 单日高峰/空闲消耗:优先取 host 汇总的 days 字段;缺失(旧 host)时用按记录算好的 fallback
147
+ var daySplit = function (d, regime, fallback) {
148
+ var peak, off;
149
+ if (d) {
150
+ if (regime === "base") { peak = d.basePeakCost; off = d.baseOffPeakCost; }
151
+ else if (regime === "peakValley") { peak = d.pvPeakCost; off = d.pvOffPeakCost; }
152
+ else { peak = d.autoPeakCost; off = d.autoOffPeakCost; }
153
+ if (peak != null && off != null) return { peak: peak, off: off };
154
+ }
155
+ if (fallback) return fallback;
156
+ return { peak: 0, off: 0 };
157
+ };
158
+ var bjKey = function (ts) { var d = new Date(ts + 8 * 3600 * 1000); return d.getUTCFullYear() + "-" + pad2(d.getUTCMonth() + 1) + "-" + pad2(d.getUTCDate()); };
159
+ var bjStartMs = function (key) { var p = String(key).split("-"); return Date.UTC(+p[0], +p[1] - 1, +p[2]) - 8 * 3600 * 1000; };
160
+ var dayLabel = function (key) { var p = String(key).split("-"); return p[0] + "年" + (+p[1]) + "月" + (+p[2]) + "日"; };
161
+ var monthDays = function (y, m) { return new Date(Date.UTC(y, m + 1, 0)).getUTCDate(); };
162
+ var monthOffset = function (y, m) { var ms = bjStartMs(y + "-" + pad2(m + 1) + "-01"); var wd = new Date(ms).getUTCDay(); return (wd + 6) % 7; };
163
+ var fmtBalance = function (s) {
164
+ var str = String(s == null ? "0" : s);
165
+ var num = parseFloat(str);
166
+ if (isNaN(num)) return str;
167
+ var parts = str.split(".");
168
+ var dec = parts[1] ? parts[1].slice(0, 2) : "00";
169
+ return fmtInt(parseInt(parts[0], 10)) + "." + (dec.length === 1 ? dec + "0" : dec);
170
+ };
171
+ var api = function (payload) {
172
+ return fetch("/usage/api", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }).then(function (r) { return r.json(); });
173
+ };
174
+ var PRICE_MODELS = ["deepseek-v4-flash", "deepseek-v4-pro"];
175
+
176
+ // ── Card ──
177
+ function Card(props) {
178
+ return el("div", { style: st.card },
179
+ el("div", { style: st.cardL }, props.label),
180
+ el("div", { style: st.cardV }, props.value),
181
+ props.hint ? el("div", { style: st.cardH }, props.hint) : null
182
+ );
183
+ }
184
+
185
+ // ── long-image report ──
186
+ function drawReport(canvas, records) {
187
+ records = records.slice();
188
+ records.sort(function (a, b) { return b.time - a.time; }); // newest first
189
+ var totalIn = records.length;
190
+ if (records.length > 2000) records = records.slice(0, 2000); // canvas height guard
191
+ var W = 1520, P = 40, count = records.length;
192
+ var scale = count <= 200 ? 2 : 1;
193
+ var rowH = count <= 200 ? 28 : count <= 800 ? 20 : 14;
194
+ var fBase = count <= 200 ? 12 : count <= 800 ? 11 : 10;
195
+ var headH = count <= 200 ? 26 : count <= 800 ? 20 : 16;
196
+
197
+ var totalHit = 0, totalMiss = 0, totalWrite = 0, totalOut = 0, totalReason = 0, totalCost = 0, totalPeakCost = 0, totalOffCost = 0;
198
+ for (var i = 0; i < count; i++) {
199
+ var r = records[i];
200
+ totalHit += r.cacheReadTokens || 0; totalMiss += r.inputTokens || 0; totalWrite += r.cacheWriteTokens || 0;
201
+ totalOut += r.outputTokens || 0; totalReason += r.reasoningTokens || 0;
202
+ var cst = r.peakValleyCost || 0;
203
+ totalCost += cst;
204
+ if (r.peak) totalPeakCost += cst; else totalOffCost += cst;
205
+ }
206
+ var hitRate = (totalHit + totalMiss + totalWrite) > 0 ? (totalHit / (totalHit + totalMiss + totalWrite) * 100).toFixed(1) + "%" : "—";
207
+ var byModel = {}, byProvider = {}, byProviderModel = {};
208
+ for (var i2 = 0; i2 < count; i2++) {
209
+ var r2 = records[i2];
210
+ var gk2 = modelGroupKey(r2);
211
+ if (!byModel[gk2]) byModel[gk2] = { key: gk2, name: modelName(r2), calls: 0, hit: 0, miss: 0, out: 0, reason: 0, cost: 0, peakCost: 0, offCost: 0, peakCalls: 0, offCalls: 0 };
212
+ var c2v = r2.peakValleyCost || 0;
213
+ byModel[gk2].calls += 1; byModel[gk2].hit += r2.cacheReadTokens || 0; byModel[gk2].miss += r2.inputTokens || 0;
214
+ byModel[gk2].out += r2.outputTokens || 0; byModel[gk2].reason += r2.reasoningTokens || 0; byModel[gk2].cost += c2v;
215
+ var pk2 = providerGroupKey(r2.provider);
216
+ if (!byProvider[pk2]) byProvider[pk2] = { key: pk2, name: providerName(r2.provider), calls: 0, cost: 0, peakCost: 0, offCost: 0, peakCalls: 0, offCalls: 0 };
217
+ byProvider[pk2].calls += 1; byProvider[pk2].cost += c2v;
218
+ var pmk2 = pk2 + "||" + gk2;
219
+ if (!byProviderModel[pmk2]) byProviderModel[pmk2] = { key: pmk2, providerKey: pk2, providerName: providerName(r2.provider), modelKey: gk2, modelName: modelName(r2), calls: 0, cost: 0, peakCost: 0, offCost: 0, peakCalls: 0, offCalls: 0 };
220
+ byProviderModel[pmk2].calls += 1; byProviderModel[pmk2].cost += c2v;
221
+ if (r2.peak) {
222
+ byModel[gk2].peakCost += c2v; byModel[gk2].peakCalls += 1;
223
+ byProvider[pk2].peakCost += c2v; byProvider[pk2].peakCalls += 1;
224
+ byProviderModel[pmk2].peakCost += c2v; byProviderModel[pmk2].peakCalls += 1;
225
+ } else {
226
+ byModel[gk2].offCost += c2v; byModel[gk2].offCalls += 1;
227
+ byProvider[pk2].offCost += c2v; byProvider[pk2].offCalls += 1;
228
+ byProviderModel[pmk2].offCost += c2v; byProviderModel[pmk2].offCalls += 1;
229
+ }
230
+ }
231
+ var modelRows = [];
232
+ for (var k in byModel) modelRows.push(byModel[k]);
233
+ modelRows.sort(function (a, b) { return a.key < b.key ? -1 : a.key > b.key ? 1 : 0; });
234
+ var providerRows = [];
235
+ for (var kp in byProvider) providerRows.push(byProvider[kp]);
236
+ providerRows.sort(function (a, b) { return b.cost - a.cost || (a.key < b.key ? -1 : a.key > b.key ? 1 : 0); });
237
+ var providerGroups = [];
238
+ for (var pg = 0; pg < providerRows.length; pg++) {
239
+ var gprov = providerRows[pg];
240
+ var gmodels = [];
241
+ for (var kgm in byProviderModel) {
242
+ if (byProviderModel[kgm].providerKey === gprov.key) gmodels.push(byProviderModel[kgm]);
243
+ }
244
+ gmodels.sort(function (a, b) { return b.cost - a.cost || (a.modelKey < b.modelKey ? -1 : 1); });
245
+ providerGroups.push({ provider: gprov, models: gmodels });
246
+ }
247
+ // 服务商×模型明细的总行数(组头 + 组内模型 + 合计行)
248
+ var pmRowTotal = 1;
249
+ for (var pgt = 0; pgt < providerGroups.length; pgt++) pmRowTotal += 1 + providerGroups[pgt].models.length;
250
+
251
+ var H = 244 + headH + count * rowH + 24 + 18 + headH + (modelRows.length + 1) * rowH + 24 + 18 + headH + pmRowTotal * rowH + 26 + 44;
252
+ if (H > 30000 && rowH > 12) { rowH = 12; fBase = 10; headH = 14; H = 244 + headH + count * rowH + 24 + 18 + headH + (modelRows.length + 1) * rowH + 24 + 18 + headH + pmRowTotal * rowH + 26 + 44; }
253
+ canvas.width = W * scale;
254
+ canvas.height = H * scale;
255
+ var ctx = canvas.getContext("2d");
256
+ if (!ctx) throw new Error("canvas 2d 不可用");
257
+ ctx.scale(scale, scale);
258
+ ctx.textBaseline = "alphabetic";
259
+ var font = function (size, weight) { ctx.font = (weight || "400") + " " + size + 'px "Segoe UI","Microsoft YaHei",sans-serif'; };
260
+
261
+ ctx.fillStyle = "#ffffff"; ctx.fillRect(0, 0, W, H);
262
+ ctx.fillStyle = "#3a7bd5"; ctx.fillRect(0, 0, W, 64);
263
+ ctx.fillStyle = "#ffffff"; ctx.textAlign = "left"; font(20, "700");
264
+ ctx.fillText("用量与消耗报告", P, 40);
265
+ ctx.fillStyle = "#7a8699"; font(13, "400");
266
+ ctx.fillText("生成时间 " + fmtTime(Date.now()) + "(北京) · 共 " + count + " 条调用 · 缓存命中率 " + hitRate +
267
+ " · 高峰价消耗 " + fmtMoney(totalPeakCost) + " · 空闲价消耗 " + fmtMoney(totalOffCost) + " · 合计 " + fmtMoney(totalCost) +
268
+ (totalIn > count ? " · 报告仅含最近 " + count + " 条" : ""), P, 92);
269
+
270
+ var cardW = (W - 2 * P - 6 * 10) / 7;
271
+ var cards = [
272
+ { l: "调用次数", v: fmtInt(count) }, { l: "输入 · 未命中", v: fmtInt(totalMiss) },
273
+ { l: "输入 · 缓存命中", v: fmtInt(totalHit) }, { l: "输出", v: fmtInt(totalOut) },
274
+ { l: "高峰消耗", v: fmtMoney(totalPeakCost) }, { l: "空闲消耗", v: fmtMoney(totalOffCost) },
275
+ { l: "总消耗 (峰谷价)", v: fmtMoney(totalCost) }
276
+ ];
277
+ for (var c = 0; c < cards.length; c++) {
278
+ var x = P + c * (cardW + 10);
279
+ ctx.fillStyle = "#f5f7fa"; ctx.fillRect(x, 112, cardW, 70);
280
+ ctx.strokeStyle = "#e4e8ee"; ctx.strokeRect(x, 112, cardW, 70);
281
+ ctx.fillStyle = "#7a8699"; font(12, "400"); ctx.fillText(cards[c].l, x + 14, 138);
282
+ ctx.fillStyle = "#1c2733"; font(19, "600"); ctx.fillText(cards[c].v, x + 14, 166);
283
+ }
284
+
285
+ var c1 = { time: P, model: P + 150, missR: P + 440, hitR: P + 540, writeR: P + 630, outR: P + 720, reasonR: P + 810, rateR: P + 900, periodR: P + 990, endR: P + 1080, costR: P + 1440 };
286
+ var y = 226;
287
+ ctx.fillStyle = "#1c2733"; font(15, "600"); ctx.fillText("缓存命中列表(共 " + count + " 条)", P, y);
288
+ y = 244;
289
+ ctx.fillStyle = "#eef1f5"; ctx.fillRect(P, y, W - 2 * P, headH);
290
+ ctx.fillStyle = "#55617a"; font(fBase + 1, "600");
291
+ var hty = y + Math.round(headH * 0.65);
292
+ ctx.textAlign = "left"; ctx.fillText("时间(北京)", c1.time, hty); ctx.fillText("模型", c1.model, hty);
293
+ ctx.textAlign = "right";
294
+ ctx.fillText("输入·未命中", c1.missR, hty); ctx.fillText("缓存命中", c1.hitR, hty); ctx.fillText("缓存写入", c1.writeR, hty);
295
+ ctx.fillText("输出", c1.outR, hty); ctx.fillText("推理", c1.reasonR, hty); ctx.fillText("命中率", c1.rateR, hty);
296
+ ctx.fillText("时段", c1.periodR, hty); ctx.fillText("结束", c1.endR, hty); ctx.fillText("消耗(峰谷)", c1.costR, hty);
297
+ y += headH;
298
+ for (var r3 = 0; r3 < count; r3++) {
299
+ var rec = records[r3];
300
+ var hit = rec.cacheReadTokens || 0, miss = rec.inputTokens || 0, cost = rec.peakValleyCost || 0;
301
+ ctx.fillStyle = r3 % 2 === 1 ? "#fafbfc" : "#ffffff"; ctx.fillRect(P, y, W - 2 * P, rowH);
302
+ var ty = y + Math.round(rowH * 0.68);
303
+ ctx.textAlign = "left"; ctx.fillStyle = "#1c2733"; font(fBase, "400");
304
+ ctx.fillText(fmtTime(rec.time), c1.time, ty); ctx.fillText(modelName(rec), c1.model, ty);
305
+ ctx.textAlign = "right";
306
+ ctx.fillText(fmtInt(miss), c1.missR, ty);
307
+ ctx.fillStyle = "#22a45d"; ctx.fillText(fmtInt(hit), c1.hitR, ty);
308
+ ctx.fillStyle = "#1c2733";
309
+ ctx.fillText(rec.cacheWriteTokens ? fmtInt(rec.cacheWriteTokens) : "—", c1.writeR, ty);
310
+ ctx.fillText(fmtInt(rec.outputTokens || 0), c1.outR, ty);
311
+ ctx.fillText(rec.reasoningTokens ? fmtInt(rec.reasoningTokens) : "—", c1.reasonR, ty);
312
+ ctx.fillStyle = "#55617a"; ctx.fillText(pct(hit, hit + miss), c1.rateR, ty);
313
+ ctx.fillStyle = rec.peak ? "#e08700" : "#3d6bd6"; ctx.fillText(rec.peak ? "峰" : "谷", c1.periodR, ty);
314
+ ctx.fillStyle = "#55617a"; ctx.fillText(finishLabel(rec.finishReason), c1.endR, ty);
315
+ ctx.fillStyle = rec.peak ? "#e08700" : "#3d6bd6"; ctx.fillText(fmtMoney(cost), c1.costR, ty);
316
+ y += rowH;
317
+ }
318
+
319
+ y += 24;
320
+ ctx.textAlign = "left"; ctx.fillStyle = "#1c2733"; font(15, "600");
321
+ ctx.fillText("消耗表(按模型 · 峰谷价 · 高峰/空闲分列)", P, y);
322
+ y += 18;
323
+ var c2 = { model: P, callsR: P + 330, missR: P + 470, hitR: P + 590, outR: P + 710, reasonR: P + 840, peakR: P + 1010, offR: P + 1140, costR: P + 1440 };
324
+ ctx.fillStyle = "#eef1f5"; ctx.fillRect(P, y, W - 2 * P, headH);
325
+ ctx.fillStyle = "#55617a"; font(fBase + 1, "600");
326
+ var hty2 = y + Math.round(headH * 0.65);
327
+ ctx.textAlign = "left"; ctx.fillText("模型", c2.model, hty2);
328
+ ctx.textAlign = "right";
329
+ ctx.fillText("调用", c2.callsR, hty2); ctx.fillText("输入·未命中", c2.missR, hty2); ctx.fillText("缓存命中", c2.hitR, hty2);
330
+ ctx.fillText("输出", c2.outR, hty2); ctx.fillText("推理", c2.reasonR, hty2);
331
+ ctx.fillText("高峰消耗", c2.peakR, hty2); ctx.fillText("空闲消耗", c2.offR, hty2); ctx.fillText("总消耗", c2.costR, hty2);
332
+ y += headH;
333
+ var t2rows = modelRows.concat([{ key: "合计", name: "合计", calls: count, miss: totalMiss, hit: totalHit, out: totalOut, reason: totalReason, cost: totalCost, peakCost: totalPeakCost, offCost: totalOffCost }]);
334
+ for (var m = 0; m < t2rows.length; m++) {
335
+ var row = t2rows[m];
336
+ var isTotal = row.key === "合计";
337
+ ctx.fillStyle = isTotal ? "#eef1f5" : (m % 2 === 1 ? "#fafbfc" : "#ffffff"); ctx.fillRect(P, y, W - 2 * P, rowH);
338
+ var ty2 = y + Math.round(rowH * 0.68);
339
+ ctx.textAlign = "left"; ctx.fillStyle = "#1c2733"; font(fBase, isTotal ? "600" : "400");
340
+ ctx.fillText(row.name || row.key, c2.model, ty2);
341
+ ctx.textAlign = "right";
342
+ ctx.fillText(fmtInt(row.calls), c2.callsR, ty2); ctx.fillText(fmtInt(row.miss), c2.missR, ty2);
343
+ ctx.fillStyle = "#22a45d"; ctx.fillText(fmtInt(row.hit), c2.hitR, ty2);
344
+ ctx.fillStyle = "#1c2733"; font(fBase, isTotal ? "600" : "400");
345
+ ctx.fillText(fmtInt(row.out), c2.outR, ty2); ctx.fillText(row.reason ? fmtInt(row.reason) : "—", c2.reasonR, ty2);
346
+ ctx.fillStyle = "#e08700"; ctx.fillText(fmtMoney(row.peakCost), c2.peakR, ty2);
347
+ ctx.fillStyle = "#3d6bd6"; ctx.fillText(fmtMoney(row.offCost), c2.offR, ty2);
348
+ ctx.fillStyle = "#1c2733"; ctx.fillText(fmtMoney(row.cost), c2.costR, ty2);
349
+ y += rowH;
350
+ }
351
+ y += 24;
352
+ ctx.textAlign = "left"; ctx.fillStyle = "#1c2733"; font(15, "600");
353
+ ctx.fillText("消耗表(按 API 服务商 × 模型 · 峰谷价)", P, y);
354
+ y += 18;
355
+ ctx.fillStyle = "#eef1f5"; ctx.fillRect(P, y, W - 2 * P, headH);
356
+ ctx.fillStyle = "#55617a"; font(fBase + 1, "600");
357
+ var hty3 = y + Math.round(headH * 0.65);
358
+ ctx.textAlign = "left"; ctx.fillText("API 服务商 / 模型", c2.model, hty3);
359
+ ctx.textAlign = "right";
360
+ ctx.fillText("调用", c2.callsR, hty3);
361
+ ctx.fillText("高峰消耗", c2.peakR, hty3); ctx.fillText("空闲消耗", c2.offR, hty3); ctx.fillText("总消耗", c2.costR, hty3);
362
+ y += headH;
363
+ var p3n = 0;
364
+ for (var p3g = 0; p3g < providerGroups.length; p3g++) {
365
+ var pgrp = providerGroups[p3g];
366
+ var gpr = pgrp.provider;
367
+ ctx.fillStyle = "#e8edf5"; ctx.fillRect(P, y, W - 2 * P, rowH);
368
+ var ty3 = y + Math.round(rowH * 0.68);
369
+ ctx.textAlign = "left"; ctx.fillStyle = "#1c2733"; font(fBase, "600");
370
+ ctx.fillText(gpr.name + "(" + pgrp.models.length + " 个模型)", c2.model, ty3);
371
+ ctx.textAlign = "right"; ctx.fillStyle = "#1c2733"; font(fBase, "600");
372
+ ctx.fillText(fmtInt(gpr.calls), c2.callsR, ty3);
373
+ ctx.fillStyle = "#e08700"; ctx.fillText(fmtMoney(gpr.peakCost), c2.peakR, ty3);
374
+ ctx.fillStyle = "#3d6bd6"; ctx.fillText(fmtMoney(gpr.offCost), c2.offR, ty3);
375
+ ctx.fillStyle = "#1c2733"; ctx.fillText(fmtMoney(gpr.cost), c2.costR, ty3);
376
+ y += rowH;
377
+ for (var p3m = 0; p3m < pgrp.models.length; p3m++) {
378
+ var pmd = pgrp.models[p3m];
379
+ ctx.fillStyle = (p3n++ % 2 === 1) ? "#fafbfc" : "#ffffff"; ctx.fillRect(P, y, W - 2 * P, rowH);
380
+ var ty4 = y + Math.round(rowH * 0.68);
381
+ ctx.textAlign = "left"; ctx.fillStyle = "#1c2733"; font(fBase, "400");
382
+ ctx.fillText(" " + pmd.modelName, c2.model, ty4);
383
+ ctx.textAlign = "right"; ctx.fillStyle = "#1c2733"; font(fBase, "400");
384
+ ctx.fillText(fmtInt(pmd.calls), c2.callsR, ty4);
385
+ ctx.fillStyle = "#e08700"; ctx.fillText(fmtMoney(pmd.peakCost), c2.peakR, ty4);
386
+ ctx.fillStyle = "#3d6bd6"; ctx.fillText(fmtMoney(pmd.offCost), c2.offR, ty4);
387
+ ctx.fillStyle = "#1c2733"; ctx.fillText(fmtMoney(pmd.cost), c2.costR, ty4);
388
+ y += rowH;
389
+ }
390
+ }
391
+ ctx.fillStyle = "#eef1f5"; ctx.fillRect(P, y, W - 2 * P, rowH);
392
+ var ty5 = y + Math.round(rowH * 0.68);
393
+ ctx.textAlign = "left"; ctx.fillStyle = "#1c2733"; font(fBase, "600");
394
+ ctx.fillText("总费用合计", c2.model, ty5);
395
+ ctx.textAlign = "right"; ctx.fillStyle = "#1c2733"; font(fBase, "600");
396
+ ctx.fillText(fmtInt(count), c2.callsR, ty5);
397
+ ctx.fillStyle = "#e08700"; ctx.fillText(fmtMoney(totalPeakCost), c2.peakR, ty5);
398
+ ctx.fillStyle = "#3d6bd6"; ctx.fillText(fmtMoney(totalOffCost), c2.offR, ty5);
399
+ ctx.fillStyle = "#1c2733"; ctx.fillText(fmtMoney(totalCost), c2.costR, ty5);
400
+ y += rowH;
401
+ y += 26;
402
+ ctx.textAlign = "left"; ctx.fillStyle = "#98a2b3"; font(11, "400");
403
+ ctx.fillText("价格:统一按 DeepSeek 官方 API 价格计费 · 高峰时段(北京 9:00–12:00、14:00–18:00)· 单位:元 / 百万 tokens · 高峰/空闲消耗分列统计 · 由 dsh-usage-plugin 生成", P, y);
404
+ }
405
+
406
+ // ── Overview view ──
407
+ function OverviewView(props) {
408
+ var records = props.records, regime = props.regime;
409
+ var totalCalls = records.length;
410
+ var totalHit = 0, totalMiss = 0, totalWrite = 0, totalOut = 0, totalReason = 0, totalCost = 0, peakTotal = 0, offTotal = 0, peakCalls = 0, offCalls = 0;
411
+ var byModel = {}, byProvider = {}, byProviderModel = {};
412
+ for (var i = 0; i < records.length; i++) {
413
+ var r = records[i];
414
+ totalHit += r.cacheReadTokens || 0; totalMiss += r.inputTokens || 0; totalWrite += r.cacheWriteTokens || 0;
415
+ totalOut += r.outputTokens || 0; totalReason += r.reasoningTokens || 0;
416
+ var cost = costOf(r, regime);
417
+ totalCost += cost;
418
+ var gk = modelGroupKey(r);
419
+ if (!byModel[gk]) byModel[gk] = { key: gk, name: modelName(r), calls: 0, hit: 0, miss: 0, out: 0, reason: 0, cost: 0, peakCost: 0, offCost: 0, peakCalls: 0, offCalls: 0 };
420
+ byModel[gk].calls += 1; byModel[gk].hit += r.cacheReadTokens || 0; byModel[gk].miss += r.inputTokens || 0;
421
+ byModel[gk].out += r.outputTokens || 0; byModel[gk].reason += r.reasoningTokens || 0; byModel[gk].cost += cost;
422
+ var pk = providerGroupKey(r.provider);
423
+ if (!byProvider[pk]) byProvider[pk] = { key: pk, name: providerName(r.provider), calls: 0, cost: 0, peakCost: 0, offCost: 0, peakCalls: 0, offCalls: 0, miss: 0, hit: 0, out: 0, reason: 0 };
424
+ byProvider[pk].calls += 1; byProvider[pk].cost += cost;
425
+ byProvider[pk].miss += r.inputTokens || 0; byProvider[pk].hit += r.cacheReadTokens || 0;
426
+ byProvider[pk].out += r.outputTokens || 0; byProvider[pk].reason += r.reasoningTokens || 0;
427
+ var pmKey = pk + "||" + gk;
428
+ if (!byProviderModel[pmKey]) byProviderModel[pmKey] = { key: pmKey, providerKey: pk, providerName: providerName(r.provider), modelKey: gk, modelName: modelName(r), calls: 0, cost: 0, peakCost: 0, offCost: 0, peakCalls: 0, offCalls: 0, miss: 0, hit: 0, out: 0, reason: 0 };
429
+ byProviderModel[pmKey].calls += 1; byProviderModel[pmKey].cost += cost;
430
+ byProviderModel[pmKey].miss += r.inputTokens || 0; byProviderModel[pmKey].hit += r.cacheReadTokens || 0;
431
+ byProviderModel[pmKey].out += r.outputTokens || 0; byProviderModel[pmKey].reason += r.reasoningTokens || 0;
432
+ if (r.peak) {
433
+ byModel[gk].peakCost += cost; byModel[gk].peakCalls += 1;
434
+ byProvider[pk].peakCost += cost; byProvider[pk].peakCalls += 1;
435
+ byProviderModel[pmKey].peakCost += cost; byProviderModel[pmKey].peakCalls += 1;
436
+ peakTotal += cost; peakCalls += 1;
437
+ } else {
438
+ byModel[gk].offCost += cost; byModel[gk].offCalls += 1;
439
+ byProvider[pk].offCost += cost; byProvider[pk].offCalls += 1;
440
+ byProviderModel[pmKey].offCost += cost; byProviderModel[pmKey].offCalls += 1;
441
+ offTotal += cost; offCalls += 1;
442
+ }
443
+ }
444
+ var hitRate = (totalHit + totalMiss + totalWrite) > 0 ? (totalHit / (totalHit + totalMiss + totalWrite) * 100).toFixed(1) + "%" : "—";
445
+ var modelRows = [];
446
+ for (var k in byModel) modelRows.push(byModel[k]);
447
+ modelRows.sort(function (a, b) { return a.key < b.key ? -1 : a.key > b.key ? 1 : 0; });
448
+ var providerRows = [];
449
+ for (var kp in byProvider) providerRows.push(byProvider[kp]);
450
+ providerRows.sort(function (a, b) { return b.cost - a.cost || (a.key < b.key ? -1 : a.key > b.key ? 1 : 0); });
451
+ // 服务商 × 模型分组:每个服务商一组,组内按消耗排序
452
+ var providerGroups = [];
453
+ for (var pg = 0; pg < providerRows.length; pg++) {
454
+ var gprov = providerRows[pg];
455
+ var models = [];
456
+ for (var km in byProviderModel) {
457
+ if (byProviderModel[km].providerKey === gprov.key) models.push(byProviderModel[km]);
458
+ }
459
+ models.sort(function (a, b) { return b.cost - a.cost || (a.modelKey < b.modelKey ? -1 : 1); });
460
+ providerGroups.push({ provider: gprov, models: models });
461
+ }
462
+ // 服务商 × 模型明细行(组头行 + 组内模型行)
463
+ var pmRows = [];
464
+ for (var gi = 0; gi < providerGroups.length; gi++) {
465
+ var g = providerGroups[gi];
466
+ var gprov2 = g.provider;
467
+ pmRows.push(el("tr", { key: "g-" + gprov2.key },
468
+ el("td", { style: st.tdGroup }, gprov2.name),
469
+ el("td", { style: st.tdGroup }, "(" + g.models.length + " 个模型)"),
470
+ el("td", { style: st.tdGroupR }, fmtInt(gprov2.calls)),
471
+ el("td", { style: st.tdGroupR }, el("span", { style: st.badgePeak }, fmtInt(gprov2.peakCalls)), " / ", el("span", { style: st.badgeValley }, fmtInt(gprov2.offCalls))),
472
+ el("td", { style: st.tdGroupR }, fmtInt(gprov2.miss)),
473
+ el("td", { style: st.tdGroupR }, fmtInt(gprov2.hit)),
474
+ el("td", { style: st.tdGroupR }, fmtInt(gprov2.out)),
475
+ el("td", { style: st.tdGroupR }, gprov2.reason ? fmtInt(gprov2.reason) : "—"),
476
+ el("td", { style: st.tdGroupR }, fmtMoney(gprov2.peakCost)),
477
+ el("td", { style: st.tdGroupR }, fmtMoney(gprov2.offCost)),
478
+ el("td", { style: st.tdGroupR }, fmtMoney(gprov2.cost))
479
+ ));
480
+ for (var mi = 0; mi < g.models.length; mi++) {
481
+ var md = g.models[mi];
482
+ pmRows.push(el("tr", { key: "pm-" + md.key },
483
+ el("td", { style: st.tdFirst }, ""),
484
+ el("td", { style: st.tdWrap }, md.modelName),
485
+ el("td", { style: st.td }, fmtInt(md.calls)),
486
+ el("td", { style: st.td }, el("span", { style: st.badgePeak }, fmtInt(md.peakCalls)), " / ", el("span", { style: st.badgeValley }, fmtInt(md.offCalls))),
487
+ el("td", { style: st.td }, fmtInt(md.miss)),
488
+ el("td", { style: st.td }, el("span", { style: st.badgeHit }, fmtInt(md.hit))),
489
+ el("td", { style: st.td }, fmtInt(md.out)),
490
+ el("td", { style: st.td }, md.reason ? fmtInt(md.reason) : "—"),
491
+ el("td", { style: st.td }, fmtMoney(md.peakCost)),
492
+ el("td", { style: st.td }, fmtMoney(md.offCost)),
493
+ el("td", { style: st.td }, fmtMoney(md.cost))
494
+ ));
495
+ }
496
+ }
497
+ pmRows.push(el("tr", { key: "g-total" },
498
+ el("td", { style: st.tdTotalFirst }, "总费用合计"),
499
+ el("td", { style: st.tdTotal }, fmtInt(providerRows.length) + " 个服务商"),
500
+ el("td", { style: st.tdTotal }, fmtInt(totalCalls)),
501
+ el("td", { style: st.tdTotal }, el("span", { style: st.badgePeak }, fmtInt(peakCalls)), " / ", el("span", { style: st.badgeValley }, fmtInt(offCalls))),
502
+ el("td", { style: st.tdTotal }, fmtInt(totalMiss)),
503
+ el("td", { style: st.tdTotal }, fmtInt(totalHit)),
504
+ el("td", { style: st.tdTotal }, fmtInt(totalOut)),
505
+ el("td", { style: st.tdTotal }, totalReason ? fmtInt(totalReason) : "—"),
506
+ el("td", { style: st.tdTotal }, fmtMoney(peakTotal)),
507
+ el("td", { style: st.tdTotal }, fmtMoney(offTotal)),
508
+ el("td", { style: st.tdTotal }, fmtMoney(totalCost))
509
+ ));
510
+
511
+ return el("div", null,
512
+ el("div", { style: st.cards },
513
+ el(Card, { label: "调用次数", value: fmtInt(totalCalls), hint: "高峰 " + fmtInt(peakCalls) + " · 空闲 " + fmtInt(offCalls) }),
514
+ el(Card, { label: "输入 · 未命中", value: fmtInt(totalMiss), hint: "token" }),
515
+ el(Card, { label: "输入 · 缓存命中", value: fmtInt(totalHit), hint: "命中率 " + hitRate }),
516
+ el(Card, { label: "输出", value: fmtInt(totalOut), hint: "token" }),
517
+ el(Card, { label: "高峰消耗", value: fmtMoney(peakTotal), hint: "高峰时段 9:00–12:00、14:00–18:00" }),
518
+ el(Card, { label: "空闲消耗", value: fmtMoney(offTotal), hint: "其余空闲时段" }),
519
+ el(Card, { label: "总消耗", value: fmtMoney(totalCost), hint: "高峰 + 空闲 · " + regimeLabel(regime) })
520
+ ),
521
+ el("div", { style: st.sec }, "消耗表(按模型)"),
522
+ el("div", { style: st.scroll },
523
+ el("table", { style: st.tbl },
524
+ el("thead", null, el("tr", null,
525
+ el("th", { style: st.thFirst }, "模型"), el("th", { style: st.th }, "调用"), el("th", { style: st.th }, "高峰/空闲"),
526
+ el("th", { style: st.th }, "输入·未命中"), el("th", { style: st.th }, "缓存命中"), el("th", { style: st.th }, "输出"), el("th", { style: st.th }, "推理"),
527
+ el("th", { style: st.th }, "高峰消耗"), el("th", { style: st.th }, "空闲消耗"), el("th", { style: st.th }, "总消耗")
528
+ )),
529
+ el("tbody", null,
530
+ modelRows.map(function (m) {
531
+ return el("tr", { key: m.key },
532
+ el("td", { style: st.tdWrap }, m.name),
533
+ el("td", { style: st.td }, fmtInt(m.calls)),
534
+ el("td", { style: st.td }, el("span", { style: st.badgePeak }, fmtInt(m.peakCalls)), " / ", el("span", { style: st.badgeValley }, fmtInt(m.offCalls))),
535
+ el("td", { style: st.td }, fmtInt(m.miss)),
536
+ el("td", { style: st.td }, el("span", { style: st.badgeHit }, fmtInt(m.hit))),
537
+ el("td", { style: st.td }, fmtInt(m.out)),
538
+ el("td", { style: st.td }, m.reason ? fmtInt(m.reason) : "—"),
539
+ el("td", { style: st.td }, fmtMoney(m.peakCost)),
540
+ el("td", { style: st.td }, fmtMoney(m.offCost)),
541
+ el("td", { style: st.td }, fmtMoney(m.cost))
542
+ );
543
+ }),
544
+ el("tr", null,
545
+ el("td", { style: st.tdTotalFirst }, "合计"),
546
+ el("td", { style: st.tdTotal }, fmtInt(totalCalls)),
547
+ el("td", { style: st.tdTotal }, el("span", { style: st.badgePeak }, fmtInt(peakCalls)), " / ", el("span", { style: st.badgeValley }, fmtInt(offCalls))),
548
+ el("td", { style: st.tdTotal }, fmtInt(totalMiss)),
549
+ el("td", { style: st.tdTotal }, fmtInt(totalHit)),
550
+ el("td", { style: st.tdTotal }, fmtInt(totalOut)),
551
+ el("td", { style: st.tdTotal }, totalReason ? fmtInt(totalReason) : "—"),
552
+ el("td", { style: st.tdTotal }, fmtMoney(peakTotal)),
553
+ el("td", { style: st.tdTotal }, fmtMoney(offTotal)),
554
+ el("td", { style: st.tdTotal }, fmtMoney(totalCost))
555
+ )
556
+ )
557
+ )
558
+ ),
559
+ el("div", { style: st.sec, marginTop: 14 }, "消耗明细(按 API 服务商 × 模型)"),
560
+ el("div", { style: st.scroll },
561
+ el("table", { style: st.tbl },
562
+ el("thead", null, el("tr", null,
563
+ el("th", { style: st.thFirst }, "API 服务商"), el("th", { style: st.thFirst }, "模型"), el("th", { style: st.th }, "调用"), el("th", { style: st.th }, "高峰/空闲"),
564
+ el("th", { style: st.th }, "输入·未命中"), el("th", { style: st.th }, "缓存命中"), el("th", { style: st.th }, "输出"), el("th", { style: st.th }, "推理"),
565
+ el("th", { style: st.th }, "高峰消耗"), el("th", { style: st.th }, "空闲消耗"), el("th", { style: st.th }, "总消耗")
566
+ )),
567
+ el("tbody", null, pmRows)
568
+ )
569
+ ),
570
+ el("div", { style: st.note, marginTop: 8 },
571
+ "高峰时段(北京时间 9:00–12:00、14:00–18:00)按高峰价,其余时段按空闲价,系统按每次调用发生的时间自动选择档位。模型名与 API 服务商均以请求参数为准;所有消耗统一按 DeepSeek 官方 API 价格计费(各厂商定价数据不完整,不另行计价),非 DeepSeek 官方模型无对应价格,消耗按 0 统计。单位:元 / 百万 tokens。")
572
+ );
573
+ }
574
+
575
+ // ── Calendar view ──
576
+ function CalendarView(props) {
577
+ var records = props.records, regime = props.regime, days = props.days || [];
578
+ var nowB = new Date(Date.now() + 8 * 3600 * 1000);
579
+ var nowY = nowB.getUTCFullYear(), nowM = nowB.getUTCMonth();
580
+ var ymState = React.useState({ y: nowY, m: nowM });
581
+ var ym = ymState[0], setYm = ymState[1];
582
+ var selState = React.useState(null);
583
+ var selectedDay = selState[0], setSelectedDay = selState[1];
584
+ var dimState = React.useState("cost");
585
+ var dimMode = dimState[0], setDim = dimState[1];
586
+
587
+ var y = ym.y, m = ym.m;
588
+ var nDays = monthDays(y, m);
589
+ var offset = monthOffset(y, m);
590
+ var dayMap = {};
591
+ for (var di = 0; di < days.length; di++) dayMap[days[di].day] = days[di];
592
+
593
+ // 高峰/空闲消耗拆分:优先取 host 汇总的 days 字段,字段缺失时用记录回退计算
594
+ var splitMap = {};
595
+ for (var sr = 0; sr < records.length; sr++) {
596
+ var rec0 = records[sr];
597
+ var k0 = bjKey(rec0.time);
598
+ var c0 = costOf(rec0, regime);
599
+ if (!splitMap[k0]) splitMap[k0] = { peak: 0, off: 0 };
600
+ if (rec0.peak) splitMap[k0].peak += c0; else splitMap[k0].off += c0;
601
+ }
602
+
603
+ function dayCost(d) { return costOf(d, regime); }
604
+ function dayVal(d) { return dimMode === "cost" ? dayCost(d) : (d ? d.calls : 0); }
605
+
606
+ var maxV = 0, monthCalls = 0, monthMiss = 0, monthHit = 0, monthOut = 0, monthCost = 0, monthPeak = 0, monthOff = 0, monthPeakCalls = 0, monthOffCalls = 0;
607
+ for (var d2 = 1; d2 <= nDays; d2++) {
608
+ var key2 = y + "-" + pad2(m + 1) + "-" + pad2(d2);
609
+ var r2 = dayMap[key2];
610
+ var v2 = r2 ? dayVal(r2) : 0;
611
+ if (v2 > maxV) maxV = v2;
612
+ if (r2) {
613
+ monthCalls += r2.calls; monthMiss += r2.miss; monthHit += r2.hit; monthOut += r2.out;
614
+ monthPeakCalls += r2.peakCalls || 0; monthOffCalls += r2.offPeakCalls || 0;
615
+ var ds2 = daySplit(r2, regime, splitMap[key2]);
616
+ monthPeak += ds2.peak; monthOff += ds2.off;
617
+ }
618
+ }
619
+ monthCost = monthPeak + monthOff;
620
+
621
+ var weeks = ["一", "二", "三", "四", "五", "六", "日"];
622
+ var cells = [];
623
+ for (var b = 0; b < offset; b++) cells.push(el("div", { key: "b" + b, style: st.calCellBlank }, ""));
624
+ for (var d3 = 1; d3 <= nDays; d3++) {
625
+ var key3 = y + "-" + pad2(m + 1) + "-" + pad2(d3);
626
+ var r3 = dayMap[key3];
627
+ var v3 = r3 ? dayVal(r3) : 0;
628
+ var inten = maxV > 0 ? v3 / maxV : 0;
629
+ var bg = r3 && v3 > 0 ? "rgba(46,134,222," + (0.12 + 0.66 * inten).toFixed(2) + ")" : "rgba(128,128,128,.06)";
630
+ var fg = inten > 0.5 ? "#fff" : "inherit";
631
+ var tip = r3
632
+ ? (function () {
633
+ var ds3 = daySplit(r3, regime, splitMap[key3]);
634
+ return dayLabel(key3) + "\n调用 " + r3.calls + " 次(高峰 " + r3.peakCalls + " / 空闲 " + r3.offPeakCalls + ")\n输入·未命中 " + fmtInt(r3.miss) + " · 缓存命中 " + fmtInt(r3.hit) + " · 输出 " + fmtInt(r3.out) + " · 推理 " + fmtInt(r3.reason) + "\n高峰消耗 " + fmtMoney(ds3.peak) + " · 空闲消耗 " + fmtMoney(ds3.off) + " · 合计 " + fmtMoney(ds3.peak + ds3.off) + "(" + costHint(regime) + ")";
635
+ })()
636
+ : dayLabel(key3) + "\n无记录";
637
+ var isSel = selectedDay === key3;
638
+ cells.push(el("div", {
639
+ key: key3,
640
+ title: tip,
641
+ onClick: (function (k) { return function () { setSelectedDay(selectedDay === k ? null : k); }; })(key3),
642
+ style: Object.assign({ background: bg, color: fg }, st.calCell, isSel ? st.calCellOn : null)
643
+ },
644
+ el("div", null, String(d3)),
645
+ r3 ? el("div", { style: st.calCellVal }, dimMode === "cost" ? fmtMoney(dayCost(r3)) : fmtInt(r3.calls)) : el("div", { style: st.calCellVal }, "—")
646
+ ));
647
+ }
648
+
649
+ // selected day detail (newest first)
650
+ var selRecords = [];
651
+ var selPeakCalls = 0, selOffCalls = 0, selPeakCost = 0, selOffCost = 0;
652
+ if (selectedDay) {
653
+ for (var si = 0; si < records.length; si++) {
654
+ if (bjKey(records[si].time) === selectedDay) selRecords.push(records[si]);
655
+ }
656
+ selRecords.sort(function (a, b) { return b.time - a.time; });
657
+ for (var sj = 0; sj < selRecords.length; sj++) {
658
+ var srec = selRecords[sj];
659
+ var scost = costOf(srec, regime);
660
+ if (srec.peak) { selPeakCalls++; selPeakCost += scost; }
661
+ else { selOffCalls++; selOffCost += scost; }
662
+ }
663
+ }
664
+
665
+ // month daily stats (desc)
666
+ var monthRows = [];
667
+ for (var dd = 1; dd <= nDays; dd++) {
668
+ var kd = y + "-" + pad2(m + 1) + "-" + pad2(dd);
669
+ if (dayMap[kd]) monthRows.push(dayMap[kd]);
670
+ }
671
+ monthRows.sort(function (a, b) { return a.day < b.day ? 1 : a.day > b.day ? -1 : 0; });
672
+
673
+ function prevMonth() { setYm({ y: m === 0 ? y - 1 : y, m: m === 0 ? 11 : m - 1 }); setSelectedDay(null); }
674
+ function nextMonth() { setYm({ y: m === 11 ? y + 1 : y, m: m === 11 ? 0 : m + 1 }); setSelectedDay(null); }
675
+ function goToday() { setYm({ y: nowY, m: nowM }); setSelectedDay(bjKey(Date.now())); }
676
+
677
+ return el("div", null,
678
+ el("div", { style: st.calBar },
679
+ el("button", { style: st.btn, onClick: prevMonth }, "‹ 上月"),
680
+ el("span", { style: { fontSize: fs(14), fontWeight: 600, minWidth: 120, textAlign: "center" } }, y + "年" + (m + 1) + "月"),
681
+ el("button", { style: st.btn, onClick: nextMonth }, "下月 ›"),
682
+ el("button", { style: st.btn, onClick: goToday }, "今天"),
683
+ el("div", { style: st.seg, marginLeft: 8 },
684
+ el("button", { style: dimMode === "cost" ? st.segBtnOn : st.segBtn, onClick: function () { setDim("cost"); } }, "按消耗"),
685
+ el("button", { style: dimMode === "calls" ? st.segBtnOn : st.segBtn, onClick: function () { setDim("calls"); } }, "按调用数")
686
+ )
687
+ ),
688
+ el("div", { style: st.calGrid },
689
+ weeks.map(function (w) { return el("div", { key: w, style: st.calWk }, w); }),
690
+ cells
691
+ ),
692
+ el("div", { style: st.legend },
693
+ el("span", null, "热力:"),
694
+ el("span", { style: st.legendBar }),
695
+ el("span", null, "低 → 高(" + (dimMode === "cost" ? "当日消耗" : "当日调用数") + ")"),
696
+ el("span", null, "· 悬停查看详情,点击某天查看当日调用")
697
+ ),
698
+ el("div", { style: st.cards, marginTop: 12 },
699
+ el(Card, { label: "本月调用", value: fmtInt(monthCalls), hint: "高峰 " + fmtInt(monthPeakCalls) + " · 空闲 " + fmtInt(monthOffCalls) + " · " + y + "年" + (m + 1) + "月" }),
700
+ el(Card, { label: "输入 · 未命中", value: fmtInt(monthMiss), hint: "token" }),
701
+ el(Card, { label: "输入 · 缓存命中", value: fmtInt(monthHit), hint: "token" }),
702
+ el(Card, { label: "输出", value: fmtInt(monthOut), hint: "token" }),
703
+ el(Card, { label: "高峰消耗", value: fmtMoney(monthPeak), hint: "高峰时段 9:00–12:00、14:00–18:00" }),
704
+ el(Card, { label: "空闲消耗", value: fmtMoney(monthOff), hint: "其余空闲时段" }),
705
+ el(Card, { label: "本月消耗", value: fmtMoney(monthCost), hint: "高峰 + 空闲 · " + costHint(regime) })
706
+ ),
707
+ el("div", { style: st.sec }, "每日统计(" + y + "年" + (m + 1) + "月)"),
708
+ monthRows.length === 0
709
+ ? el("div", { style: st.empty }, "本月暂无记录。")
710
+ : el("div", { style: st.scroll },
711
+ el("table", { style: st.tbl },
712
+ el("thead", null, el("tr", null,
713
+ el("th", { style: st.thFirst }, "日期"), el("th", { style: st.th }, "调用"), el("th", { style: st.th }, "高峰/空闲"), el("th", { style: st.th }, "输入·未命中"), el("th", { style: st.th }, "缓存命中"), el("th", { style: st.th }, "输出"), el("th", { style: st.th }, "高峰消耗"), el("th", { style: st.th }, "空闲消耗"), el("th", { style: st.th }, "总消耗(自动)")
714
+ )),
715
+ el("tbody", null, monthRows.map(function (drow) {
716
+ var sel = selectedDay === drow.day;
717
+ var ds4 = daySplit(drow, regime, splitMap[drow.day]);
718
+ return el("tr", { key: drow.day },
719
+ el("td", { style: sel ? st.tdClickSel : st.tdClick, onClick: (function (k) { return function () { setSelectedDay(selectedDay === k ? null : k); }; })(drow.day) }, dayLabel(drow.day)),
720
+ el("td", { style: st.td }, fmtInt(drow.calls)),
721
+ el("td", { style: st.td }, el("span", { style: st.badgePeak }, drow.peakCalls) , " / ", el("span", { style: st.badgeValley }, drow.offPeakCalls)),
722
+ el("td", { style: st.td }, fmtInt(drow.miss)),
723
+ el("td", { style: st.td }, el("span", { style: st.badgeHit }, fmtInt(drow.hit))),
724
+ el("td", { style: st.td }, fmtInt(drow.out)),
725
+ el("td", { style: st.td }, fmtMoney(ds4.peak)),
726
+ el("td", { style: st.td }, fmtMoney(ds4.off)),
727
+ el("td", { style: st.td }, fmtMoney(ds4.peak + ds4.off))
728
+ );
729
+ }))
730
+ )
731
+ ),
732
+ selectedDay
733
+ ? el("div", null,
734
+ el("div", { style: st.selDayTitle }, dayLabel(selectedDay) + " 调用明细"),
735
+ el("div", { style: st.note, marginTop: 6 },
736
+ "高峰 " + selPeakCalls + " 条 · " + fmtMoney(selPeakCost) +
737
+ " | 空闲 " + selOffCalls + " 条 · " + fmtMoney(selOffCost) +
738
+ " | 合计 " + fmtMoney(selPeakCost + selOffCost) + "(" + costHint(regime) + ")"),
739
+ selRecords.length === 0
740
+ ? el("div", { style: st.empty }, "该日无记录。")
741
+ : el("div", { style: st.scroll },
742
+ el("table", { style: st.tbl },
743
+ el("thead", null, el("tr", null,
744
+ el("th", { style: st.thFirst }, "时间(北京)"), el("th", { style: st.thFirst }, "模型"), el("th", { style: st.th }, "输入·未命中"), el("th", { style: st.th }, "缓存命中"), el("th", { style: st.th }, "输出"), el("th", { style: st.th }, "推理"), el("th", { style: st.th }, "时段"), el("th", { style: st.th }, "结束"), el("th", { style: st.th }, "消耗")
745
+ )),
746
+ el("tbody", null, selRecords.map(function (rr) {
747
+ var hit2 = rr.cacheReadTokens || 0, miss2 = rr.inputTokens || 0;
748
+ var cost2 = costOf(rr, regime);
749
+ return el("tr", { key: rr.time },
750
+ el("td", { style: st.tdFirst }, fmtTime(rr.time)),
751
+ el("td", { style: st.tdWrap },
752
+ el("div", null, modelName(rr)),
753
+ el("div", { style: st.sub }, providerName(rr.provider))
754
+ ),
755
+ el("td", { style: st.td }, fmtInt(miss2)),
756
+ el("td", { style: st.td }, el("span", { style: st.badgeHit }, fmtInt(hit2))),
757
+ el("td", { style: st.td }, fmtInt(rr.outputTokens || 0)),
758
+ el("td", { style: st.td }, rr.reasoningTokens ? fmtInt(rr.reasoningTokens) : "—"),
759
+ el("td", { style: st.td }, rr.peak ? el("span", { style: st.badgePeak }, "峰") : el("span", { style: st.badgeValley }, "谷")),
760
+ el("td", { style: st.td }, finishLabel(rr.finishReason)),
761
+ el("td", { style: st.td }, el("span", { style: rr.peak ? st.costPeak : st.costOff }, fmtMoney(cost2)))
762
+ );
763
+ }))
764
+ )
765
+ )
766
+ )
767
+ : null
768
+ );
769
+ }
770
+
771
+ // ── Cache hit list view (newest first + time filters) ──
772
+ function CacheListView(props) {
773
+ var records = props.records, regime = props.regime;
774
+ var presetState = React.useState("all");
775
+ var preset = presetState[0], setPreset = presetState[1];
776
+ var fromState = React.useState("");
777
+ var fromDate = fromState[0], setFromDate = fromState[1];
778
+ var toState = React.useState("");
779
+ var toDate = toState[0], setToDate = toState[1];
780
+ var pageState = React.useState(1);
781
+ var page = pageState[0], setPage = pageState[1];
782
+
783
+ var nowKey = bjKey(Date.now());
784
+ var fromMs = null, toMs = null;
785
+ if (preset === "today") { fromMs = bjStartMs(nowKey); toMs = Date.now(); }
786
+ else if (preset === "7d") { fromMs = bjStartMs(nowKey) - 6 * 86400000; toMs = Date.now(); }
787
+ else if (preset === "30d") { fromMs = bjStartMs(nowKey) - 29 * 86400000; toMs = Date.now(); }
788
+ else if (preset === "custom") {
789
+ if (fromDate) fromMs = bjStartMs(fromDate);
790
+ if (toDate) toMs = bjStartMs(toDate) + 86400000 - 1;
791
+ }
792
+
793
+ var filtered = [];
794
+ for (var i = 0; i < records.length; i++) {
795
+ var r = records[i];
796
+ if (fromMs != null && r.time < fromMs) continue;
797
+ if (toMs != null && r.time > toMs) continue;
798
+ filtered.push(r);
799
+ }
800
+ filtered.sort(function (a, b) { return b.time - a.time; }); // newest first
801
+
802
+ var sumHit = 0, sumMiss = 0, sumOut = 0, sumCost = 0, sumWrite = 0, sumReason = 0;
803
+ for (var s = 0; s < filtered.length; s++) {
804
+ var rr2 = filtered[s];
805
+ sumHit += rr2.cacheReadTokens || 0; sumMiss += rr2.inputTokens || 0; sumOut += rr2.outputTokens || 0;
806
+ sumWrite += rr2.cacheWriteTokens || 0; sumReason += rr2.reasoningTokens || 0;
807
+ sumCost += costOf(rr2, regime);
808
+ }
809
+
810
+ // 分页渲染:只渲染当前页,避免上千行 DOM 导致卡顿
811
+ var PAGE_SIZE = 100;
812
+ var totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
813
+ var curPage = Math.min(Math.max(1, page), totalPages);
814
+ var pageRows = filtered.slice((curPage - 1) * PAGE_SIZE, curPage * PAGE_SIZE);
815
+
816
+ function setCustomFrom(v) { setFromDate(v); setPreset("custom"); setPage(1); }
817
+ function setCustomTo(v) { setToDate(v); setPreset("custom"); setPage(1); }
818
+ function presetBtn(k, label) {
819
+ return el("button", { style: preset === k ? st.segBtnOn : st.segBtn, onClick: function () { setPreset(k); setPage(1); } }, label);
820
+ }
821
+ function pageBtn(kind, label, disabled, onClick) {
822
+ return el("button", { style: disabled ? st.btnDisabled : st.btn, onClick: onClick }, label);
823
+ }
824
+ var sums = splitTotals(filtered, regime);
825
+
826
+ return el("div", null,
827
+ el("div", { style: st.calBar },
828
+ el("div", { style: st.seg },
829
+ presetBtn("today", "今天"), presetBtn("7d", "近7天"), presetBtn("30d", "近30天"), presetBtn("all", "全部")
830
+ ),
831
+ el("span", { style: st.note }, "自定义区间:"),
832
+ el("input", { type: "date", style: st.dateInput, value: fromDate, onChange: function (e) { setCustomFrom(e.target.value); } }),
833
+ el("span", { style: st.note }, "至"),
834
+ el("input", { type: "date", style: st.dateInput, value: toDate, onChange: function (e) { setCustomTo(e.target.value); } }),
835
+ (fromDate || toDate) ? el("button", { style: st.btn, onClick: function () { setFromDate(""); setToDate(""); setPreset("all"); setPage(1); } }, "清除区间") : null
836
+ ),
837
+ el("div", { style: st.note, marginTop: 8 },
838
+ "当前范围:共 " + fmtInt(filtered.length) + " 条 · 输入·未命中 " + fmtInt(sumMiss) + " · 缓存命中 " + fmtInt(sumHit) + " · 缓存写入 " + fmtInt(sumWrite) + " · 输出 " + fmtInt(sumOut) +
839
+ " · 高峰消耗 " + fmtMoney(sums.peak) + " · 空闲消耗 " + fmtMoney(sums.off) + " · 总消耗 " + fmtMoney(sumCost) + "(" + costHint(regime) + ")"
840
+ ),
841
+ filtered.length === 0
842
+ ? el("div", { style: st.empty }, "该时间范围内没有记录。")
843
+ : el("div", null,
844
+ el("div", { style: st.calBar },
845
+ pageBtn("prev", "‹ 上一页", curPage <= 1, function () { setPage(curPage - 1); }),
846
+ el("span", { style: st.note }, "第 " + curPage + " / " + totalPages + " 页 · 每页 " + PAGE_SIZE + " 条 · 共 " + fmtInt(filtered.length) + " 条"),
847
+ pageBtn("next", "下一页 ›", curPage >= totalPages, function () { setPage(curPage + 1); })
848
+ ),
849
+ el("div", { style: st.scroll },
850
+ el("table", { style: st.tbl },
851
+ el("thead", null, el("tr", null,
852
+ el("th", { style: st.thFirst }, "时间(北京)"), el("th", { style: st.thFirst }, "模型"), el("th", { style: st.th }, "输入·未命中"), el("th", { style: st.th }, "缓存命中"), el("th", { style: st.th }, "缓存写入"), el("th", { style: st.th }, "输出"), el("th", { style: st.th }, "推理"), el("th", { style: st.th }, "命中率"), el("th", { style: st.th }, "时段"), el("th", { style: st.th }, "结束"), el("th", { style: st.th }, "消耗")
853
+ )),
854
+ el("tbody", null,
855
+ pageRows.map(function (rr3, idx) {
856
+ var hit3 = rr3.cacheReadTokens || 0, miss3 = rr3.inputTokens || 0;
857
+ var cost3 = costOf(rr3, regime);
858
+ return el("tr", { key: rr3.time + "-" + idx },
859
+ el("td", { style: st.tdFirst }, fmtTime(rr3.time)),
860
+ el("td", { style: st.tdWrap },
861
+ el("div", null, modelName(rr3)),
862
+ el("div", { style: st.sub }, (rr3.provider || "") + (rr3.purpose ? " · " + rr3.purpose : ""))
863
+ ),
864
+ el("td", { style: st.td }, fmtInt(miss3)),
865
+ el("td", { style: st.td }, el("span", { style: st.badgeHit }, fmtInt(hit3))),
866
+ el("td", { style: st.td }, rr3.cacheWriteTokens ? fmtInt(rr3.cacheWriteTokens) : "—"),
867
+ el("td", { style: st.td }, fmtInt(rr3.outputTokens || 0)),
868
+ el("td", { style: st.td }, rr3.reasoningTokens ? fmtInt(rr3.reasoningTokens) : "—"),
869
+ el("td", { style: st.td }, pct(hit3, hit3 + miss3)),
870
+ el("td", { style: st.td }, rr3.peak ? el("span", { style: st.badgePeak }, "峰") : el("span", { style: st.badgeValley }, "谷")),
871
+ el("td", { style: st.td }, finishLabel(rr3.finishReason)),
872
+ el("td", { style: st.td }, el("span", { style: rr3.peak ? st.costPeak : st.costOff }, fmtMoney(cost3)))
873
+ );
874
+ }),
875
+ el("tr", null,
876
+ el("td", { style: st.tdTotalFirst }, "总费用合计"),
877
+ el("td", { style: st.tdTotal }, fmtInt(filtered.length) + " 条"),
878
+ el("td", { style: st.tdTotal }, fmtInt(sumMiss)),
879
+ el("td", { style: st.tdTotal }, fmtInt(sumHit)),
880
+ el("td", { style: st.tdTotal }, fmtInt(sumWrite)),
881
+ el("td", { style: st.tdTotal }, fmtInt(sumOut)),
882
+ el("td", { style: st.tdTotal }, sumReason ? fmtInt(sumReason) : "—"),
883
+ el("td", { style: st.tdTotal }, pct(sumHit, sumHit + sumMiss)),
884
+ el("td", { style: st.tdTotal }, "—"),
885
+ el("td", { style: st.tdTotal }, "—"),
886
+ el("td", { style: st.tdTotal }, fmtMoney(sumCost))
887
+ )
888
+ )
889
+ )
890
+ )
891
+ )
892
+ );
893
+ }
894
+
895
+ // ── Price table view ──
896
+ // 价格表按官方固定,不可编辑。顶部可切换查看「自动 / 峰谷价 / 基础价」三档;
897
+ // 自动档展示当前生效价格(生效前=基础价,生效后=峰谷价)。
898
+ function PriceView(props) {
899
+ var pricing = props.pricing || { base: {}, peakValley: {} };
900
+ var effectiveAt = props.effectiveAt || 0;
901
+ var regimeState = React.useState("auto");
902
+ var regime = regimeState[0], setRegime = regimeState[1];
903
+ var effectiveIn = function () {
904
+ if (!effectiveAt) return "新价格表生效时间未知";
905
+ var now = Date.now();
906
+ return now < effectiveAt ? "新价格表(峰谷价)将于 " + fmtTime(effectiveAt) + "(北京时间)生效,当前按旧价格表(基础价)计费" : "新价格表(峰谷价)已生效(自 " + fmtTime(effectiveAt) + " 起)";
907
+ };
908
+ var showRegime = function (r) {
909
+ if (r === "auto") return Date.now() < effectiveAt ? "base" : "peakValley";
910
+ return r;
911
+ };
912
+ // auto 档:显示当前生效的价格表(生效前=基础价 base,生效后=峰谷价 peakValley)
913
+ var table = pricing && pricing[showRegime(regime)] || {};
914
+ var cell = function (mk, a, b) {
915
+ var row = table && table[mk];
916
+ if (!row) return "—";
917
+ if (showRegime(regime) === "base") return fmtPrice(row[a]);
918
+ var sub = row[a];
919
+ return sub ? fmtPrice(sub[b]) : "—";
920
+ };
921
+ var rows = PRICE_MODELS.map(function (mk) {
922
+ if (showRegime(regime) === "base") {
923
+ return el("tr", { key: mk },
924
+ el("td", { style: st.tdFirst }, modelLabel(mk)),
925
+ el("td", { style: st.td }, el("span", { style: st.badgeHit }, cell(mk, "cacheHit"))),
926
+ el("td", { style: st.td }, cell(mk, "cacheMiss")),
927
+ el("td", { style: st.td }, cell(mk, "output"))
928
+ );
929
+ }
930
+ return el("tr", { key: mk },
931
+ el("td", { style: st.tdFirst }, modelLabel(mk)),
932
+ el("td", { style: st.td }, el("span", { style: st.badgeValley }, cell(mk, "offPeak", "cacheHit"))),
933
+ el("td", { style: st.td }, cell(mk, "offPeak", "cacheMiss")),
934
+ el("td", { style: st.td }, cell(mk, "offPeak", "output")),
935
+ el("td", { style: st.td }, el("span", { style: st.badgePeak }, cell(mk, "peak", "cacheHit"))),
936
+ el("td", { style: st.td }, cell(mk, "peak", "cacheMiss")),
937
+ el("td", { style: st.td }, cell(mk, "peak", "output"))
938
+ );
939
+ });
940
+
941
+ return el("div", null,
942
+ el("div", { style: st.sec }, "DeepSeek 官方 API 价格表"),
943
+ el("div", { style: st.note, marginTop: 6 },
944
+ "本表为 DeepSeek 官方 API 价格(单位:元 / 百万 tokens),仅涵盖官方模型 deepseek-v4-flash 与 deepseek-v4-pro。各模型与 API 服务商的消耗统一按本价格表计费:因各厂商定价数据不完整,不按第三方厂商另行计价,无 DeepSeek 官方价格的模型消耗按 0 统计。价格按官方公布固定,不可编辑。"),
945
+ el("div", { style: st.calBar },
946
+ el("div", { style: st.seg },
947
+ el("button", { style: regime === "auto" ? st.segBtnOn : st.segBtn, onClick: function () { setRegime("auto"); } }, "自动"),
948
+ el("button", { style: regime === "peakValley" ? st.segBtnOn : st.segBtn, onClick: function () { setRegime("peakValley"); } }, "峰谷价"),
949
+ el("button", { style: regime === "base" ? st.segBtnOn : st.segBtn, onClick: function () { setRegime("base"); } }, "基础价")
950
+ )
951
+ ),
952
+ el("div", { style: st.note, marginTop: 6 }, effectiveIn()),
953
+ regime === "auto"
954
+ ? el("div", { style: st.note, marginTop: 6 },
955
+ "自动模式:按调用时间自动选择计费档位——新价格表生效前的调用按基础价(旧价格表),生效后的调用按峰谷价(高峰时段 9:00–12:00、14:00–18:00 用高峰价,其余空闲时段用空闲价)。当前显示:",
956
+ el("strong", null, showRegime(regime) === "base" ? "基础价表(旧价格表,新价格生效前)" : "峰谷价表(新价格表)"),
957
+ "。价格按官方公布固定,不可编辑。"
958
+ )
959
+ : null,
960
+ showRegime(regime) === "base"
961
+ ? el("div", { style: st.scroll },
962
+ el("table", { style: st.tbl },
963
+ el("thead", null, el("tr", null,
964
+ el("th", { style: st.thFirst }, "模型"), el("th", { style: st.th }, "缓存命中(输入)"), el("th", { style: st.th }, "输入 · 未命中"), el("th", { style: st.th }, "输出")
965
+ )),
966
+ el("tbody", null, rows)
967
+ )
968
+ )
969
+ : el("div", { style: st.scroll },
970
+ el("table", { style: st.tbl },
971
+ el("thead", null, el("tr", null,
972
+ el("th", { style: st.thFirst }, "模型"),
973
+ el("th", { style: st.th }, "空闲 · 缓存命中"), el("th", { style: st.th }, "空闲 · 输入"), el("th", { style: st.th }, "空闲 · 输出"),
974
+ el("th", { style: st.th }, "高峰 · 缓存命中"), el("th", { style: st.th }, "高峰 · 输入"), el("th", { style: st.th }, "高峰 · 输出")
975
+ )),
976
+ el("tbody", null, rows)
977
+ )
978
+ ),
979
+ el("div", { style: st.note, marginTop: 8 },
980
+ "单位:元 / 百万 tokens。高峰时段(北京时间 9:00–12:00、14:00–18:00)用高峰价,其余空闲时段用空闲价(空闲价 = 高峰价的一半)。概览、用量日历、缓存命中列表中的消耗均已按高峰 / 空闲分列统计。"
981
+ )
982
+ );
983
+ }
984
+ // ── Usage panel ──
985
+ var SUBTABS = [
986
+ { k: "overview", t: "概览" },
987
+ { k: "calendar", t: "用量日历" },
988
+ { k: "cache", t: "缓存命中列表" },
989
+ { k: "prices", t: "价格表" }
990
+ ];
991
+
992
+ function UsagePanel(props) {
993
+ var state = React.useState({ records: [], count: 0, dataPath: "", persistOk: false, persistError: "", pricing: null, days: [] });
994
+ var data = state[0], setData = state[1];
995
+ var tabState = React.useState("overview");
996
+ var tab = tabState[0], setTab = tabState[1];
997
+ var errState = React.useState("");
998
+ var error = errState[0], setError = errState[1];
999
+ var expState = React.useState("");
1000
+ var exportMsg = expState[0], setExportMsg = expState[1];
1001
+ var impState = React.useState("");
1002
+ var importMsg = impState[0], setImportMsg = impState[1];
1003
+ var destState = React.useState("");
1004
+ var destDir = destState[0], setDestDir = destState[1];
1005
+ var canvasNode = null, fileInputNode = null;
1006
+ var timer = props.timer;
1007
+
1008
+ function refresh() {
1009
+ api({ action: "list" }).then(function (res) {
1010
+ setData(res || { records: [], count: 0, dataPath: "", persistOk: false, persistError: "", pricing: null, days: [] });
1011
+ setError("");
1012
+ }).catch(function (e) { setError(String((e && e.message) || e)); });
1013
+ }
1014
+
1015
+ React.useEffect(function () {
1016
+ refresh();
1017
+ // 每 10 秒自动刷新(列表已分页渲染,避免 3 秒全量重取 + 全量重渲染导致的卡顿)
1018
+ if (timer && timer.interval) return timer.interval(refresh, 10000);
1019
+ return undefined;
1020
+ }, []);
1021
+
1022
+ function showExport(res) {
1023
+ if (res && res.ok) {
1024
+ setExportMsg("已导出:" + res.path);
1025
+ if (res.dir) api({ action: "reveal", dir: res.dir }).catch(function () {});
1026
+ } else {
1027
+ setExportMsg("导出失败:" + ((res && res.error) || "未知错误"));
1028
+ }
1029
+ }
1030
+
1031
+ function doExport(kind) {
1032
+ setExportMsg("导出中…");
1033
+ api({ action: "export", kind: kind, dir: destDir || undefined }).then(showExport).catch(function (e) { setExportMsg("导出失败:" + String((e && e.message) || e)); });
1034
+ }
1035
+
1036
+ function doExportImage() {
1037
+ setExportMsg("生成图片中…");
1038
+ if (!canvasNode) { setExportMsg("画布不可用"); return; }
1039
+ try {
1040
+ drawReport(canvasNode, (data.records || []).slice());
1041
+ var dataUrl = canvasNode.toDataURL("image/png");
1042
+ api({ action: "exportPng", dataUrl: dataUrl, dir: destDir || undefined }).then(showExport).catch(function (e) { setExportMsg("导出失败:" + String((e && e.message) || e)); });
1043
+ } catch (e) {
1044
+ setExportMsg("生成图片失败:" + String((e && e.message) || e));
1045
+ }
1046
+ }
1047
+
1048
+ function pickDestDir() {
1049
+ setExportMsg("打开目录选择…");
1050
+ api({ action: "pickDir" }).then(function (res) {
1051
+ if (res && res.ok) { setDestDir(res.path); setExportMsg("导出目标:" + res.path); }
1052
+ else if (res && res.cancelled) { setExportMsg(""); }
1053
+ else { setExportMsg("选择目录失败:" + ((res && res.error) || "")); }
1054
+ }).catch(function (e) { setExportMsg("选择目录失败:" + String((e && e.message) || e)); });
1055
+ }
1056
+
1057
+ function doReveal() {
1058
+ api({ action: "reveal", dir: destDir || "data" }).then(function (res) {
1059
+ if (!res || !res.ok) setExportMsg("打开文件夹失败:" + ((res && res.error) || ""));
1060
+ }).catch(function (e) { setExportMsg("打开文件夹失败:" + String((e && e.message) || e)); });
1061
+ }
1062
+
1063
+ function doClear() {
1064
+ api({ action: "clear" }).then(refresh).catch(function (e) { setError(String((e && e.message) || e)); });
1065
+ }
1066
+
1067
+ function pickFile() { if (fileInputNode) fileInputNode.click(); }
1068
+
1069
+ function onFileChange(e) {
1070
+ var f = e.target.files && e.target.files[0];
1071
+ if (!f) return;
1072
+ setImportMsg("读取文件…");
1073
+ var readPromise = typeof f.text === "function"
1074
+ ? f.text()
1075
+ : new Promise(function (resolve, reject) { var r = new FileReader(); r.onload = function () { resolve(r.result); }; r.onerror = reject; r.readAsText(f); });
1076
+ readPromise.then(function (content) {
1077
+ try { e.target.value = ""; } catch (err) {}
1078
+ api({ action: "import", content: String(content), filename: f.name }).then(function (res) {
1079
+ if (res && res.ok) {
1080
+ setImportMsg("导入成功:新增 " + res.imported + " 条,跳过重复 " + res.skipped + " 条,忽略无效 " + res.invalid + " 条,现有共 " + res.total + " 条");
1081
+ refresh();
1082
+ } else {
1083
+ setImportMsg("导入失败:" + ((res && res.error) || "未知错误"));
1084
+ }
1085
+ }).catch(function (err) { setImportMsg("导入失败:" + String((err && err.message) || err)); });
1086
+ }).catch(function () { setImportMsg("读取文件失败"); });
1087
+ }
1088
+
1089
+ var records = data.records || [];
1090
+ var dataPath = data.dataPath || "";
1091
+ var nowPeak = isPeakNow(Date.now());
1092
+ var effectiveAt = data.effectiveAt || 0;
1093
+ var nowEffective = effectiveAt ? (Date.now() >= effectiveAt) : true;
1094
+ return el("div", { style: st.root },
1095
+ el("canvas", { ref: function (n) { canvasNode = n; }, style: { display: "none" } }),
1096
+ el("div", { style: st.head },
1097
+ el("div", { style: st.headleft },
1098
+ el("div", null,
1099
+ el("div", { style: st.title }, "用量与消耗"),
1100
+ el("div", { style: st.sub }, "记录插件激活后的每一次模型调用")
1101
+ ),
1102
+ el("span", { style: nowPeak ? st.badgePeak : st.badgeValley }, nowPeak ? "当前 · 高峰时段" : "当前 · 空闲时段"),
1103
+ el("span", { style: nowEffective ? st.badgeHit : st.badgeValley }, nowEffective ? "新价格已生效" : "新价格未生效")
1104
+ ),
1105
+ el("div", { style: st.actions },
1106
+ el("button", { style: st.btn, onClick: refresh }, "刷新"),
1107
+ el("button", { style: st.btn, onClick: doClear }, "清空")
1108
+ )
1109
+ ),
1110
+ el("div", { style: st.subtabBar },
1111
+ SUBTABS.map(function (t) {
1112
+ return el("button", {
1113
+ key: t.k,
1114
+ style: tab === t.k ? st.subtabOn : st.subtab,
1115
+ onClick: function () { setTab(t.k); }
1116
+ }, t.t);
1117
+ })
1118
+ ),
1119
+ el("div", { style: st.note, marginTop: 6 },
1120
+ "计价说明:各模型与 API 服务商的消耗统一按 DeepSeek 官方 API 价格计费(因各厂商定价数据不完整,不按第三方厂商另行计价;无 DeepSeek 官方价格的模型消耗按 0 统计)。"
1121
+ ),
1122
+ tab === "overview" ? el(OverviewView, { records: records, regime: "auto" }) : null,
1123
+ tab === "calendar" ? el(CalendarView, { records: records, regime: "auto", days: data.days || [] }) : null,
1124
+ tab === "cache" ? el(CacheListView, { records: records, regime: "auto" }) : null,
1125
+ tab === "prices" ? el(PriceView, { pricing: data.pricing, effectiveAt: data.effectiveAt, onChanged: refresh }) : null,
1126
+ el("div", { style: st.actions, marginTop: 4 },
1127
+ el("button", { style: st.btn, onClick: function () { doExport("csv"); } }, "导出 CSV"),
1128
+ el("button", { style: st.btn, onClick: function () { doExport("json"); } }, "导出 JSON"),
1129
+ el("button", { style: st.btn, onClick: doExportImage }, "导出图片 (PNG)"),
1130
+ el("button", { style: st.btn, onClick: doReveal }, "打开目录"),
1131
+ exportMsg ? el("span", { style: st.sub }, exportMsg) : null
1132
+ ),
1133
+ el("div", { style: st.actions },
1134
+ el("input", { style: st.input, placeholder: "导出目标目录(留空 = 默认数据目录)", value: destDir, onChange: function (e) { setDestDir(e.target.value); } }),
1135
+ el("button", { style: st.btn, onClick: pickDestDir }, "选择目录…"),
1136
+ destDir ? el("button", { style: st.btn, onClick: function () { setDestDir(""); setExportMsg("已恢复默认数据目录"); } }, "重置") : null
1137
+ ),
1138
+ el("div", { style: st.actions },
1139
+ el("input", { type: "file", accept: ".json,.csv", style: { display: "none" }, ref: function (n) { fileInputNode = n; }, onChange: onFileChange }),
1140
+ el("button", { style: st.btn, onClick: pickFile }, "选择文件导入"),
1141
+ importMsg ? el("span", { style: st.sub }, importMsg) : null
1142
+ ),
1143
+ error ? el("div", { style: st.err }, error) : null,
1144
+ dataPath
1145
+ ? el("div", { style: st.note }, "数据持久化:" + dataPath + "(每次调用实时落盘,插件重启后自动恢复,最多保留 100000 条)")
1146
+ : el("div", { style: st.note, color: "#ff6b6b", opacity: 1 }, "持久化未启用:" + (data.persistError || "未知原因"))
1147
+ );
1148
+ }
1149
+
1150
+ // ── Balance panel ──
1151
+ function BalancePanel() {
1152
+ var state = React.useState({ status: "idle", data: null, error: "" });
1153
+ var s = state[0], setS = state[1];
1154
+
1155
+ function query() {
1156
+ setS({ status: "loading", data: null, error: "" });
1157
+ api({ action: "balance" }).then(function (res) {
1158
+ if (res && res.ok) setS({ status: "done", data: res, error: "" });
1159
+ else setS({ status: "error", data: res, error: (res && res.error) || "查询失败" });
1160
+ }).catch(function (e) {
1161
+ setS({ status: "error", data: null, error: String((e && e.message) || e) });
1162
+ });
1163
+ }
1164
+
1165
+ React.useEffect(function () { query(); }, []);
1166
+ var d = s.data;
1167
+ var info = (d && d.infos && d.infos[0]) || null;
1168
+
1169
+ return el("div", { style: st.root },
1170
+ el("div", { style: st.head },
1171
+ el("div", null,
1172
+ el("div", { style: st.title }, "剩余余额查询"),
1173
+ el("div", { style: st.sub }, "使用当前配置的 DeepSeek API Key 查询账户余额")
1174
+ ),
1175
+ el("div", { style: st.actions },
1176
+ el("button", { style: s.status === "loading" ? st.btnDisabled : st.btn, onClick: query }, s.status === "loading" ? "查询中…" : "查询余额")
1177
+ )
1178
+ ),
1179
+ s.status === "error"
1180
+ ? el("div", { style: st.errbox },
1181
+ el("div", { style: st.errboxTitle }, "查询失败"),
1182
+ el("div", null, s.error),
1183
+ el("div", { style: st.note, marginTop: 6 }, "请确认已在「设置 → 模型」中配置 DEEPSEEK_API_KEY,且网络可访问 api.deepseek.com。")
1184
+ )
1185
+ : null,
1186
+ s.status === "loading" && !info ? el("div", { style: st.empty }, "正在查询余额…") : null,
1187
+ info
1188
+ ? el("div", { style: st.hero },
1189
+ el("div", { style: st.heroLabel }, "账户总余额"),
1190
+ el("div", { style: st.heroValue }, "¥ " + fmtBalance(info.totalBalance)),
1191
+ el("div", { style: st.heroCurrency }, info.currency + (d.isAvailable ? " · 账户可用" : " · 账户不可用")),
1192
+ el("span", { style: d.isAvailable ? st.badgeHit : st.badgePeak }, d.isAvailable ? "可用" : "不可用")
1193
+ )
1194
+ : null,
1195
+ info
1196
+ ? el("div", { style: st.cards },
1197
+ el(Card, { label: "充值余额", value: "¥ " + fmtBalance(info.toppedUpBalance), hint: "实际充值" }),
1198
+ el(Card, { label: "赠送余额", value: "¥ " + fmtBalance(info.grantedBalance), hint: "平台赠送" }),
1199
+ el(Card, { label: "查询时间", value: fmtTime(d.queriedAt), hint: "北京时间" })
1200
+ )
1201
+ : null,
1202
+ el("div", { style: st.note }, "余额数据来自 DeepSeek 官方接口 /user/balance。充值余额与赠送余额合计为账户总余额。")
1203
+ );
1204
+ }
1205
+
1206
+ // ── plugin ──
1207
+ var inject = ["slots"];
1208
+
1209
+ function apply(ctx) {
1210
+ var slots = ctx.get("slots");
1211
+ if (slots === undefined) return;
1212
+ var timer = ctx.get("timer");
1213
+
1214
+ slots.inject("conversation.view", function () {
1215
+ return slots.register(
1216
+ { name: "conversation.view", id: "usage-cost-view", order: 20, label: "用量与消耗" },
1217
+ function () { return el("div", { style: st.tab }, el(UsagePanel, { timer: timer })); }
1218
+ );
1219
+ });
1220
+
1221
+ slots.inject("conversation.view", function () {
1222
+ return slots.register(
1223
+ { name: "conversation.view", id: "balance-view", order: 30, label: "剩余余额查询" },
1224
+ function () { return el("div", { style: st.tab }, el(BalancePanel, null)); }
1225
+ );
1226
+ });
1227
+
1228
+ slots.inject("settings.section", function () {
1229
+ return slots.register(
1230
+ { name: "settings.section", id: "usage-cost", order: 30, label: "用量与消耗" },
1231
+ function () { return el(UsagePanel, { timer: timer }); }
1232
+ );
1233
+ });
1234
+
1235
+ slots.inject("settings.section", function () {
1236
+ return slots.register(
1237
+ { name: "settings.section", id: "balance", order: 31, label: "剩余余额查询" },
1238
+ function () { return el(BalancePanel, null); }
1239
+ );
1240
+ });
1241
+ }
1242
+
1243
+ exports.inject = inject;
1244
+ exports.apply = apply;
1245
+ return module.exports;
1246
+ }
1247
+ });