opencode-tokenwatch 0.4.0 → 0.6.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/README.en.md +32 -38
- package/README.md +33 -39
- package/dist/host/command-actions.d.ts +20 -0
- package/dist/host/runtime.d.ts +7 -0
- package/dist/host/types.d.ts +148 -0
- package/dist/host/v1/adapter.d.ts +9 -0
- package/dist/host/v1/commands.d.ts +9 -0
- package/dist/{queries.d.ts → host/v1/data-source.d.ts} +1 -6
- package/dist/host/v2/adapter.d.ts +63 -0
- package/dist/host/v2/commands.d.ts +8 -0
- package/dist/host/v2/data-source.d.ts +26 -0
- package/dist/kernel/config.d.ts +33 -0
- package/dist/{formatter.d.ts → kernel/format.d.ts} +9 -10
- package/dist/kernel/model.d.ts +19 -0
- package/dist/kernel/perf-aggregate.d.ts +62 -0
- package/dist/{perf-tracker.d.ts → kernel/perf.d.ts} +13 -7
- package/dist/{generate-usage-html.d.ts → kernel/report-html.d.ts} +1 -1
- package/dist/kernel/report.d.ts +19 -0
- package/dist/{stats-store.d.ts → kernel/store.d.ts} +5 -5
- package/dist/server.d.ts +15 -0
- package/dist/server.js +15 -0
- package/dist/tui.d.ts +11 -12
- package/dist/tui.js +4802 -0
- package/dist/ui/sidebar.d.ts +14 -0
- package/index.ts +11 -0
- package/package.json +39 -12
- package/tui.ts +9 -0
- package/dist/commands.d.ts +0 -2
- package/dist/commands.js +0 -208
- package/dist/commands.jsx +0 -381
- package/dist/formatter.js +0 -289
- package/dist/generate-usage-html.js +0 -962
- package/dist/i18n.js +0 -173
- package/dist/index.d.ts +0 -5
- package/dist/index.js +0 -7
- package/dist/perf-tracker.js +0 -299
- package/dist/queries.js +0 -393
- package/dist/sidebar.d.ts +0 -22
- package/dist/sidebar.jsx +0 -604
- package/dist/stats-store.js +0 -258
- package/dist/tui.jsx +0 -178
- /package/dist/{i18n.d.ts → kernel/i18n.d.ts} +0 -0
package/dist/tui.js
ADDED
|
@@ -0,0 +1,4802 @@
|
|
|
1
|
+
// src/host/runtime.tsx
|
|
2
|
+
import { createComponent as _$createComponent2 } from "@opentui/solid";
|
|
3
|
+
import { createSignal as createSignal2 } from "solid-js";
|
|
4
|
+
|
|
5
|
+
// src/ui/sidebar.tsx
|
|
6
|
+
import { createComponent as _$createComponent } from "@opentui/solid";
|
|
7
|
+
import { effect as _$effect } from "@opentui/solid";
|
|
8
|
+
import { createTextNode as _$createTextNode } from "@opentui/solid";
|
|
9
|
+
import { insertNode as _$insertNode } from "@opentui/solid";
|
|
10
|
+
import { insert as _$insert } from "@opentui/solid";
|
|
11
|
+
import { memo as _$memo } from "@opentui/solid";
|
|
12
|
+
import { setProp as _$setProp } from "@opentui/solid";
|
|
13
|
+
import { use as _$use } from "@opentui/solid";
|
|
14
|
+
import { createElement as _$createElement } from "@opentui/solid";
|
|
15
|
+
import { createSignal, createMemo, createEffect, For, Show, onMount, onCleanup } from "solid-js";
|
|
16
|
+
import { RGBA } from "@opentui/core";
|
|
17
|
+
|
|
18
|
+
// src/kernel/format.ts
|
|
19
|
+
function formatTokens(n) {
|
|
20
|
+
if (n >= 1e9) return `${(n / 1e9).toFixed(1)}B`;
|
|
21
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
22
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
|
|
23
|
+
return String(n);
|
|
24
|
+
}
|
|
25
|
+
function formatCost(n) {
|
|
26
|
+
if (n === 0) return "$0.00";
|
|
27
|
+
if (n < 0.01) return `$${n.toFixed(4)}`;
|
|
28
|
+
return `$${n.toFixed(2)}`;
|
|
29
|
+
}
|
|
30
|
+
function formatDuration(ms) {
|
|
31
|
+
if (ms === null) return "\u2014";
|
|
32
|
+
if (ms < 1e3) return `${ms.toFixed(0)}ms`;
|
|
33
|
+
if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
|
|
34
|
+
const m = Math.floor(ms / 6e4);
|
|
35
|
+
const s = Math.floor(ms % 6e4 / 1e3);
|
|
36
|
+
return `${m}m ${s}s`;
|
|
37
|
+
}
|
|
38
|
+
function formatFilters(filters) {
|
|
39
|
+
const parts = [];
|
|
40
|
+
if (filters.sessionId) parts.push(`session=${filters.sessionId}`);
|
|
41
|
+
if (filters.provider) parts.push(`provider=${filters.provider}`);
|
|
42
|
+
if (filters.model) parts.push(`model=${filters.model}`);
|
|
43
|
+
if (filters.startDate || filters.endDate) {
|
|
44
|
+
parts.push(`date=${filters.startDate ?? "..."}..${filters.endDate ?? "..."}`);
|
|
45
|
+
}
|
|
46
|
+
return parts.length ? parts.join(" | ") : "scope=all local sessions";
|
|
47
|
+
}
|
|
48
|
+
function getVisualWidth(str) {
|
|
49
|
+
let width = 0;
|
|
50
|
+
for (let i = 0; i < str.length; i++) {
|
|
51
|
+
width += str.charCodeAt(i) > 255 ? 2 : 1;
|
|
52
|
+
}
|
|
53
|
+
return width;
|
|
54
|
+
}
|
|
55
|
+
function truncateByWidth(str, maxWidth) {
|
|
56
|
+
if (getVisualWidth(str) <= maxWidth) return str;
|
|
57
|
+
let width = 0;
|
|
58
|
+
let res = "";
|
|
59
|
+
for (let i = 0; i < str.length; i++) {
|
|
60
|
+
const charWidth = str.charCodeAt(i) > 255 ? 2 : 1;
|
|
61
|
+
if (width + charWidth > maxWidth - 3) {
|
|
62
|
+
return res + "...";
|
|
63
|
+
}
|
|
64
|
+
width += charWidth;
|
|
65
|
+
res += str[i];
|
|
66
|
+
}
|
|
67
|
+
return res;
|
|
68
|
+
}
|
|
69
|
+
function table(columns, rows, totalRow) {
|
|
70
|
+
const widths = columns.map((col, i) => {
|
|
71
|
+
const rowWidths = rows.map((row) => getVisualWidth(row[i] ?? ""));
|
|
72
|
+
const maxRow = rowWidths.length ? Math.max(...rowWidths) : 0;
|
|
73
|
+
const totalWidth = totalRow ? getVisualWidth(totalRow[i] ?? "") : 0;
|
|
74
|
+
return Math.max(getVisualWidth(col.label), maxRow, totalWidth);
|
|
75
|
+
});
|
|
76
|
+
const renderRow = (cells) => "\u2551" + cells.map((cell, i) => {
|
|
77
|
+
const value = cell ?? "";
|
|
78
|
+
const vWidth = getVisualWidth(value);
|
|
79
|
+
const padding = " ".repeat(widths[i] - vWidth);
|
|
80
|
+
const content = columns[i].align === "right" ? padding + value : value + padding;
|
|
81
|
+
return ` ${content} \u2551`;
|
|
82
|
+
}).join("");
|
|
83
|
+
const sep = (left, mid, right, fill) => left + widths.map((w) => fill.repeat(w + 2)).join(mid) + right;
|
|
84
|
+
const lines = [
|
|
85
|
+
sep("\u2554", "\u2566", "\u2557", "\u2550"),
|
|
86
|
+
renderRow(columns.map((col) => col.label)),
|
|
87
|
+
sep("\u2560", "\u256C", "\u2563", "\u2550"),
|
|
88
|
+
...rows.map(renderRow)
|
|
89
|
+
];
|
|
90
|
+
if (totalRow) {
|
|
91
|
+
lines.push(sep("\u2560", "\u256C", "\u2563", "\u2550"));
|
|
92
|
+
lines.push(renderRow(totalRow));
|
|
93
|
+
}
|
|
94
|
+
lines.push(sep("\u255A", "\u2569", "\u255D", "\u2550"));
|
|
95
|
+
return lines.join("\n");
|
|
96
|
+
}
|
|
97
|
+
function formatSessionSummary(data, title = "Current Session") {
|
|
98
|
+
const modelLabel = data.modelsUsed.length > 1 ? `${data.modelsUsed.length} models` : data.model || "(unknown)";
|
|
99
|
+
return [
|
|
100
|
+
`\u2550\u2550\u2550 ${title} \u2550\u2550\u2550`,
|
|
101
|
+
`Models: ${modelLabel}`,
|
|
102
|
+
`Provider: ${data.provider || "(mixed)"}`,
|
|
103
|
+
`Req: ${data.requestCount}`,
|
|
104
|
+
`Total Tokens: ${formatTokens(data.totalTokens)}`,
|
|
105
|
+
` Input: ${formatTokens(data.inputTokens)}`,
|
|
106
|
+
` Output: ${formatTokens(data.outputTokens)}`,
|
|
107
|
+
` Reasoning: ${formatTokens(data.reasoningTokens)}`,
|
|
108
|
+
` C.Read: ${formatTokens(data.cacheRead)}`,
|
|
109
|
+
` C.Write: ${formatTokens(data.cacheWrite)}`,
|
|
110
|
+
` Cost: ${formatCost(data.totalCost)}`
|
|
111
|
+
].join("\n");
|
|
112
|
+
}
|
|
113
|
+
function formatModelBreakdown(items) {
|
|
114
|
+
if (items.length === 0) return "\u2550\u2550\u2550 Model Breakdown \u2550\u2550\u2550\n(no data)";
|
|
115
|
+
const total = items.reduce((acc, item) => ({
|
|
116
|
+
requests: acc.requests + item.requests,
|
|
117
|
+
totalTokens: acc.totalTokens + item.totalTokens,
|
|
118
|
+
inputTokens: acc.inputTokens + item.inputTokens,
|
|
119
|
+
outputTokens: acc.outputTokens + item.outputTokens,
|
|
120
|
+
cacheRead: acc.cacheRead + item.cacheRead
|
|
121
|
+
}), {
|
|
122
|
+
requests: 0,
|
|
123
|
+
totalTokens: 0,
|
|
124
|
+
inputTokens: 0,
|
|
125
|
+
outputTokens: 0,
|
|
126
|
+
cacheRead: 0
|
|
127
|
+
});
|
|
128
|
+
const rows = items.map((item) => [
|
|
129
|
+
item.provider || "-",
|
|
130
|
+
truncateByWidth(item.model, 20),
|
|
131
|
+
String(item.requests),
|
|
132
|
+
formatTokens(item.totalTokens),
|
|
133
|
+
formatTokens(item.inputTokens),
|
|
134
|
+
formatTokens(item.outputTokens),
|
|
135
|
+
formatTokens(item.cacheRead)
|
|
136
|
+
]);
|
|
137
|
+
return [
|
|
138
|
+
"\u2550\u2550\u2550 Model Breakdown \u2550\u2550\u2550",
|
|
139
|
+
table([
|
|
140
|
+
{ label: "Provider", align: "left" },
|
|
141
|
+
{ label: "Model", align: "left" },
|
|
142
|
+
{ label: "Req", align: "right" },
|
|
143
|
+
{ label: "Total", align: "right" },
|
|
144
|
+
{ label: "In", align: "right" },
|
|
145
|
+
{ label: "Out", align: "right" },
|
|
146
|
+
{ label: "Cache", align: "right" }
|
|
147
|
+
], rows, [
|
|
148
|
+
"TOTAL",
|
|
149
|
+
"",
|
|
150
|
+
String(total.requests),
|
|
151
|
+
formatTokens(total.totalTokens),
|
|
152
|
+
formatTokens(total.inputTokens),
|
|
153
|
+
formatTokens(total.outputTokens),
|
|
154
|
+
formatTokens(total.cacheRead)
|
|
155
|
+
])
|
|
156
|
+
].join("\n");
|
|
157
|
+
}
|
|
158
|
+
function formatProviderBreakdown(items) {
|
|
159
|
+
if (items.length === 0) return "\u2550\u2550\u2550 Provider Breakdown \u2550\u2550\u2550\n(no data)";
|
|
160
|
+
const total = items.reduce((acc, item) => ({
|
|
161
|
+
requests: acc.requests + item.requests,
|
|
162
|
+
totalTokens: acc.totalTokens + item.totalTokens,
|
|
163
|
+
inputTokens: acc.inputTokens + item.inputTokens,
|
|
164
|
+
outputTokens: acc.outputTokens + item.outputTokens,
|
|
165
|
+
cacheRead: acc.cacheRead + item.cacheRead
|
|
166
|
+
}), {
|
|
167
|
+
requests: 0,
|
|
168
|
+
totalTokens: 0,
|
|
169
|
+
inputTokens: 0,
|
|
170
|
+
outputTokens: 0,
|
|
171
|
+
cacheRead: 0
|
|
172
|
+
});
|
|
173
|
+
const rows = items.map((item) => [
|
|
174
|
+
item.provider || "-",
|
|
175
|
+
String(item.requests),
|
|
176
|
+
formatTokens(item.totalTokens),
|
|
177
|
+
formatTokens(item.inputTokens),
|
|
178
|
+
formatTokens(item.outputTokens),
|
|
179
|
+
formatTokens(item.cacheRead)
|
|
180
|
+
]);
|
|
181
|
+
return [
|
|
182
|
+
"\u2550\u2550\u2550 Provider Breakdown \u2550\u2550\u2550",
|
|
183
|
+
table([
|
|
184
|
+
{ label: "Provider", align: "left" },
|
|
185
|
+
{ label: "Req", align: "right" },
|
|
186
|
+
{ label: "Total", align: "right" },
|
|
187
|
+
{ label: "In", align: "right" },
|
|
188
|
+
{ label: "Out", align: "right" },
|
|
189
|
+
{ label: "Cache", align: "right" }
|
|
190
|
+
], rows, [
|
|
191
|
+
"TOTAL",
|
|
192
|
+
String(total.requests),
|
|
193
|
+
formatTokens(total.totalTokens),
|
|
194
|
+
formatTokens(total.inputTokens),
|
|
195
|
+
formatTokens(total.outputTokens),
|
|
196
|
+
formatTokens(total.cacheRead)
|
|
197
|
+
])
|
|
198
|
+
].join("\n");
|
|
199
|
+
}
|
|
200
|
+
function formatDailyBreakdown(items) {
|
|
201
|
+
if (items.length === 0) return "\u2550\u2550\u2550 Daily Breakdown \u2550\u2550\u2550\n(no data)";
|
|
202
|
+
const total = items.reduce((acc, item) => ({
|
|
203
|
+
requests: acc.requests + item.requests,
|
|
204
|
+
totalTokens: acc.totalTokens + item.totalTokens,
|
|
205
|
+
inputTokens: acc.inputTokens + item.inputTokens,
|
|
206
|
+
outputTokens: acc.outputTokens + item.outputTokens,
|
|
207
|
+
cacheRead: acc.cacheRead + item.cacheRead
|
|
208
|
+
}), {
|
|
209
|
+
requests: 0,
|
|
210
|
+
totalTokens: 0,
|
|
211
|
+
inputTokens: 0,
|
|
212
|
+
outputTokens: 0,
|
|
213
|
+
cacheRead: 0
|
|
214
|
+
});
|
|
215
|
+
const rows = items.map((item) => [
|
|
216
|
+
item.day,
|
|
217
|
+
String(item.requests),
|
|
218
|
+
formatTokens(item.totalTokens),
|
|
219
|
+
formatTokens(item.inputTokens),
|
|
220
|
+
formatTokens(item.outputTokens),
|
|
221
|
+
formatTokens(item.cacheRead)
|
|
222
|
+
]);
|
|
223
|
+
return [
|
|
224
|
+
"\u2550\u2550\u2550 Daily Breakdown \u2550\u2550\u2550",
|
|
225
|
+
table([
|
|
226
|
+
{ label: "Day", align: "left" },
|
|
227
|
+
{ label: "Req", align: "right" },
|
|
228
|
+
{ label: "Total", align: "right" },
|
|
229
|
+
{ label: "In", align: "right" },
|
|
230
|
+
{ label: "Out", align: "right" },
|
|
231
|
+
{ label: "Cache", align: "right" }
|
|
232
|
+
], rows, [
|
|
233
|
+
"TOTAL",
|
|
234
|
+
String(total.requests),
|
|
235
|
+
formatTokens(total.totalTokens),
|
|
236
|
+
formatTokens(total.inputTokens),
|
|
237
|
+
formatTokens(total.outputTokens),
|
|
238
|
+
formatTokens(total.cacheRead)
|
|
239
|
+
])
|
|
240
|
+
].join("\n");
|
|
241
|
+
}
|
|
242
|
+
function formatSessionBreakdown(items) {
|
|
243
|
+
if (items.length === 0) return "\u2550\u2550\u2550 Session Breakdown \u2550\u2550\u2550\n(no data)";
|
|
244
|
+
const rows = items.map((item) => {
|
|
245
|
+
const title = truncateByWidth(item.title, 40);
|
|
246
|
+
return [
|
|
247
|
+
item.day,
|
|
248
|
+
item.provider || "-",
|
|
249
|
+
truncateByWidth(item.model, 20),
|
|
250
|
+
String(item.requests),
|
|
251
|
+
formatTokens(item.totalTokens),
|
|
252
|
+
formatTokens(item.cacheRead),
|
|
253
|
+
title
|
|
254
|
+
];
|
|
255
|
+
});
|
|
256
|
+
return [
|
|
257
|
+
"\u2550\u2550\u2550 Session Breakdown \u2550\u2550\u2550",
|
|
258
|
+
table([
|
|
259
|
+
{ label: "Day", align: "left" },
|
|
260
|
+
{ label: "Provider", align: "left" },
|
|
261
|
+
{ label: "Model", align: "left" },
|
|
262
|
+
{ label: "Req", align: "right" },
|
|
263
|
+
{ label: "Total", align: "right" },
|
|
264
|
+
{ label: "Cache", align: "right" },
|
|
265
|
+
{ label: "Title", align: "left" }
|
|
266
|
+
], rows)
|
|
267
|
+
].join("\n");
|
|
268
|
+
}
|
|
269
|
+
function formatUsageReport(report) {
|
|
270
|
+
return [
|
|
271
|
+
`Filters: ${formatFilters(report.filters)}`,
|
|
272
|
+
"",
|
|
273
|
+
formatSessionSummary(report.summary, "Usage Summary"),
|
|
274
|
+
"",
|
|
275
|
+
formatModelBreakdown(report.models),
|
|
276
|
+
"",
|
|
277
|
+
formatProviderBreakdown(report.providers),
|
|
278
|
+
"",
|
|
279
|
+
formatDailyBreakdown(report.daily),
|
|
280
|
+
"",
|
|
281
|
+
formatSessionBreakdown(report.sessions)
|
|
282
|
+
].join("\n");
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// src/kernel/i18n.ts
|
|
286
|
+
var zh = {
|
|
287
|
+
panelTitle: "TokenWatch",
|
|
288
|
+
collapse: "\u6298\u53E0",
|
|
289
|
+
expand: "\u5C55\u5F00",
|
|
290
|
+
sessionSummary: "\u4F1A\u8BDD\u7D2F\u8BA1",
|
|
291
|
+
input: "\u8F93\u5165",
|
|
292
|
+
output: "\u8F93\u51FA",
|
|
293
|
+
cacheRead: "\u7F13\u5B58",
|
|
294
|
+
cacheWrite: "\u7F13\u5B58\u5199",
|
|
295
|
+
cacheMiss: "\u672A\u547D\u4E2D",
|
|
296
|
+
hitRate: "\u547D\u4E2D\u7387",
|
|
297
|
+
requests: "\u8BF7\u6C42",
|
|
298
|
+
cost: "\u6210\u672C",
|
|
299
|
+
trendUp: "\u2191",
|
|
300
|
+
trendDown: "\u2193",
|
|
301
|
+
cache: "\u7F13\u5B58",
|
|
302
|
+
lat: "\u5EF6\u8FDF",
|
|
303
|
+
performance: "\u6027\u80FD",
|
|
304
|
+
pricing: "Pricing",
|
|
305
|
+
tokenDistribution: "Token\u5206\u5E03",
|
|
306
|
+
modelLabel: "\u6A21\u578B",
|
|
307
|
+
provider: "\u63D0\u4F9B\u5546",
|
|
308
|
+
ttft: "TTFT",
|
|
309
|
+
tps: "TPS",
|
|
310
|
+
latency: "\u5EF6\u8FDF",
|
|
311
|
+
avg: "\u5E73\u5747",
|
|
312
|
+
max: "\u6700\u5927",
|
|
313
|
+
min: "\u6700\u5C0F",
|
|
314
|
+
read: "\u8BFB",
|
|
315
|
+
write: "\u5199",
|
|
316
|
+
sessionAccumulated: "Session\u7D2F\u8BA1",
|
|
317
|
+
saving: "\u8282\u7701",
|
|
318
|
+
priceInput: "\u8F93\u5165",
|
|
319
|
+
priceCacheRead: "\u7F13\u5B58\u8BFB",
|
|
320
|
+
priceCacheWrite: "\u7F13\u5B58\u5199",
|
|
321
|
+
priceOutput: "\u8F93\u51FA",
|
|
322
|
+
total: "\u603B\u8BA1",
|
|
323
|
+
system: "\u7CFB\u7EDF\u63D0\u793A",
|
|
324
|
+
user: "\u7528\u6237",
|
|
325
|
+
other: "\u5176\u4ED6",
|
|
326
|
+
toolCall: "Tool\u8C03\u7528",
|
|
327
|
+
toolResult: "Tool\u7ED3\u679C",
|
|
328
|
+
outputTokens: "\u8F93\u51FA",
|
|
329
|
+
showPerformance: "\u663E\u793A\u6027\u80FD\u6307\u6807",
|
|
330
|
+
showPricing: "\u663E\u793A\u6A21\u578B\u5B9A\u4EF7",
|
|
331
|
+
showTokenDistribution: "\u663E\u793AToken\u5206\u5E03",
|
|
332
|
+
showTrend: "\u663E\u793A\u8D8B\u52BF\u6307\u793A\u5668",
|
|
333
|
+
language: "\u8BED\u8A00",
|
|
334
|
+
auto: "\u81EA\u52A8",
|
|
335
|
+
cmdTitleHtml: "HTML\u62A5\u544A",
|
|
336
|
+
cmdDescHtml: "\u751F\u6210\u4EA4\u4E92\u5F0FHTML\u4EEA\u8868\u76D8\uFF0C\u5C55\u793AToken\u7528\u91CF\u3001\u7F13\u5B58\u548C\u6027\u80FD\u56FE\u8868",
|
|
337
|
+
cmdTitleJson: "JSON\u5BFC\u51FA",
|
|
338
|
+
cmdDescJson: "\u5BFC\u51FA\u539F\u59CB\u7528\u91CF\u6570\u636E\u4E3AJSON\u6587\u4EF6",
|
|
339
|
+
cmdTitleText: "\u6587\u672C\u62A5\u544A",
|
|
340
|
+
cmdDescText: "\u751F\u6210\u7EAF\u6587\u672C\u62A5\u544A\u6587\u4EF6",
|
|
341
|
+
cmdTitleSettings: "\u8BBE\u7F6E",
|
|
342
|
+
cmdDescSettings: "\u914D\u7F6E\u4FA7\u8FB9\u680F\u663E\u793A\u9009\u9879",
|
|
343
|
+
descShowPerformance: "\u5728\u4FA7\u8FB9\u680F\u663E\u793ATPS\u3001TTFT\u3001\u5EF6\u8FDF\u7B49\u6307\u6807",
|
|
344
|
+
descShowPricing: "\u5728\u4FA7\u8FB9\u680F\u663E\u793A\u6210\u672C\u4F30\u7B97",
|
|
345
|
+
descShowTokenDistribution: "\u5728\u4FA7\u8FB9\u680F\u663E\u793A\u8F93\u5165/\u8F93\u51FA/\u63A8\u7406Token\u7EC6\u5206",
|
|
346
|
+
descShowTrend: "\u5728\u4FA7\u8FB9\u680F\u663E\u793AToken\u7528\u91CF\u8D8B\u52BF",
|
|
347
|
+
settingsLanguage: "\u8BED\u8A00",
|
|
348
|
+
descSettingsLanguage: "\u5207\u6362\u663E\u793A\u8BED\u8A00",
|
|
349
|
+
settingsTitle: "TokenWatch\u8BBE\u7F6E",
|
|
350
|
+
settingsPlaceholder: "\u5207\u6362\u8BBE\u7F6E\u9879...",
|
|
351
|
+
langAuto: "\u81EA\u52A8",
|
|
352
|
+
done: "\u5B8C\u6210",
|
|
353
|
+
closeSettings: "\u5173\u95ED\u8BBE\u7F6E",
|
|
354
|
+
menuToday: "\u4ECA\u5929",
|
|
355
|
+
menu7d: "\u6700\u8FD1 7 \u5929",
|
|
356
|
+
menu30d: "\u6700\u8FD1 30 \u5929",
|
|
357
|
+
menuAll: "\u5168\u90E8\u65F6\u95F4",
|
|
358
|
+
noticeFirstScan: "\u9996\u6B21\u7EDF\u8BA1\u9700\u904D\u5386\u5168\u90E8\u5386\u53F2\u4F1A\u8BDD\uFF0C\u53EF\u80FD\u8017\u65F6\u6570\u79D2\uFF0C\u8BF7\u7A0D\u5019\u2026",
|
|
359
|
+
noticeSkippedSessions: "TokenWatch\uFF1A{n} \u4E2A\u4F1A\u8BDD\u7684\u6D88\u606F\u52A0\u8F7D\u5931\u8D25\uFF0C\u5DF2\u8DF3\u8FC7\uFF0C\u62A5\u544A\u6570\u5B57\u53EF\u80FD\u504F\u5C0F",
|
|
360
|
+
noticeScanTruncatedSessions: "TokenWatch\uFF1A\u4F1A\u8BDD\u6570\u8D85\u51FA\u626B\u63CF\u4E0A\u9650\uFF0C\u62A5\u544A\u4EC5\u5305\u542B\u6700\u8FD1\u7684\u4F1A\u8BDD",
|
|
361
|
+
noticeScanTruncatedMessages: "TokenWatch\uFF1A{n} \u4E2A\u4F1A\u8BDD\u7684\u6D88\u606F\u6570\u8D85\u51FA\u4E0A\u9650\uFF0C\u5176\u66F4\u65E9\u7684\u6D88\u606F\u672A\u8BA1\u5165",
|
|
362
|
+
toastReportSaved: "\u62A5\u544A\u5DF2\u4FDD\u5B58",
|
|
363
|
+
toastJsonSaved: "JSON \u5DF2\u5BFC\u51FA",
|
|
364
|
+
toastError: "\u51FA\u9519"
|
|
365
|
+
};
|
|
366
|
+
var en = {
|
|
367
|
+
panelTitle: "TokenWatch",
|
|
368
|
+
collapse: "Collapse",
|
|
369
|
+
expand: "Expand",
|
|
370
|
+
sessionSummary: "Session",
|
|
371
|
+
input: "Input",
|
|
372
|
+
output: "Output",
|
|
373
|
+
cacheRead: "Cache",
|
|
374
|
+
cacheWrite: "C.Write",
|
|
375
|
+
cacheMiss: "Cache Miss",
|
|
376
|
+
hitRate: "Hit Rate",
|
|
377
|
+
requests: "Req",
|
|
378
|
+
cost: "Cost",
|
|
379
|
+
trendUp: "\u2191",
|
|
380
|
+
trendDown: "\u2193",
|
|
381
|
+
cache: "Cache",
|
|
382
|
+
lat: "Lat",
|
|
383
|
+
performance: "Performance",
|
|
384
|
+
pricing: "Pricing",
|
|
385
|
+
tokenDistribution: "Token Distribution",
|
|
386
|
+
modelLabel: "Model",
|
|
387
|
+
provider: "Provider",
|
|
388
|
+
ttft: "TTFT",
|
|
389
|
+
tps: "TPS",
|
|
390
|
+
latency: "Latency",
|
|
391
|
+
avg: "Avg",
|
|
392
|
+
max: "Max",
|
|
393
|
+
min: "Min",
|
|
394
|
+
read: "Read",
|
|
395
|
+
write: "Write",
|
|
396
|
+
sessionAccumulated: "Session Accumulated",
|
|
397
|
+
saving: "Saving",
|
|
398
|
+
priceInput: "Input",
|
|
399
|
+
priceCacheRead: "Cache Read",
|
|
400
|
+
priceCacheWrite: "Cache Write",
|
|
401
|
+
priceOutput: "Output",
|
|
402
|
+
total: "Total",
|
|
403
|
+
system: "System",
|
|
404
|
+
user: "User",
|
|
405
|
+
other: "Other",
|
|
406
|
+
toolCall: "Tool Call",
|
|
407
|
+
toolResult: "Tool Result",
|
|
408
|
+
outputTokens: "Output",
|
|
409
|
+
showPerformance: "Show Performance",
|
|
410
|
+
showPricing: "Show Pricing",
|
|
411
|
+
showTokenDistribution: "Show Token Distribution",
|
|
412
|
+
showTrend: "Show Trend",
|
|
413
|
+
language: "Language",
|
|
414
|
+
auto: "Auto",
|
|
415
|
+
cmdTitleHtml: "HTML Report",
|
|
416
|
+
cmdDescHtml: "Generate interactive HTML dashboard with token usage, cache, and performance charts",
|
|
417
|
+
cmdTitleJson: "JSON Export",
|
|
418
|
+
cmdDescJson: "Export raw usage data as JSON file",
|
|
419
|
+
cmdTitleText: "Text Report",
|
|
420
|
+
cmdDescText: "Generate plain text report file",
|
|
421
|
+
cmdTitleSettings: "Settings",
|
|
422
|
+
cmdDescSettings: "Configure sidebar display options",
|
|
423
|
+
descShowPerformance: "Display TPS, TTFT, latency metrics in sidebar",
|
|
424
|
+
descShowPricing: "Display cost estimates in sidebar",
|
|
425
|
+
descShowTokenDistribution: "Display input/output/reasoning token breakdown in sidebar",
|
|
426
|
+
descShowTrend: "Display token usage trend in sidebar",
|
|
427
|
+
settingsLanguage: "Language",
|
|
428
|
+
descSettingsLanguage: "Switch display language",
|
|
429
|
+
settingsTitle: "TokenWatch Settings",
|
|
430
|
+
settingsPlaceholder: "Toggle settings...",
|
|
431
|
+
langAuto: "Auto",
|
|
432
|
+
done: "Done",
|
|
433
|
+
closeSettings: "Close settings",
|
|
434
|
+
menuToday: "Today",
|
|
435
|
+
menu7d: "Last 7 Days",
|
|
436
|
+
menu30d: "Last 30 Days",
|
|
437
|
+
menuAll: "All Time",
|
|
438
|
+
noticeFirstScan: "First run scans all historical sessions, this may take a few seconds\u2026",
|
|
439
|
+
noticeSkippedSessions: "TokenWatch: {n} session(s) failed to load and were skipped; report totals may be understated",
|
|
440
|
+
noticeScanTruncatedSessions: "TokenWatch: session count exceeded the scan cap; only the most recent sessions are included",
|
|
441
|
+
noticeScanTruncatedMessages: "TokenWatch: {n} session(s) exceeded the message cap; their older messages are not included",
|
|
442
|
+
toastReportSaved: "Report saved",
|
|
443
|
+
toastJsonSaved: "JSON exported",
|
|
444
|
+
toastError: "Error"
|
|
445
|
+
};
|
|
446
|
+
var currentLang = detectLanguage();
|
|
447
|
+
function detectLanguage() {
|
|
448
|
+
try {
|
|
449
|
+
const locale = Intl.DateTimeFormat().resolvedOptions().locale;
|
|
450
|
+
if (locale.startsWith("zh")) return "zh";
|
|
451
|
+
} catch {
|
|
452
|
+
}
|
|
453
|
+
return "en";
|
|
454
|
+
}
|
|
455
|
+
function setLanguage(lang) {
|
|
456
|
+
if (lang === "auto") {
|
|
457
|
+
currentLang = detectLanguage();
|
|
458
|
+
} else {
|
|
459
|
+
currentLang = lang;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
function t(key) {
|
|
463
|
+
const table2 = currentLang === "zh" ? zh : en;
|
|
464
|
+
return table2[key] ?? key;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// src/kernel/config.ts
|
|
468
|
+
var DEFAULT_CONFIG = {
|
|
469
|
+
sidebar: { showPerformance: true, showPricing: true, showTokenDistribution: true, showTrend: true },
|
|
470
|
+
language: "auto"
|
|
471
|
+
};
|
|
472
|
+
var KEY_CONFIG = "tokenwatch-config";
|
|
473
|
+
var KEY_VERSION = "tokenwatch-config-version";
|
|
474
|
+
function cloneDefaults() {
|
|
475
|
+
return { sidebar: { ...DEFAULT_CONFIG.sidebar }, language: DEFAULT_CONFIG.language };
|
|
476
|
+
}
|
|
477
|
+
function loadConfig(store, pluginConfig) {
|
|
478
|
+
const base = cloneDefaults();
|
|
479
|
+
try {
|
|
480
|
+
const pluginCfg = pluginConfig?.["opencode-tokenwatch"];
|
|
481
|
+
if (pluginCfg?.sidebar) Object.assign(base.sidebar, pluginCfg.sidebar);
|
|
482
|
+
if (pluginCfg?.language) base.language = pluginCfg.language;
|
|
483
|
+
const overrides = store.get(KEY_CONFIG, void 0);
|
|
484
|
+
if (overrides?.sidebar) Object.assign(base.sidebar, overrides.sidebar);
|
|
485
|
+
if (overrides?.language) base.language = overrides.language;
|
|
486
|
+
} catch {
|
|
487
|
+
}
|
|
488
|
+
return base;
|
|
489
|
+
}
|
|
490
|
+
function saveConfig(store, cfg) {
|
|
491
|
+
store.set(KEY_CONFIG, cfg);
|
|
492
|
+
bumpVersion(store);
|
|
493
|
+
}
|
|
494
|
+
function bumpVersion(store) {
|
|
495
|
+
const v = (store.get(KEY_VERSION, 0) ?? 0) + 1;
|
|
496
|
+
store.set(KEY_VERSION, v);
|
|
497
|
+
}
|
|
498
|
+
function toggleSidebarSetting(store, key) {
|
|
499
|
+
const current = loadConfig(store);
|
|
500
|
+
current.sidebar[key] = !current.sidebar[key];
|
|
501
|
+
saveConfig(store, current);
|
|
502
|
+
return current;
|
|
503
|
+
}
|
|
504
|
+
function setLanguageSetting(store, language) {
|
|
505
|
+
const current = loadConfig(store);
|
|
506
|
+
current.language = language;
|
|
507
|
+
saveConfig(store, current);
|
|
508
|
+
return current;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// src/ui/sidebar.tsx
|
|
512
|
+
function progressBarWidth(percent, width) {
|
|
513
|
+
if (percent >= 100) return width;
|
|
514
|
+
return Math.floor(percent / 100 * width);
|
|
515
|
+
}
|
|
516
|
+
function progressFilled(percent, width) {
|
|
517
|
+
return "\u2588".repeat(Math.max(0, progressBarWidth(percent, width)));
|
|
518
|
+
}
|
|
519
|
+
function progressRemaining(percent, width) {
|
|
520
|
+
return "\u2591".repeat(Math.max(0, width - progressBarWidth(percent, width)));
|
|
521
|
+
}
|
|
522
|
+
function getVisualWidth2(str) {
|
|
523
|
+
let w = 0;
|
|
524
|
+
for (const c of str) {
|
|
525
|
+
const code = c.codePointAt(0) ?? 0;
|
|
526
|
+
if (code >= 19968 && code <= 40959 || code >= 12352 && code <= 12543 || code >= 44032 && code <= 55203 || code >= 4352 && code <= 4607 || code >= 11904 && code <= 12031) {
|
|
527
|
+
w += 2;
|
|
528
|
+
} else {
|
|
529
|
+
w += 1;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return w;
|
|
533
|
+
}
|
|
534
|
+
function centerAlign(text, width) {
|
|
535
|
+
const visualW = getVisualWidth2(text);
|
|
536
|
+
if (visualW >= width) return text;
|
|
537
|
+
const left = Math.floor((width - visualW) / 2);
|
|
538
|
+
const right = width - visualW - left;
|
|
539
|
+
return " ".repeat(left) + text + " ".repeat(right);
|
|
540
|
+
}
|
|
541
|
+
function hitRateColor(rate) {
|
|
542
|
+
if (rate >= 85) return RGBA.fromInts(76, 175, 80, 255);
|
|
543
|
+
if (rate >= 70) return RGBA.fromInts(255, 193, 7, 255);
|
|
544
|
+
return RGBA.fromInts(244, 67, 54, 255);
|
|
545
|
+
}
|
|
546
|
+
function distRoleColor(role) {
|
|
547
|
+
const map = {
|
|
548
|
+
system: RGBA.fromInts(130, 80, 255, 255),
|
|
549
|
+
user: RGBA.fromInts(88, 166, 255, 255),
|
|
550
|
+
toolCall: RGBA.fromInts(210, 153, 34, 255),
|
|
551
|
+
toolResult: RGBA.fromInts(219, 109, 40, 255),
|
|
552
|
+
output: RGBA.fromInts(63, 185, 80, 255),
|
|
553
|
+
other: RGBA.fromInts(72, 79, 88, 255)
|
|
554
|
+
};
|
|
555
|
+
return map[role] ?? RGBA.fromInts(72, 79, 88, 255);
|
|
556
|
+
}
|
|
557
|
+
function estimateTokens(text) {
|
|
558
|
+
if (!text || text.length === 0) return 0;
|
|
559
|
+
let ascii = 0, cjk = 0;
|
|
560
|
+
for (const c of text) {
|
|
561
|
+
const code = c.codePointAt(0) ?? 0;
|
|
562
|
+
if (code >= 19968 && code <= 40959 || code >= 12352 && code <= 12543 || code >= 44032 && code <= 55203 || code >= 4352 && code <= 4607 || code >= 11904 && code <= 12031) cjk++;
|
|
563
|
+
else ascii++;
|
|
564
|
+
}
|
|
565
|
+
const trimmed = text.trimStart();
|
|
566
|
+
const jsonLike = (trimmed.startsWith("{") || trimmed.startsWith("[")) && /"[^"]+"\s*:/.test(text);
|
|
567
|
+
const codeLike = !jsonLike && /```|^import |^export |^function |^const |^let |^var |^class |^interface |^type |^def |^fn |^pub |^use |^mod |^package /m.test(text);
|
|
568
|
+
const asciiPerToken = jsonLike ? 2 : codeLike ? 2.5 : 4;
|
|
569
|
+
return Math.max(1, Math.ceil(ascii / asciiPerToken + cjk / 1.5));
|
|
570
|
+
}
|
|
571
|
+
function loadCollapseState(store) {
|
|
572
|
+
try {
|
|
573
|
+
return store.get("tokenwatch-collapse", {
|
|
574
|
+
global: false,
|
|
575
|
+
models: {},
|
|
576
|
+
subBlocks: {}
|
|
577
|
+
});
|
|
578
|
+
} catch {
|
|
579
|
+
return {
|
|
580
|
+
global: false,
|
|
581
|
+
models: {},
|
|
582
|
+
subBlocks: {}
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
function saveCollapseState(store, state) {
|
|
587
|
+
try {
|
|
588
|
+
store.set("tokenwatch-collapse", state);
|
|
589
|
+
} catch {
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
function TokenWatchPanel(props) {
|
|
593
|
+
const {
|
|
594
|
+
host,
|
|
595
|
+
perfTracker
|
|
596
|
+
} = props;
|
|
597
|
+
const store = host.store;
|
|
598
|
+
const theme = () => host.theme();
|
|
599
|
+
const getMessages = () => props.messages();
|
|
600
|
+
const [config, setConfig] = createSignal(loadConfig(store, host.appConfig()));
|
|
601
|
+
setLanguage(config().language);
|
|
602
|
+
let knownCfgVer = store.get("tokenwatch-config-version", void 0);
|
|
603
|
+
const t2 = (key) => {
|
|
604
|
+
void config().language;
|
|
605
|
+
return t(key);
|
|
606
|
+
};
|
|
607
|
+
const isEnglish = (str) => /^[a-zA-Z\s\.\/]+$/.test(str);
|
|
608
|
+
createEffect(() => {
|
|
609
|
+
const timer = setInterval(() => {
|
|
610
|
+
const v = store.get("tokenwatch-config-version", void 0);
|
|
611
|
+
if (v != null && v !== knownCfgVer) {
|
|
612
|
+
knownCfgVer = v;
|
|
613
|
+
setConfig(loadConfig(store, host.appConfig()));
|
|
614
|
+
}
|
|
615
|
+
}, 500);
|
|
616
|
+
onCleanup(() => clearInterval(timer));
|
|
617
|
+
});
|
|
618
|
+
const [collapse, setCollapse] = createSignal(loadCollapseState(store));
|
|
619
|
+
const [panelWidth, setPanelWidth] = createSignal(38);
|
|
620
|
+
let outerBoxRef = null;
|
|
621
|
+
createEffect(() => setLanguage(config().language));
|
|
622
|
+
const primaryColor = () => theme().primary;
|
|
623
|
+
const mutedColor = () => theme().textMuted;
|
|
624
|
+
const dimColor = () => RGBA.fromInts(72, 79, 88, 255);
|
|
625
|
+
const greenColor = () => RGBA.fromInts(63, 185, 80, 255);
|
|
626
|
+
const borderColor = () => RGBA.fromInts(55, 65, 80, 255);
|
|
627
|
+
const modelStats = createMemo(() => {
|
|
628
|
+
const map = /* @__PURE__ */ new Map();
|
|
629
|
+
const msgs = props.allTokenMessages();
|
|
630
|
+
for (let i = 0; i < msgs.length; i++) {
|
|
631
|
+
const msg = msgs[i];
|
|
632
|
+
const key = `${msg.providerID}/${msg.modelID}`;
|
|
633
|
+
let e = map.get(key);
|
|
634
|
+
if (!e) {
|
|
635
|
+
e = {
|
|
636
|
+
providerID: msg.providerID,
|
|
637
|
+
modelID: msg.modelID,
|
|
638
|
+
totalInput: 0,
|
|
639
|
+
totalOutput: 0,
|
|
640
|
+
totalReasoning: 0,
|
|
641
|
+
cacheRead: 0,
|
|
642
|
+
cacheWrite: 0,
|
|
643
|
+
totalCost: 0,
|
|
644
|
+
requestCount: 0,
|
|
645
|
+
lastMessageIndex: -1
|
|
646
|
+
};
|
|
647
|
+
map.set(key, e);
|
|
648
|
+
}
|
|
649
|
+
e.totalInput += msg.inputTokens;
|
|
650
|
+
e.totalOutput += msg.outputTokens;
|
|
651
|
+
e.totalReasoning += msg.reasoningTokens;
|
|
652
|
+
e.cacheRead += msg.cacheRead;
|
|
653
|
+
e.cacheWrite += msg.cacheWrite;
|
|
654
|
+
e.totalCost += msg.cost;
|
|
655
|
+
e.requestCount++;
|
|
656
|
+
e.lastMessageIndex = i;
|
|
657
|
+
}
|
|
658
|
+
return Array.from(map.entries()).filter(([, s]) => s.totalInput + s.totalOutput + s.totalReasoning + s.cacheRead + s.cacheWrite > 0).sort((a, b) => b[1].lastMessageIndex - a[1].lastMessageIndex);
|
|
659
|
+
});
|
|
660
|
+
const sessionTotals = createMemo(() => {
|
|
661
|
+
let i = 0, o = 0, ir = 0, cr = 0, cw = 0, r = 0, c = 0;
|
|
662
|
+
for (const [, s] of modelStats()) {
|
|
663
|
+
i += s.totalInput;
|
|
664
|
+
o += s.totalOutput;
|
|
665
|
+
ir += s.totalReasoning;
|
|
666
|
+
cr += s.cacheRead;
|
|
667
|
+
cw += s.cacheWrite;
|
|
668
|
+
r += s.requestCount;
|
|
669
|
+
c += s.totalCost;
|
|
670
|
+
}
|
|
671
|
+
return {
|
|
672
|
+
totalInput: i,
|
|
673
|
+
totalOutput: o,
|
|
674
|
+
totalReasoning: ir,
|
|
675
|
+
totalCacheRead: cr,
|
|
676
|
+
totalCacheWrite: cw,
|
|
677
|
+
totalRequests: r,
|
|
678
|
+
totalCost: c,
|
|
679
|
+
totalTokens: i + o + ir + cr + cw
|
|
680
|
+
};
|
|
681
|
+
});
|
|
682
|
+
const globalHitRate = createMemo(() => {
|
|
683
|
+
const denom = sessionTotals().totalInput + sessionTotals().totalCacheRead;
|
|
684
|
+
return denom > 0 ? sessionTotals().totalCacheRead / denom * 100 : -1;
|
|
685
|
+
});
|
|
686
|
+
const modelHitRate = createMemo(() => {
|
|
687
|
+
return modelStats().map(([key, stat]) => {
|
|
688
|
+
const denom = stat.totalInput + stat.cacheRead;
|
|
689
|
+
if (denom === 0) return {
|
|
690
|
+
key,
|
|
691
|
+
rate: 0,
|
|
692
|
+
msgs: []
|
|
693
|
+
};
|
|
694
|
+
const msgs = [];
|
|
695
|
+
for (const msg of props.allTokenMessages()) {
|
|
696
|
+
const pk = `${msg.providerID}/${msg.modelID}`;
|
|
697
|
+
if (pk !== key) continue;
|
|
698
|
+
msgs.push(msg);
|
|
699
|
+
}
|
|
700
|
+
return {
|
|
701
|
+
key,
|
|
702
|
+
rate: stat.cacheRead / denom * 100,
|
|
703
|
+
msgs
|
|
704
|
+
};
|
|
705
|
+
});
|
|
706
|
+
});
|
|
707
|
+
const modelTrend = createMemo(() => {
|
|
708
|
+
return modelHitRate().map(({
|
|
709
|
+
key,
|
|
710
|
+
msgs
|
|
711
|
+
}) => {
|
|
712
|
+
if (msgs.length < 6) return {
|
|
713
|
+
key,
|
|
714
|
+
trend: null
|
|
715
|
+
};
|
|
716
|
+
const sumSlice = (start, end) => {
|
|
717
|
+
let sumCache = 0, sumTotal = 0;
|
|
718
|
+
for (let i = start; i < end && i < msgs.length; i++) {
|
|
719
|
+
sumCache += msgs[i].cacheRead;
|
|
720
|
+
sumTotal += msgs[i].inputTokens + msgs[i].cacheRead;
|
|
721
|
+
}
|
|
722
|
+
return {
|
|
723
|
+
sumCache,
|
|
724
|
+
sumTotal
|
|
725
|
+
};
|
|
726
|
+
};
|
|
727
|
+
const n = msgs.length;
|
|
728
|
+
const recent = sumSlice(n - 3, n);
|
|
729
|
+
const prev = sumSlice(n - 6, n - 3);
|
|
730
|
+
const rateRecent = recent.sumTotal > 0 ? recent.sumCache / recent.sumTotal * 100 : 0;
|
|
731
|
+
const ratePrev = prev.sumTotal > 0 ? prev.sumCache / prev.sumTotal * 100 : 0;
|
|
732
|
+
return {
|
|
733
|
+
key,
|
|
734
|
+
trend: rateRecent - ratePrev
|
|
735
|
+
};
|
|
736
|
+
});
|
|
737
|
+
});
|
|
738
|
+
const [partVersion, setPartVersion] = createSignal(0);
|
|
739
|
+
const perfStats = createMemo(() => {
|
|
740
|
+
props.perfRevision?.();
|
|
741
|
+
void props.allTokenMessages();
|
|
742
|
+
return perfTracker.getSessionStats();
|
|
743
|
+
});
|
|
744
|
+
const tokenDistribution = createMemo(() => {
|
|
745
|
+
void props.allTokenMessages();
|
|
746
|
+
void partVersion();
|
|
747
|
+
const dist = {};
|
|
748
|
+
try {
|
|
749
|
+
const cfg = host.appConfig();
|
|
750
|
+
const agents = cfg?.agent;
|
|
751
|
+
if (agents) {
|
|
752
|
+
for (const ac of Object.values(agents)) {
|
|
753
|
+
const a = ac;
|
|
754
|
+
if (typeof a?.prompt === "string" && a.prompt) {
|
|
755
|
+
dist.system = estimateTokens(a.prompt);
|
|
756
|
+
break;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
} catch {
|
|
761
|
+
}
|
|
762
|
+
for (const msg of getMessages()) {
|
|
763
|
+
const role = msg.type ?? msg.role;
|
|
764
|
+
if (role === "user") {
|
|
765
|
+
if (msg.system) dist.system = (dist.system ?? 0) + estimateTokens(msg.system);
|
|
766
|
+
let parts = [];
|
|
767
|
+
try {
|
|
768
|
+
parts = props.messageParts(msg.id);
|
|
769
|
+
} catch {
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
for (const p of parts) {
|
|
773
|
+
if (p.type === "text" && !p.synthetic && !p.ignored) {
|
|
774
|
+
dist.user = (dist.user ?? 0) + estimateTokens(p.text ?? "");
|
|
775
|
+
} else if (p.type === "file" && p.source?.text?.value) {
|
|
776
|
+
dist.user = (dist.user ?? 0) + estimateTokens(p.source.text.value);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
} else if (role === "assistant") {
|
|
780
|
+
let parts = [];
|
|
781
|
+
try {
|
|
782
|
+
parts = props.messageParts(msg.id);
|
|
783
|
+
} catch {
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
let msgEstimatedOutput = 0;
|
|
787
|
+
for (const p of parts) {
|
|
788
|
+
if (p.type === "tool") {
|
|
789
|
+
let rawInput = "";
|
|
790
|
+
try {
|
|
791
|
+
rawInput = p.state?.raw ?? JSON.stringify(p.state?.input);
|
|
792
|
+
} catch {
|
|
793
|
+
try {
|
|
794
|
+
rawInput = JSON.stringify(p.state);
|
|
795
|
+
} catch {
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
if (rawInput) dist.toolCall = (dist.toolCall ?? 0) + estimateTokens(rawInput);
|
|
799
|
+
if (p.state?.status === "completed" && p.state?.output) {
|
|
800
|
+
dist.toolResult = (dist.toolResult ?? 0) + estimateTokens(p.state.output);
|
|
801
|
+
} else if (p.state?.status === "error" && p.state?.error) {
|
|
802
|
+
dist.toolResult = (dist.toolResult ?? 0) + estimateTokens(p.state.error);
|
|
803
|
+
}
|
|
804
|
+
} else if (p.type === "text" && p.text) {
|
|
805
|
+
msgEstimatedOutput += estimateTokens(p.text);
|
|
806
|
+
} else if (p.type === "reasoning") {
|
|
807
|
+
msgEstimatedOutput += estimateTokens(p.text ?? "");
|
|
808
|
+
} else if (p.type === "subtask") {
|
|
809
|
+
msgEstimatedOutput += estimateTokens(p.prompt || p.description || "");
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
const tokens = msg.tokens;
|
|
813
|
+
if (tokens?.output !== void 0 || tokens?.reasoning !== void 0) {
|
|
814
|
+
dist.output = (dist.output ?? 0) + (tokens?.output ?? 0) + (tokens?.reasoning ?? 0);
|
|
815
|
+
} else {
|
|
816
|
+
dist.output = (dist.output ?? 0) + msgEstimatedOutput;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
const realInput = sessionTotals().totalInput;
|
|
821
|
+
if (realInput > 0) {
|
|
822
|
+
const estimated = (dist.system ?? 0) + (dist.user ?? 0) + (dist.toolCall ?? 0) + (dist.toolResult ?? 0);
|
|
823
|
+
const other = realInput - estimated;
|
|
824
|
+
if (other > 50) dist.other = other;
|
|
825
|
+
}
|
|
826
|
+
return dist;
|
|
827
|
+
});
|
|
828
|
+
const toggle = {
|
|
829
|
+
global: () => setCollapse((p) => {
|
|
830
|
+
const n = {
|
|
831
|
+
...p,
|
|
832
|
+
global: !p.global
|
|
833
|
+
};
|
|
834
|
+
saveCollapseState(store, n);
|
|
835
|
+
return n;
|
|
836
|
+
}),
|
|
837
|
+
model: (k) => setCollapse((p) => {
|
|
838
|
+
const n = {
|
|
839
|
+
...p,
|
|
840
|
+
models: {
|
|
841
|
+
...p.models,
|
|
842
|
+
[k]: !p.models[k]
|
|
843
|
+
}
|
|
844
|
+
};
|
|
845
|
+
saveCollapseState(store, n);
|
|
846
|
+
return n;
|
|
847
|
+
}),
|
|
848
|
+
sub: (k) => setCollapse((p) => {
|
|
849
|
+
const n = {
|
|
850
|
+
...p,
|
|
851
|
+
subBlocks: {
|
|
852
|
+
...p.subBlocks,
|
|
853
|
+
[k]: !p.subBlocks[k]
|
|
854
|
+
}
|
|
855
|
+
};
|
|
856
|
+
saveCollapseState(store, n);
|
|
857
|
+
return n;
|
|
858
|
+
})
|
|
859
|
+
};
|
|
860
|
+
onMount(() => {
|
|
861
|
+
let lastBump = 0;
|
|
862
|
+
let pendingTimer = null;
|
|
863
|
+
const bump = () => {
|
|
864
|
+
lastBump = Date.now();
|
|
865
|
+
setPartVersion((v) => v + 1);
|
|
866
|
+
};
|
|
867
|
+
const unsubPart = host.onPartUpdated(() => {
|
|
868
|
+
const elapsed = Date.now() - lastBump;
|
|
869
|
+
if (elapsed >= 500) {
|
|
870
|
+
bump();
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
if (!pendingTimer) {
|
|
874
|
+
pendingTimer = setTimeout(() => {
|
|
875
|
+
pendingTimer = null;
|
|
876
|
+
bump();
|
|
877
|
+
}, 500 - elapsed);
|
|
878
|
+
}
|
|
879
|
+
});
|
|
880
|
+
onCleanup(() => {
|
|
881
|
+
if (pendingTimer) clearTimeout(pendingTimer);
|
|
882
|
+
try {
|
|
883
|
+
unsubPart?.();
|
|
884
|
+
} catch {
|
|
885
|
+
}
|
|
886
|
+
});
|
|
887
|
+
});
|
|
888
|
+
const innerWidth = () => panelWidth() - 2;
|
|
889
|
+
const barWidth = () => Math.max(8, innerWidth() - 19);
|
|
890
|
+
const divider = () => {
|
|
891
|
+
const w = innerWidth();
|
|
892
|
+
if (w <= 2) return "\u2500".repeat(w);
|
|
893
|
+
return " " + "\u2500".repeat(w - 2) + " ";
|
|
894
|
+
};
|
|
895
|
+
return (() => {
|
|
896
|
+
var _el$ = _$createElement("box"), _el$2 = _$createElement("box"), _el$3 = _$createElement("text"), _el$4 = _$createTextNode(` `), _el$5 = _$createElement("text");
|
|
897
|
+
_$insertNode(_el$, _el$2);
|
|
898
|
+
_$use((el) => {
|
|
899
|
+
outerBoxRef = el;
|
|
900
|
+
}, _el$);
|
|
901
|
+
_$setProp(_el$, "onSizeChange", () => {
|
|
902
|
+
if (outerBoxRef) setPanelWidth(outerBoxRef.width);
|
|
903
|
+
});
|
|
904
|
+
_$setProp(_el$, "flexDirection", "column");
|
|
905
|
+
_$setProp(_el$, "border", true);
|
|
906
|
+
_$setProp(_el$, "borderStyle", "rounded");
|
|
907
|
+
_$insertNode(_el$2, _el$3);
|
|
908
|
+
_$insertNode(_el$2, _el$5);
|
|
909
|
+
_$setProp(_el$2, "flexDirection", "row");
|
|
910
|
+
_$setProp(_el$2, "justifyContent", "space-between");
|
|
911
|
+
_$setProp(_el$2, "paddingX", 1);
|
|
912
|
+
_$insertNode(_el$3, _el$4);
|
|
913
|
+
_$insert(_el$3, () => collapse().global ? "\u25B6" : "\u25BE", _el$4);
|
|
914
|
+
_$insert(_el$3, () => t2("panelTitle"), null);
|
|
915
|
+
_$insert(_el$5, (() => {
|
|
916
|
+
var _c$ = _$memo(() => !!collapse().global);
|
|
917
|
+
return () => _c$() ? [_$memo(() => formatTokens(sessionTotals().totalTokens)), _$memo(() => _$memo(() => globalHitRate() >= 0)() ? (() => {
|
|
918
|
+
var _el$17 = _$createElement("span");
|
|
919
|
+
_$insert(_el$17, () => ` (${globalHitRate().toFixed(1)}% hit)`);
|
|
920
|
+
_$effect((_$p) => _$setProp(_el$17, "style", {
|
|
921
|
+
fg: hitRateColor(globalHitRate())
|
|
922
|
+
}, _$p));
|
|
923
|
+
return _el$17;
|
|
924
|
+
})() : "")] : _$memo(() => globalHitRate() >= 0)() ? (() => {
|
|
925
|
+
var _el$18 = _$createElement("span");
|
|
926
|
+
_$insert(_el$18, () => `${globalHitRate().toFixed(1)}% hit`);
|
|
927
|
+
_$effect((_$p) => _$setProp(_el$18, "style", {
|
|
928
|
+
fg: hitRateColor(globalHitRate())
|
|
929
|
+
}, _$p));
|
|
930
|
+
return _el$18;
|
|
931
|
+
})() : "";
|
|
932
|
+
})());
|
|
933
|
+
_$insert(_el$, _$createComponent(Show, {
|
|
934
|
+
get when() {
|
|
935
|
+
return !collapse().global;
|
|
936
|
+
},
|
|
937
|
+
get children() {
|
|
938
|
+
return [(() => {
|
|
939
|
+
var _el$6 = _$createElement("text");
|
|
940
|
+
_$insert(_el$6, divider);
|
|
941
|
+
_$effect((_$p) => _$setProp(_el$6, "fg", borderColor(), _$p));
|
|
942
|
+
return _el$6;
|
|
943
|
+
})(), (() => {
|
|
944
|
+
var _el$7 = _$createElement("box");
|
|
945
|
+
_$setProp(_el$7, "flexDirection", "row");
|
|
946
|
+
_$setProp(_el$7, "paddingX", 1);
|
|
947
|
+
_$insert(_el$7, _$createComponent(For, {
|
|
948
|
+
get each() {
|
|
949
|
+
return [{
|
|
950
|
+
val: formatTokens(sessionTotals().totalTokens),
|
|
951
|
+
lbl: t2("total")
|
|
952
|
+
}, {
|
|
953
|
+
val: sessionTotals().totalRequests.toString(),
|
|
954
|
+
lbl: t2("requests")
|
|
955
|
+
}, {
|
|
956
|
+
val: formatTokens(sessionTotals().totalInput),
|
|
957
|
+
lbl: t2("input")
|
|
958
|
+
}, {
|
|
959
|
+
val: formatTokens(sessionTotals().totalOutput),
|
|
960
|
+
lbl: t2("output")
|
|
961
|
+
}];
|
|
962
|
+
},
|
|
963
|
+
children: (item, idx) => {
|
|
964
|
+
const colW = () => {
|
|
965
|
+
const totalW = panelWidth() - 4;
|
|
966
|
+
const base = Math.floor(totalW / 4);
|
|
967
|
+
return idx() === 3 ? totalW - base * 3 : base;
|
|
968
|
+
};
|
|
969
|
+
return (() => {
|
|
970
|
+
var _el$19 = _$createElement("box"), _el$20 = _$createElement("text"), _el$21 = _$createElement("text");
|
|
971
|
+
_$insertNode(_el$19, _el$20);
|
|
972
|
+
_$insertNode(_el$19, _el$21);
|
|
973
|
+
_$setProp(_el$19, "flexDirection", "column");
|
|
974
|
+
_$insert(_el$20, () => centerAlign(item.val, colW()));
|
|
975
|
+
_$insert(_el$21, () => centerAlign(isEnglish(item.lbl) ? item.lbl.toUpperCase() : item.lbl, colW()));
|
|
976
|
+
_$effect((_p$) => {
|
|
977
|
+
var _v$9 = colW(), _v$0 = primaryColor(), _v$1 = dimColor();
|
|
978
|
+
_v$9 !== _p$.e && (_p$.e = _$setProp(_el$19, "width", _v$9, _p$.e));
|
|
979
|
+
_v$0 !== _p$.t && (_p$.t = _$setProp(_el$20, "fg", _v$0, _p$.t));
|
|
980
|
+
_v$1 !== _p$.a && (_p$.a = _$setProp(_el$21, "fg", _v$1, _p$.a));
|
|
981
|
+
return _p$;
|
|
982
|
+
}, {
|
|
983
|
+
e: void 0,
|
|
984
|
+
t: void 0,
|
|
985
|
+
a: void 0
|
|
986
|
+
});
|
|
987
|
+
return _el$19;
|
|
988
|
+
})();
|
|
989
|
+
}
|
|
990
|
+
}));
|
|
991
|
+
return _el$7;
|
|
992
|
+
})(), _$createComponent(Show, {
|
|
993
|
+
get when() {
|
|
994
|
+
return _$memo(() => !!config().sidebar.showPricing)() && sessionTotals().totalCost > 0;
|
|
995
|
+
},
|
|
996
|
+
get children() {
|
|
997
|
+
var _el$8 = _$createElement("box"), _el$9 = _$createElement("text"), _el$0 = _$createTextNode(`: `), _el$10 = _$createElement("span");
|
|
998
|
+
_$insertNode(_el$8, _el$9);
|
|
999
|
+
_$setProp(_el$8, "flexDirection", "row");
|
|
1000
|
+
_$setProp(_el$8, "justifyContent", "center");
|
|
1001
|
+
_$setProp(_el$8, "marginTop", 1);
|
|
1002
|
+
_$insertNode(_el$9, _el$0);
|
|
1003
|
+
_$insertNode(_el$9, _el$10);
|
|
1004
|
+
_$insert(_el$9, () => t2("cost"), _el$0);
|
|
1005
|
+
_$insert(_el$10, () => formatCost(sessionTotals().totalCost));
|
|
1006
|
+
_$effect((_p$) => {
|
|
1007
|
+
var _v$ = mutedColor(), _v$2 = {
|
|
1008
|
+
fg: greenColor()
|
|
1009
|
+
};
|
|
1010
|
+
_v$ !== _p$.e && (_p$.e = _$setProp(_el$9, "fg", _v$, _p$.e));
|
|
1011
|
+
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$10, "style", _v$2, _p$.t));
|
|
1012
|
+
return _p$;
|
|
1013
|
+
}, {
|
|
1014
|
+
e: void 0,
|
|
1015
|
+
t: void 0
|
|
1016
|
+
});
|
|
1017
|
+
return _el$8;
|
|
1018
|
+
}
|
|
1019
|
+
}), _$createComponent(For, {
|
|
1020
|
+
get each() {
|
|
1021
|
+
return modelStats();
|
|
1022
|
+
},
|
|
1023
|
+
children: ([key, stat]) => {
|
|
1024
|
+
const isExpanded = () => collapse().models[key] !== true;
|
|
1025
|
+
const hitDenom = stat.totalInput + stat.cacheRead;
|
|
1026
|
+
const hitRate = hitDenom > 0 ? stat.cacheRead / hitDenom * 100 : 0;
|
|
1027
|
+
const trendStr = () => {
|
|
1028
|
+
if (!config().sidebar.showTrend) return "";
|
|
1029
|
+
const td = modelTrend().find((h) => h.key === key);
|
|
1030
|
+
if (!td?.trend || td.trend === 0) return "";
|
|
1031
|
+
return td.trend > 0 ? ` ${t2("trendUp")}${td.trend.toFixed(1)}%` : ` ${t2("trendDown")}${Math.abs(td.trend).toFixed(1)}%`;
|
|
1032
|
+
};
|
|
1033
|
+
const trendColor = () => {
|
|
1034
|
+
const td = modelTrend().find((h) => h.key === key);
|
|
1035
|
+
return (td?.trend ?? 0) >= 0 ? RGBA.fromInts(63, 185, 80, 255) : RGBA.fromInts(244, 67, 54, 255);
|
|
1036
|
+
};
|
|
1037
|
+
const MAX_PROVIDER_LEN = 12;
|
|
1038
|
+
let providerDisplay = stat.providerID;
|
|
1039
|
+
if (providerDisplay.length > MAX_PROVIDER_LEN) {
|
|
1040
|
+
providerDisplay = providerDisplay.slice(0, MAX_PROVIDER_LEN - 1) + "\u2026";
|
|
1041
|
+
}
|
|
1042
|
+
let fullTitle = `${providerDisplay}/${stat.modelID}`;
|
|
1043
|
+
if (fullTitle.length > 22) {
|
|
1044
|
+
const parts = fullTitle.split("/");
|
|
1045
|
+
if (parts.length >= 3) {
|
|
1046
|
+
fullTitle = `${parts[0]}/${parts[parts.length - 1]}`;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
const maxNameLen = Math.max(8, innerWidth() - 12);
|
|
1050
|
+
const shortTitle = fullTitle.length > maxNameLen ? fullTitle.slice(0, maxNameLen - 1) + "\u2026" : fullTitle;
|
|
1051
|
+
const modelHeaderRight = () => {
|
|
1052
|
+
if (!isExpanded()) {
|
|
1053
|
+
const total = stat.totalInput + stat.totalOutput + stat.totalReasoning + stat.cacheRead + stat.cacheWrite;
|
|
1054
|
+
return `${formatTokens(total)} \u25B6`;
|
|
1055
|
+
}
|
|
1056
|
+
return `\xD7${stat.requestCount} \u25BE`;
|
|
1057
|
+
};
|
|
1058
|
+
const modelTotalTokens = stat.totalInput + stat.totalOutput + stat.totalReasoning + stat.cacheRead + stat.cacheWrite;
|
|
1059
|
+
const targetW = () => {
|
|
1060
|
+
const cacheLabel = t2("cache") + ":";
|
|
1061
|
+
const costLabel = t2("cost") + ":";
|
|
1062
|
+
return Math.max(getVisualWidth2(cacheLabel), getVisualWidth2(costLabel));
|
|
1063
|
+
};
|
|
1064
|
+
const paddedCachePrefix = () => {
|
|
1065
|
+
const label = t2("cache") + ":";
|
|
1066
|
+
return label + " ".repeat(targetW() - getVisualWidth2(label));
|
|
1067
|
+
};
|
|
1068
|
+
const paddedCostPrefix = () => {
|
|
1069
|
+
const label = t2("cost") + ":";
|
|
1070
|
+
return label + " ".repeat(targetW() - getVisualWidth2(label));
|
|
1071
|
+
};
|
|
1072
|
+
const modelBarWidth = () => Math.max(8, panelWidth() - 4 - targetW() - 11);
|
|
1073
|
+
return (
|
|
1074
|
+
// marginTop=1 提供模型间视觉间距(TUI最小单位为1行)
|
|
1075
|
+
(() => {
|
|
1076
|
+
var _el$22 = _$createElement("box"), _el$23 = _$createElement("box"), _el$24 = _$createElement("text"), _el$25 = _$createElement("span"), _el$27 = _$createTextNode(` `), _el$28 = _$createElement("span"), _el$29 = _$createElement("text");
|
|
1077
|
+
_$insertNode(_el$22, _el$23);
|
|
1078
|
+
_$setProp(_el$22, "flexDirection", "column");
|
|
1079
|
+
_$setProp(_el$22, "marginTop", 1);
|
|
1080
|
+
_$insertNode(_el$23, _el$24);
|
|
1081
|
+
_$insertNode(_el$23, _el$29);
|
|
1082
|
+
_$setProp(_el$23, "flexDirection", "row");
|
|
1083
|
+
_$setProp(_el$23, "justifyContent", "space-between");
|
|
1084
|
+
_$setProp(_el$23, "onMouseDown", () => toggle.model(key));
|
|
1085
|
+
_$setProp(_el$23, "paddingX", 1);
|
|
1086
|
+
_$insertNode(_el$24, _el$25);
|
|
1087
|
+
_$insertNode(_el$24, _el$27);
|
|
1088
|
+
_$insertNode(_el$24, _el$28);
|
|
1089
|
+
_$insertNode(_el$25, _$createTextNode(`\u25CF`));
|
|
1090
|
+
_$insert(_el$28, shortTitle);
|
|
1091
|
+
_$insert(_el$29, modelHeaderRight);
|
|
1092
|
+
_$insert(_el$22, _$createComponent(Show, {
|
|
1093
|
+
get when() {
|
|
1094
|
+
return isExpanded();
|
|
1095
|
+
},
|
|
1096
|
+
get children() {
|
|
1097
|
+
var _el$30 = _$createElement("box"), _el$31 = _$createElement("box"), _el$32 = _$createElement("box"), _el$33 = _$createElement("text"), _el$34 = _$createElement("span"), _el$35 = _$createTextNode(` `), _el$36 = _$createTextNode(`%`);
|
|
1098
|
+
_$insertNode(_el$30, _el$31);
|
|
1099
|
+
_$insertNode(_el$30, _el$33);
|
|
1100
|
+
_$setProp(_el$30, "flexDirection", "column");
|
|
1101
|
+
_$setProp(_el$30, "paddingX", 1);
|
|
1102
|
+
_$insertNode(_el$31, _el$32);
|
|
1103
|
+
_$setProp(_el$31, "flexDirection", "column");
|
|
1104
|
+
_$setProp(_el$31, "border", true);
|
|
1105
|
+
_$setProp(_el$31, "borderStyle", "rounded");
|
|
1106
|
+
_$setProp(_el$32, "flexDirection", "row");
|
|
1107
|
+
_$insert(_el$32, _$createComponent(For, {
|
|
1108
|
+
get each() {
|
|
1109
|
+
return [{
|
|
1110
|
+
val: formatTokens(modelTotalTokens),
|
|
1111
|
+
lbl: t2("total")
|
|
1112
|
+
}, {
|
|
1113
|
+
val: formatTokens(stat.totalInput),
|
|
1114
|
+
lbl: t2("input")
|
|
1115
|
+
}, {
|
|
1116
|
+
val: formatTokens(stat.totalOutput),
|
|
1117
|
+
lbl: t2("output")
|
|
1118
|
+
}];
|
|
1119
|
+
},
|
|
1120
|
+
children: (item, idx) => {
|
|
1121
|
+
const colW = () => {
|
|
1122
|
+
const totalW = panelWidth() - 6;
|
|
1123
|
+
const base = Math.floor(totalW / 3);
|
|
1124
|
+
return idx() === 2 ? totalW - base * 2 : base;
|
|
1125
|
+
};
|
|
1126
|
+
return (() => {
|
|
1127
|
+
var _el$46 = _$createElement("box"), _el$47 = _$createElement("text"), _el$48 = _$createElement("text");
|
|
1128
|
+
_$insertNode(_el$46, _el$47);
|
|
1129
|
+
_$insertNode(_el$46, _el$48);
|
|
1130
|
+
_$setProp(_el$46, "flexDirection", "column");
|
|
1131
|
+
_$insert(_el$47, () => centerAlign(item.val, colW()));
|
|
1132
|
+
_$insert(_el$48, () => centerAlign(isEnglish(item.lbl) ? item.lbl.toUpperCase() : item.lbl, colW()));
|
|
1133
|
+
_$effect((_p$) => {
|
|
1134
|
+
var _v$20 = colW(), _v$21 = primaryColor(), _v$22 = dimColor();
|
|
1135
|
+
_v$20 !== _p$.e && (_p$.e = _$setProp(_el$46, "width", _v$20, _p$.e));
|
|
1136
|
+
_v$21 !== _p$.t && (_p$.t = _$setProp(_el$47, "fg", _v$21, _p$.t));
|
|
1137
|
+
_v$22 !== _p$.a && (_p$.a = _$setProp(_el$48, "fg", _v$22, _p$.a));
|
|
1138
|
+
return _p$;
|
|
1139
|
+
}, {
|
|
1140
|
+
e: void 0,
|
|
1141
|
+
t: void 0,
|
|
1142
|
+
a: void 0
|
|
1143
|
+
});
|
|
1144
|
+
return _el$46;
|
|
1145
|
+
})();
|
|
1146
|
+
}
|
|
1147
|
+
}));
|
|
1148
|
+
_$insertNode(_el$33, _el$34);
|
|
1149
|
+
_$insert(_el$33, paddedCachePrefix, _el$34);
|
|
1150
|
+
_$insertNode(_el$34, _el$35);
|
|
1151
|
+
_$insertNode(_el$34, _el$36);
|
|
1152
|
+
_$insert(_el$34, () => progressFilled(hitRate, modelBarWidth()), _el$35);
|
|
1153
|
+
_$insert(_el$34, () => progressRemaining(hitRate, modelBarWidth()), _el$35);
|
|
1154
|
+
_$insert(_el$34, () => hitRate.toFixed(0), _el$36);
|
|
1155
|
+
_$insert(_el$33, (() => {
|
|
1156
|
+
var _c$2 = _$memo(() => !!trendStr());
|
|
1157
|
+
return () => _c$2() ? (() => {
|
|
1158
|
+
var _el$49 = _$createElement("span");
|
|
1159
|
+
_$insert(_el$49, trendStr);
|
|
1160
|
+
_$effect((_$p) => _$setProp(_el$49, "style", {
|
|
1161
|
+
fg: trendColor()
|
|
1162
|
+
}, _$p));
|
|
1163
|
+
return _el$49;
|
|
1164
|
+
})() : null;
|
|
1165
|
+
})(), null);
|
|
1166
|
+
_$insert(_el$30, _$createComponent(Show, {
|
|
1167
|
+
get when() {
|
|
1168
|
+
return _$memo(() => !!config().sidebar.showPerformance)() && !!perfStats().models[key];
|
|
1169
|
+
},
|
|
1170
|
+
get children() {
|
|
1171
|
+
var _el$37 = _$createElement("text"), _el$38 = _$createTextNode(`~`), _el$39 = _$createTextNode(` `), _el$40 = _$createElement("span"), _el$41 = _$createTextNode(` ~`), _el$43 = _$createTextNode(` `), _el$44 = _$createElement("span");
|
|
1172
|
+
_$insertNode(_el$37, _el$38);
|
|
1173
|
+
_$insertNode(_el$37, _el$39);
|
|
1174
|
+
_$insertNode(_el$37, _el$40);
|
|
1175
|
+
_$insertNode(_el$37, _el$41);
|
|
1176
|
+
_$insertNode(_el$37, _el$43);
|
|
1177
|
+
_$insertNode(_el$37, _el$44);
|
|
1178
|
+
_$setProp(_el$37, "marginTop", 1);
|
|
1179
|
+
_$insert(_el$37, () => t2("ttft"), _el$39);
|
|
1180
|
+
_$insert(_el$40, () => formatDuration(perfStats().models[key]?.avgTTFT ?? null));
|
|
1181
|
+
_$insert(_el$37, () => t2("tps"), _el$43);
|
|
1182
|
+
_$insert(_el$44, () => perfStats().models[key]?.avgTPS?.toFixed(1) ?? "\u2014");
|
|
1183
|
+
_$effect((_p$) => {
|
|
1184
|
+
var _v$10 = mutedColor(), _v$11 = {
|
|
1185
|
+
fg: primaryColor()
|
|
1186
|
+
}, _v$12 = {
|
|
1187
|
+
fg: primaryColor()
|
|
1188
|
+
};
|
|
1189
|
+
_v$10 !== _p$.e && (_p$.e = _$setProp(_el$37, "fg", _v$10, _p$.e));
|
|
1190
|
+
_v$11 !== _p$.t && (_p$.t = _$setProp(_el$40, "style", _v$11, _p$.t));
|
|
1191
|
+
_v$12 !== _p$.a && (_p$.a = _$setProp(_el$44, "style", _v$12, _p$.a));
|
|
1192
|
+
return _p$;
|
|
1193
|
+
}, {
|
|
1194
|
+
e: void 0,
|
|
1195
|
+
t: void 0,
|
|
1196
|
+
a: void 0
|
|
1197
|
+
});
|
|
1198
|
+
return _el$37;
|
|
1199
|
+
}
|
|
1200
|
+
}), null);
|
|
1201
|
+
_$insert(_el$30, _$createComponent(Show, {
|
|
1202
|
+
get when() {
|
|
1203
|
+
return _$memo(() => !!config().sidebar.showPricing)() && stat.totalCost > 0;
|
|
1204
|
+
},
|
|
1205
|
+
get children() {
|
|
1206
|
+
var _el$45 = _$createElement("text");
|
|
1207
|
+
_$insert(_el$45, paddedCostPrefix, null);
|
|
1208
|
+
_$insert(_el$45, () => formatCost(stat.totalCost), null);
|
|
1209
|
+
_$effect((_$p) => _$setProp(_el$45, "fg", mutedColor(), _$p));
|
|
1210
|
+
return _el$45;
|
|
1211
|
+
}
|
|
1212
|
+
}), null);
|
|
1213
|
+
_$effect((_p$) => {
|
|
1214
|
+
var _v$13 = borderColor(), _v$14 = mutedColor(), _v$15 = {
|
|
1215
|
+
fg: hitRateColor(hitRate)
|
|
1216
|
+
};
|
|
1217
|
+
_v$13 !== _p$.e && (_p$.e = _$setProp(_el$31, "borderColor", _v$13, _p$.e));
|
|
1218
|
+
_v$14 !== _p$.t && (_p$.t = _$setProp(_el$33, "fg", _v$14, _p$.t));
|
|
1219
|
+
_v$15 !== _p$.a && (_p$.a = _$setProp(_el$34, "style", _v$15, _p$.a));
|
|
1220
|
+
return _p$;
|
|
1221
|
+
}, {
|
|
1222
|
+
e: void 0,
|
|
1223
|
+
t: void 0,
|
|
1224
|
+
a: void 0
|
|
1225
|
+
});
|
|
1226
|
+
return _el$30;
|
|
1227
|
+
}
|
|
1228
|
+
}), null);
|
|
1229
|
+
_$effect((_p$) => {
|
|
1230
|
+
var _v$16 = mutedColor(), _v$17 = {
|
|
1231
|
+
fg: hitRateColor(hitRate)
|
|
1232
|
+
}, _v$18 = {
|
|
1233
|
+
fg: primaryColor()
|
|
1234
|
+
}, _v$19 = mutedColor();
|
|
1235
|
+
_v$16 !== _p$.e && (_p$.e = _$setProp(_el$24, "fg", _v$16, _p$.e));
|
|
1236
|
+
_v$17 !== _p$.t && (_p$.t = _$setProp(_el$25, "style", _v$17, _p$.t));
|
|
1237
|
+
_v$18 !== _p$.a && (_p$.a = _$setProp(_el$28, "style", _v$18, _p$.a));
|
|
1238
|
+
_v$19 !== _p$.o && (_p$.o = _$setProp(_el$29, "fg", _v$19, _p$.o));
|
|
1239
|
+
return _p$;
|
|
1240
|
+
}, {
|
|
1241
|
+
e: void 0,
|
|
1242
|
+
t: void 0,
|
|
1243
|
+
a: void 0,
|
|
1244
|
+
o: void 0
|
|
1245
|
+
});
|
|
1246
|
+
return _el$22;
|
|
1247
|
+
})()
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
}), _$createComponent(Show, {
|
|
1251
|
+
get when() {
|
|
1252
|
+
return config().sidebar.showTokenDistribution;
|
|
1253
|
+
},
|
|
1254
|
+
get children() {
|
|
1255
|
+
var _el$11 = _$createElement("box"), _el$12 = _$createElement("text"), _el$13 = _$createElement("box"), _el$14 = _$createElement("text"), _el$15 = _$createTextNode(` `);
|
|
1256
|
+
_$insertNode(_el$11, _el$12);
|
|
1257
|
+
_$insertNode(_el$11, _el$13);
|
|
1258
|
+
_$setProp(_el$11, "flexDirection", "column");
|
|
1259
|
+
_$setProp(_el$11, "marginTop", 1);
|
|
1260
|
+
_$insert(_el$12, divider);
|
|
1261
|
+
_$insertNode(_el$13, _el$14);
|
|
1262
|
+
_$setProp(_el$13, "flexDirection", "row");
|
|
1263
|
+
_$setProp(_el$13, "onMouseDown", () => toggle.sub("token-dist"));
|
|
1264
|
+
_$setProp(_el$13, "paddingX", 1);
|
|
1265
|
+
_$insertNode(_el$14, _el$15);
|
|
1266
|
+
_$insert(_el$14, () => !collapse().subBlocks["token-dist"] ? "\u25BE" : "\u25B6", _el$15);
|
|
1267
|
+
_$insert(_el$14, () => t2("tokenDistribution"), null);
|
|
1268
|
+
_$insert(_el$11, _$createComponent(Show, {
|
|
1269
|
+
get when() {
|
|
1270
|
+
return !collapse().subBlocks["token-dist"];
|
|
1271
|
+
},
|
|
1272
|
+
get children() {
|
|
1273
|
+
var _el$16 = _$createElement("box");
|
|
1274
|
+
_$setProp(_el$16, "flexDirection", "column");
|
|
1275
|
+
_$setProp(_el$16, "paddingX", 1);
|
|
1276
|
+
_$setProp(_el$16, "marginTop", 1);
|
|
1277
|
+
_$insert(_el$16, _$createComponent(For, {
|
|
1278
|
+
get each() {
|
|
1279
|
+
return Object.entries(tokenDistribution()).filter(([_, val]) => val > 0);
|
|
1280
|
+
},
|
|
1281
|
+
children: ([role, val]) => (() => {
|
|
1282
|
+
var _el$50 = _$createElement("box"), _el$51 = _$createElement("box"), _el$52 = _$createElement("text"), _el$54 = _$createElement("text"), _el$55 = _$createElement("text");
|
|
1283
|
+
_$insertNode(_el$50, _el$51);
|
|
1284
|
+
_$insertNode(_el$50, _el$55);
|
|
1285
|
+
_$setProp(_el$50, "flexDirection", "row");
|
|
1286
|
+
_$setProp(_el$50, "justifyContent", "space-between");
|
|
1287
|
+
_$insertNode(_el$51, _el$52);
|
|
1288
|
+
_$insertNode(_el$51, _el$54);
|
|
1289
|
+
_$setProp(_el$51, "flexDirection", "row");
|
|
1290
|
+
_$insertNode(_el$52, _$createTextNode(`\u2588 `));
|
|
1291
|
+
_$insert(_el$54, () => t2(role));
|
|
1292
|
+
_$insert(_el$55, () => formatTokens(val));
|
|
1293
|
+
_$effect((_p$) => {
|
|
1294
|
+
var _v$23 = distRoleColor(role), _v$24 = mutedColor(), _v$25 = mutedColor();
|
|
1295
|
+
_v$23 !== _p$.e && (_p$.e = _$setProp(_el$52, "fg", _v$23, _p$.e));
|
|
1296
|
+
_v$24 !== _p$.t && (_p$.t = _$setProp(_el$54, "fg", _v$24, _p$.t));
|
|
1297
|
+
_v$25 !== _p$.a && (_p$.a = _$setProp(_el$55, "fg", _v$25, _p$.a));
|
|
1298
|
+
return _p$;
|
|
1299
|
+
}, {
|
|
1300
|
+
e: void 0,
|
|
1301
|
+
t: void 0,
|
|
1302
|
+
a: void 0
|
|
1303
|
+
});
|
|
1304
|
+
return _el$50;
|
|
1305
|
+
})()
|
|
1306
|
+
}));
|
|
1307
|
+
return _el$16;
|
|
1308
|
+
}
|
|
1309
|
+
}), null);
|
|
1310
|
+
_$effect((_p$) => {
|
|
1311
|
+
var _v$3 = borderColor(), _v$4 = greenColor();
|
|
1312
|
+
_v$3 !== _p$.e && (_p$.e = _$setProp(_el$12, "fg", _v$3, _p$.e));
|
|
1313
|
+
_v$4 !== _p$.t && (_p$.t = _$setProp(_el$14, "fg", _v$4, _p$.t));
|
|
1314
|
+
return _p$;
|
|
1315
|
+
}, {
|
|
1316
|
+
e: void 0,
|
|
1317
|
+
t: void 0
|
|
1318
|
+
});
|
|
1319
|
+
return _el$11;
|
|
1320
|
+
}
|
|
1321
|
+
})];
|
|
1322
|
+
}
|
|
1323
|
+
}), null);
|
|
1324
|
+
_$effect((_p$) => {
|
|
1325
|
+
var _v$5 = borderColor(), _v$6 = toggle.global, _v$7 = primaryColor(), _v$8 = mutedColor();
|
|
1326
|
+
_v$5 !== _p$.e && (_p$.e = _$setProp(_el$, "borderColor", _v$5, _p$.e));
|
|
1327
|
+
_v$6 !== _p$.t && (_p$.t = _$setProp(_el$2, "onMouseDown", _v$6, _p$.t));
|
|
1328
|
+
_v$7 !== _p$.a && (_p$.a = _$setProp(_el$3, "fg", _v$7, _p$.a));
|
|
1329
|
+
_v$8 !== _p$.o && (_p$.o = _$setProp(_el$5, "fg", _v$8, _p$.o));
|
|
1330
|
+
return _p$;
|
|
1331
|
+
}, {
|
|
1332
|
+
e: void 0,
|
|
1333
|
+
t: void 0,
|
|
1334
|
+
a: void 0,
|
|
1335
|
+
o: void 0
|
|
1336
|
+
});
|
|
1337
|
+
return _el$;
|
|
1338
|
+
})();
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// src/kernel/perf-aggregate.ts
|
|
1342
|
+
function computePercentile(sortedArr, p) {
|
|
1343
|
+
if (sortedArr.length === 0) return null;
|
|
1344
|
+
if (sortedArr.length === 1) return sortedArr[0];
|
|
1345
|
+
const idx = p / 100 * (sortedArr.length - 1);
|
|
1346
|
+
const lo = Math.floor(idx);
|
|
1347
|
+
const hi = Math.ceil(idx);
|
|
1348
|
+
if (lo === hi) return sortedArr[lo];
|
|
1349
|
+
return sortedArr[lo] + (sortedArr[hi] - sortedArr[lo]) * (idx - lo);
|
|
1350
|
+
}
|
|
1351
|
+
var RESERVOIR_SIZE = 500;
|
|
1352
|
+
function reservoirAdd(reservoir, value, totalCount) {
|
|
1353
|
+
if (reservoir.length < RESERVOIR_SIZE) {
|
|
1354
|
+
return [...reservoir, value];
|
|
1355
|
+
}
|
|
1356
|
+
const j = Math.floor(Math.random() * totalCount);
|
|
1357
|
+
if (j < RESERVOIR_SIZE) {
|
|
1358
|
+
const next = [...reservoir];
|
|
1359
|
+
next[j] = value;
|
|
1360
|
+
return next;
|
|
1361
|
+
}
|
|
1362
|
+
return reservoir;
|
|
1363
|
+
}
|
|
1364
|
+
function createAccumulator(model, providerID) {
|
|
1365
|
+
return {
|
|
1366
|
+
model,
|
|
1367
|
+
providerID,
|
|
1368
|
+
requestCount: 0,
|
|
1369
|
+
ttftCount: 0,
|
|
1370
|
+
tpsCount: 0,
|
|
1371
|
+
latencyCount: 0,
|
|
1372
|
+
totalInput: 0,
|
|
1373
|
+
totalOutput: 0,
|
|
1374
|
+
totalCacheRead: 0,
|
|
1375
|
+
totalCacheWrite: 0,
|
|
1376
|
+
totalCost: 0,
|
|
1377
|
+
avgTTFT: null,
|
|
1378
|
+
maxTTFT: null,
|
|
1379
|
+
minTTFT: null,
|
|
1380
|
+
avgTPS: null,
|
|
1381
|
+
maxTPS: null,
|
|
1382
|
+
minTPS: null,
|
|
1383
|
+
avgLatency: null,
|
|
1384
|
+
maxLatency: null,
|
|
1385
|
+
minLatency: null,
|
|
1386
|
+
ttftReservoir: [],
|
|
1387
|
+
latencyReservoir: [],
|
|
1388
|
+
lastTTFT: null,
|
|
1389
|
+
lastTPS: null,
|
|
1390
|
+
lastLatency: null
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
function accumulateEntry(acc, entry) {
|
|
1394
|
+
if (entry.inputTokens + entry.outputTokens + entry.reasoningTokens + entry.cacheReadTokens + entry.cacheWriteTokens === 0) return;
|
|
1395
|
+
acc.requestCount++;
|
|
1396
|
+
acc.totalInput += entry.inputTokens;
|
|
1397
|
+
acc.totalOutput += entry.outputTokens;
|
|
1398
|
+
acc.totalCacheRead += entry.cacheReadTokens;
|
|
1399
|
+
acc.totalCacheWrite += entry.cacheWriteTokens;
|
|
1400
|
+
acc.totalCost += entry.cost;
|
|
1401
|
+
if (entry.ttft_ms != null) {
|
|
1402
|
+
acc.ttftCount++;
|
|
1403
|
+
const c = acc.ttftCount;
|
|
1404
|
+
acc.avgTTFT = acc.avgTTFT != null ? acc.avgTTFT + (entry.ttft_ms - acc.avgTTFT) / c : entry.ttft_ms;
|
|
1405
|
+
acc.maxTTFT = acc.maxTTFT != null ? Math.max(acc.maxTTFT, entry.ttft_ms) : entry.ttft_ms;
|
|
1406
|
+
acc.minTTFT = acc.minTTFT != null ? Math.min(acc.minTTFT, entry.ttft_ms) : entry.ttft_ms;
|
|
1407
|
+
acc.ttftReservoir = reservoirAdd(acc.ttftReservoir, entry.ttft_ms, acc.ttftCount);
|
|
1408
|
+
acc.lastTTFT = entry.ttft_ms;
|
|
1409
|
+
}
|
|
1410
|
+
if (entry.tps != null) {
|
|
1411
|
+
acc.tpsCount++;
|
|
1412
|
+
const c = acc.tpsCount;
|
|
1413
|
+
acc.avgTPS = acc.avgTPS != null ? acc.avgTPS + (entry.tps - acc.avgTPS) / c : entry.tps;
|
|
1414
|
+
acc.maxTPS = acc.maxTPS != null ? Math.max(acc.maxTPS, entry.tps) : entry.tps;
|
|
1415
|
+
acc.minTPS = acc.minTPS != null ? Math.min(acc.minTPS, entry.tps) : entry.tps;
|
|
1416
|
+
acc.lastTPS = entry.tps;
|
|
1417
|
+
}
|
|
1418
|
+
if (entry.latency_ms != null) {
|
|
1419
|
+
acc.latencyCount++;
|
|
1420
|
+
const c = acc.latencyCount;
|
|
1421
|
+
acc.avgLatency = acc.avgLatency != null ? acc.avgLatency + (entry.latency_ms - acc.avgLatency) / c : entry.latency_ms;
|
|
1422
|
+
acc.maxLatency = acc.maxLatency != null ? Math.max(acc.maxLatency, entry.latency_ms) : entry.latency_ms;
|
|
1423
|
+
acc.minLatency = acc.minLatency != null ? Math.min(acc.minLatency, entry.latency_ms) : entry.latency_ms;
|
|
1424
|
+
acc.latencyReservoir = reservoirAdd(acc.latencyReservoir, entry.latency_ms, acc.latencyCount);
|
|
1425
|
+
acc.lastLatency = entry.latency_ms;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
function finalizeAccumulator(acc) {
|
|
1429
|
+
const ttftArr = [...acc.ttftReservoir].sort((a, b) => a - b);
|
|
1430
|
+
const latArr = [...acc.latencyReservoir].sort((a, b) => a - b);
|
|
1431
|
+
const denom = acc.totalInput + acc.totalCacheRead;
|
|
1432
|
+
return {
|
|
1433
|
+
model: acc.model,
|
|
1434
|
+
providerID: acc.providerID,
|
|
1435
|
+
requestCount: acc.requestCount,
|
|
1436
|
+
ttftCount: acc.ttftCount,
|
|
1437
|
+
tpsCount: acc.tpsCount,
|
|
1438
|
+
latencyCount: acc.latencyCount,
|
|
1439
|
+
totalInput: acc.totalInput,
|
|
1440
|
+
totalOutput: acc.totalOutput,
|
|
1441
|
+
totalCacheRead: acc.totalCacheRead,
|
|
1442
|
+
totalCacheWrite: acc.totalCacheWrite,
|
|
1443
|
+
totalCost: acc.totalCost,
|
|
1444
|
+
avgTTFT: acc.avgTTFT,
|
|
1445
|
+
maxTTFT: acc.maxTTFT,
|
|
1446
|
+
minTTFT: acc.minTTFT,
|
|
1447
|
+
p50TTFT: computePercentile(ttftArr, 50),
|
|
1448
|
+
p95TTFT: computePercentile(ttftArr, 95),
|
|
1449
|
+
p99TTFT: computePercentile(ttftArr, 99),
|
|
1450
|
+
avgTPS: acc.avgTPS,
|
|
1451
|
+
maxTPS: acc.maxTPS,
|
|
1452
|
+
minTPS: acc.minTPS,
|
|
1453
|
+
avgLatency: acc.avgLatency,
|
|
1454
|
+
maxLatency: acc.maxLatency,
|
|
1455
|
+
minLatency: acc.minLatency,
|
|
1456
|
+
p50Latency: computePercentile(latArr, 50),
|
|
1457
|
+
p95Latency: computePercentile(latArr, 95),
|
|
1458
|
+
p99Latency: computePercentile(latArr, 99),
|
|
1459
|
+
cacheHitRate: denom > 0 ? acc.totalCacheRead / denom * 100 : null,
|
|
1460
|
+
// 旧版本持久化文件可能缺少 last* 字段(undefined),统一归一化为 null
|
|
1461
|
+
lastTTFT: acc.lastTTFT ?? null,
|
|
1462
|
+
lastTPS: acc.lastTPS ?? null,
|
|
1463
|
+
lastLatency: acc.lastLatency ?? null
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
// src/kernel/perf.ts
|
|
1468
|
+
import { appendFileSync, readFileSync as readFileSync2, existsSync as existsSync2, statSync, renameSync as renameSync2, unlinkSync as unlinkSync2 } from "node:fs";
|
|
1469
|
+
import { readFile } from "node:fs/promises";
|
|
1470
|
+
import { join as join2 } from "node:path";
|
|
1471
|
+
import { homedir as homedir2 } from "node:os";
|
|
1472
|
+
|
|
1473
|
+
// src/kernel/store.ts
|
|
1474
|
+
import { readFileSync, writeFileSync, existsSync, renameSync } from "node:fs";
|
|
1475
|
+
import { join } from "node:path";
|
|
1476
|
+
import { homedir } from "node:os";
|
|
1477
|
+
var STATS_PATH = join(homedir(), ".opencode", "tokenwatch-stats.json");
|
|
1478
|
+
var LOG_PATH = join(homedir(), ".opencode", "tokenwatch.jsonl");
|
|
1479
|
+
var LOG_PATH_ROTATED = LOG_PATH + ".1";
|
|
1480
|
+
var CURRENT_VERSION = 1;
|
|
1481
|
+
var WRITE_COALESCE_MS = 1e3;
|
|
1482
|
+
function loadStatsFile() {
|
|
1483
|
+
try {
|
|
1484
|
+
if (!existsSync(STATS_PATH)) {
|
|
1485
|
+
return { version: CURRENT_VERSION, updatedAt: "", migratedFromLogs: false, models: {} };
|
|
1486
|
+
}
|
|
1487
|
+
const content = readFileSync(STATS_PATH, "utf-8");
|
|
1488
|
+
const parsed = JSON.parse(content);
|
|
1489
|
+
if (parsed?.version === CURRENT_VERSION && parsed.models) return parsed;
|
|
1490
|
+
} catch {
|
|
1491
|
+
}
|
|
1492
|
+
return { version: CURRENT_VERSION, updatedAt: "", migratedFromLogs: false, models: {} };
|
|
1493
|
+
}
|
|
1494
|
+
function saveStatsFileNow(file) {
|
|
1495
|
+
try {
|
|
1496
|
+
file.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1497
|
+
const tmpPath = STATS_PATH + ".tmp";
|
|
1498
|
+
writeFileSync(tmpPath, JSON.stringify(file), "utf-8");
|
|
1499
|
+
renameSync(tmpPath, STATS_PATH);
|
|
1500
|
+
} catch {
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
var pending = null;
|
|
1504
|
+
var flushScheduled = false;
|
|
1505
|
+
var lastFlushAt = 0;
|
|
1506
|
+
function flushNow() {
|
|
1507
|
+
const file = pending;
|
|
1508
|
+
if (!file) return;
|
|
1509
|
+
pending = null;
|
|
1510
|
+
lastFlushAt = Date.now();
|
|
1511
|
+
saveStatsFileNow(file);
|
|
1512
|
+
}
|
|
1513
|
+
function flushSoon() {
|
|
1514
|
+
if (flushScheduled) return;
|
|
1515
|
+
flushScheduled = true;
|
|
1516
|
+
const timer = setTimeout(() => {
|
|
1517
|
+
flushScheduled = false;
|
|
1518
|
+
flushNow();
|
|
1519
|
+
}, Math.max(0, WRITE_COALESCE_MS - (Date.now() - lastFlushAt)));
|
|
1520
|
+
timer.unref?.();
|
|
1521
|
+
}
|
|
1522
|
+
process.once("exit", () => {
|
|
1523
|
+
flushNow();
|
|
1524
|
+
});
|
|
1525
|
+
function readLogLines(path) {
|
|
1526
|
+
try {
|
|
1527
|
+
if (!existsSync(path)) return [];
|
|
1528
|
+
return readFileSync(path, "utf-8").trim().split("\n").filter(Boolean);
|
|
1529
|
+
} catch {
|
|
1530
|
+
return [];
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
function migrateFromLogsIfNeeded(file) {
|
|
1534
|
+
if (file.migratedFromLogs) return false;
|
|
1535
|
+
const lines = [...readLogLines(LOG_PATH_ROTATED), ...readLogLines(LOG_PATH)];
|
|
1536
|
+
file.migratedFromLogs = true;
|
|
1537
|
+
if (lines.length === 0) return true;
|
|
1538
|
+
try {
|
|
1539
|
+
let migrated = 0;
|
|
1540
|
+
for (const line of lines) {
|
|
1541
|
+
if (!line) continue;
|
|
1542
|
+
try {
|
|
1543
|
+
const entry = JSON.parse(line);
|
|
1544
|
+
if (entry.model && entry.ts) {
|
|
1545
|
+
let acc = file.models[entry.model];
|
|
1546
|
+
if (!acc) {
|
|
1547
|
+
acc = createAccumulator(entry.model, entry.providerID);
|
|
1548
|
+
file.models[entry.model] = acc;
|
|
1549
|
+
}
|
|
1550
|
+
accumulateEntry(acc, entry);
|
|
1551
|
+
migrated++;
|
|
1552
|
+
}
|
|
1553
|
+
} catch {
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
if (migrated > 0) {
|
|
1557
|
+
;
|
|
1558
|
+
file._migratedFrom = `${LOG_PATH} (${migrated} entries)`;
|
|
1559
|
+
}
|
|
1560
|
+
return true;
|
|
1561
|
+
} catch {
|
|
1562
|
+
return true;
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
function updatePersistedStats(entry) {
|
|
1566
|
+
try {
|
|
1567
|
+
const file = pending ?? loadStatsFile();
|
|
1568
|
+
pending = file;
|
|
1569
|
+
let acc = file.models[entry.model];
|
|
1570
|
+
if (!acc) {
|
|
1571
|
+
acc = createAccumulator(entry.model, entry.providerID);
|
|
1572
|
+
file.models[entry.model] = acc;
|
|
1573
|
+
}
|
|
1574
|
+
accumulateEntry(acc, entry);
|
|
1575
|
+
flushSoon();
|
|
1576
|
+
} catch {
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
function readPersistedStats() {
|
|
1580
|
+
try {
|
|
1581
|
+
const file = pending ?? loadStatsFile();
|
|
1582
|
+
pending = file;
|
|
1583
|
+
if (!file.migratedFromLogs) {
|
|
1584
|
+
file.models = {};
|
|
1585
|
+
migrateFromLogsIfNeeded(file);
|
|
1586
|
+
flushNow();
|
|
1587
|
+
}
|
|
1588
|
+
return Object.values(file.models).map((s) => finalizeAccumulator(s));
|
|
1589
|
+
} catch {
|
|
1590
|
+
return [];
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
// src/kernel/perf.ts
|
|
1595
|
+
var LOG_PATH2 = join2(homedir2(), ".opencode", "tokenwatch.jsonl");
|
|
1596
|
+
var LOG_PATH_ROTATED2 = LOG_PATH2 + ".1";
|
|
1597
|
+
var PerfTracker = class {
|
|
1598
|
+
/** 最早的任意 part(含 step 起点)—— 用作 TPS 的流式窗口起点(对齐宿主官方口径) */
|
|
1599
|
+
firstPartTimes = /* @__PURE__ */ new Map();
|
|
1600
|
+
/** 最早的输出 part(text/reasoning)—— 用作 TTFT(用户等待首个可见 token 的时间) */
|
|
1601
|
+
firstOutputTimes = /* @__PURE__ */ new Map();
|
|
1602
|
+
statsMap = /* @__PURE__ */ new Map();
|
|
1603
|
+
/** 会话加载令牌:异步重放期间再次切会话时,旧加载结果按令牌过期丢弃 */
|
|
1604
|
+
loadToken = 0;
|
|
1605
|
+
handlePartUpdated(event) {
|
|
1606
|
+
if (!event.time?.start || !event.message_id) return;
|
|
1607
|
+
const cur = this.firstPartTimes.get(event.message_id) ?? Number.POSITIVE_INFINITY;
|
|
1608
|
+
if (event.time.start < cur) {
|
|
1609
|
+
this.firstPartTimes.set(event.message_id, event.time.start);
|
|
1610
|
+
}
|
|
1611
|
+
const type = event.type;
|
|
1612
|
+
const isDelta = type != null && type.endsWith("-delta");
|
|
1613
|
+
const isWindowAnchor = type === "step" || type === "part-open" || type === "step-start" || type === "step-finish" || type != null && type.endsWith("-started");
|
|
1614
|
+
const isFirstTokenAnchor = isDelta || !isWindowAnchor;
|
|
1615
|
+
if (isFirstTokenAnchor) {
|
|
1616
|
+
const curOut = this.firstOutputTimes.get(event.message_id) ?? Number.POSITIVE_INFINITY;
|
|
1617
|
+
if (event.time.start < curOut) {
|
|
1618
|
+
this.firstOutputTimes.set(event.message_id, event.time.start);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
handleMessageUpdated(event) {
|
|
1623
|
+
const info = event.properties?.info;
|
|
1624
|
+
if (!info || info.role !== "assistant") return;
|
|
1625
|
+
if (!info.time?.completed) return;
|
|
1626
|
+
const messageID = info.id ?? "";
|
|
1627
|
+
const created = info.time.created;
|
|
1628
|
+
const completed = info.time.completed;
|
|
1629
|
+
if (!created || !completed) {
|
|
1630
|
+
this.firstPartTimes.delete(messageID);
|
|
1631
|
+
this.firstOutputTimes.delete(messageID);
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
const sessionID = info.sessionID ?? "";
|
|
1635
|
+
const providerID = info.providerID ?? "unknown";
|
|
1636
|
+
const modelID = info.modelID ?? "unknown";
|
|
1637
|
+
const model = `${providerID}/${modelID}`;
|
|
1638
|
+
const tokens = info.tokens;
|
|
1639
|
+
const inputTokens = tokens?.input ?? 0;
|
|
1640
|
+
const outputTokens = tokens?.output ?? 0;
|
|
1641
|
+
const reasoningTokens = tokens?.reasoning ?? 0;
|
|
1642
|
+
const cacheRead = tokens?.cache?.read ?? 0;
|
|
1643
|
+
const cacheWrite = tokens?.cache?.write ?? 0;
|
|
1644
|
+
const cost = info.cost ?? 0;
|
|
1645
|
+
if (inputTokens + outputTokens + reasoningTokens + cacheRead + cacheWrite === 0) {
|
|
1646
|
+
this.firstPartTimes.delete(messageID);
|
|
1647
|
+
this.firstOutputTimes.delete(messageID);
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1650
|
+
const firstPart = this.firstPartTimes.get(messageID) ?? null;
|
|
1651
|
+
const firstOutput = this.firstOutputTimes.get(messageID) ?? firstPart;
|
|
1652
|
+
const latencyMs = completed - created;
|
|
1653
|
+
const ttftMs = firstOutput !== null ? firstOutput - created : null;
|
|
1654
|
+
const genMs = firstPart !== null ? completed - firstPart : null;
|
|
1655
|
+
const tps = genMs !== null && genMs > 0 && outputTokens > 0 ? outputTokens / genMs * 1e3 : null;
|
|
1656
|
+
this.firstPartTimes.delete(messageID);
|
|
1657
|
+
this.firstOutputTimes.delete(messageID);
|
|
1658
|
+
const entry = {
|
|
1659
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1660
|
+
model,
|
|
1661
|
+
providerID,
|
|
1662
|
+
modelID,
|
|
1663
|
+
sessionID,
|
|
1664
|
+
ttft_ms: ttftMs,
|
|
1665
|
+
tps,
|
|
1666
|
+
// 只在有可靠 genMs 时才有值
|
|
1667
|
+
latency_ms: latencyMs,
|
|
1668
|
+
inputTokens,
|
|
1669
|
+
outputTokens,
|
|
1670
|
+
reasoningTokens,
|
|
1671
|
+
cacheReadTokens: cacheRead,
|
|
1672
|
+
cacheWriteTokens: cacheWrite,
|
|
1673
|
+
cost
|
|
1674
|
+
};
|
|
1675
|
+
this.appendLog(entry);
|
|
1676
|
+
this.updateStats(model, entry);
|
|
1677
|
+
}
|
|
1678
|
+
appendLog(entry) {
|
|
1679
|
+
try {
|
|
1680
|
+
const MAX_SIZE = 5 * 1024 * 1024;
|
|
1681
|
+
if (existsSync2(LOG_PATH2) && statSync(LOG_PATH2).size > MAX_SIZE) {
|
|
1682
|
+
try {
|
|
1683
|
+
unlinkSync2(LOG_PATH_ROTATED2);
|
|
1684
|
+
} catch {
|
|
1685
|
+
}
|
|
1686
|
+
renameSync2(LOG_PATH2, LOG_PATH_ROTATED2);
|
|
1687
|
+
}
|
|
1688
|
+
appendFileSync(LOG_PATH2, JSON.stringify(entry) + "\n");
|
|
1689
|
+
} catch {
|
|
1690
|
+
}
|
|
1691
|
+
updatePersistedStats(entry);
|
|
1692
|
+
}
|
|
1693
|
+
handleMessageRemoved(event) {
|
|
1694
|
+
const mid = event.properties?.messageID ?? "";
|
|
1695
|
+
if (mid) {
|
|
1696
|
+
this.firstPartTimes.delete(mid);
|
|
1697
|
+
this.firstOutputTimes.delete(mid);
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
updateStats(model, entry) {
|
|
1701
|
+
let acc = this.statsMap.get(model);
|
|
1702
|
+
if (!acc) {
|
|
1703
|
+
acc = createAccumulator(model, entry.providerID);
|
|
1704
|
+
this.statsMap.set(model, acc);
|
|
1705
|
+
}
|
|
1706
|
+
accumulateEntry(acc, entry);
|
|
1707
|
+
}
|
|
1708
|
+
getSessionStats() {
|
|
1709
|
+
let totalInput = 0, totalOutput = 0, totalCacheRead = 0, totalCacheWrite = 0;
|
|
1710
|
+
let totalRequests = 0, totalCost = 0;
|
|
1711
|
+
let weightedHitSum = 0, totalReqForHit = 0;
|
|
1712
|
+
const models = {};
|
|
1713
|
+
for (const [model, acc] of this.statsMap) {
|
|
1714
|
+
const stats = finalizeAccumulator(acc);
|
|
1715
|
+
models[model] = stats;
|
|
1716
|
+
totalInput += stats.totalInput;
|
|
1717
|
+
totalOutput += stats.totalOutput;
|
|
1718
|
+
totalCacheRead += stats.totalCacheRead;
|
|
1719
|
+
totalCacheWrite += stats.totalCacheWrite;
|
|
1720
|
+
totalRequests += stats.requestCount;
|
|
1721
|
+
totalCost += stats.totalCost;
|
|
1722
|
+
if (stats.cacheHitRate !== null) {
|
|
1723
|
+
weightedHitSum += stats.cacheHitRate * stats.requestCount;
|
|
1724
|
+
totalReqForHit += stats.requestCount;
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
const weightedCacheHitRate = totalReqForHit > 0 ? weightedHitSum / totalReqForHit : null;
|
|
1728
|
+
return {
|
|
1729
|
+
models,
|
|
1730
|
+
totals: { totalInput, totalOutput, totalCacheRead, totalCacheWrite, totalRequests, totalCost, weightedCacheHitRate }
|
|
1731
|
+
};
|
|
1732
|
+
}
|
|
1733
|
+
readLogs(last = 50) {
|
|
1734
|
+
try {
|
|
1735
|
+
if (!existsSync2(LOG_PATH2)) return [];
|
|
1736
|
+
const content = readFileSync2(LOG_PATH2, "utf-8").trim();
|
|
1737
|
+
if (!content) return [];
|
|
1738
|
+
const lines = content.split("\n");
|
|
1739
|
+
const entries = [];
|
|
1740
|
+
for (let i = Math.max(0, lines.length - last); i < lines.length; i++) {
|
|
1741
|
+
try {
|
|
1742
|
+
entries.push(JSON.parse(lines[i]));
|
|
1743
|
+
} catch {
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
return entries;
|
|
1747
|
+
} catch {
|
|
1748
|
+
return [];
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
reset() {
|
|
1752
|
+
this.loadToken++;
|
|
1753
|
+
this.firstPartTimes.clear();
|
|
1754
|
+
this.firstOutputTimes.clear();
|
|
1755
|
+
this.statsMap.clear();
|
|
1756
|
+
}
|
|
1757
|
+
/**
|
|
1758
|
+
* 切换会话:清空内存态后异步重放该会话的 JSONL 历史。
|
|
1759
|
+
*
|
|
1760
|
+
* 异步化是为了不阻塞 TUI 渲染线程(5MB 日志的同步读可达数十毫秒);
|
|
1761
|
+
* 重放完成前若又切换了会话,由 loadToken 令牌丢弃过期结果。
|
|
1762
|
+
*/
|
|
1763
|
+
async loadSession(sessionID) {
|
|
1764
|
+
const token = ++this.loadToken;
|
|
1765
|
+
this.firstPartTimes.clear();
|
|
1766
|
+
this.firstOutputTimes.clear();
|
|
1767
|
+
this.statsMap.clear();
|
|
1768
|
+
if (!sessionID) return;
|
|
1769
|
+
try {
|
|
1770
|
+
const files = [LOG_PATH_ROTATED2, LOG_PATH2].filter((f) => existsSync2(f));
|
|
1771
|
+
if (files.length === 0) return;
|
|
1772
|
+
for (const filePath of files) {
|
|
1773
|
+
if (token !== this.loadToken) return;
|
|
1774
|
+
const content = await readFile(filePath, "utf-8");
|
|
1775
|
+
if (token !== this.loadToken) return;
|
|
1776
|
+
const trimmed = content.trim();
|
|
1777
|
+
if (!trimmed) continue;
|
|
1778
|
+
for (const line of trimmed.split("\n")) {
|
|
1779
|
+
if (!line) continue;
|
|
1780
|
+
try {
|
|
1781
|
+
const entry = JSON.parse(line);
|
|
1782
|
+
if (entry.sessionID === sessionID) {
|
|
1783
|
+
this.updateStats(entry.model, entry);
|
|
1784
|
+
}
|
|
1785
|
+
} catch {
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
} catch {
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
};
|
|
1793
|
+
function createPerfTracker() {
|
|
1794
|
+
return new PerfTracker();
|
|
1795
|
+
}
|
|
1796
|
+
function readLogs(last = 50) {
|
|
1797
|
+
const tracker = new PerfTracker();
|
|
1798
|
+
return tracker.readLogs(last);
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
// src/host/runtime.tsx
|
|
1802
|
+
function kvKey(sessionID) {
|
|
1803
|
+
return "tokenwatch-msgs-" + sessionID;
|
|
1804
|
+
}
|
|
1805
|
+
function toPerfMessageEvent(e) {
|
|
1806
|
+
return {
|
|
1807
|
+
properties: {
|
|
1808
|
+
info: {
|
|
1809
|
+
id: e.messageID,
|
|
1810
|
+
sessionID: e.sessionID,
|
|
1811
|
+
role: e.role,
|
|
1812
|
+
providerID: e.providerID,
|
|
1813
|
+
modelID: e.modelID,
|
|
1814
|
+
tokens: {
|
|
1815
|
+
input: e.input,
|
|
1816
|
+
output: e.output,
|
|
1817
|
+
reasoning: e.reasoning,
|
|
1818
|
+
cache: {
|
|
1819
|
+
read: e.cacheRead,
|
|
1820
|
+
write: e.cacheWrite
|
|
1821
|
+
},
|
|
1822
|
+
total: e.total
|
|
1823
|
+
},
|
|
1824
|
+
cost: e.cost,
|
|
1825
|
+
time: {
|
|
1826
|
+
created: e.timeCreated,
|
|
1827
|
+
completed: e.timeCompleted
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
};
|
|
1832
|
+
}
|
|
1833
|
+
function toPerfPartEvent(p) {
|
|
1834
|
+
return {
|
|
1835
|
+
message_id: p.messageID,
|
|
1836
|
+
type: p.type,
|
|
1837
|
+
text: p.text,
|
|
1838
|
+
time: {
|
|
1839
|
+
start: p.timeStart
|
|
1840
|
+
}
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1843
|
+
function rebuildFromMessages(sessionID, messages) {
|
|
1844
|
+
const out = [];
|
|
1845
|
+
for (const msg of messages) {
|
|
1846
|
+
const info = msg?.info ?? {};
|
|
1847
|
+
const m = {
|
|
1848
|
+
...info,
|
|
1849
|
+
...msg
|
|
1850
|
+
};
|
|
1851
|
+
const kind = m.type ?? m.role;
|
|
1852
|
+
if (kind !== "assistant") continue;
|
|
1853
|
+
const tokens = m.tokens;
|
|
1854
|
+
if (!tokens) continue;
|
|
1855
|
+
const total = (tokens.input ?? 0) + (tokens.output ?? 0) + (tokens.reasoning ?? 0) + (tokens.cache?.read ?? 0) + (tokens.cache?.write ?? 0);
|
|
1856
|
+
if (total === 0) continue;
|
|
1857
|
+
out.push({
|
|
1858
|
+
id: m.id,
|
|
1859
|
+
sessionID,
|
|
1860
|
+
// v1 平铺 providerID/modelID;v2 嵌套在 model 里
|
|
1861
|
+
providerID: m.model?.providerID ?? m.providerID ?? "unknown",
|
|
1862
|
+
modelID: m.model?.id ?? m.modelID ?? "unknown",
|
|
1863
|
+
inputTokens: tokens.input ?? 0,
|
|
1864
|
+
outputTokens: tokens.output ?? 0,
|
|
1865
|
+
reasoningTokens: tokens.reasoning ?? 0,
|
|
1866
|
+
cacheRead: tokens.cache?.read ?? 0,
|
|
1867
|
+
cacheWrite: tokens.cache?.write ?? 0,
|
|
1868
|
+
cost: m.cost ?? 0
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1871
|
+
return out;
|
|
1872
|
+
}
|
|
1873
|
+
var PERSIST_DEBOUNCE_MS = 500;
|
|
1874
|
+
var KV_SESSION_KEEP = 20;
|
|
1875
|
+
var SESSION_INDEX_KEY = "tokenwatch-session-index";
|
|
1876
|
+
function startTokenWatch(host) {
|
|
1877
|
+
const perfTracker = createPerfTracker();
|
|
1878
|
+
const [sidebarRevision, setSidebarRevision] = createSignal2(0);
|
|
1879
|
+
const [perfRevision, setPerfRevision] = createSignal2(0);
|
|
1880
|
+
const [allTokenMessages, setAllTokenMessages] = createSignal2([]);
|
|
1881
|
+
let currentSessionID = "";
|
|
1882
|
+
let pollTimer = null;
|
|
1883
|
+
let persistTimer = null;
|
|
1884
|
+
let persistSession = "";
|
|
1885
|
+
let persistMsgs = [];
|
|
1886
|
+
const persistNow = (sessionID, msgs) => {
|
|
1887
|
+
try {
|
|
1888
|
+
host.store.set(kvKey(sessionID), msgs);
|
|
1889
|
+
} catch {
|
|
1890
|
+
}
|
|
1891
|
+
};
|
|
1892
|
+
const persistThrottled = (sessionID, msgs) => {
|
|
1893
|
+
persistSession = sessionID;
|
|
1894
|
+
persistMsgs = msgs;
|
|
1895
|
+
if (persistTimer) return;
|
|
1896
|
+
persistTimer = setTimeout(() => {
|
|
1897
|
+
persistTimer = null;
|
|
1898
|
+
persistNow(persistSession, persistMsgs);
|
|
1899
|
+
}, PERSIST_DEBOUNCE_MS);
|
|
1900
|
+
};
|
|
1901
|
+
const flushPersist = () => {
|
|
1902
|
+
if (persistTimer) {
|
|
1903
|
+
clearTimeout(persistTimer);
|
|
1904
|
+
persistTimer = null;
|
|
1905
|
+
}
|
|
1906
|
+
if (persistMsgs.length > 0) persistNow(persistSession, persistMsgs);
|
|
1907
|
+
};
|
|
1908
|
+
const evictOldSessions = (sessionID) => {
|
|
1909
|
+
try {
|
|
1910
|
+
const index = host.store.get(SESSION_INDEX_KEY, []).filter(Boolean);
|
|
1911
|
+
const next = [sessionID, ...index.filter((id) => id !== sessionID)].slice(0, KV_SESSION_KEEP);
|
|
1912
|
+
host.store.set(SESSION_INDEX_KEY, next);
|
|
1913
|
+
if (typeof host.store.delete === "function") {
|
|
1914
|
+
for (const id of index.slice(KV_SESSION_KEEP)) {
|
|
1915
|
+
if (!next.includes(id)) {
|
|
1916
|
+
try {
|
|
1917
|
+
host.store.delete(kvKey(id));
|
|
1918
|
+
} catch {
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
} catch {
|
|
1924
|
+
}
|
|
1925
|
+
};
|
|
1926
|
+
const switchSession = (sessionID) => {
|
|
1927
|
+
if (!sessionID || sessionID === currentSessionID) return;
|
|
1928
|
+
currentSessionID = sessionID;
|
|
1929
|
+
void perfTracker.loadSession(sessionID).then(() => {
|
|
1930
|
+
setPerfRevision((v) => v + 1);
|
|
1931
|
+
setSidebarRevision((v) => v + 1);
|
|
1932
|
+
}).catch(() => {
|
|
1933
|
+
});
|
|
1934
|
+
evictOldSessions(sessionID);
|
|
1935
|
+
if (pollTimer) {
|
|
1936
|
+
clearInterval(pollTimer);
|
|
1937
|
+
pollTimer = null;
|
|
1938
|
+
}
|
|
1939
|
+
let loaded = [];
|
|
1940
|
+
try {
|
|
1941
|
+
const saved = host.store.get(kvKey(sessionID), void 0);
|
|
1942
|
+
if (saved && saved.length > 0) loaded = saved;
|
|
1943
|
+
} catch {
|
|
1944
|
+
}
|
|
1945
|
+
setAllTokenMessages(loaded);
|
|
1946
|
+
let pollCount = 0;
|
|
1947
|
+
const maxPolls = 50;
|
|
1948
|
+
pollTimer = setInterval(() => {
|
|
1949
|
+
pollCount++;
|
|
1950
|
+
let existing = [];
|
|
1951
|
+
try {
|
|
1952
|
+
existing = host.sessionMessages(sessionID);
|
|
1953
|
+
} catch {
|
|
1954
|
+
existing = [];
|
|
1955
|
+
}
|
|
1956
|
+
if (!existing || existing.length === 0) {
|
|
1957
|
+
if (pollCount >= maxPolls && pollTimer) {
|
|
1958
|
+
clearInterval(pollTimer);
|
|
1959
|
+
pollTimer = null;
|
|
1960
|
+
}
|
|
1961
|
+
return;
|
|
1962
|
+
}
|
|
1963
|
+
const rebuilt = rebuildFromMessages(sessionID, existing);
|
|
1964
|
+
setAllTokenMessages((prev) => {
|
|
1965
|
+
if (prev.length >= rebuilt.length) return prev;
|
|
1966
|
+
return rebuilt;
|
|
1967
|
+
});
|
|
1968
|
+
persistThrottled(sessionID, rebuilt);
|
|
1969
|
+
if (pollTimer) {
|
|
1970
|
+
clearInterval(pollTimer);
|
|
1971
|
+
pollTimer = null;
|
|
1972
|
+
}
|
|
1973
|
+
}, 200);
|
|
1974
|
+
};
|
|
1975
|
+
const unsubscribe = host.subscribe({
|
|
1976
|
+
onMessageUpdated(event) {
|
|
1977
|
+
perfTracker.handleMessageUpdated(toPerfMessageEvent(event));
|
|
1978
|
+
setPerfRevision((v) => v + 1);
|
|
1979
|
+
if (event.role === "assistant" && event.total > 0) {
|
|
1980
|
+
const msg = {
|
|
1981
|
+
id: event.messageID,
|
|
1982
|
+
sessionID: event.sessionID,
|
|
1983
|
+
providerID: event.providerID,
|
|
1984
|
+
modelID: event.modelID,
|
|
1985
|
+
inputTokens: event.input,
|
|
1986
|
+
outputTokens: event.output,
|
|
1987
|
+
reasoningTokens: event.reasoning,
|
|
1988
|
+
cacheRead: event.cacheRead,
|
|
1989
|
+
cacheWrite: event.cacheWrite,
|
|
1990
|
+
cost: event.cost
|
|
1991
|
+
};
|
|
1992
|
+
const prev = allTokenMessages();
|
|
1993
|
+
const idx = prev.findIndex((m) => m.id === msg.id);
|
|
1994
|
+
const next = idx >= 0 ? prev.map((m, i) => i === idx ? msg : m) : [...prev, msg];
|
|
1995
|
+
setAllTokenMessages(next);
|
|
1996
|
+
persistThrottled(event.sessionID || currentSessionID, next);
|
|
1997
|
+
}
|
|
1998
|
+
setSidebarRevision((v) => v + 1);
|
|
1999
|
+
},
|
|
2000
|
+
onPartUpdated(event) {
|
|
2001
|
+
perfTracker.handlePartUpdated(toPerfPartEvent(event));
|
|
2002
|
+
},
|
|
2003
|
+
onInvalidate() {
|
|
2004
|
+
setSidebarRevision((v) => v + 1);
|
|
2005
|
+
}
|
|
2006
|
+
});
|
|
2007
|
+
host.registerSidebar((input) => {
|
|
2008
|
+
sidebarRevision();
|
|
2009
|
+
const sid = input.sessionID;
|
|
2010
|
+
queueMicrotask(() => switchSession(sid));
|
|
2011
|
+
return _$createComponent2(TokenWatchPanel, {
|
|
2012
|
+
host,
|
|
2013
|
+
perfTracker,
|
|
2014
|
+
perfRevision,
|
|
2015
|
+
messages: () => {
|
|
2016
|
+
try {
|
|
2017
|
+
return host.sessionMessages(currentSessionID);
|
|
2018
|
+
} catch {
|
|
2019
|
+
return [];
|
|
2020
|
+
}
|
|
2021
|
+
},
|
|
2022
|
+
messageParts: (messageID) => host.messageParts(messageID, currentSessionID),
|
|
2023
|
+
allTokenMessages
|
|
2024
|
+
});
|
|
2025
|
+
});
|
|
2026
|
+
const disposeAll = () => {
|
|
2027
|
+
unsubscribe();
|
|
2028
|
+
flushPersist();
|
|
2029
|
+
if (pollTimer) {
|
|
2030
|
+
clearInterval(pollTimer);
|
|
2031
|
+
pollTimer = null;
|
|
2032
|
+
}
|
|
2033
|
+
};
|
|
2034
|
+
host.onDispose(disposeAll);
|
|
2035
|
+
return disposeAll;
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
// src/host/v1/data-source.ts
|
|
2039
|
+
import { exec } from "node:child_process";
|
|
2040
|
+
import { dirname, join as join3 } from "node:path";
|
|
2041
|
+
function buildEnvWithOpencodePath() {
|
|
2042
|
+
const nodeDir = dirname(process.execPath);
|
|
2043
|
+
const candidateDirs = [
|
|
2044
|
+
// nvm on Windows: opencode-ai 包的 bin 目录
|
|
2045
|
+
join3(nodeDir, "node_modules", "opencode-ai", "node_modules", "opencode-windows-x64", "bin"),
|
|
2046
|
+
// npm global install: opencode 直接在 node_modules/.bin
|
|
2047
|
+
join3(nodeDir, "node_modules", ".bin"),
|
|
2048
|
+
// 也加入 node.exe 所在目录本身(nvm 可能在此放 shim)
|
|
2049
|
+
nodeDir
|
|
2050
|
+
];
|
|
2051
|
+
const pathSep = process.platform === "win32" ? ";" : ":";
|
|
2052
|
+
const extraPath = candidateDirs.join(pathSep);
|
|
2053
|
+
return {
|
|
2054
|
+
...process.env,
|
|
2055
|
+
PATH: `${extraPath}${pathSep}${process.env.PATH ?? ""}`
|
|
2056
|
+
};
|
|
2057
|
+
}
|
|
2058
|
+
var OPENCODE_ENV = buildEnvWithOpencodePath();
|
|
2059
|
+
function execAsync(cmd) {
|
|
2060
|
+
return new Promise((resolve, reject) => {
|
|
2061
|
+
exec(cmd, { windowsHide: true, timeout: 3e4, env: OPENCODE_ENV }, (error, stdout, stderr) => {
|
|
2062
|
+
if (error) reject(Object.assign(error, { stderr }));
|
|
2063
|
+
else if (stderr.trim() && !stdout.trim()) reject(Object.assign(new Error(stderr.trim()), { stderr }));
|
|
2064
|
+
else resolve({ stdout, stderr });
|
|
2065
|
+
});
|
|
2066
|
+
});
|
|
2067
|
+
}
|
|
2068
|
+
async function queryDb(sql) {
|
|
2069
|
+
const flatSql = sql.replace(/\s+/g, " ").trim();
|
|
2070
|
+
const { stdout, stderr } = await execAsync(`opencode db ${JSON.stringify(flatSql)} --format json`);
|
|
2071
|
+
if (stderr) throw new Error(stderr.trim());
|
|
2072
|
+
const parsed = JSON.parse(stdout.trim());
|
|
2073
|
+
return Array.isArray(parsed) ? parsed : parsed.data ?? [];
|
|
2074
|
+
}
|
|
2075
|
+
function escapeSql(value) {
|
|
2076
|
+
return value.replace(/'/g, "''").replace(/["%\r\n\0]/g, "");
|
|
2077
|
+
}
|
|
2078
|
+
function isValidDate(s) {
|
|
2079
|
+
return /^\d{4}-\d{2}-\d{2}$/.test(s);
|
|
2080
|
+
}
|
|
2081
|
+
function messageWhere(filters) {
|
|
2082
|
+
const where = [
|
|
2083
|
+
"json_extract(m.data, '$.role') = 'assistant'",
|
|
2084
|
+
"coalesce(json_extract(m.data, '$.tokens.total'), 0) > 0"
|
|
2085
|
+
];
|
|
2086
|
+
if (filters.sessionId) where.push(`m.session_id = '${escapeSql(filters.sessionId)}'`);
|
|
2087
|
+
if (filters.provider) where.push(`coalesce(json_extract(m.data, '$.providerID'), '') = '${escapeSql(filters.provider)}'`);
|
|
2088
|
+
if (filters.model) where.push(`coalesce(json_extract(m.data, '$.modelID'), '') = '${escapeSql(filters.model)}'`);
|
|
2089
|
+
if (filters.startDate && isValidDate(filters.startDate)) {
|
|
2090
|
+
where.push(`date(m.time_created / 1000, 'unixepoch', 'localtime') >= '${filters.startDate}'`);
|
|
2091
|
+
}
|
|
2092
|
+
if (filters.endDate && isValidDate(filters.endDate)) {
|
|
2093
|
+
where.push(`date(m.time_created / 1000, 'unixepoch', 'localtime') <= '${filters.endDate}'`);
|
|
2094
|
+
}
|
|
2095
|
+
return where.join(" AND ");
|
|
2096
|
+
}
|
|
2097
|
+
function parseList(value) {
|
|
2098
|
+
if (!value) return [];
|
|
2099
|
+
return value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
2100
|
+
}
|
|
2101
|
+
function toSessionTokenData(row) {
|
|
2102
|
+
const models = parseList(row?.models_used);
|
|
2103
|
+
const providers = parseList(row?.providers_used);
|
|
2104
|
+
return {
|
|
2105
|
+
model: models.length === 1 ? models[0] : "",
|
|
2106
|
+
provider: providers.length === 1 ? providers[0] : "",
|
|
2107
|
+
modelsUsed: models,
|
|
2108
|
+
totalTokens: row?.total_tokens ?? 0,
|
|
2109
|
+
inputTokens: row?.input_tokens ?? 0,
|
|
2110
|
+
outputTokens: row?.output_tokens ?? 0,
|
|
2111
|
+
reasoningTokens: row?.reasoning_tokens ?? 0,
|
|
2112
|
+
cacheRead: row?.cache_read ?? 0,
|
|
2113
|
+
cacheWrite: row?.cache_write ?? 0,
|
|
2114
|
+
totalCost: row?.total_cost ?? 0,
|
|
2115
|
+
requestCount: row?.request_count ?? 0
|
|
2116
|
+
};
|
|
2117
|
+
}
|
|
2118
|
+
function getPresetRange(preset) {
|
|
2119
|
+
if (preset === "all") return {};
|
|
2120
|
+
const end = /* @__PURE__ */ new Date();
|
|
2121
|
+
const start = new Date(end);
|
|
2122
|
+
if (preset === "7d") start.setDate(end.getDate() - 6);
|
|
2123
|
+
if (preset === "30d") start.setDate(end.getDate() - 29);
|
|
2124
|
+
if (preset === "month") start.setDate(1);
|
|
2125
|
+
const format = (date) => {
|
|
2126
|
+
const year = date.getFullYear();
|
|
2127
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
2128
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
2129
|
+
return `${year}-${month}-${day}`;
|
|
2130
|
+
};
|
|
2131
|
+
return { startDate: format(start), endDate: format(end) };
|
|
2132
|
+
}
|
|
2133
|
+
async function getSummary(filters = {}) {
|
|
2134
|
+
const sql = `
|
|
2135
|
+
SELECT
|
|
2136
|
+
group_concat(distinct coalesce(json_extract(m.data, '$.modelID'), 'unknown')) as models_used,
|
|
2137
|
+
group_concat(distinct coalesce(json_extract(m.data, '$.providerID'), 'unknown')) as providers_used,
|
|
2138
|
+
count(*) as request_count,
|
|
2139
|
+
sum(coalesce(json_extract(m.data, '$.tokens.total'), 0)) as total_tokens,
|
|
2140
|
+
sum(coalesce(json_extract(m.data, '$.tokens.input'), 0)) as input_tokens,
|
|
2141
|
+
sum(coalesce(json_extract(m.data, '$.tokens.output'), 0)) as output_tokens,
|
|
2142
|
+
sum(coalesce(json_extract(m.data, '$.tokens.reasoning'), 0)) as reasoning_tokens,
|
|
2143
|
+
sum(coalesce(json_extract(m.data, '$.tokens.cache.read'), 0)) as cache_read,
|
|
2144
|
+
sum(coalesce(json_extract(m.data, '$.tokens.cache.write'), 0)) as cache_write,
|
|
2145
|
+
sum(coalesce(json_extract(m.data, '$.cost'), 0)) as total_cost
|
|
2146
|
+
FROM message m
|
|
2147
|
+
WHERE ${messageWhere(filters)}
|
|
2148
|
+
`.trim();
|
|
2149
|
+
const rows = await queryDb(sql);
|
|
2150
|
+
return toSessionTokenData(rows[0]);
|
|
2151
|
+
}
|
|
2152
|
+
async function getModelBreakdown(filters = {}) {
|
|
2153
|
+
const sql = `
|
|
2154
|
+
SELECT
|
|
2155
|
+
coalesce(json_extract(m.data, '$.providerID'), 'unknown') as provider,
|
|
2156
|
+
coalesce(json_extract(m.data, '$.modelID'), 'unknown') as model,
|
|
2157
|
+
count(*) as requests,
|
|
2158
|
+
count(distinct m.session_id) as sessions,
|
|
2159
|
+
sum(coalesce(json_extract(m.data, '$.tokens.total'), 0)) as total_tokens,
|
|
2160
|
+
sum(coalesce(json_extract(m.data, '$.tokens.input'), 0)) as input_tokens,
|
|
2161
|
+
sum(coalesce(json_extract(m.data, '$.tokens.output'), 0)) as output_tokens,
|
|
2162
|
+
sum(coalesce(json_extract(m.data, '$.tokens.reasoning'), 0)) as reasoning_tokens,
|
|
2163
|
+
sum(coalesce(json_extract(m.data, '$.tokens.cache.read'), 0)) as cache_read,
|
|
2164
|
+
sum(coalesce(json_extract(m.data, '$.cost'), 0)) as total_cost
|
|
2165
|
+
FROM message m
|
|
2166
|
+
WHERE ${messageWhere(filters)}
|
|
2167
|
+
GROUP BY provider, model
|
|
2168
|
+
ORDER BY total_tokens DESC
|
|
2169
|
+
`.trim();
|
|
2170
|
+
const rows = await queryDb(sql);
|
|
2171
|
+
return rows.map((row) => ({
|
|
2172
|
+
provider: row.provider ?? "unknown",
|
|
2173
|
+
model: row.model ?? "unknown",
|
|
2174
|
+
requests: row.requests ?? 0,
|
|
2175
|
+
sessions: row.sessions ?? 0,
|
|
2176
|
+
totalTokens: row.total_tokens ?? 0,
|
|
2177
|
+
inputTokens: row.input_tokens ?? 0,
|
|
2178
|
+
outputTokens: row.output_tokens ?? 0,
|
|
2179
|
+
reasoningTokens: row.reasoning_tokens ?? 0,
|
|
2180
|
+
cacheRead: row.cache_read ?? 0,
|
|
2181
|
+
totalCost: row.total_cost ?? 0
|
|
2182
|
+
}));
|
|
2183
|
+
}
|
|
2184
|
+
async function getProviderBreakdown(filters = {}) {
|
|
2185
|
+
const sql = `
|
|
2186
|
+
SELECT
|
|
2187
|
+
coalesce(json_extract(m.data, '$.providerID'), 'unknown') as provider,
|
|
2188
|
+
count(*) as requests,
|
|
2189
|
+
count(distinct m.session_id) as sessions,
|
|
2190
|
+
sum(coalesce(json_extract(m.data, '$.tokens.total'), 0)) as total_tokens,
|
|
2191
|
+
sum(coalesce(json_extract(m.data, '$.tokens.input'), 0)) as input_tokens,
|
|
2192
|
+
sum(coalesce(json_extract(m.data, '$.tokens.output'), 0)) as output_tokens,
|
|
2193
|
+
sum(coalesce(json_extract(m.data, '$.tokens.reasoning'), 0)) as reasoning_tokens,
|
|
2194
|
+
sum(coalesce(json_extract(m.data, '$.tokens.cache.read'), 0)) as cache_read,
|
|
2195
|
+
sum(coalesce(json_extract(m.data, '$.cost'), 0)) as total_cost
|
|
2196
|
+
FROM message m
|
|
2197
|
+
WHERE ${messageWhere(filters)}
|
|
2198
|
+
GROUP BY provider
|
|
2199
|
+
ORDER BY total_tokens DESC
|
|
2200
|
+
`.trim();
|
|
2201
|
+
const rows = await queryDb(sql);
|
|
2202
|
+
return rows.map((row) => ({
|
|
2203
|
+
provider: row.provider ?? "unknown",
|
|
2204
|
+
requests: row.requests ?? 0,
|
|
2205
|
+
sessions: row.sessions ?? 0,
|
|
2206
|
+
totalTokens: row.total_tokens ?? 0,
|
|
2207
|
+
inputTokens: row.input_tokens ?? 0,
|
|
2208
|
+
outputTokens: row.output_tokens ?? 0,
|
|
2209
|
+
reasoningTokens: row.reasoning_tokens ?? 0,
|
|
2210
|
+
cacheRead: row.cache_read ?? 0,
|
|
2211
|
+
totalCost: row.total_cost ?? 0
|
|
2212
|
+
}));
|
|
2213
|
+
}
|
|
2214
|
+
async function getDailyBreakdown(filters = {}) {
|
|
2215
|
+
const limit = filters.limit ?? 365;
|
|
2216
|
+
const sql = `
|
|
2217
|
+
SELECT
|
|
2218
|
+
date(m.time_created / 1000, 'unixepoch', 'localtime') as day,
|
|
2219
|
+
count(*) as requests,
|
|
2220
|
+
count(distinct m.session_id) as sessions,
|
|
2221
|
+
sum(coalesce(json_extract(m.data, '$.tokens.total'), 0)) as total_tokens,
|
|
2222
|
+
sum(coalesce(json_extract(m.data, '$.tokens.input'), 0)) as input_tokens,
|
|
2223
|
+
sum(coalesce(json_extract(m.data, '$.tokens.output'), 0)) as output_tokens,
|
|
2224
|
+
sum(coalesce(json_extract(m.data, '$.tokens.reasoning'), 0)) as reasoning_tokens,
|
|
2225
|
+
sum(coalesce(json_extract(m.data, '$.tokens.cache.read'), 0)) as cache_read,
|
|
2226
|
+
sum(coalesce(json_extract(m.data, '$.cost'), 0)) as total_cost
|
|
2227
|
+
FROM message m
|
|
2228
|
+
WHERE ${messageWhere(filters)}
|
|
2229
|
+
GROUP BY day
|
|
2230
|
+
ORDER BY day DESC
|
|
2231
|
+
LIMIT ${Math.max(1, limit)}
|
|
2232
|
+
`.trim();
|
|
2233
|
+
const rows = await queryDb(sql);
|
|
2234
|
+
return rows.map((row) => ({
|
|
2235
|
+
day: row.day ?? "",
|
|
2236
|
+
requests: row.requests ?? 0,
|
|
2237
|
+
sessions: row.sessions ?? 0,
|
|
2238
|
+
totalTokens: row.total_tokens ?? 0,
|
|
2239
|
+
inputTokens: row.input_tokens ?? 0,
|
|
2240
|
+
outputTokens: row.output_tokens ?? 0,
|
|
2241
|
+
reasoningTokens: row.reasoning_tokens ?? 0,
|
|
2242
|
+
cacheRead: row.cache_read ?? 0,
|
|
2243
|
+
totalCost: row.total_cost ?? 0
|
|
2244
|
+
}));
|
|
2245
|
+
}
|
|
2246
|
+
async function getSessionBreakdown(filters = {}) {
|
|
2247
|
+
const limit = filters.limit ?? 15;
|
|
2248
|
+
const sql = `
|
|
2249
|
+
SELECT
|
|
2250
|
+
s.id as session_id,
|
|
2251
|
+
s.title as title,
|
|
2252
|
+
coalesce(json_extract(m.data, '$.providerID'), json_extract(s.model, '$.providerID'), 'unknown') as provider,
|
|
2253
|
+
coalesce(json_extract(m.data, '$.modelID'), json_extract(s.model, '$.id'), 'unknown') as model,
|
|
2254
|
+
count(*) as requests,
|
|
2255
|
+
sum(coalesce(json_extract(m.data, '$.tokens.total'), 0)) as total_tokens,
|
|
2256
|
+
sum(coalesce(json_extract(m.data, '$.tokens.input'), 0)) as input_tokens,
|
|
2257
|
+
sum(coalesce(json_extract(m.data, '$.tokens.output'), 0)) as output_tokens,
|
|
2258
|
+
sum(coalesce(json_extract(m.data, '$.tokens.reasoning'), 0)) as reasoning_tokens,
|
|
2259
|
+
sum(coalesce(json_extract(m.data, '$.tokens.cache.read'), 0)) as cache_read,
|
|
2260
|
+
sum(coalesce(json_extract(m.data, '$.cost'), 0)) as total_cost,
|
|
2261
|
+
date(max(m.time_created) / 1000, 'unixepoch', 'localtime') as day
|
|
2262
|
+
FROM message m
|
|
2263
|
+
JOIN session s ON s.id = m.session_id
|
|
2264
|
+
WHERE ${messageWhere(filters)}
|
|
2265
|
+
GROUP BY s.id, s.title, provider, model
|
|
2266
|
+
ORDER BY max(m.time_created) DESC
|
|
2267
|
+
LIMIT ${Math.max(1, limit)}
|
|
2268
|
+
`.trim();
|
|
2269
|
+
const rows = await queryDb(sql);
|
|
2270
|
+
return rows.map((row) => ({
|
|
2271
|
+
sessionId: row.session_id ?? "",
|
|
2272
|
+
title: row.title ?? "(untitled)",
|
|
2273
|
+
provider: row.provider ?? "unknown",
|
|
2274
|
+
model: row.model ?? "unknown",
|
|
2275
|
+
requests: row.requests ?? 0,
|
|
2276
|
+
totalTokens: row.total_tokens ?? 0,
|
|
2277
|
+
inputTokens: row.input_tokens ?? 0,
|
|
2278
|
+
outputTokens: row.output_tokens ?? 0,
|
|
2279
|
+
reasoningTokens: row.reasoning_tokens ?? 0,
|
|
2280
|
+
cacheRead: row.cache_read ?? 0,
|
|
2281
|
+
totalCost: row.total_cost ?? 0,
|
|
2282
|
+
day: row.day ?? ""
|
|
2283
|
+
}));
|
|
2284
|
+
}
|
|
2285
|
+
async function getErrorStats(filters = {}) {
|
|
2286
|
+
const baseConds = [
|
|
2287
|
+
"json_extract(m.data, '$.role') = 'assistant'"
|
|
2288
|
+
];
|
|
2289
|
+
if (filters.sessionId) baseConds.push(`m.session_id = '${escapeSql(filters.sessionId)}'`);
|
|
2290
|
+
if (filters.provider) baseConds.push(`coalesce(json_extract(m.data, '$.providerID'), '') = '${escapeSql(filters.provider)}'`);
|
|
2291
|
+
if (filters.model) baseConds.push(`coalesce(json_extract(m.data, '$.modelID'), '') = '${escapeSql(filters.model)}'`);
|
|
2292
|
+
if (filters.startDate && isValidDate(filters.startDate)) {
|
|
2293
|
+
baseConds.push(`date(m.time_created / 1000, 'unixepoch', 'localtime') >= '${filters.startDate}'`);
|
|
2294
|
+
}
|
|
2295
|
+
if (filters.endDate && isValidDate(filters.endDate)) {
|
|
2296
|
+
baseConds.push(`date(m.time_created / 1000, 'unixepoch', 'localtime') <= '${filters.endDate}'`);
|
|
2297
|
+
}
|
|
2298
|
+
const baseWhere = baseConds.join(" AND ");
|
|
2299
|
+
const sql = `
|
|
2300
|
+
SELECT
|
|
2301
|
+
coalesce(json_extract(m.data, '$.providerID'), 'unknown') as provider,
|
|
2302
|
+
coalesce(json_extract(m.data, '$.modelID'), 'unknown') as model,
|
|
2303
|
+
count(*) as total,
|
|
2304
|
+
sum(CASE WHEN coalesce(json_extract(m.data, '$.tokens.total'), 0) = 0 THEN 1 ELSE 0 END) as failed
|
|
2305
|
+
FROM message m
|
|
2306
|
+
WHERE ${baseWhere}
|
|
2307
|
+
GROUP BY provider, model
|
|
2308
|
+
ORDER BY failed DESC
|
|
2309
|
+
`.trim();
|
|
2310
|
+
try {
|
|
2311
|
+
const rows = await queryDb(sql);
|
|
2312
|
+
let successCount = 0, failedCount = 0;
|
|
2313
|
+
const byModel = rows.map((r) => {
|
|
2314
|
+
const total = r.total ?? 0;
|
|
2315
|
+
const failed = r.failed ?? 0;
|
|
2316
|
+
const success = total - failed;
|
|
2317
|
+
successCount += success;
|
|
2318
|
+
failedCount += failed;
|
|
2319
|
+
return { provider: r.provider ?? "unknown", model: r.model ?? "unknown", failed, total };
|
|
2320
|
+
});
|
|
2321
|
+
const errorRate = successCount + failedCount > 0 ? failedCount / (successCount + failedCount) : 0;
|
|
2322
|
+
return { successCount, failedCount, errorRate, byModel };
|
|
2323
|
+
} catch {
|
|
2324
|
+
return { successCount: 0, failedCount: 0, errorRate: 0, byModel: [] };
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
async function getUsageReport(filters = {}) {
|
|
2328
|
+
const dailyLimit = filters.limit ?? 365;
|
|
2329
|
+
const [summary, models, providers, daily, sessions, errors] = await Promise.all([
|
|
2330
|
+
getSummary(filters),
|
|
2331
|
+
getModelBreakdown(filters),
|
|
2332
|
+
getProviderBreakdown(filters),
|
|
2333
|
+
getDailyBreakdown(filters),
|
|
2334
|
+
getSessionBreakdown(filters),
|
|
2335
|
+
getErrorStats(filters)
|
|
2336
|
+
]);
|
|
2337
|
+
return {
|
|
2338
|
+
filters,
|
|
2339
|
+
summary,
|
|
2340
|
+
models,
|
|
2341
|
+
providers,
|
|
2342
|
+
daily,
|
|
2343
|
+
sessions,
|
|
2344
|
+
errors,
|
|
2345
|
+
// 恰好等于上限视为可能截断(无法从数据本身区分"正好 365 天")
|
|
2346
|
+
dailyTruncated: daily.length >= dailyLimit
|
|
2347
|
+
};
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
// src/host/v1/adapter.ts
|
|
2351
|
+
var sqlDataSource = {
|
|
2352
|
+
kind: "sql",
|
|
2353
|
+
needsFirstRunNotice: false,
|
|
2354
|
+
isCold: () => false,
|
|
2355
|
+
getUsageReport: (filters) => getUsageReport(filters)
|
|
2356
|
+
};
|
|
2357
|
+
function makeStore(api) {
|
|
2358
|
+
return {
|
|
2359
|
+
get(key, fallback) {
|
|
2360
|
+
try {
|
|
2361
|
+
const v = api.kv?.get?.(key);
|
|
2362
|
+
return v === void 0 || v === null ? fallback : v;
|
|
2363
|
+
} catch {
|
|
2364
|
+
return fallback;
|
|
2365
|
+
}
|
|
2366
|
+
},
|
|
2367
|
+
set(key, value) {
|
|
2368
|
+
try {
|
|
2369
|
+
api.kv?.set?.(key, value);
|
|
2370
|
+
} catch {
|
|
2371
|
+
}
|
|
2372
|
+
},
|
|
2373
|
+
delete(key) {
|
|
2374
|
+
try {
|
|
2375
|
+
api.kv?.delete?.(key);
|
|
2376
|
+
} catch {
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
};
|
|
2380
|
+
}
|
|
2381
|
+
function readTheme(api) {
|
|
2382
|
+
const c = api.theme?.current ?? {};
|
|
2383
|
+
const fallback = c.text ?? c.primary;
|
|
2384
|
+
return {
|
|
2385
|
+
primary: c.primary ?? fallback,
|
|
2386
|
+
text: c.text ?? fallback,
|
|
2387
|
+
textMuted: c.textMuted ?? fallback,
|
|
2388
|
+
background: c.background ?? fallback,
|
|
2389
|
+
border: c.border ?? c.borderSubtle ?? fallback,
|
|
2390
|
+
success: c.success ?? fallback,
|
|
2391
|
+
warning: c.warning ?? fallback,
|
|
2392
|
+
error: c.error ?? fallback
|
|
2393
|
+
};
|
|
2394
|
+
}
|
|
2395
|
+
function normalizeMessageUpdated(event) {
|
|
2396
|
+
const info = event?.properties?.info ?? event?.info;
|
|
2397
|
+
if (!info) return null;
|
|
2398
|
+
const tokens = info.tokens ?? {};
|
|
2399
|
+
const cache2 = tokens.cache ?? {};
|
|
2400
|
+
return {
|
|
2401
|
+
messageID: info.id ?? "",
|
|
2402
|
+
sessionID: info.sessionID ?? "",
|
|
2403
|
+
role: info.role ?? "",
|
|
2404
|
+
providerID: info.providerID ?? "unknown",
|
|
2405
|
+
modelID: info.modelID ?? "unknown",
|
|
2406
|
+
input: tokens.input ?? 0,
|
|
2407
|
+
output: tokens.output ?? 0,
|
|
2408
|
+
reasoning: tokens.reasoning ?? 0,
|
|
2409
|
+
cacheRead: cache2.read ?? 0,
|
|
2410
|
+
cacheWrite: cache2.write ?? 0,
|
|
2411
|
+
total: tokens.total ?? 0,
|
|
2412
|
+
cost: info.cost ?? 0,
|
|
2413
|
+
timeCreated: info.time?.created,
|
|
2414
|
+
timeCompleted: info.time?.completed,
|
|
2415
|
+
raw: event
|
|
2416
|
+
};
|
|
2417
|
+
}
|
|
2418
|
+
function normalizePartUpdated(event) {
|
|
2419
|
+
const part = event?.properties?.part ?? event?.part ?? {};
|
|
2420
|
+
return {
|
|
2421
|
+
messageID: part.messageID,
|
|
2422
|
+
type: part.type,
|
|
2423
|
+
text: part.type === "text" ? part.text : void 0,
|
|
2424
|
+
timeStart: part.time?.start,
|
|
2425
|
+
raw: event
|
|
2426
|
+
};
|
|
2427
|
+
}
|
|
2428
|
+
function createV1Adapter(api) {
|
|
2429
|
+
return {
|
|
2430
|
+
kind: "v1",
|
|
2431
|
+
hostVersion: api?.app?.version ?? "1.x",
|
|
2432
|
+
theme: () => readTheme(api),
|
|
2433
|
+
store: makeStore(api),
|
|
2434
|
+
subscribe(handlers) {
|
|
2435
|
+
const off = [];
|
|
2436
|
+
off.push(
|
|
2437
|
+
api.event.on("message.updated", (event) => {
|
|
2438
|
+
const normalized = normalizeMessageUpdated(event);
|
|
2439
|
+
if (normalized) handlers.onMessageUpdated(normalized);
|
|
2440
|
+
handlers.onInvalidate();
|
|
2441
|
+
})
|
|
2442
|
+
);
|
|
2443
|
+
off.push(
|
|
2444
|
+
api.event.on("message.part.updated", (event) => {
|
|
2445
|
+
handlers.onPartUpdated(normalizePartUpdated(event));
|
|
2446
|
+
})
|
|
2447
|
+
);
|
|
2448
|
+
off.push(
|
|
2449
|
+
api.event.on("message.removed", () => {
|
|
2450
|
+
handlers.onInvalidate();
|
|
2451
|
+
})
|
|
2452
|
+
);
|
|
2453
|
+
return () => {
|
|
2454
|
+
for (const fn of off) {
|
|
2455
|
+
try {
|
|
2456
|
+
fn();
|
|
2457
|
+
} catch {
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
};
|
|
2461
|
+
},
|
|
2462
|
+
registerSidebar(render) {
|
|
2463
|
+
try {
|
|
2464
|
+
api.slots.register({
|
|
2465
|
+
order: 50,
|
|
2466
|
+
slots: {
|
|
2467
|
+
sidebar_content: (_ctx, input) => render({ sessionID: input?.session_id ?? "" })
|
|
2468
|
+
}
|
|
2469
|
+
});
|
|
2470
|
+
} catch {
|
|
2471
|
+
}
|
|
2472
|
+
return () => {
|
|
2473
|
+
};
|
|
2474
|
+
},
|
|
2475
|
+
// registerCommands 不在此实现(审查 #13):v1 的命令经由 host/v1/commands.tsx
|
|
2476
|
+
// 用原生 DialogSelect 实现(UX 优于通用 select),HostAdapter.registerCommands
|
|
2477
|
+
// 在 v1 下不可达,契约中已改为可选 —— 详见 docs/CODE-REVIEW-2026-09-13.md
|
|
2478
|
+
notify(message, variant = "info") {
|
|
2479
|
+
try {
|
|
2480
|
+
api.ui?.toast?.({ message, variant });
|
|
2481
|
+
} catch {
|
|
2482
|
+
}
|
|
2483
|
+
},
|
|
2484
|
+
async alert(input) {
|
|
2485
|
+
try {
|
|
2486
|
+
api.ui?.toast?.({ message: `${input.title}: ${input.message}`, variant: "info" });
|
|
2487
|
+
} catch {
|
|
2488
|
+
}
|
|
2489
|
+
},
|
|
2490
|
+
async select() {
|
|
2491
|
+
return void 0;
|
|
2492
|
+
},
|
|
2493
|
+
onDispose(fn) {
|
|
2494
|
+
try {
|
|
2495
|
+
api.lifecycle?.onDispose?.(fn);
|
|
2496
|
+
} catch {
|
|
2497
|
+
}
|
|
2498
|
+
},
|
|
2499
|
+
sessionMessages(sessionID) {
|
|
2500
|
+
try {
|
|
2501
|
+
return api.state?.session?.messages?.(sessionID) ?? [];
|
|
2502
|
+
} catch {
|
|
2503
|
+
return [];
|
|
2504
|
+
}
|
|
2505
|
+
},
|
|
2506
|
+
messageParts(messageID) {
|
|
2507
|
+
try {
|
|
2508
|
+
return api.state?.part?.(messageID) ?? [];
|
|
2509
|
+
} catch {
|
|
2510
|
+
return [];
|
|
2511
|
+
}
|
|
2512
|
+
},
|
|
2513
|
+
appConfig() {
|
|
2514
|
+
try {
|
|
2515
|
+
return api.state?.config ?? {};
|
|
2516
|
+
} catch {
|
|
2517
|
+
return {};
|
|
2518
|
+
}
|
|
2519
|
+
},
|
|
2520
|
+
onPartUpdated(handler) {
|
|
2521
|
+
try {
|
|
2522
|
+
return api.event.on("message.part.updated", () => handler());
|
|
2523
|
+
} catch {
|
|
2524
|
+
return () => {
|
|
2525
|
+
};
|
|
2526
|
+
}
|
|
2527
|
+
},
|
|
2528
|
+
dataSource: sqlDataSource
|
|
2529
|
+
};
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
// src/host/v1/commands.tsx
|
|
2533
|
+
import { createComponent as _$createComponent3 } from "@opentui/solid";
|
|
2534
|
+
|
|
2535
|
+
// src/kernel/report.ts
|
|
2536
|
+
import { existsSync as existsSync3, mkdirSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
2537
|
+
import { join as join4 } from "node:path";
|
|
2538
|
+
import { homedir as homedir3 } from "node:os";
|
|
2539
|
+
import { spawn } from "node:child_process";
|
|
2540
|
+
|
|
2541
|
+
// src/kernel/report-html.ts
|
|
2542
|
+
function jsonLit(value) {
|
|
2543
|
+
return JSON.stringify(value).replace(/</g, "\\u003c");
|
|
2544
|
+
}
|
|
2545
|
+
function escapeHtml(value) {
|
|
2546
|
+
return String(value ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
2547
|
+
}
|
|
2548
|
+
function fmtPercent(n) {
|
|
2549
|
+
return (n * 100).toFixed(1) + "%";
|
|
2550
|
+
}
|
|
2551
|
+
function cacheHitRate(input, cacheRead) {
|
|
2552
|
+
if (input + cacheRead === 0) return 0;
|
|
2553
|
+
return cacheRead / (input + cacheRead);
|
|
2554
|
+
}
|
|
2555
|
+
function sortModelsByUsage(models) {
|
|
2556
|
+
return [...models].filter((m) => m.totalTokens > 0).sort((a, b) => b.totalTokens - a.totalTokens);
|
|
2557
|
+
}
|
|
2558
|
+
function renderMeta(data) {
|
|
2559
|
+
const m = data.meta;
|
|
2560
|
+
return `TokenWatch Usage Report \xB7 ${m.dateRange.start} \u2192 ${m.dateRange.end} \xB7 generated ${m.generatedAt}`;
|
|
2561
|
+
}
|
|
2562
|
+
function renderKpiCards(data) {
|
|
2563
|
+
const s = data.summary;
|
|
2564
|
+
const hitRate = cacheHitRate(s.inputTokens, s.cacheRead);
|
|
2565
|
+
const hitRatePct = fmtPercent(hitRate);
|
|
2566
|
+
let tpsSum = 0, tpsReqs = 0;
|
|
2567
|
+
for (const p of data.perfSummary) {
|
|
2568
|
+
if (p.avgTPS != null && p.avgTPS > 0) {
|
|
2569
|
+
tpsSum += p.avgTPS * p.tpsCount;
|
|
2570
|
+
tpsReqs += p.tpsCount;
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
const avgTpsRaw = tpsReqs > 0 ? tpsSum / tpsReqs : 0;
|
|
2574
|
+
const avgTps = tpsReqs > 0 ? avgTpsRaw.toFixed(1) : "\u2014";
|
|
2575
|
+
const isHighCache = hitRate >= 0.5;
|
|
2576
|
+
const errors = data.errors;
|
|
2577
|
+
const errorRatePct = errors ? (errors.errorRate * 100).toFixed(1) + "%" : "\u2014";
|
|
2578
|
+
const errorColor = errors && errors.errorRate >= 0.05 ? "var(--output)" : errors && errors.errorRate > 0 ? "var(--tps)" : "var(--cache)";
|
|
2579
|
+
return `
|
|
2580
|
+
<div class="kpi-row">
|
|
2581
|
+
<div class="kpi-card">
|
|
2582
|
+
<div class="kpi-label">Total Tokens</div>
|
|
2583
|
+
<div class="kpi-value">${formatTokens(s.totalTokens)}</div>
|
|
2584
|
+
</div>
|
|
2585
|
+
<div class="kpi-card${isHighCache ? " kpi-glow" : ""}">
|
|
2586
|
+
<div class="kpi-label">Cache Hit Rate</div>
|
|
2587
|
+
<div class="kpi-value" style="color:var(--cache)">${hitRatePct}</div>
|
|
2588
|
+
</div>
|
|
2589
|
+
<div class="kpi-card">
|
|
2590
|
+
<div class="kpi-label">Avg TPS</div>
|
|
2591
|
+
<div class="kpi-value" style="color:var(--tps)">${avgTps}</div>
|
|
2592
|
+
</div>
|
|
2593
|
+
<div class="kpi-card">
|
|
2594
|
+
<div class="kpi-label">Requests</div>
|
|
2595
|
+
<div class="kpi-value">${s.requestCount}</div>
|
|
2596
|
+
</div>
|
|
2597
|
+
<div class="kpi-card">
|
|
2598
|
+
<div class="kpi-label">Total Cost</div>
|
|
2599
|
+
<div class="kpi-value" style="color:var(--tps)">${formatCost(s.totalCost)}</div>
|
|
2600
|
+
</div>
|
|
2601
|
+
<div class="kpi-card">
|
|
2602
|
+
<div class="kpi-label">Error Rate</div>
|
|
2603
|
+
<div class="kpi-value" style="color:${errorColor}">${errorRatePct}</div>
|
|
2604
|
+
</div>
|
|
2605
|
+
</div>`;
|
|
2606
|
+
}
|
|
2607
|
+
function renderModelChartInit(data) {
|
|
2608
|
+
const models = sortModelsByUsage(data.models).filter((m) => m.totalTokens >= 1e6);
|
|
2609
|
+
const names = models.map((m) => m.model);
|
|
2610
|
+
const inputData = models.map((m) => m.inputTokens);
|
|
2611
|
+
const outputData = models.map((m) => m.outputTokens);
|
|
2612
|
+
const cacheData = models.map((m) => m.cacheRead);
|
|
2613
|
+
const tpsData = models.map((m) => {
|
|
2614
|
+
const perf = data.perfSummary.find((p) => p.model === `${m.provider}/${m.model}`);
|
|
2615
|
+
return perf?.avgTPS ?? null;
|
|
2616
|
+
});
|
|
2617
|
+
return `var modelNames = ${jsonLit(names)};
|
|
2618
|
+
var modelInput = ${jsonLit(inputData)};
|
|
2619
|
+
var modelOutput = ${jsonLit(outputData)};
|
|
2620
|
+
var modelCache = ${jsonLit(cacheData)};
|
|
2621
|
+
var modelTps = ${jsonLit(tpsData)};
|
|
2622
|
+
|
|
2623
|
+
function initModelChart() {
|
|
2624
|
+
var el = document.getElementById('model-chart');
|
|
2625
|
+
if (!el) return;
|
|
2626
|
+
var chart = echarts.init(el);
|
|
2627
|
+
window.modelChart = chart;
|
|
2628
|
+
renderModelChart(chart);
|
|
2629
|
+
return chart;
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2632
|
+
function renderModelChart(chart) {
|
|
2633
|
+
var totals = modelInput.map(function(v, i) { return v + modelOutput[i] + modelCache[i]; });
|
|
2634
|
+
var option = {
|
|
2635
|
+
tooltip: {
|
|
2636
|
+
trigger: 'axis',
|
|
2637
|
+
axisPointer: { type: 'shadow' },
|
|
2638
|
+
formatter: function(params) {
|
|
2639
|
+
var html = '<b>' + params[0].axisValue + '</b><br/>';
|
|
2640
|
+
var total = 0;
|
|
2641
|
+
params.forEach(function(p) {
|
|
2642
|
+
if (p.seriesName !== 'TPS') {
|
|
2643
|
+
html += p.marker + ' ' + p.seriesName + ': ' + fmt(p.value) + '<br/>';
|
|
2644
|
+
total += p.value;
|
|
2645
|
+
}
|
|
2646
|
+
});
|
|
2647
|
+
html += 'Total: ' + fmt(total) + '<br/>';
|
|
2648
|
+
var tpsParam = params.find(function(p) { return p.seriesName === 'TPS'; });
|
|
2649
|
+
if (tpsParam && tpsParam.value != null) {
|
|
2650
|
+
html += tpsParam.marker + ' TPS: ' + tpsParam.value.toFixed(1) + '<br/>';
|
|
2651
|
+
}
|
|
2652
|
+
return html;
|
|
2653
|
+
}
|
|
2654
|
+
},
|
|
2655
|
+
legend: {
|
|
2656
|
+
data: ['Input', 'Output', 'Cache', 'TPS'],
|
|
2657
|
+
textStyle: { color: '#B0B0C0' },
|
|
2658
|
+
top: 5
|
|
2659
|
+
},
|
|
2660
|
+
grid: { left: 60, right: 60, bottom: 100, top: 50 },
|
|
2661
|
+
xAxis: {
|
|
2662
|
+
type: 'category',
|
|
2663
|
+
data: modelNames,
|
|
2664
|
+
axisLabel: { color: '#B0B0C0', rotate: 45, interval: 0, fontSize: 10 },
|
|
2665
|
+
axisLine: { lineStyle: { color: '#2A2A35' } }
|
|
2666
|
+
},
|
|
2667
|
+
yAxis: [
|
|
2668
|
+
{
|
|
2669
|
+
type: 'value',
|
|
2670
|
+
name: 'Tokens',
|
|
2671
|
+
nameTextStyle: { color: '#B0B0C0' },
|
|
2672
|
+
axisLabel: {
|
|
2673
|
+
color: '#B0B0C0',
|
|
2674
|
+
formatter: fmt
|
|
2675
|
+
},
|
|
2676
|
+
splitLine: { lineStyle: { color: '#2A2A35', type: 'dashed' } }
|
|
2677
|
+
},
|
|
2678
|
+
{
|
|
2679
|
+
type: 'value',
|
|
2680
|
+
name: 'TPS',
|
|
2681
|
+
nameTextStyle: { color: '#FFB800' },
|
|
2682
|
+
axisLabel: { color: '#FFB800', formatter: function(v) { return v.toFixed(1); } },
|
|
2683
|
+
splitLine: { show: false }
|
|
2684
|
+
}
|
|
2685
|
+
],
|
|
2686
|
+
series: [
|
|
2687
|
+
{
|
|
2688
|
+
name: 'Input',
|
|
2689
|
+
type: 'bar',
|
|
2690
|
+
stack: 'tokens',
|
|
2691
|
+
data: modelInput,
|
|
2692
|
+
itemStyle: { color: '#00D1FF' },
|
|
2693
|
+
barMaxWidth: 40
|
|
2694
|
+
},
|
|
2695
|
+
{
|
|
2696
|
+
name: 'Cache',
|
|
2697
|
+
type: 'bar',
|
|
2698
|
+
stack: 'tokens',
|
|
2699
|
+
data: modelCache,
|
|
2700
|
+
itemStyle: { color: '#00F593' },
|
|
2701
|
+
barMaxWidth: 40,
|
|
2702
|
+
label: {
|
|
2703
|
+
show: true,
|
|
2704
|
+
position: 'inside',
|
|
2705
|
+
formatter: function(p) {
|
|
2706
|
+
var cache = modelCache[p.dataIndex];
|
|
2707
|
+
var input = modelInput[p.dataIndex];
|
|
2708
|
+
if (cache === 0) return '';
|
|
2709
|
+
return (cache / (input + cache) * 100).toFixed(0) + '%';
|
|
2710
|
+
},
|
|
2711
|
+
color: '#fff', fontSize: 10, fontWeight: 'bold'
|
|
2712
|
+
}
|
|
2713
|
+
},
|
|
2714
|
+
{
|
|
2715
|
+
name: 'Output',
|
|
2716
|
+
type: 'bar',
|
|
2717
|
+
stack: 'tokens',
|
|
2718
|
+
data: modelOutput,
|
|
2719
|
+
itemStyle: { color: '#B545FF' },
|
|
2720
|
+
barMaxWidth: 40,
|
|
2721
|
+
label: {
|
|
2722
|
+
show: true,
|
|
2723
|
+
position: 'top',
|
|
2724
|
+
formatter: function(p) {
|
|
2725
|
+
var total = modelInput[p.dataIndex] + modelOutput[p.dataIndex] + modelCache[p.dataIndex];
|
|
2726
|
+
return total > 0 ? fmt(total) : '';
|
|
2727
|
+
},
|
|
2728
|
+
color: '#fff', fontSize: 10, fontWeight: 'bold'
|
|
2729
|
+
}
|
|
2730
|
+
},
|
|
2731
|
+
{
|
|
2732
|
+
name: 'TPS',
|
|
2733
|
+
type: 'scatter',
|
|
2734
|
+
yAxisIndex: 1,
|
|
2735
|
+
data: modelTps,
|
|
2736
|
+
symbol: 'diamond',
|
|
2737
|
+
symbolSize: function(val) { return val != null && val > 0 ? 13 : 0; },
|
|
2738
|
+
itemStyle: { color: '#FFB800' },
|
|
2739
|
+
label: {
|
|
2740
|
+
show: true,
|
|
2741
|
+
position: 'right',
|
|
2742
|
+
formatter: function(p) { return p.value != null && p.value > 0 ? p.value.toFixed(1) : ''; },
|
|
2743
|
+
color: '#FFB800', fontSize: 10
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
]
|
|
2747
|
+
};
|
|
2748
|
+
chart.setOption(option);
|
|
2749
|
+
chart.resize();
|
|
2750
|
+
}`;
|
|
2751
|
+
}
|
|
2752
|
+
function renderScatterChartInit(data) {
|
|
2753
|
+
const validPerf = data.perfSummary.filter(
|
|
2754
|
+
(p) => p.requestCount > 0 && p.totalInput + p.totalOutput + p.totalCacheRead + p.totalCacheWrite > 0
|
|
2755
|
+
);
|
|
2756
|
+
if (validPerf.length === 0) return "";
|
|
2757
|
+
const sorted = [...validPerf].sort((a, b) => {
|
|
2758
|
+
if (a.avgTPS == null && b.avgTPS == null) return 0;
|
|
2759
|
+
if (a.avgTPS == null) return 1;
|
|
2760
|
+
if (b.avgTPS == null) return -1;
|
|
2761
|
+
return b.avgTPS - a.avgTPS;
|
|
2762
|
+
});
|
|
2763
|
+
const names = sorted.map((p) => p.model);
|
|
2764
|
+
const tpsValues = sorted.map((p) => p.avgTPS ?? 0);
|
|
2765
|
+
const ttftValues = sorted.map((p) => p.avgTTFT ?? 0);
|
|
2766
|
+
const costValues = sorted.map((p) => {
|
|
2767
|
+
const billable = p.totalInput + p.totalOutput + p.totalCacheRead + p.totalCacheWrite;
|
|
2768
|
+
return billable > 0 ? p.totalCost / billable * 1e3 : 0;
|
|
2769
|
+
});
|
|
2770
|
+
const hitRates = sorted.map((p) => p.cacheHitRate ?? 0);
|
|
2771
|
+
const reqCounts = sorted.map((p) => p.requestCount);
|
|
2772
|
+
return `
|
|
2773
|
+
var effNames = ${jsonLit(names)};
|
|
2774
|
+
var effTps = ${jsonLit(tpsValues)};
|
|
2775
|
+
var effTtft = ${jsonLit(ttftValues)};
|
|
2776
|
+
var effCost = ${jsonLit(costValues)};
|
|
2777
|
+
var effHit = ${jsonLit(hitRates)};
|
|
2778
|
+
var effReq = ${jsonLit(reqCounts)};
|
|
2779
|
+
|
|
2780
|
+
function initScatterChart() {
|
|
2781
|
+
var el = document.getElementById('scatter-chart');
|
|
2782
|
+
if (!el) return;
|
|
2783
|
+
var chart = echarts.init(el);
|
|
2784
|
+
window.scatterChart = chart;
|
|
2785
|
+
|
|
2786
|
+
// TPS \u8D8A\u9AD8\u8D8A\u7EFF\uFF0C\u8D8A\u4F4E\u8D8A\u7D2B\uFF0C\u65E0\u6570\u636E\u4E3A\u7070
|
|
2787
|
+
var maxTps = Math.max.apply(null, effTps.filter(function(v){ return v > 0; })) || 1;
|
|
2788
|
+
var barColors = effTps.map(function(v) {
|
|
2789
|
+
if (v <= 0) return '#444455';
|
|
2790
|
+
var r = v / maxTps;
|
|
2791
|
+
if (r >= 0.8) return '#00F593';
|
|
2792
|
+
if (r >= 0.5) return '#FFB800';
|
|
2793
|
+
return '#B545FF';
|
|
2794
|
+
});
|
|
2795
|
+
|
|
2796
|
+
var option = {
|
|
2797
|
+
tooltip: {
|
|
2798
|
+
trigger: 'axis',
|
|
2799
|
+
axisPointer: { type: 'none' },
|
|
2800
|
+
formatter: function(params) {
|
|
2801
|
+
var i = params[0].dataIndex;
|
|
2802
|
+
var tps = effTps[i] > 0 ? effTps[i].toFixed(1) + ' tok/s' : '\u2014';
|
|
2803
|
+
var ttft = effTtft[i] > 0 ? effTtft[i].toFixed(0) + ' ms' : '\u2014';
|
|
2804
|
+
var cost = effCost[i] > 0 ? '$' + effCost[i].toFixed(4) + '/1K' : '\u2014';
|
|
2805
|
+
var hit = effHit[i] > 0 ? effHit[i].toFixed(1) + '%' : '\u2014';
|
|
2806
|
+
return '<b>' + effNames[i] + '</b><br/>' +
|
|
2807
|
+
'\u25B6 TPS: ' + tps + '<br/>' +
|
|
2808
|
+
'\u23F1 TTFT: ' + ttft + '<br/>' +
|
|
2809
|
+
'\u{1F4B0} Cost/1K: ' + cost + '<br/>' +
|
|
2810
|
+
'\u{1F4BE} Cache Hit: ' + hit + '<br/>' +
|
|
2811
|
+
'Requests: ' + effReq[i];
|
|
2812
|
+
}
|
|
2813
|
+
},
|
|
2814
|
+
grid: { left: 20, right: 280, bottom: 30, top: 20, containLabel: true },
|
|
2815
|
+
xAxis: {
|
|
2816
|
+
type: 'value',
|
|
2817
|
+
name: 'Avg TPS (tokens / sec)',
|
|
2818
|
+
nameTextStyle: { color: '#B0B0C0', fontSize: 11 },
|
|
2819
|
+
axisLabel: { color: '#B0B0C0', formatter: function(v) { return v > 0 ? v.toFixed(0) : '0'; } },
|
|
2820
|
+
splitLine: { lineStyle: { color: '#2A2A35', type: 'dashed' } }
|
|
2821
|
+
},
|
|
2822
|
+
yAxis: {
|
|
2823
|
+
type: 'category',
|
|
2824
|
+
data: effNames,
|
|
2825
|
+
inverse: true,
|
|
2826
|
+
axisLabel: {
|
|
2827
|
+
color: '#E0E0F0',
|
|
2828
|
+
fontSize: 11,
|
|
2829
|
+
formatter: function(v) { return v.length > 50 ? v.slice(0, 48) + '\u2026' : v; }
|
|
2830
|
+
},
|
|
2831
|
+
axisLine: { show: false },
|
|
2832
|
+
axisTick: { show: false }
|
|
2833
|
+
},
|
|
2834
|
+
series: [{
|
|
2835
|
+
type: 'bar',
|
|
2836
|
+
data: effTps.map(function(v, i) {
|
|
2837
|
+
return { value: v > 0 ? v : 0.001, itemStyle: { color: barColors[i], borderRadius: [0, 4, 4, 0] } };
|
|
2838
|
+
}),
|
|
2839
|
+
barMaxWidth: 22,
|
|
2840
|
+
label: {
|
|
2841
|
+
show: true,
|
|
2842
|
+
position: 'right',
|
|
2843
|
+
color: '#E0E0F0',
|
|
2844
|
+
fontSize: 10,
|
|
2845
|
+
formatter: function(p) {
|
|
2846
|
+
var i = p.dataIndex;
|
|
2847
|
+
var parts = [effTps[i] > 0 ? effTps[i].toFixed(1) + ' t/s' : '\u2014'];
|
|
2848
|
+
if (effTtft[i] > 0) parts.push('TTFT ' + effTtft[i].toFixed(0) + 'ms');
|
|
2849
|
+
if (effCost[i] > 0) parts.push('$' + effCost[i].toFixed(4) + '/1K');
|
|
2850
|
+
return parts.join(' ');
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
}]
|
|
2854
|
+
};
|
|
2855
|
+
chart.setOption(option);
|
|
2856
|
+
chart.resize();
|
|
2857
|
+
}
|
|
2858
|
+
`;
|
|
2859
|
+
}
|
|
2860
|
+
function providerBorderColor(provider) {
|
|
2861
|
+
const colors = { opencode: "#00F593", deepseek: "#00D1FF", nvidia: "#B545FF", modelscope: "#FFB800" };
|
|
2862
|
+
return colors[provider] || "#2A2A35";
|
|
2863
|
+
}
|
|
2864
|
+
function renderProviderCards(data) {
|
|
2865
|
+
const sorted = [...data.providers].sort((a, b) => b.totalTokens - a.totalTokens);
|
|
2866
|
+
const top = sorted.slice(0, 10);
|
|
2867
|
+
const remaining = sorted.length - 10;
|
|
2868
|
+
const cards = top.map((p) => {
|
|
2869
|
+
const modelCount = data.models.filter((m) => m.provider === p.provider).length;
|
|
2870
|
+
const perfItems = data.perfSummary.filter((ps) => ps.providerID === p.provider);
|
|
2871
|
+
let ttftSum = 0, ttftReqs = 0;
|
|
2872
|
+
for (const x of perfItems) {
|
|
2873
|
+
if (x.avgTTFT != null && x.avgTTFT > 0) {
|
|
2874
|
+
ttftSum += x.avgTTFT * x.ttftCount;
|
|
2875
|
+
ttftReqs += x.ttftCount;
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
const avgTtft = ttftReqs > 0 ? ttftSum / ttftReqs : null;
|
|
2879
|
+
let tpsSum = 0, tpsReqs = 0;
|
|
2880
|
+
for (const x of perfItems) {
|
|
2881
|
+
if (x.avgTPS != null && x.avgTPS > 0) {
|
|
2882
|
+
tpsSum += x.avgTPS * x.tpsCount;
|
|
2883
|
+
tpsReqs += x.tpsCount;
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
const avgTps = tpsReqs > 0 ? tpsSum / tpsReqs : null;
|
|
2887
|
+
return `
|
|
2888
|
+
<div class="provider-card" style="border-color:${providerBorderColor(p.provider)}">
|
|
2889
|
+
<div class="provider-name">${escapeHtml(p.provider)}</div>
|
|
2890
|
+
<div class="provider-stat"><span class="stat-label">Tokens</span><span>${formatTokens(p.totalTokens)}</span></div>
|
|
2891
|
+
<div class="provider-stat"><span class="stat-label">Cost</span><span>${formatCost(p.totalCost)}</span></div>
|
|
2892
|
+
<div class="provider-stat"><span class="stat-label">Avg TTFT</span><span>${avgTtft != null ? avgTtft.toFixed(0) + "ms" : "\u2014"}</span></div>
|
|
2893
|
+
<div class="provider-stat"><span class="stat-label">Avg TPS</span><span>${avgTps != null ? avgTps.toFixed(1) : "\u2014"}</span></div>
|
|
2894
|
+
<div class="provider-stat"><span class="stat-label">Models</span><span>${modelCount}</span></div>
|
|
2895
|
+
</div>`;
|
|
2896
|
+
}).join("\n");
|
|
2897
|
+
const moreHint = remaining > 0 ? `<div class="provider-more">+ ${remaining} more provider${remaining > 1 ? "s" : ""} not shown</div>` : "";
|
|
2898
|
+
return cards + moreHint;
|
|
2899
|
+
}
|
|
2900
|
+
function renderModelAnalyticsSection(data) {
|
|
2901
|
+
const usageRows = sortModelsByUsage(data.models).map((m) => {
|
|
2902
|
+
const hitRate = cacheHitRate(m.inputTokens, m.cacheRead);
|
|
2903
|
+
const hitColor = hitRate >= 0.85 ? "var(--cache)" : hitRate >= 0.7 ? "var(--tps)" : "var(--output)";
|
|
2904
|
+
const perf = data.perfSummary.find((p) => p.model === `${m.provider}/${m.model}`);
|
|
2905
|
+
const ttft = perf?.avgTTFT != null ? perf.avgTTFT.toFixed(0) + "ms" : "\u2014";
|
|
2906
|
+
const p95ttft = perf?.p95TTFT != null ? perf.p95TTFT.toFixed(0) + "ms" : "\u2014";
|
|
2907
|
+
const tps = perf?.avgTPS != null ? perf.avgTPS.toFixed(1) : "\u2014";
|
|
2908
|
+
return `<tr>
|
|
2909
|
+
<td>${escapeHtml(m.model)}</td>
|
|
2910
|
+
<td>${escapeHtml(m.provider)}</td>
|
|
2911
|
+
<td>${m.requests}</td>
|
|
2912
|
+
<td>${formatTokens(m.totalTokens)}</td>
|
|
2913
|
+
<td>${formatTokens(m.inputTokens)}</td>
|
|
2914
|
+
<td>${formatTokens(m.outputTokens)}</td>
|
|
2915
|
+
<td>${formatTokens(m.cacheRead)}</td>
|
|
2916
|
+
<td style="color:${hitColor};font-weight:600">${fmtPercent(hitRate)}</td>
|
|
2917
|
+
<td>${ttft}</td>
|
|
2918
|
+
<td style="color:var(--tps);font-size:0.85em">${p95ttft}</td>
|
|
2919
|
+
<td>${tps}</td>
|
|
2920
|
+
<td>${formatCost(m.totalCost)}</td>
|
|
2921
|
+
</tr>`;
|
|
2922
|
+
}).join("\n");
|
|
2923
|
+
const validPerf = data.perfSummary.filter(
|
|
2924
|
+
(p) => p.requestCount > 0 && p.totalInput + p.totalOutput + p.totalCacheRead + p.totalCacheWrite > 0
|
|
2925
|
+
);
|
|
2926
|
+
const fmtMs = (v) => v != null ? v.toFixed(0) + "ms" : "\u2014";
|
|
2927
|
+
const perfRows = validPerf.map((p) => {
|
|
2928
|
+
const hitColor = p.cacheHitRate != null && p.cacheHitRate >= 85 ? "var(--cache)" : p.cacheHitRate != null && p.cacheHitRate >= 70 ? "var(--tps)" : "var(--output)";
|
|
2929
|
+
return `<tr>
|
|
2930
|
+
<td>${escapeHtml(p.model)}</td>
|
|
2931
|
+
<td>${p.requestCount}</td>
|
|
2932
|
+
<td>${fmtMs(p.avgTTFT)}</td>
|
|
2933
|
+
<td>${fmtMs(p.p50TTFT)}</td>
|
|
2934
|
+
<td>${fmtMs(p.p95TTFT)}</td>
|
|
2935
|
+
<td>${fmtMs(p.p99TTFT)}</td>
|
|
2936
|
+
<td>${fmtMs(p.avgLatency)}</td>
|
|
2937
|
+
<td>${fmtMs(p.p50Latency)}</td>
|
|
2938
|
+
<td>${fmtMs(p.p95Latency)}</td>
|
|
2939
|
+
<td>${fmtMs(p.p99Latency)}</td>
|
|
2940
|
+
<td style="color:${hitColor};font-weight:600">${p.cacheHitRate != null ? p.cacheHitRate.toFixed(1) + "%" : "\u2014"}</td>
|
|
2941
|
+
</tr>`;
|
|
2942
|
+
}).join("\n");
|
|
2943
|
+
const errors = data.errors;
|
|
2944
|
+
const hasErrors = !!(errors && errors.failedCount > 0);
|
|
2945
|
+
let errorTabBtn = "";
|
|
2946
|
+
let errorTabContent = "";
|
|
2947
|
+
if (hasErrors) {
|
|
2948
|
+
const errorRatePct = (errors.errorRate * 100).toFixed(2) + "%";
|
|
2949
|
+
const rateColor = errors.errorRate >= 0.05 ? "var(--output)" : "var(--tps)";
|
|
2950
|
+
const cellColor = errors.errorRate >= 0.05 ? "var(--output)" : "var(--tps)";
|
|
2951
|
+
const errorRows = errors.byModel.filter((m) => m.failed > 0).map((m) => {
|
|
2952
|
+
const modelRate = m.total > 0 ? (m.failed / m.total * 100).toFixed(1) + "%" : "\u2014";
|
|
2953
|
+
return `<tr>
|
|
2954
|
+
<td>${escapeHtml(m.provider)}</td>
|
|
2955
|
+
<td>${escapeHtml(m.model)}</td>
|
|
2956
|
+
<td>${m.total}</td>
|
|
2957
|
+
<td style="color:var(--output)">${m.failed}</td>
|
|
2958
|
+
<td style="color:var(--tps)">${m.total - m.failed}</td>
|
|
2959
|
+
<td style="color:${cellColor}">${modelRate}</td>
|
|
2960
|
+
</tr>`;
|
|
2961
|
+
}).join("\n");
|
|
2962
|
+
errorTabBtn = `
|
|
2963
|
+
<button class="tab-btn" data-mtab="errors" onclick="switchModelTab('errors')">
|
|
2964
|
+
Failed Requests <span style="color:var(--output);margin-left:4px;font-size:0.85em">(${errors.failedCount})</span>
|
|
2965
|
+
</button>`;
|
|
2966
|
+
errorTabContent = `
|
|
2967
|
+
<div id="model-tab-errors" class="tab-content">
|
|
2968
|
+
<p style="font-size:12px;color:${rateColor};padding:8px 0 6px">
|
|
2969
|
+
Overall error rate: <strong>${errorRatePct}</strong> —
|
|
2970
|
+
${errors.failedCount} failed / ${errors.successCount + errors.failedCount} total
|
|
2971
|
+
</p>
|
|
2972
|
+
<table id="errors-table" class="data-table">
|
|
2973
|
+
<thead><tr>
|
|
2974
|
+
<th>Provider</th><th>Model</th><th>Total</th>
|
|
2975
|
+
<th>Failed</th><th>Success</th><th>Error Rate</th>
|
|
2976
|
+
</tr></thead>
|
|
2977
|
+
<tbody>${errorRows}</tbody>
|
|
2978
|
+
</table>
|
|
2979
|
+
<div class="pagination-ctrl" id="errors-table-ctrl">
|
|
2980
|
+
<button class="page-btn" id="errors-table-prev">\u2190 Prev</button>
|
|
2981
|
+
<span class="page-info" id="errors-table-info"></span>
|
|
2982
|
+
<button class="page-btn" id="errors-table-next">Next \u2192</button>
|
|
2983
|
+
</div>
|
|
2984
|
+
</div>`;
|
|
2985
|
+
}
|
|
2986
|
+
return `
|
|
2987
|
+
<div class="section">
|
|
2988
|
+
<div class="section-title">Model Analytics</div>
|
|
2989
|
+
<div class="tab-bar">
|
|
2990
|
+
<button class="tab-btn active" data-mtab="usage" onclick="switchModelTab('usage')">Usage Breakdown</button>
|
|
2991
|
+
<button class="tab-btn" data-mtab="perf" onclick="switchModelTab('perf')">Latency Percentiles</button>
|
|
2992
|
+
${errorTabBtn}
|
|
2993
|
+
</div>
|
|
2994
|
+
|
|
2995
|
+
<div id="model-tab-usage" class="tab-content active">
|
|
2996
|
+
<table id="usage-table" class="data-table">
|
|
2997
|
+
<thead><tr>
|
|
2998
|
+
<th>Model</th><th>Provider</th><th>Req</th><th>Total</th>
|
|
2999
|
+
<th>Input</th><th>Output</th><th>Cache</th><th>Hit Rate</th>
|
|
3000
|
+
<th>Avg TTFT</th><th>P95 TTFT</th><th>TPS</th><th>Cost</th>
|
|
3001
|
+
</tr></thead>
|
|
3002
|
+
<tbody>${usageRows}</tbody>
|
|
3003
|
+
</table>
|
|
3004
|
+
<div class="pagination-ctrl" id="usage-table-ctrl">
|
|
3005
|
+
<button class="page-btn" id="usage-table-prev">\u2190 Prev</button>
|
|
3006
|
+
<span class="page-info" id="usage-table-info"></span>
|
|
3007
|
+
<button class="page-btn" id="usage-table-next">Next \u2192</button>
|
|
3008
|
+
</div>
|
|
3009
|
+
</div>
|
|
3010
|
+
|
|
3011
|
+
<div id="model-tab-perf" class="tab-content">
|
|
3012
|
+
${validPerf.length > 0 ? `
|
|
3013
|
+
<table id="perf-table" class="data-table">
|
|
3014
|
+
<thead><tr>
|
|
3015
|
+
<th>Model</th><th>Req</th>
|
|
3016
|
+
<th>Avg TTFT</th><th>P50 TTFT</th><th>P95 TTFT</th><th>P99 TTFT</th>
|
|
3017
|
+
<th>Avg E2E</th><th>P50 E2E</th><th>P95 E2E</th><th>P99 E2E</th>
|
|
3018
|
+
<th>Cache Hit</th>
|
|
3019
|
+
</tr></thead>
|
|
3020
|
+
<tbody>${perfRows}</tbody>
|
|
3021
|
+
</table>
|
|
3022
|
+
<div class="pagination-ctrl" id="perf-table-ctrl">
|
|
3023
|
+
<button class="page-btn" id="perf-table-prev">\u2190 Prev</button>
|
|
3024
|
+
<span class="page-info" id="perf-table-info"></span>
|
|
3025
|
+
<button class="page-btn" id="perf-table-next">Next \u2192</button>
|
|
3026
|
+
</div>` : '<div class="empty-state">No performance data available for this period.</div>'}
|
|
3027
|
+
</div>
|
|
3028
|
+
|
|
3029
|
+
${errorTabContent}
|
|
3030
|
+
</div>`;
|
|
3031
|
+
}
|
|
3032
|
+
function renderDailyTrendInit(data) {
|
|
3033
|
+
const days = data.daily.slice().reverse().map((d) => d.day);
|
|
3034
|
+
const tokens = data.daily.slice().reverse().map((d) => d.totalTokens);
|
|
3035
|
+
const costs = data.daily.slice().reverse().map((d) => d.totalCost);
|
|
3036
|
+
return `
|
|
3037
|
+
var dailyDays = ${jsonLit(days)};
|
|
3038
|
+
var dailyTokens = ${jsonLit(tokens)};
|
|
3039
|
+
var dailyCosts = ${jsonLit(costs)};
|
|
3040
|
+
|
|
3041
|
+
function initDailyChart() {
|
|
3042
|
+
var el = document.getElementById('daily-chart');
|
|
3043
|
+
if (!el) return;
|
|
3044
|
+
var chart = echarts.init(el);
|
|
3045
|
+
window.dailyChart = chart;
|
|
3046
|
+
var option = {
|
|
3047
|
+
tooltip: {
|
|
3048
|
+
trigger: 'axis',
|
|
3049
|
+
formatter: function(params) {
|
|
3050
|
+
var html = '<b>' + params[0].axisValue + '</b><br/>';
|
|
3051
|
+
params.forEach(function(p) {
|
|
3052
|
+
html += p.marker + ' ' + p.seriesName + ': ' + (p.seriesName === 'Cost' ? '$' + p.value.toFixed(4) : fmt(p.value)) + '<br/>';
|
|
3053
|
+
});
|
|
3054
|
+
return html;
|
|
3055
|
+
}
|
|
3056
|
+
},
|
|
3057
|
+
legend: {
|
|
3058
|
+
data: ['Tokens', 'Cost'],
|
|
3059
|
+
textStyle: { color: '#B0B0C0' },
|
|
3060
|
+
top: 5
|
|
3061
|
+
},
|
|
3062
|
+
grid: { left: 60, right: 60, bottom: 80, top: 40 },
|
|
3063
|
+
xAxis: {
|
|
3064
|
+
type: 'category',
|
|
3065
|
+
data: dailyDays,
|
|
3066
|
+
axisLabel: { color: '#B0B0C0', rotate: 45, interval: 0, fontSize: 10 },
|
|
3067
|
+
axisLine: { lineStyle: { color: '#2A2A35' } }
|
|
3068
|
+
},
|
|
3069
|
+
yAxis: [
|
|
3070
|
+
{
|
|
3071
|
+
type: 'value',
|
|
3072
|
+
name: 'Tokens',
|
|
3073
|
+
nameTextStyle: { color: '#B0B0C0' },
|
|
3074
|
+
axisLabel: { color: '#B0B0C0', formatter: fmt },
|
|
3075
|
+
splitLine: { lineStyle: { color: '#2A2A35', type: 'dashed' } }
|
|
3076
|
+
},
|
|
3077
|
+
{
|
|
3078
|
+
type: 'value',
|
|
3079
|
+
name: 'Cost',
|
|
3080
|
+
nameTextStyle: { color: '#FFB800' },
|
|
3081
|
+
axisLabel: { color: '#FFB800', formatter: function(v) { return '$' + v.toFixed(4); } },
|
|
3082
|
+
splitLine: { show: false }
|
|
3083
|
+
}
|
|
3084
|
+
],
|
|
3085
|
+
dataZoom: [{
|
|
3086
|
+
type: 'slider',
|
|
3087
|
+
bottom: 5,
|
|
3088
|
+
height: 20,
|
|
3089
|
+
borderColor: '#2A2A35',
|
|
3090
|
+
fillerColor: 'rgba(0,213,255,0.1)',
|
|
3091
|
+
handleStyle: { color: '#00D1FF' },
|
|
3092
|
+
textStyle: { color: '#B0B0C0' }
|
|
3093
|
+
}],
|
|
3094
|
+
series: [
|
|
3095
|
+
{
|
|
3096
|
+
name: 'Tokens',
|
|
3097
|
+
type: 'line',
|
|
3098
|
+
data: dailyTokens,
|
|
3099
|
+
smooth: true,
|
|
3100
|
+
symbol: 'none',
|
|
3101
|
+
lineStyle: { color: '#00D1FF', width: 2 },
|
|
3102
|
+
areaStyle: { color: 'rgba(0,209,255,0.15)' }
|
|
3103
|
+
},
|
|
3104
|
+
{
|
|
3105
|
+
name: 'Cost',
|
|
3106
|
+
type: 'line',
|
|
3107
|
+
yAxisIndex: 1,
|
|
3108
|
+
data: dailyCosts,
|
|
3109
|
+
smooth: true,
|
|
3110
|
+
symbol: 'none',
|
|
3111
|
+
lineStyle: { color: '#FFB800', width: 2 },
|
|
3112
|
+
areaStyle: { color: 'rgba(255,184,0,0.1)' }
|
|
3113
|
+
}
|
|
3114
|
+
]
|
|
3115
|
+
};
|
|
3116
|
+
chart.setOption(option);
|
|
3117
|
+
chart.resize();
|
|
3118
|
+
}`;
|
|
3119
|
+
}
|
|
3120
|
+
function renderHeatmapInit(data) {
|
|
3121
|
+
const days = data.daily.slice().reverse();
|
|
3122
|
+
const heatData = days.map((d) => [d.day, Math.log10(d.totalTokens + 1)]);
|
|
3123
|
+
const minDate = days.length > 0 ? days[0].day : "";
|
|
3124
|
+
const maxDate = days.length > 0 ? days[days.length - 1].day : "";
|
|
3125
|
+
return `
|
|
3126
|
+
var heatData = ${jsonLit(heatData)};
|
|
3127
|
+
|
|
3128
|
+
function initHeatmapChart() {
|
|
3129
|
+
var el = document.getElementById('heatmap-chart');
|
|
3130
|
+
if (!el) return;
|
|
3131
|
+
var chart = echarts.init(el);
|
|
3132
|
+
window.heatmapChart = chart;
|
|
3133
|
+
var option = {
|
|
3134
|
+
tooltip: {
|
|
3135
|
+
formatter: function(params) {
|
|
3136
|
+
var val = params.value;
|
|
3137
|
+
var rawTokens = Math.pow(10, val[1]) - 1;
|
|
3138
|
+
return '<b>' + val[0] + '</b><br/>Tokens: ' + fmt(Math.round(rawTokens));
|
|
3139
|
+
}
|
|
3140
|
+
},
|
|
3141
|
+
visualMap: {
|
|
3142
|
+
min: 0,
|
|
3143
|
+
max: Math.max.apply(null, heatData.map(function(d) { return d[1]; })) || 5,
|
|
3144
|
+
calculable: true,
|
|
3145
|
+
orient: 'horizontal',
|
|
3146
|
+
left: 'center',
|
|
3147
|
+
bottom: 10,
|
|
3148
|
+
textStyle: { color: '#B0B0C0' },
|
|
3149
|
+
inRange: {
|
|
3150
|
+
color: ['#0C0C0E', '#1a3a2a', '#00F593', '#00D1FF', '#B545FF']
|
|
3151
|
+
}
|
|
3152
|
+
},
|
|
3153
|
+
calendar: {
|
|
3154
|
+
left: 30,
|
|
3155
|
+
right: 30,
|
|
3156
|
+
top: 20,
|
|
3157
|
+
bottom: 60,
|
|
3158
|
+
range: ['${escapeHtml(minDate)}', '${escapeHtml(maxDate)}'],
|
|
3159
|
+
splitLine: { lineStyle: { color: '#2A2A35' } },
|
|
3160
|
+
dayLabel: { color: '#B0B0C0' },
|
|
3161
|
+
monthLabel: { color: '#B0B0C0' },
|
|
3162
|
+
yearLabel: { color: '#B0B0C0' },
|
|
3163
|
+
itemStyle: { color: '#16161A', borderColor: '#0C0C0E', borderWidth: 2 }
|
|
3164
|
+
},
|
|
3165
|
+
series: [{
|
|
3166
|
+
type: 'heatmap',
|
|
3167
|
+
coordinateSystem: 'calendar',
|
|
3168
|
+
data: heatData
|
|
3169
|
+
}]
|
|
3170
|
+
};
|
|
3171
|
+
chart.setOption(option);
|
|
3172
|
+
chart.resize();
|
|
3173
|
+
}`;
|
|
3174
|
+
}
|
|
3175
|
+
function generateUsageHtml(data) {
|
|
3176
|
+
const metaStr = renderMeta(data);
|
|
3177
|
+
const kpiStr = renderKpiCards(data);
|
|
3178
|
+
const modelChartVisible = data.models.filter((m) => m.totalTokens >= 1e6).length > 0;
|
|
3179
|
+
const modelChartJs = modelChartVisible ? renderModelChartInit(data) : "";
|
|
3180
|
+
const scatterChartJs = renderScatterChartInit(data);
|
|
3181
|
+
const providerStr = renderProviderCards(data);
|
|
3182
|
+
const modelAnalyticsStr = renderModelAnalyticsSection(data);
|
|
3183
|
+
const dailyChartJs = renderDailyTrendInit(data);
|
|
3184
|
+
const heatmapJs = renderHeatmapInit(data);
|
|
3185
|
+
const hasPerf = data.perfSummary.some(
|
|
3186
|
+
(p) => p.requestCount > 0 && p.totalInput + p.totalOutput + p.totalCacheRead + p.totalCacheWrite > 0
|
|
3187
|
+
);
|
|
3188
|
+
const jsonData = jsonLit(data);
|
|
3189
|
+
return `<!DOCTYPE html>
|
|
3190
|
+
<html lang="en">
|
|
3191
|
+
<head>
|
|
3192
|
+
<meta charset="UTF-8">
|
|
3193
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
3194
|
+
<title>TokenWatch Usage Report</title>
|
|
3195
|
+
<style>
|
|
3196
|
+
/* \u4F7F\u7528\u7CFB\u7EDF\u5B57\u4F53 stack\uFF0C\u65E0\u9700\u52A0\u8F7D\u5916\u90E8\u5B57\u4F53 */
|
|
3197
|
+
:root {
|
|
3198
|
+
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
|
3199
|
+
--font-mono: 'JetBrains Mono', 'Cascadia Code', 'Fira Code', 'Consolas', 'Monaco', monospace;
|
|
3200
|
+
}
|
|
3201
|
+
</style>
|
|
3202
|
+
<!-- ECharts: \u591A CDN fallback (staticfile -> bootcdn -> cdnjs -> unpkg -> jsDelivr) -->
|
|
3203
|
+
<script>
|
|
3204
|
+
(function() {
|
|
3205
|
+
var cdns = [
|
|
3206
|
+
'https://cdn.staticfile.org/echarts/5.5.0/echarts.min.js',
|
|
3207
|
+
'https://cdn.bootcdn.net/ajax/libs/echarts/5.5.0/echarts.min.js',
|
|
3208
|
+
'https://cdnjs.cloudflare.com/ajax/libs/echarts/5.5.0/echarts.min.js',
|
|
3209
|
+
'https://unpkg.com/echarts@5.5.0/dist/echarts.min.js',
|
|
3210
|
+
'https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js'
|
|
3211
|
+
];
|
|
3212
|
+
var idx = 0;
|
|
3213
|
+
function loadNext() {
|
|
3214
|
+
if (idx >= cdns.length) {
|
|
3215
|
+
var err = document.getElementById('echarts-error');
|
|
3216
|
+
if (err) err.style.display = 'block';
|
|
3217
|
+
return;
|
|
3218
|
+
}
|
|
3219
|
+
var s = document.createElement('script');
|
|
3220
|
+
s.src = cdns[idx++];
|
|
3221
|
+
s.onload = function() {
|
|
3222
|
+
if (typeof window.tryInitCharts === 'function') {
|
|
3223
|
+
window.tryInitCharts();
|
|
3224
|
+
}
|
|
3225
|
+
};
|
|
3226
|
+
s.onerror = loadNext;
|
|
3227
|
+
document.head.appendChild(s);
|
|
3228
|
+
}
|
|
3229
|
+
loadNext();
|
|
3230
|
+
})();
|
|
3231
|
+
</script>
|
|
3232
|
+
<style>
|
|
3233
|
+
:root {
|
|
3234
|
+
--bg: #0C0C0E;
|
|
3235
|
+
--card: #16161A;
|
|
3236
|
+
--border: #2A2A35;
|
|
3237
|
+
--text: #E0E0F0;
|
|
3238
|
+
--text-dim: #B0B0C0;
|
|
3239
|
+
--cache: #00F593;
|
|
3240
|
+
--input: #00D1FF;
|
|
3241
|
+
--output: #B545FF;
|
|
3242
|
+
--tps: #FFB800;
|
|
3243
|
+
--radius: 8px;
|
|
3244
|
+
}
|
|
3245
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
3246
|
+
body {
|
|
3247
|
+
background: var(--bg);
|
|
3248
|
+
color: var(--text);
|
|
3249
|
+
font-family: var(--font-sans);
|
|
3250
|
+
font-size: 14px;
|
|
3251
|
+
line-height: 1.5;
|
|
3252
|
+
min-height: 100vh;
|
|
3253
|
+
}
|
|
3254
|
+
.container { max-width: 1400px; margin: 0 auto; padding: 24px 20px; }
|
|
3255
|
+
.header {
|
|
3256
|
+
display: flex; justify-content: space-between; align-items: center;
|
|
3257
|
+
padding: 16px 0; border-bottom: 1px solid var(--border); margin-bottom: 24px;
|
|
3258
|
+
}
|
|
3259
|
+
.header h1 { font-size: 22px; font-weight: 600; color: var(--text); }
|
|
3260
|
+
.header h1 span { color: var(--input); }
|
|
3261
|
+
.header .meta { font-size: 12px; color: var(--text-dim); font-family: var(--font-mono); }
|
|
3262
|
+
|
|
3263
|
+
.kpi-row { display: grid; grid-template-columns: repeat(6, 1fr); gap: 12px; margin-bottom: 24px; }
|
|
3264
|
+
.kpi-card {
|
|
3265
|
+
background: var(--card); border: 1px solid var(--border); border-radius: var(--radius);
|
|
3266
|
+
padding: 16px; text-align: center;
|
|
3267
|
+
}
|
|
3268
|
+
.kpi-card.kpi-glow { box-shadow: 0 0 20px rgba(0,245,147,0.15); border-color: var(--cache); }
|
|
3269
|
+
.kpi-label { font-size: 11px; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px; }
|
|
3270
|
+
.kpi-value { font-size: 26px; font-weight: 700; font-family: var(--font-mono); color: var(--text); }
|
|
3271
|
+
|
|
3272
|
+
.section { margin-bottom: 28px; }
|
|
3273
|
+
.section-title {
|
|
3274
|
+
font-size: 16px; font-weight: 600; margin-bottom: 12px;
|
|
3275
|
+
padding-bottom: 6px; border-bottom: 1px solid var(--border);
|
|
3276
|
+
}
|
|
3277
|
+
.chart-box {
|
|
3278
|
+
background: var(--card); border: 1px solid var(--border); border-radius: var(--radius);
|
|
3279
|
+
padding: 12px; height: 400px;
|
|
3280
|
+
}
|
|
3281
|
+
|
|
3282
|
+
.tab-bar { display: flex; gap: 4px; margin-bottom: 12px; }
|
|
3283
|
+
.tab-btn {
|
|
3284
|
+
background: var(--card); border: 1px solid var(--border); color: var(--text-dim);
|
|
3285
|
+
padding: 6px 18px; border-radius: 4px 4px 0 0; cursor: pointer; font-size: 13px; font-family: var(--font-sans);
|
|
3286
|
+
}
|
|
3287
|
+
.tab-btn:hover { border-color: var(--input); color: var(--text); }
|
|
3288
|
+
.tab-btn.active {
|
|
3289
|
+
background: var(--border); color: var(--text); border-bottom-color: var(--border);
|
|
3290
|
+
}
|
|
3291
|
+
.tab-content { display: none; }
|
|
3292
|
+
.tab-content.active { display: block; }
|
|
3293
|
+
|
|
3294
|
+
.provider-row { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px; }
|
|
3295
|
+
.provider-card {
|
|
3296
|
+
background: var(--card); border: 1px solid var(--border); border-radius: var(--radius);
|
|
3297
|
+
padding: 14px;
|
|
3298
|
+
}
|
|
3299
|
+
.provider-name { font-size: 14px; font-weight: 600; margin-bottom: 8px; color: var(--input); }
|
|
3300
|
+
.provider-stat { display: flex; justify-content: space-between; font-size: 12px; padding: 2px 0; }
|
|
3301
|
+
.provider-stat .stat-label { color: var(--text-dim); }
|
|
3302
|
+
.provider-more { color: var(--text-dim); font-size: 11px; padding: 10px 4px 0; grid-column: 1 / -1; }
|
|
3303
|
+
|
|
3304
|
+
.data-table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
|
3305
|
+
.data-table th {
|
|
3306
|
+
background: var(--card); color: var(--text-dim); padding: 8px 10px;
|
|
3307
|
+
text-align: right; border-bottom: 1px solid var(--border); font-weight: 500;
|
|
3308
|
+
white-space: nowrap;
|
|
3309
|
+
}
|
|
3310
|
+
.data-table th:first-child { text-align: left; }
|
|
3311
|
+
.data-table td {
|
|
3312
|
+
padding: 6px 10px; text-align: right; border-bottom: 1px solid var(--border);
|
|
3313
|
+
font-family: var(--font-mono);
|
|
3314
|
+
}
|
|
3315
|
+
.data-table td:first-child {
|
|
3316
|
+
text-align: left; color: var(--text); font-family: var(--font-sans);
|
|
3317
|
+
}
|
|
3318
|
+
.data-table tbody tr:hover { background: rgba(42,42,53,0.4); }
|
|
3319
|
+
|
|
3320
|
+
.pagination-ctrl {
|
|
3321
|
+
display: none; align-items: center; gap: 14px; justify-content: center;
|
|
3322
|
+
padding: 14px 0 4px;
|
|
3323
|
+
}
|
|
3324
|
+
.page-btn {
|
|
3325
|
+
background: var(--card); border: 1px solid var(--border); color: var(--text);
|
|
3326
|
+
padding: 5px 16px; border-radius: 4px; cursor: pointer;
|
|
3327
|
+
font-size: 12px; font-family: var(--font-sans); transition: border-color 0.15s, color 0.15s;
|
|
3328
|
+
}
|
|
3329
|
+
.page-btn:hover:not(:disabled) { border-color: var(--input); color: var(--input); }
|
|
3330
|
+
.page-btn:disabled { opacity: 0.35; cursor: not-allowed; }
|
|
3331
|
+
.page-info {
|
|
3332
|
+
color: var(--text-dim); font-size: 12px;
|
|
3333
|
+
font-family: var(--font-mono); min-width: 110px; text-align: center;
|
|
3334
|
+
}
|
|
3335
|
+
|
|
3336
|
+
.empty-state {
|
|
3337
|
+
background: var(--card); border: 1px solid var(--border); border-radius: var(--radius);
|
|
3338
|
+
padding: 40px; text-align: center; color: var(--text-dim);
|
|
3339
|
+
}
|
|
3340
|
+
|
|
3341
|
+
.footer {
|
|
3342
|
+
margin-top: 40px; padding: 16px 0; border-top: 1px solid var(--border);
|
|
3343
|
+
text-align: center; font-size: 11px; color: var(--text-dim);
|
|
3344
|
+
}
|
|
3345
|
+
|
|
3346
|
+
@media (max-width: 768px) {
|
|
3347
|
+
.kpi-row { grid-template-columns: repeat(2, 1fr); }
|
|
3348
|
+
.provider-row { grid-template-columns: 1fr; }
|
|
3349
|
+
.container { padding: 12px 10px; }
|
|
3350
|
+
.header { flex-direction: column; gap: 6px; align-items: flex-start; }
|
|
3351
|
+
.chart-box { height: 300px; }
|
|
3352
|
+
.data-table { font-size: 11px; }
|
|
3353
|
+
.data-table th, .data-table td { padding: 4px 6px; }
|
|
3354
|
+
}
|
|
3355
|
+
</style>
|
|
3356
|
+
</head>
|
|
3357
|
+
<body>
|
|
3358
|
+
<div id="echarts-error" style="display:none;position:fixed;top:0;left:0;right:0;padding:14px 24px;background:#7f1d1d;color:#fca5a5;font-size:13px;z-index:9999;text-align:center">
|
|
3359
|
+
\u26A0\uFE0F \u56FE\u8868\u5E93\u52A0\u8F7D\u5931\u8D25\uFF08ECharts CDN \u5747\u65E0\u6CD5\u8BBF\u95EE\uFF09\u3002\u8BF7\u68C0\u67E5\u7F51\u7EDC\u8FDE\u63A5\u540E\u5237\u65B0\u9875\u9762\uFF0C\u6216\u5728\u79BB\u7EBF\u73AF\u5883\u4E0B\u5C06 ECharts JS \u6587\u4EF6\u4E0B\u8F7D\u5230\u672C\u5730\u540E\u624B\u52A8\u5F15\u5165\u3002
|
|
3360
|
+
</div>
|
|
3361
|
+
<div class="container">
|
|
3362
|
+
<div class="header">
|
|
3363
|
+
<h1><span>TokenWatch</span> Usage Report</h1>
|
|
3364
|
+
<div class="meta">${metaStr}</div>
|
|
3365
|
+
</div>
|
|
3366
|
+
|
|
3367
|
+
${kpiStr}
|
|
3368
|
+
|
|
3369
|
+
<div class="section">
|
|
3370
|
+
<div class="section-title">Model Comparison Matrix</div>
|
|
3371
|
+
${modelChartVisible ? '<div class="chart-box" id="model-chart"></div>' : '<div class="empty-state">No models with ≥1M tokens in this period.</div>'}
|
|
3372
|
+
</div>
|
|
3373
|
+
|
|
3374
|
+
<div class="section">
|
|
3375
|
+
<div class="section-title">Provider Summary</div>
|
|
3376
|
+
<div class="provider-row">${providerStr}</div>
|
|
3377
|
+
</div>
|
|
3378
|
+
|
|
3379
|
+
${modelAnalyticsStr}
|
|
3380
|
+
|
|
3381
|
+
<div class="section">
|
|
3382
|
+
<div class="section-title">Efficiency vs Cost</div>
|
|
3383
|
+
${hasPerf ? '<div class="chart-box" id="scatter-chart"></div>' : '<div class="empty-state">No performance data available for this period.</div>'}
|
|
3384
|
+
</div>
|
|
3385
|
+
|
|
3386
|
+
<div class="section">
|
|
3387
|
+
<div class="section-title">Usage Timeline</div>
|
|
3388
|
+
${data.dailyTruncated ? '<div class="provider-more">\u26A0\uFE0F \u65E5\u671F\u660E\u7EC6\u8D85\u51FA\u6570\u636E\u6E90\u4E0A\u9650\u5DF2\u88AB\u622A\u65AD\uFF0C\u672C\u56FE\u8868\u4EC5\u8986\u76D6\u5176\u4E2D\u6700\u8FD1\u7684\u65E5\u671F\uFF1B\u6C47\u603B\u6570\u5B57\uFF08KPI/\u8868\u683C\uFF09\u4E0D\u53D7\u5F71\u54CD\u3002</div>' : ""}
|
|
3389
|
+
<div class="tab-bar">
|
|
3390
|
+
<button class="tab-btn active" data-tab="daily" onclick="switchTab('daily')">Daily Trend</button>
|
|
3391
|
+
<button class="tab-btn" data-tab="heatmap" onclick="switchTab('heatmap')">Heatmap</button>
|
|
3392
|
+
</div>
|
|
3393
|
+
<div id="tab-daily" class="tab-content active">
|
|
3394
|
+
<div class="chart-box" id="daily-chart"></div>
|
|
3395
|
+
</div>
|
|
3396
|
+
<div id="tab-heatmap" class="tab-content">
|
|
3397
|
+
<div class="chart-box" id="heatmap-chart"></div>
|
|
3398
|
+
</div>
|
|
3399
|
+
</div>
|
|
3400
|
+
|
|
3401
|
+
<div class="footer">
|
|
3402
|
+
Generated by TokenWatch · Data: SQLite + JSONL · Export: <a href="javascript:void(0)" onclick="downloadJSON()" style="color:var(--input);text-decoration:none">JSON</a> <span style="color:var(--text-dim)">(saves to browser download folder)</span>
|
|
3403
|
+
</div>
|
|
3404
|
+
</div>
|
|
3405
|
+
|
|
3406
|
+
<script id="report-data" type="application/json">${jsonData}</script>
|
|
3407
|
+
|
|
3408
|
+
<script>
|
|
3409
|
+
var fmt = function(v) {
|
|
3410
|
+
if (v == null) return '\u2014';
|
|
3411
|
+
if (v >= 1000000000) return (v/1000000000).toFixed(1)+'B';
|
|
3412
|
+
if (v >= 1000000) return (v/1000000).toFixed(1)+'M';
|
|
3413
|
+
if (v >= 1000) return (v/1000).toFixed(1)+'K';
|
|
3414
|
+
return String(v);
|
|
3415
|
+
};
|
|
3416
|
+
|
|
3417
|
+
// Usage Timeline tab switcher
|
|
3418
|
+
window.switchTab = function(name) {
|
|
3419
|
+
document.querySelectorAll('.tab-content').forEach(function(el) { el.classList.remove('active'); });
|
|
3420
|
+
document.querySelectorAll('.tab-btn[data-tab]').forEach(function(el) { el.classList.remove('active'); });
|
|
3421
|
+
document.getElementById('tab-' + name).classList.add('active');
|
|
3422
|
+
document.querySelector('[data-tab="' + name + '"]').classList.add('active');
|
|
3423
|
+
setTimeout(function() {
|
|
3424
|
+
if (name === 'daily' && window.dailyChart) window.dailyChart.resize();
|
|
3425
|
+
if (name === 'heatmap' && window.heatmapChart) window.heatmapChart.resize();
|
|
3426
|
+
}, 50);
|
|
3427
|
+
};
|
|
3428
|
+
|
|
3429
|
+
// Model Analytics tab switcher
|
|
3430
|
+
window.switchModelTab = function(name) {
|
|
3431
|
+
document.querySelectorAll('[data-mtab]').forEach(function(el) { el.classList.remove('active'); });
|
|
3432
|
+
['model-tab-usage', 'model-tab-perf', 'model-tab-errors'].forEach(function(id) {
|
|
3433
|
+
var el = document.getElementById(id);
|
|
3434
|
+
if (el) el.classList.remove('active');
|
|
3435
|
+
});
|
|
3436
|
+
var activeTab = document.getElementById('model-tab-' + name);
|
|
3437
|
+
if (activeTab) activeTab.classList.add('active');
|
|
3438
|
+
var activeBtn = document.querySelector('[data-mtab="' + name + '"]');
|
|
3439
|
+
if (activeBtn) activeBtn.classList.add('active');
|
|
3440
|
+
};
|
|
3441
|
+
|
|
3442
|
+
// Generic paginator: hide/show tbody rows, show prev/next controls
|
|
3443
|
+
function initPaginator(tableId, pageSize) {
|
|
3444
|
+
var tbody = document.querySelector('#' + tableId + ' tbody');
|
|
3445
|
+
if (!tbody) return;
|
|
3446
|
+
var rows = Array.from(tbody.querySelectorAll('tr'));
|
|
3447
|
+
if (rows.length <= pageSize) return; // \u884C\u6570\u4E0D\u8D85\u8FC7\u4E00\u9875\u65F6\u65E0\u9700\u5206\u9875
|
|
3448
|
+
var totalPages = Math.ceil(rows.length / pageSize);
|
|
3449
|
+
var cur = 1;
|
|
3450
|
+
|
|
3451
|
+
function render() {
|
|
3452
|
+
rows.forEach(function(r, i) {
|
|
3453
|
+
r.style.display = (i >= (cur - 1) * pageSize && i < cur * pageSize) ? '' : 'none';
|
|
3454
|
+
});
|
|
3455
|
+
var info = document.getElementById(tableId + '-info');
|
|
3456
|
+
if (info) info.textContent = 'Page ' + cur + ' / ' + totalPages + ' (' + rows.length + ' rows)';
|
|
3457
|
+
var prevEl = document.getElementById(tableId + '-prev');
|
|
3458
|
+
var nextEl = document.getElementById(tableId + '-next');
|
|
3459
|
+
if (prevEl) prevEl.disabled = cur === 1;
|
|
3460
|
+
if (nextEl) nextEl.disabled = cur === totalPages;
|
|
3461
|
+
}
|
|
3462
|
+
|
|
3463
|
+
var prevEl = document.getElementById(tableId + '-prev');
|
|
3464
|
+
var nextEl = document.getElementById(tableId + '-next');
|
|
3465
|
+
if (prevEl) prevEl.addEventListener('click', function() { if (cur > 1) { cur--; render(); } });
|
|
3466
|
+
if (nextEl) nextEl.addEventListener('click', function() { if (cur < totalPages) { cur++; render(); } });
|
|
3467
|
+
|
|
3468
|
+
var ctrl = document.getElementById(tableId + '-ctrl');
|
|
3469
|
+
if (ctrl) ctrl.style.display = 'flex';
|
|
3470
|
+
render();
|
|
3471
|
+
}
|
|
3472
|
+
|
|
3473
|
+
${modelChartJs}
|
|
3474
|
+
${scatterChartJs}
|
|
3475
|
+
${dailyChartJs}
|
|
3476
|
+
${heatmapJs}
|
|
3477
|
+
|
|
3478
|
+
window.downloadJSON = function() {
|
|
3479
|
+
var d = document.getElementById('report-data');
|
|
3480
|
+
if (!d) return;
|
|
3481
|
+
var b = new Blob([d.textContent], { type: 'application/json' });
|
|
3482
|
+
var a = document.createElement('a');
|
|
3483
|
+
a.href = URL.createObjectURL(b);
|
|
3484
|
+
a.download = 'tokenwatch-data.json';
|
|
3485
|
+
document.body.appendChild(a);
|
|
3486
|
+
a.click();
|
|
3487
|
+
document.body.removeChild(a);
|
|
3488
|
+
setTimeout(function() { URL.revokeObjectURL(a.href); }, 100);
|
|
3489
|
+
};
|
|
3490
|
+
|
|
3491
|
+
window.tryInitCharts = function() {
|
|
3492
|
+
if (typeof echarts === 'undefined' || window.__chartsInitialized) return;
|
|
3493
|
+
window.__chartsInitialized = true;
|
|
3494
|
+
${modelChartVisible ? "initModelChart();" : ""}
|
|
3495
|
+
${hasPerf ? "initScatterChart();" : ""}
|
|
3496
|
+
initDailyChart();
|
|
3497
|
+
initHeatmapChart();
|
|
3498
|
+
};
|
|
3499
|
+
|
|
3500
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
3501
|
+
initPaginator('usage-table', 10);
|
|
3502
|
+
initPaginator('perf-table', 10);
|
|
3503
|
+
initPaginator('errors-table', 10);
|
|
3504
|
+
window.tryInitCharts();
|
|
3505
|
+
});
|
|
3506
|
+
|
|
3507
|
+
window.addEventListener('resize', function() {
|
|
3508
|
+
${modelChartVisible ? "if (window.modelChart) window.modelChart.resize();" : ""}
|
|
3509
|
+
if (window.scatterChart) window.scatterChart.resize();
|
|
3510
|
+
if (window.dailyChart) window.dailyChart.resize();
|
|
3511
|
+
if (window.heatmapChart) window.heatmapChart.resize();
|
|
3512
|
+
});
|
|
3513
|
+
</script>
|
|
3514
|
+
</body>
|
|
3515
|
+
</html>`;
|
|
3516
|
+
}
|
|
3517
|
+
|
|
3518
|
+
// src/kernel/report.ts
|
|
3519
|
+
function localDateStr(d) {
|
|
3520
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
3521
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
3522
|
+
}
|
|
3523
|
+
function ensureReportDir() {
|
|
3524
|
+
const dir = join4(homedir3(), ".opencode", "reports");
|
|
3525
|
+
if (!existsSync3(dir)) mkdirSync(dir, { recursive: true });
|
|
3526
|
+
return dir;
|
|
3527
|
+
}
|
|
3528
|
+
function openInBrowser(filePath) {
|
|
3529
|
+
try {
|
|
3530
|
+
const platform = process.platform;
|
|
3531
|
+
let child;
|
|
3532
|
+
if (platform === "win32") {
|
|
3533
|
+
child = spawn("cmd.exe", ["/d", "/s", "/c", "start", "", filePath], {
|
|
3534
|
+
detached: true,
|
|
3535
|
+
stdio: "ignore",
|
|
3536
|
+
windowsHide: true
|
|
3537
|
+
});
|
|
3538
|
+
} else if (platform === "darwin") {
|
|
3539
|
+
child = spawn("open", [filePath], { detached: true, stdio: "ignore" });
|
|
3540
|
+
} else {
|
|
3541
|
+
child = spawn("xdg-open", [filePath], { detached: true, stdio: "ignore" });
|
|
3542
|
+
}
|
|
3543
|
+
child.once("error", () => {
|
|
3544
|
+
});
|
|
3545
|
+
child.unref();
|
|
3546
|
+
} catch {
|
|
3547
|
+
}
|
|
3548
|
+
}
|
|
3549
|
+
function getRangeSlug(filters, presetTag) {
|
|
3550
|
+
if (presetTag) return presetTag;
|
|
3551
|
+
if (!filters.startDate && !filters.endDate) return "all";
|
|
3552
|
+
if (filters.startDate && filters.endDate) {
|
|
3553
|
+
if (filters.startDate === filters.endDate) {
|
|
3554
|
+
const today = localDateStr(/* @__PURE__ */ new Date());
|
|
3555
|
+
if (filters.startDate === today) return "today";
|
|
3556
|
+
return filters.startDate;
|
|
3557
|
+
}
|
|
3558
|
+
return `${filters.startDate.replace(/-/g, "")}_${filters.endDate.replace(/-/g, "")}`;
|
|
3559
|
+
}
|
|
3560
|
+
if (filters.startDate) return `from_${filters.startDate.replace(/-/g, "")}`;
|
|
3561
|
+
if (filters.endDate) return `until_${filters.endDate.replace(/-/g, "")}`;
|
|
3562
|
+
return "custom";
|
|
3563
|
+
}
|
|
3564
|
+
function generateUniqueReportPath(dir, rangeSlug) {
|
|
3565
|
+
const dateStr = localDateStr(/* @__PURE__ */ new Date());
|
|
3566
|
+
const baseName = `tokenwatch-${rangeSlug}-${dateStr}`;
|
|
3567
|
+
let targetPath = join4(dir, `${baseName}.html`);
|
|
3568
|
+
if (!existsSync3(targetPath)) return targetPath;
|
|
3569
|
+
const now = /* @__PURE__ */ new Date();
|
|
3570
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
3571
|
+
const timeSuffix = `${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
3572
|
+
targetPath = join4(dir, `${baseName}_${timeSuffix}.html`);
|
|
3573
|
+
let counter = 1;
|
|
3574
|
+
while (existsSync3(targetPath)) {
|
|
3575
|
+
targetPath = join4(dir, `${baseName}_${timeSuffix}_${counter}.html`);
|
|
3576
|
+
counter++;
|
|
3577
|
+
}
|
|
3578
|
+
return targetPath;
|
|
3579
|
+
}
|
|
3580
|
+
function nowStamp() {
|
|
3581
|
+
const now = /* @__PURE__ */ new Date();
|
|
3582
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
3583
|
+
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
|
|
3584
|
+
}
|
|
3585
|
+
function buildCombinedData(usage) {
|
|
3586
|
+
const logs = readLogs(200);
|
|
3587
|
+
const perfSummary = readPersistedStats();
|
|
3588
|
+
const daily = usage.daily ?? [];
|
|
3589
|
+
const meta = {
|
|
3590
|
+
generatedAt: nowStamp(),
|
|
3591
|
+
dateRange: {
|
|
3592
|
+
start: daily.length > 0 ? daily[daily.length - 1].day : "\u2014",
|
|
3593
|
+
end: daily.length > 0 ? daily[0].day : "\u2014"
|
|
3594
|
+
}
|
|
3595
|
+
};
|
|
3596
|
+
return {
|
|
3597
|
+
...usage,
|
|
3598
|
+
perfLogs: logs,
|
|
3599
|
+
perfSummary,
|
|
3600
|
+
meta
|
|
3601
|
+
};
|
|
3602
|
+
}
|
|
3603
|
+
function writeHtmlReport(data, rangeSlug) {
|
|
3604
|
+
const html = generateUsageHtml(data);
|
|
3605
|
+
const dir = ensureReportDir();
|
|
3606
|
+
const filePath = generateUniqueReportPath(dir, rangeSlug);
|
|
3607
|
+
writeFileSync3(filePath, html, "utf-8");
|
|
3608
|
+
return filePath;
|
|
3609
|
+
}
|
|
3610
|
+
|
|
3611
|
+
// src/host/v1/commands.tsx
|
|
3612
|
+
import { writeFileSync as writeFileSync4 } from "node:fs";
|
|
3613
|
+
import { join as join5 } from "node:path";
|
|
3614
|
+
async function registerCommands(api) {
|
|
3615
|
+
api.command?.register(() => [{
|
|
3616
|
+
value: "tokenwatch-usage",
|
|
3617
|
+
title: "TokenWatch",
|
|
3618
|
+
description: "Token usage reports, export, and settings",
|
|
3619
|
+
category: "Stats",
|
|
3620
|
+
slash: {
|
|
3621
|
+
name: "usage"
|
|
3622
|
+
},
|
|
3623
|
+
onSelect: async (dialog) => {
|
|
3624
|
+
if (dialog) showUsageMenu(api, dialog);
|
|
3625
|
+
}
|
|
3626
|
+
}]);
|
|
3627
|
+
}
|
|
3628
|
+
function loadConfigFromStore(api) {
|
|
3629
|
+
let pluginConfig;
|
|
3630
|
+
try {
|
|
3631
|
+
pluginConfig = api.state?.config;
|
|
3632
|
+
} catch {
|
|
3633
|
+
}
|
|
3634
|
+
return loadConfig(makeStore(api), pluginConfig);
|
|
3635
|
+
}
|
|
3636
|
+
function showHtmlReport(api, filters = {}, presetTag) {
|
|
3637
|
+
void (async () => {
|
|
3638
|
+
try {
|
|
3639
|
+
const usage = await getUsageReport(filters);
|
|
3640
|
+
const data = buildCombinedData(usage);
|
|
3641
|
+
const filePath = writeHtmlReport(data, getRangeSlug(filters, presetTag));
|
|
3642
|
+
api.ui.toast?.({
|
|
3643
|
+
message: `Report: ${filePath}`,
|
|
3644
|
+
variant: "info"
|
|
3645
|
+
});
|
|
3646
|
+
openInBrowser(filePath);
|
|
3647
|
+
} catch (err) {
|
|
3648
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3649
|
+
api.ui.toast?.({
|
|
3650
|
+
message: `Error: ${msg}`,
|
|
3651
|
+
variant: "error"
|
|
3652
|
+
});
|
|
3653
|
+
}
|
|
3654
|
+
})();
|
|
3655
|
+
}
|
|
3656
|
+
function showHtmlReportRangeMenu(api, dialog) {
|
|
3657
|
+
dialog.replace(() => _$createComponent3(api.ui.DialogSelect, {
|
|
3658
|
+
get title() {
|
|
3659
|
+
return t("cmdTitleHtml");
|
|
3660
|
+
},
|
|
3661
|
+
placeholder: "Select date range...",
|
|
3662
|
+
get options() {
|
|
3663
|
+
return [{
|
|
3664
|
+
title: t("menuToday"),
|
|
3665
|
+
value: "today",
|
|
3666
|
+
onSelect: () => {
|
|
3667
|
+
dialog.clear();
|
|
3668
|
+
const today = localDateStr(/* @__PURE__ */ new Date());
|
|
3669
|
+
showHtmlReport(api, {
|
|
3670
|
+
startDate: today,
|
|
3671
|
+
endDate: today
|
|
3672
|
+
}, "today");
|
|
3673
|
+
}
|
|
3674
|
+
}, {
|
|
3675
|
+
title: t("menu7d"),
|
|
3676
|
+
value: "7d",
|
|
3677
|
+
onSelect: () => {
|
|
3678
|
+
dialog.clear();
|
|
3679
|
+
showHtmlReport(api, getPresetRange("7d"), "7d");
|
|
3680
|
+
}
|
|
3681
|
+
}, {
|
|
3682
|
+
title: t("menu30d"),
|
|
3683
|
+
value: "30d",
|
|
3684
|
+
onSelect: () => {
|
|
3685
|
+
dialog.clear();
|
|
3686
|
+
showHtmlReport(api, getPresetRange("30d"), "30d");
|
|
3687
|
+
}
|
|
3688
|
+
}, {
|
|
3689
|
+
title: t("menuAll"),
|
|
3690
|
+
value: "all",
|
|
3691
|
+
onSelect: () => {
|
|
3692
|
+
dialog.clear();
|
|
3693
|
+
showHtmlReport(api, getPresetRange("all"), "all");
|
|
3694
|
+
}
|
|
3695
|
+
}];
|
|
3696
|
+
},
|
|
3697
|
+
flat: true
|
|
3698
|
+
}));
|
|
3699
|
+
}
|
|
3700
|
+
function showUsageMenu(api, dialog) {
|
|
3701
|
+
try {
|
|
3702
|
+
setLanguage(loadConfigFromStore(api).language);
|
|
3703
|
+
} catch {
|
|
3704
|
+
}
|
|
3705
|
+
dialog.replace(() => _$createComponent3(api.ui.DialogSelect, {
|
|
3706
|
+
get title() {
|
|
3707
|
+
return t("panelTitle");
|
|
3708
|
+
},
|
|
3709
|
+
placeholder: "Select an action...",
|
|
3710
|
+
get options() {
|
|
3711
|
+
return [{
|
|
3712
|
+
title: `${t("cmdTitleHtml")} \u25B8`,
|
|
3713
|
+
value: "html",
|
|
3714
|
+
description: t("cmdDescHtml"),
|
|
3715
|
+
onSelect: () => showHtmlReportRangeMenu(api, dialog)
|
|
3716
|
+
}, {
|
|
3717
|
+
title: t("cmdTitleJson"),
|
|
3718
|
+
value: "json",
|
|
3719
|
+
description: t("cmdDescJson"),
|
|
3720
|
+
onSelect: () => {
|
|
3721
|
+
dialog.clear();
|
|
3722
|
+
showJsonExport(api);
|
|
3723
|
+
}
|
|
3724
|
+
}, {
|
|
3725
|
+
title: t("cmdTitleText"),
|
|
3726
|
+
value: "text",
|
|
3727
|
+
description: t("cmdDescText"),
|
|
3728
|
+
onSelect: () => {
|
|
3729
|
+
dialog.clear();
|
|
3730
|
+
showTextReport(api);
|
|
3731
|
+
}
|
|
3732
|
+
}, {
|
|
3733
|
+
title: `${t("cmdTitleSettings")} \u25B8`,
|
|
3734
|
+
value: "settings",
|
|
3735
|
+
description: t("cmdDescSettings"),
|
|
3736
|
+
onSelect: () => showSettingsDialog(api, dialog)
|
|
3737
|
+
}];
|
|
3738
|
+
},
|
|
3739
|
+
flat: true
|
|
3740
|
+
}));
|
|
3741
|
+
}
|
|
3742
|
+
async function showJsonExport(api) {
|
|
3743
|
+
try {
|
|
3744
|
+
const report = await getUsageReport({});
|
|
3745
|
+
const dir = ensureReportDir();
|
|
3746
|
+
const dateStr = localDateStr(/* @__PURE__ */ new Date());
|
|
3747
|
+
const filePath = join5(dir, `tokenwatch-${dateStr}.json`);
|
|
3748
|
+
writeFileSync4(filePath, JSON.stringify(report, null, 2), "utf-8");
|
|
3749
|
+
api.ui.toast?.({
|
|
3750
|
+
message: `JSON: ${filePath}`,
|
|
3751
|
+
variant: "info"
|
|
3752
|
+
});
|
|
3753
|
+
} catch (err) {
|
|
3754
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3755
|
+
api.ui.toast?.({
|
|
3756
|
+
message: `Error: ${msg}`,
|
|
3757
|
+
variant: "error"
|
|
3758
|
+
});
|
|
3759
|
+
}
|
|
3760
|
+
}
|
|
3761
|
+
async function showTextReport(api) {
|
|
3762
|
+
try {
|
|
3763
|
+
const report = await getUsageReport({});
|
|
3764
|
+
const formatted = formatUsageReport(report);
|
|
3765
|
+
const dir = ensureReportDir();
|
|
3766
|
+
const dateStr = localDateStr(/* @__PURE__ */ new Date());
|
|
3767
|
+
const filePath = join5(dir, `tokenwatch-${dateStr}.md`);
|
|
3768
|
+
writeFileSync4(filePath, formatted, "utf-8");
|
|
3769
|
+
api.ui.toast?.({
|
|
3770
|
+
message: `Report saved to ${filePath}`,
|
|
3771
|
+
variant: "info"
|
|
3772
|
+
});
|
|
3773
|
+
} catch (err) {
|
|
3774
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3775
|
+
api.ui.toast?.({
|
|
3776
|
+
message: `Error: ${msg}`,
|
|
3777
|
+
variant: "error"
|
|
3778
|
+
});
|
|
3779
|
+
}
|
|
3780
|
+
}
|
|
3781
|
+
var lastSelectedSetting;
|
|
3782
|
+
function showSettingsDialog(api, dialog) {
|
|
3783
|
+
if (!dialog) return;
|
|
3784
|
+
const reopen = (value) => {
|
|
3785
|
+
lastSelectedSetting = value;
|
|
3786
|
+
setTimeout(() => showSettingsDialog(api, dialog), 0);
|
|
3787
|
+
};
|
|
3788
|
+
const cfg = loadConfigFromStore(api).sidebar;
|
|
3789
|
+
dialog.replace(() => _$createComponent3(api.ui.DialogSelect, {
|
|
3790
|
+
get title() {
|
|
3791
|
+
return t("settingsTitle");
|
|
3792
|
+
},
|
|
3793
|
+
get placeholder() {
|
|
3794
|
+
return t("settingsPlaceholder");
|
|
3795
|
+
},
|
|
3796
|
+
get options() {
|
|
3797
|
+
return [{
|
|
3798
|
+
title: `${cfg.showPerformance ? "\u2713 " : " "}${t("showPerformance")}`,
|
|
3799
|
+
value: "showPerformance",
|
|
3800
|
+
description: t("descShowPerformance"),
|
|
3801
|
+
onSelect: () => {
|
|
3802
|
+
toggleSidebarSetting2(api, "showPerformance");
|
|
3803
|
+
reopen("showPerformance");
|
|
3804
|
+
}
|
|
3805
|
+
}, {
|
|
3806
|
+
title: `${cfg.showPricing ? "\u2713 " : " "}${t("showPricing")}`,
|
|
3807
|
+
value: "showPricing",
|
|
3808
|
+
description: t("descShowPricing"),
|
|
3809
|
+
onSelect: () => {
|
|
3810
|
+
toggleSidebarSetting2(api, "showPricing");
|
|
3811
|
+
reopen("showPricing");
|
|
3812
|
+
}
|
|
3813
|
+
}, {
|
|
3814
|
+
title: `${cfg.showTokenDistribution ? "\u2713 " : " "}${t("showTokenDistribution")}`,
|
|
3815
|
+
value: "showTokenDistribution",
|
|
3816
|
+
description: t("descShowTokenDistribution"),
|
|
3817
|
+
onSelect: () => {
|
|
3818
|
+
toggleSidebarSetting2(api, "showTokenDistribution");
|
|
3819
|
+
reopen("showTokenDistribution");
|
|
3820
|
+
}
|
|
3821
|
+
}, {
|
|
3822
|
+
title: `${cfg.showTrend ? "\u2713 " : " "}${t("showTrend")}`,
|
|
3823
|
+
value: "showTrend",
|
|
3824
|
+
description: t("descShowTrend"),
|
|
3825
|
+
onSelect: () => {
|
|
3826
|
+
toggleSidebarSetting2(api, "showTrend");
|
|
3827
|
+
reopen("showTrend");
|
|
3828
|
+
}
|
|
3829
|
+
}, {
|
|
3830
|
+
title: `${t("settingsLanguage")} \u25B8`,
|
|
3831
|
+
value: "language",
|
|
3832
|
+
description: t("descSettingsLanguage"),
|
|
3833
|
+
onSelect: () => showLanguageMenu(api, dialog)
|
|
3834
|
+
}, {
|
|
3835
|
+
title: t("done"),
|
|
3836
|
+
value: "done",
|
|
3837
|
+
description: t("closeSettings"),
|
|
3838
|
+
onSelect: () => {
|
|
3839
|
+
lastSelectedSetting = void 0;
|
|
3840
|
+
dialog.clear();
|
|
3841
|
+
}
|
|
3842
|
+
}];
|
|
3843
|
+
},
|
|
3844
|
+
flat: true,
|
|
3845
|
+
current: lastSelectedSetting
|
|
3846
|
+
}));
|
|
3847
|
+
}
|
|
3848
|
+
function showLanguageMenu(api, dialog) {
|
|
3849
|
+
const current = loadConfigFromStore(api).language;
|
|
3850
|
+
dialog.replace(() => _$createComponent3(api.ui.DialogSelect, {
|
|
3851
|
+
get title() {
|
|
3852
|
+
return t("settingsLanguage");
|
|
3853
|
+
},
|
|
3854
|
+
get placeholder() {
|
|
3855
|
+
return t("settingsLanguage");
|
|
3856
|
+
},
|
|
3857
|
+
get options() {
|
|
3858
|
+
return [{
|
|
3859
|
+
title: `${current === "auto" ? "\u2713 " : " "}${t("langAuto")}`,
|
|
3860
|
+
value: "auto",
|
|
3861
|
+
description: "\u81EA\u52A8\u68C0\u6D4B / Auto-detect",
|
|
3862
|
+
onSelect: () => {
|
|
3863
|
+
setLanguageSetting2(api, "auto");
|
|
3864
|
+
lastSelectedSetting = "language";
|
|
3865
|
+
dialog.clear();
|
|
3866
|
+
showSettingsDialog(api, dialog);
|
|
3867
|
+
}
|
|
3868
|
+
}, {
|
|
3869
|
+
title: `${current === "zh" ? "\u2713 " : " "}\u4E2D\u6587`,
|
|
3870
|
+
value: "zh",
|
|
3871
|
+
description: "\u7B80\u4F53\u4E2D\u6587",
|
|
3872
|
+
onSelect: () => {
|
|
3873
|
+
setLanguageSetting2(api, "zh");
|
|
3874
|
+
lastSelectedSetting = "language";
|
|
3875
|
+
dialog.clear();
|
|
3876
|
+
showSettingsDialog(api, dialog);
|
|
3877
|
+
}
|
|
3878
|
+
}, {
|
|
3879
|
+
title: `${current === "en" ? "\u2713 " : " "}English`,
|
|
3880
|
+
value: "en",
|
|
3881
|
+
description: "English",
|
|
3882
|
+
onSelect: () => {
|
|
3883
|
+
setLanguageSetting2(api, "en");
|
|
3884
|
+
lastSelectedSetting = "language";
|
|
3885
|
+
dialog.clear();
|
|
3886
|
+
showSettingsDialog(api, dialog);
|
|
3887
|
+
}
|
|
3888
|
+
}];
|
|
3889
|
+
},
|
|
3890
|
+
flat: true
|
|
3891
|
+
}));
|
|
3892
|
+
}
|
|
3893
|
+
function setLanguageSetting2(api, lang) {
|
|
3894
|
+
setLanguageSetting(makeStore(api), lang);
|
|
3895
|
+
setLanguage(lang);
|
|
3896
|
+
}
|
|
3897
|
+
function toggleSidebarSetting2(api, key) {
|
|
3898
|
+
toggleSidebarSetting(makeStore(api), key);
|
|
3899
|
+
}
|
|
3900
|
+
|
|
3901
|
+
// src/host/v2/data-source.ts
|
|
3902
|
+
var cache = null;
|
|
3903
|
+
var inflight = null;
|
|
3904
|
+
var dirty = false;
|
|
3905
|
+
function dayOf(ts) {
|
|
3906
|
+
const d = new Date(ts);
|
|
3907
|
+
const y = d.getFullYear();
|
|
3908
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
3909
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
3910
|
+
return `${y}-${m}-${day}`;
|
|
3911
|
+
}
|
|
3912
|
+
function matchesDay(day, filters) {
|
|
3913
|
+
if (filters.startDate && day < filters.startDate) return false;
|
|
3914
|
+
if (filters.endDate && day > filters.endDate) return false;
|
|
3915
|
+
return true;
|
|
3916
|
+
}
|
|
3917
|
+
function matchesModel(provider, model, filters) {
|
|
3918
|
+
if (filters.model && model !== filters.model) return false;
|
|
3919
|
+
if (filters.provider && provider !== filters.provider) return false;
|
|
3920
|
+
return true;
|
|
3921
|
+
}
|
|
3922
|
+
function yieldToEventLoop() {
|
|
3923
|
+
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
3924
|
+
}
|
|
3925
|
+
var MAX_SESSION_PAGES = 64;
|
|
3926
|
+
var MAX_MESSAGE_PAGES = 400;
|
|
3927
|
+
async function listAllSessions(ctx) {
|
|
3928
|
+
const api = ctx?.client;
|
|
3929
|
+
if (typeof api?.session?.list !== "function") {
|
|
3930
|
+
return { sessions: ctx?.data?.session?.list?.() ?? [], truncated: false };
|
|
3931
|
+
}
|
|
3932
|
+
const out = [];
|
|
3933
|
+
let cursor;
|
|
3934
|
+
for (let page = 0; page < MAX_SESSION_PAGES; page++) {
|
|
3935
|
+
const input = cursor ? { cursor } : { limit: 500, order: "asc" };
|
|
3936
|
+
const resp = await api.session.list(input);
|
|
3937
|
+
out.push(...resp?.data ?? []);
|
|
3938
|
+
cursor = resp?.cursor?.next ?? void 0;
|
|
3939
|
+
if (!cursor) break;
|
|
3940
|
+
}
|
|
3941
|
+
return { sessions: out, truncated: cursor != null };
|
|
3942
|
+
}
|
|
3943
|
+
async function listAllMessages(ctx, sessionID) {
|
|
3944
|
+
const api = ctx?.client;
|
|
3945
|
+
if (typeof api?.message?.list !== "function") {
|
|
3946
|
+
try {
|
|
3947
|
+
await ctx?.data?.session?.message?.sync?.(sessionID);
|
|
3948
|
+
} catch {
|
|
3949
|
+
}
|
|
3950
|
+
return { messages: ctx?.data?.session?.message?.list?.(sessionID) ?? [], truncated: false };
|
|
3951
|
+
}
|
|
3952
|
+
const out = [];
|
|
3953
|
+
let cursor;
|
|
3954
|
+
for (let page = 0; page < MAX_MESSAGE_PAGES; page++) {
|
|
3955
|
+
const resp = await (cursor ? api.message.list({ sessionID, cursor }) : api.message.list({ sessionID, limit: 200, order: "desc" }));
|
|
3956
|
+
out.push(...resp?.data ?? []);
|
|
3957
|
+
cursor = resp?.cursor?.next ?? void 0;
|
|
3958
|
+
if (!cursor) break;
|
|
3959
|
+
}
|
|
3960
|
+
return { messages: out, truncated: cursor != null };
|
|
3961
|
+
}
|
|
3962
|
+
function isAssistantMessage(message) {
|
|
3963
|
+
return message?.type === "assistant" || message?.role === "assistant";
|
|
3964
|
+
}
|
|
3965
|
+
async function scan(ctx) {
|
|
3966
|
+
const sessionAgg = /* @__PURE__ */ new Map();
|
|
3967
|
+
const modelAgg = /* @__PURE__ */ new Map();
|
|
3968
|
+
const providerAgg = /* @__PURE__ */ new Map();
|
|
3969
|
+
const dailyAgg = /* @__PURE__ */ new Map();
|
|
3970
|
+
const modelSessions = /* @__PURE__ */ new Map();
|
|
3971
|
+
const providerSessions = /* @__PURE__ */ new Map();
|
|
3972
|
+
const dailySessions = /* @__PURE__ */ new Map();
|
|
3973
|
+
const errorByModel = /* @__PURE__ */ new Map();
|
|
3974
|
+
let successCount = 0;
|
|
3975
|
+
let failedCount = 0;
|
|
3976
|
+
const { sessions, truncated: sessionsTruncated } = await listAllSessions(ctx);
|
|
3977
|
+
let skippedSessions = 0;
|
|
3978
|
+
let truncatedMessageSessions = 0;
|
|
3979
|
+
for (const session of sessions) {
|
|
3980
|
+
const sessionID = session?.id;
|
|
3981
|
+
if (!sessionID) continue;
|
|
3982
|
+
let messages;
|
|
3983
|
+
try {
|
|
3984
|
+
const result = await listAllMessages(ctx, sessionID);
|
|
3985
|
+
messages = result.messages;
|
|
3986
|
+
if (result.truncated) truncatedMessageSessions++;
|
|
3987
|
+
} catch {
|
|
3988
|
+
skippedSessions++;
|
|
3989
|
+
continue;
|
|
3990
|
+
}
|
|
3991
|
+
let sawAssistant = false;
|
|
3992
|
+
for (const message of messages) {
|
|
3993
|
+
if (!isAssistantMessage(message)) continue;
|
|
3994
|
+
const tokens = message?.tokens;
|
|
3995
|
+
if (!tokens) continue;
|
|
3996
|
+
const input = tokens.input ?? 0;
|
|
3997
|
+
const output = tokens.output ?? 0;
|
|
3998
|
+
const reasoning = tokens.reasoning ?? 0;
|
|
3999
|
+
const cacheRead = tokens.cache?.read ?? 0;
|
|
4000
|
+
const cacheWrite = tokens.cache?.write ?? 0;
|
|
4001
|
+
const total = input + output + reasoning + cacheRead + cacheWrite;
|
|
4002
|
+
const cost = typeof message?.cost === "number" ? message.cost : 0;
|
|
4003
|
+
const provider = message?.model?.providerID ?? "unknown";
|
|
4004
|
+
const model = message?.model?.id ?? "unknown";
|
|
4005
|
+
const day = dayOf(message?.time?.created ?? session?.time?.created ?? Date.now());
|
|
4006
|
+
const isFailure = message?.finish === "error" || total === 0;
|
|
4007
|
+
const modelKey = `${provider}/${model}`;
|
|
4008
|
+
const err = errorByModel.get(modelKey) ?? { provider, model, failed: 0, total: 0 };
|
|
4009
|
+
err.total++;
|
|
4010
|
+
if (isFailure) {
|
|
4011
|
+
err.failed++;
|
|
4012
|
+
failedCount++;
|
|
4013
|
+
} else {
|
|
4014
|
+
successCount++;
|
|
4015
|
+
}
|
|
4016
|
+
errorByModel.set(modelKey, err);
|
|
4017
|
+
if (isFailure) continue;
|
|
4018
|
+
sawAssistant = true;
|
|
4019
|
+
let s = sessionAgg.get(sessionID);
|
|
4020
|
+
if (!s) {
|
|
4021
|
+
s = {
|
|
4022
|
+
sessionId: sessionID,
|
|
4023
|
+
title: session?.title ?? "",
|
|
4024
|
+
provider,
|
|
4025
|
+
model,
|
|
4026
|
+
requests: 0,
|
|
4027
|
+
totalTokens: 0,
|
|
4028
|
+
inputTokens: 0,
|
|
4029
|
+
outputTokens: 0,
|
|
4030
|
+
reasoningTokens: 0,
|
|
4031
|
+
cacheRead: 0,
|
|
4032
|
+
cacheWrite: 0,
|
|
4033
|
+
totalCost: 0,
|
|
4034
|
+
day
|
|
4035
|
+
};
|
|
4036
|
+
sessionAgg.set(sessionID, s);
|
|
4037
|
+
}
|
|
4038
|
+
s.requests++;
|
|
4039
|
+
s.totalTokens += total;
|
|
4040
|
+
s.inputTokens += input;
|
|
4041
|
+
s.outputTokens += output;
|
|
4042
|
+
s.reasoningTokens += reasoning;
|
|
4043
|
+
s.cacheRead += cacheRead;
|
|
4044
|
+
s.cacheWrite += cacheWrite;
|
|
4045
|
+
s.totalCost += cost;
|
|
4046
|
+
if (!s.provider || s.provider === "unknown") s.provider = provider;
|
|
4047
|
+
if (!s.model || s.model === "unknown") s.model = model;
|
|
4048
|
+
let m = modelAgg.get(modelKey);
|
|
4049
|
+
if (!m) {
|
|
4050
|
+
m = {
|
|
4051
|
+
provider,
|
|
4052
|
+
model,
|
|
4053
|
+
requests: 0,
|
|
4054
|
+
sessions: 0,
|
|
4055
|
+
totalTokens: 0,
|
|
4056
|
+
inputTokens: 0,
|
|
4057
|
+
outputTokens: 0,
|
|
4058
|
+
reasoningTokens: 0,
|
|
4059
|
+
cacheRead: 0,
|
|
4060
|
+
totalCost: 0
|
|
4061
|
+
};
|
|
4062
|
+
modelAgg.set(modelKey, m);
|
|
4063
|
+
}
|
|
4064
|
+
m.requests++;
|
|
4065
|
+
m.totalTokens += total;
|
|
4066
|
+
m.inputTokens += input;
|
|
4067
|
+
m.outputTokens += output;
|
|
4068
|
+
m.reasoningTokens += reasoning;
|
|
4069
|
+
m.cacheRead += cacheRead;
|
|
4070
|
+
m.totalCost += cost;
|
|
4071
|
+
(modelSessions.get(modelKey) ?? modelSessions.set(modelKey, /* @__PURE__ */ new Set()).get(modelKey)).add(sessionID);
|
|
4072
|
+
let p = providerAgg.get(provider);
|
|
4073
|
+
if (!p) {
|
|
4074
|
+
p = {
|
|
4075
|
+
provider,
|
|
4076
|
+
requests: 0,
|
|
4077
|
+
sessions: 0,
|
|
4078
|
+
totalTokens: 0,
|
|
4079
|
+
inputTokens: 0,
|
|
4080
|
+
outputTokens: 0,
|
|
4081
|
+
reasoningTokens: 0,
|
|
4082
|
+
cacheRead: 0,
|
|
4083
|
+
totalCost: 0
|
|
4084
|
+
};
|
|
4085
|
+
providerAgg.set(provider, p);
|
|
4086
|
+
}
|
|
4087
|
+
p.requests++;
|
|
4088
|
+
p.totalTokens += total;
|
|
4089
|
+
p.inputTokens += input;
|
|
4090
|
+
p.outputTokens += output;
|
|
4091
|
+
p.reasoningTokens += reasoning;
|
|
4092
|
+
p.cacheRead += cacheRead;
|
|
4093
|
+
p.totalCost += cost;
|
|
4094
|
+
(providerSessions.get(provider) ?? providerSessions.set(provider, /* @__PURE__ */ new Set()).get(provider)).add(sessionID);
|
|
4095
|
+
let d = dailyAgg.get(day);
|
|
4096
|
+
if (!d) {
|
|
4097
|
+
d = {
|
|
4098
|
+
day,
|
|
4099
|
+
requests: 0,
|
|
4100
|
+
sessions: 0,
|
|
4101
|
+
totalTokens: 0,
|
|
4102
|
+
inputTokens: 0,
|
|
4103
|
+
outputTokens: 0,
|
|
4104
|
+
reasoningTokens: 0,
|
|
4105
|
+
cacheRead: 0,
|
|
4106
|
+
totalCost: 0
|
|
4107
|
+
};
|
|
4108
|
+
dailyAgg.set(day, d);
|
|
4109
|
+
}
|
|
4110
|
+
d.requests++;
|
|
4111
|
+
d.totalTokens += total;
|
|
4112
|
+
d.inputTokens += input;
|
|
4113
|
+
d.outputTokens += output;
|
|
4114
|
+
d.reasoningTokens += reasoning;
|
|
4115
|
+
d.cacheRead += cacheRead;
|
|
4116
|
+
d.totalCost += cost;
|
|
4117
|
+
(dailySessions.get(day) ?? dailySessions.set(day, /* @__PURE__ */ new Set()).get(day)).add(sessionID);
|
|
4118
|
+
}
|
|
4119
|
+
if (!sawAssistant && session?.id) {
|
|
4120
|
+
sessionAgg.delete(sessionID);
|
|
4121
|
+
}
|
|
4122
|
+
await yieldToEventLoop();
|
|
4123
|
+
}
|
|
4124
|
+
const warn = (message) => {
|
|
4125
|
+
try {
|
|
4126
|
+
ctx?.ui?.toast?.show({ message, variant: "warning" });
|
|
4127
|
+
} catch {
|
|
4128
|
+
console.warn(`[tokenwatch] ${message}`);
|
|
4129
|
+
}
|
|
4130
|
+
};
|
|
4131
|
+
if (skippedSessions > 0) {
|
|
4132
|
+
warn(t("noticeSkippedSessions").replace("{n}", String(skippedSessions)));
|
|
4133
|
+
}
|
|
4134
|
+
if (sessionsTruncated) {
|
|
4135
|
+
warn(t("noticeScanTruncatedSessions"));
|
|
4136
|
+
}
|
|
4137
|
+
if (truncatedMessageSessions > 0) {
|
|
4138
|
+
warn(t("noticeScanTruncatedMessages").replace("{n}", String(truncatedMessageSessions)));
|
|
4139
|
+
}
|
|
4140
|
+
for (const [key, set] of modelSessions) {
|
|
4141
|
+
const m = modelAgg.get(key);
|
|
4142
|
+
if (m) m.sessions = set.size;
|
|
4143
|
+
}
|
|
4144
|
+
for (const [key, set] of providerSessions) {
|
|
4145
|
+
const p = providerAgg.get(key);
|
|
4146
|
+
if (p) p.sessions = set.size;
|
|
4147
|
+
}
|
|
4148
|
+
for (const [key, set] of dailySessions) {
|
|
4149
|
+
const d = dailyAgg.get(key);
|
|
4150
|
+
if (d) d.sessions = set.size;
|
|
4151
|
+
}
|
|
4152
|
+
return {
|
|
4153
|
+
builtAt: Date.now(),
|
|
4154
|
+
sessions: Array.from(sessionAgg.values()).sort((a, b) => a.day < b.day ? 1 : a.day > b.day ? -1 : 0),
|
|
4155
|
+
models: Array.from(modelAgg.values()).sort((a, b) => b.totalTokens - a.totalTokens),
|
|
4156
|
+
providers: Array.from(providerAgg.values()).sort((a, b) => b.totalTokens - a.totalTokens),
|
|
4157
|
+
daily: Array.from(dailyAgg.values()).sort((a, b) => a.day < b.day ? 1 : a.day > b.day ? -1 : 0),
|
|
4158
|
+
errors: {
|
|
4159
|
+
successCount,
|
|
4160
|
+
failedCount,
|
|
4161
|
+
// 小数口径(0~1),与 v1 的 getErrorStats 及 HTML 报告的消费方一致
|
|
4162
|
+
errorRate: successCount + failedCount > 0 ? failedCount / (successCount + failedCount) : 0,
|
|
4163
|
+
byModel: Array.from(errorByModel.values())
|
|
4164
|
+
}
|
|
4165
|
+
};
|
|
4166
|
+
}
|
|
4167
|
+
function summarize(input) {
|
|
4168
|
+
let totalTokens = 0, inputTokens = 0, outputTokens = 0, reasoningTokens = 0;
|
|
4169
|
+
let cacheRead = 0, cacheWrite = 0, totalCost = 0, requestCount = 0;
|
|
4170
|
+
for (const s of input.sessions) {
|
|
4171
|
+
totalTokens += s.totalTokens;
|
|
4172
|
+
inputTokens += s.inputTokens;
|
|
4173
|
+
outputTokens += s.outputTokens;
|
|
4174
|
+
reasoningTokens += s.reasoningTokens;
|
|
4175
|
+
cacheRead += s.cacheRead;
|
|
4176
|
+
cacheWrite += s.cacheWrite;
|
|
4177
|
+
totalCost += s.totalCost;
|
|
4178
|
+
requestCount += s.requests;
|
|
4179
|
+
}
|
|
4180
|
+
const modelsUsed = input.models.map((m) => `${m.provider}/${m.model}`);
|
|
4181
|
+
return {
|
|
4182
|
+
model: input.sessions[0]?.model ?? "",
|
|
4183
|
+
provider: input.sessions[0]?.provider ?? "",
|
|
4184
|
+
modelsUsed,
|
|
4185
|
+
totalTokens,
|
|
4186
|
+
inputTokens,
|
|
4187
|
+
outputTokens,
|
|
4188
|
+
reasoningTokens,
|
|
4189
|
+
cacheRead,
|
|
4190
|
+
cacheWrite,
|
|
4191
|
+
totalCost,
|
|
4192
|
+
requestCount
|
|
4193
|
+
};
|
|
4194
|
+
}
|
|
4195
|
+
function startScan(ctx) {
|
|
4196
|
+
const p = scan(ctx).then((snapshot) => {
|
|
4197
|
+
cache = snapshot;
|
|
4198
|
+
inflight = null;
|
|
4199
|
+
return snapshot;
|
|
4200
|
+
}).catch((err) => {
|
|
4201
|
+
inflight = null;
|
|
4202
|
+
throw err;
|
|
4203
|
+
});
|
|
4204
|
+
p.catch(() => {
|
|
4205
|
+
});
|
|
4206
|
+
inflight = p;
|
|
4207
|
+
return p;
|
|
4208
|
+
}
|
|
4209
|
+
function buildReport(snapshot, filters) {
|
|
4210
|
+
const sessions = snapshot.sessions.filter(
|
|
4211
|
+
(s) => matchesDay(s.day, filters) && matchesModel(s.provider, s.model, filters)
|
|
4212
|
+
);
|
|
4213
|
+
const models = snapshot.models.filter((m) => matchesModel(m.provider, m.model, filters));
|
|
4214
|
+
const providers = snapshot.providers.filter((p) => !filters.provider || p.provider === filters.provider);
|
|
4215
|
+
const daily = snapshot.daily.filter((d) => matchesDay(d.day, filters));
|
|
4216
|
+
return {
|
|
4217
|
+
filters,
|
|
4218
|
+
summary: summarize({ sessions, models }),
|
|
4219
|
+
models,
|
|
4220
|
+
providers,
|
|
4221
|
+
daily,
|
|
4222
|
+
sessions: sessions.map((s) => ({
|
|
4223
|
+
sessionId: s.sessionId,
|
|
4224
|
+
title: s.title,
|
|
4225
|
+
provider: s.provider,
|
|
4226
|
+
model: s.model,
|
|
4227
|
+
requests: s.requests,
|
|
4228
|
+
totalTokens: s.totalTokens,
|
|
4229
|
+
inputTokens: s.inputTokens,
|
|
4230
|
+
outputTokens: s.outputTokens,
|
|
4231
|
+
reasoningTokens: s.reasoningTokens,
|
|
4232
|
+
cacheRead: s.cacheRead,
|
|
4233
|
+
totalCost: s.totalCost,
|
|
4234
|
+
day: s.day
|
|
4235
|
+
})),
|
|
4236
|
+
errors: snapshot.errors
|
|
4237
|
+
};
|
|
4238
|
+
}
|
|
4239
|
+
async function getUsageReport2(ctx, filters = {}) {
|
|
4240
|
+
if (!inflight) {
|
|
4241
|
+
if (!cache) {
|
|
4242
|
+
startScan(ctx);
|
|
4243
|
+
} else if (dirty) {
|
|
4244
|
+
dirty = false;
|
|
4245
|
+
startScan(ctx);
|
|
4246
|
+
}
|
|
4247
|
+
}
|
|
4248
|
+
if (!cache) {
|
|
4249
|
+
const snapshot = await inflight;
|
|
4250
|
+
return buildReport(snapshot, filters);
|
|
4251
|
+
}
|
|
4252
|
+
return buildReport(cache, filters);
|
|
4253
|
+
}
|
|
4254
|
+
function markUsageCacheDirty() {
|
|
4255
|
+
dirty = true;
|
|
4256
|
+
}
|
|
4257
|
+
function isUsageCacheCold() {
|
|
4258
|
+
return cache === null;
|
|
4259
|
+
}
|
|
4260
|
+
|
|
4261
|
+
// src/host/v2/adapter.ts
|
|
4262
|
+
function readTheme2(ctx) {
|
|
4263
|
+
const t2 = ctx.theme ?? {};
|
|
4264
|
+
const fallback = t2?.text?.default;
|
|
4265
|
+
const actionPrimary = t2?.text?.action?.primary?.default;
|
|
4266
|
+
const feedback = t2?.text?.feedback ?? {};
|
|
4267
|
+
return {
|
|
4268
|
+
primary: actionPrimary ?? fallback,
|
|
4269
|
+
text: t2?.text?.default ?? fallback,
|
|
4270
|
+
textMuted: t2?.text?.subdued ?? fallback,
|
|
4271
|
+
background: t2?.background?.default ?? fallback,
|
|
4272
|
+
border: t2?.border?.default ?? fallback,
|
|
4273
|
+
success: feedback?.success?.default ?? fallback,
|
|
4274
|
+
warning: feedback?.warning?.default ?? fallback,
|
|
4275
|
+
error: feedback?.error?.default ?? fallback
|
|
4276
|
+
};
|
|
4277
|
+
}
|
|
4278
|
+
function makeStore2(ctx, dispose) {
|
|
4279
|
+
const [state, mutate] = ctx.storage.store("tokenwatch", {
|
|
4280
|
+
initial: { values: {} }
|
|
4281
|
+
});
|
|
4282
|
+
return {
|
|
4283
|
+
get(key, fallback) {
|
|
4284
|
+
try {
|
|
4285
|
+
const v = state?.values?.[key];
|
|
4286
|
+
return v === void 0 || v === null ? fallback : v;
|
|
4287
|
+
} catch {
|
|
4288
|
+
return fallback;
|
|
4289
|
+
}
|
|
4290
|
+
},
|
|
4291
|
+
set(key, value) {
|
|
4292
|
+
void mutate((draft) => {
|
|
4293
|
+
draft.values[key] = value;
|
|
4294
|
+
}).catch(() => {
|
|
4295
|
+
});
|
|
4296
|
+
},
|
|
4297
|
+
delete(key) {
|
|
4298
|
+
void mutate((draft) => {
|
|
4299
|
+
delete draft.values[key];
|
|
4300
|
+
}).catch(() => {
|
|
4301
|
+
});
|
|
4302
|
+
}
|
|
4303
|
+
};
|
|
4304
|
+
}
|
|
4305
|
+
function normalizeParts(message) {
|
|
4306
|
+
const content = message?.content;
|
|
4307
|
+
if (!Array.isArray(content)) return [];
|
|
4308
|
+
const out = [];
|
|
4309
|
+
for (const part of content) {
|
|
4310
|
+
if (part?.type === "text") {
|
|
4311
|
+
out.push({ type: "text", text: part.text ?? "" });
|
|
4312
|
+
} else if (part?.type === "reasoning") {
|
|
4313
|
+
out.push({ type: "reasoning", text: part.text ?? "" });
|
|
4314
|
+
} else if (part?.type === "tool") {
|
|
4315
|
+
const st = part.state ?? {};
|
|
4316
|
+
out.push({
|
|
4317
|
+
type: "tool",
|
|
4318
|
+
name: part.name,
|
|
4319
|
+
state: {
|
|
4320
|
+
status: st.status,
|
|
4321
|
+
raw: st.input != null ? JSON.stringify(st.input) : void 0,
|
|
4322
|
+
input: st.input,
|
|
4323
|
+
// v2 工具输出在 content 数组里,需拼接为字符串
|
|
4324
|
+
output: st.status === "completed" ? stringifyToolContent(st.content) : void 0,
|
|
4325
|
+
error: st.status === "error" ? String(st.error ?? "") : void 0
|
|
4326
|
+
}
|
|
4327
|
+
});
|
|
4328
|
+
}
|
|
4329
|
+
}
|
|
4330
|
+
return out;
|
|
4331
|
+
}
|
|
4332
|
+
function stringifyToolContent(content) {
|
|
4333
|
+
if (typeof content === "string") return content;
|
|
4334
|
+
if (!Array.isArray(content)) return "";
|
|
4335
|
+
let out = "";
|
|
4336
|
+
for (const item of content) {
|
|
4337
|
+
if (typeof item === "string") out += item;
|
|
4338
|
+
else if (item && typeof item.text === "string") out += item.text;
|
|
4339
|
+
else if (item?.type === "text" && typeof item?.text === "string") out += item.text;
|
|
4340
|
+
}
|
|
4341
|
+
return out;
|
|
4342
|
+
}
|
|
4343
|
+
function buildMessageEvent(ctx, data) {
|
|
4344
|
+
const sessionID = data?.sessionID ?? "";
|
|
4345
|
+
const messageID = data?.assistantMessageID ?? "";
|
|
4346
|
+
const tokens = data?.tokens ?? {};
|
|
4347
|
+
const cache2 = tokens.cache ?? {};
|
|
4348
|
+
const message = messageID ? ctx.data.session.message.get(sessionID, messageID) : void 0;
|
|
4349
|
+
const model = message?.model ?? {};
|
|
4350
|
+
const total = (tokens.input ?? 0) + (tokens.output ?? 0) + (tokens.reasoning ?? 0) + (cache2.read ?? 0) + (cache2.write ?? 0);
|
|
4351
|
+
return {
|
|
4352
|
+
messageID,
|
|
4353
|
+
sessionID,
|
|
4354
|
+
role: "assistant",
|
|
4355
|
+
providerID: model.providerID ?? "unknown",
|
|
4356
|
+
modelID: model.id ?? "unknown",
|
|
4357
|
+
input: tokens.input ?? 0,
|
|
4358
|
+
output: tokens.output ?? 0,
|
|
4359
|
+
reasoning: tokens.reasoning ?? 0,
|
|
4360
|
+
cacheRead: cache2.read ?? 0,
|
|
4361
|
+
cacheWrite: cache2.write ?? 0,
|
|
4362
|
+
total,
|
|
4363
|
+
cost: typeof data?.cost === "number" ? data.cost : 0,
|
|
4364
|
+
// step.ended 事件本身不带时间;从消息对象补齐(性能追踪需要 created/completed)。
|
|
4365
|
+
// TPS 的流式终点优先取 time.streamed(provider 响应体接收完毕,剔除收尾
|
|
4366
|
+
// settlement 时间),与宿主官方 tok/s 口径一致;缺失时回退 completed。
|
|
4367
|
+
timeCreated: message?.time?.created ?? data?.time?.created,
|
|
4368
|
+
timeCompleted: message?.time?.streamed ?? message?.time?.completed ?? data?.time?.completed,
|
|
4369
|
+
raw: data
|
|
4370
|
+
};
|
|
4371
|
+
}
|
|
4372
|
+
function createV2Adapter(ctx, dispose) {
|
|
4373
|
+
let storeRef;
|
|
4374
|
+
let mountKeymapLayer;
|
|
4375
|
+
const clientScanDataSource = {
|
|
4376
|
+
kind: "client-scan",
|
|
4377
|
+
// v2 没有 `opencode db`,历史统计需遍历会话消息重算,首次必须提示用户
|
|
4378
|
+
needsFirstRunNotice: true,
|
|
4379
|
+
isCold: () => isUsageCacheCold(),
|
|
4380
|
+
getUsageReport: (filters) => getUsageReport2(ctx, filters)
|
|
4381
|
+
};
|
|
4382
|
+
return {
|
|
4383
|
+
kind: "v2",
|
|
4384
|
+
hostVersion: ctx.app?.version ?? "2.x",
|
|
4385
|
+
theme: () => readTheme2(ctx),
|
|
4386
|
+
get store() {
|
|
4387
|
+
if (!storeRef) storeRef = makeStore2(ctx, dispose);
|
|
4388
|
+
return storeRef;
|
|
4389
|
+
},
|
|
4390
|
+
subscribe(handlers) {
|
|
4391
|
+
const off = [];
|
|
4392
|
+
off.push(
|
|
4393
|
+
ctx.data.on("session.step.ended", (event) => {
|
|
4394
|
+
const normalized = buildMessageEvent(ctx, event?.data);
|
|
4395
|
+
if (normalized) handlers.onMessageUpdated(normalized);
|
|
4396
|
+
handlers.onInvalidate();
|
|
4397
|
+
})
|
|
4398
|
+
);
|
|
4399
|
+
off.push(
|
|
4400
|
+
ctx.data.on("session.step.started", (event) => {
|
|
4401
|
+
const data = event?.data ?? {};
|
|
4402
|
+
const part = {
|
|
4403
|
+
messageID: data.assistantMessageID,
|
|
4404
|
+
type: "step",
|
|
4405
|
+
timeStart: event?.created,
|
|
4406
|
+
raw: event
|
|
4407
|
+
};
|
|
4408
|
+
handlers.onPartUpdated(part);
|
|
4409
|
+
})
|
|
4410
|
+
);
|
|
4411
|
+
off.push(
|
|
4412
|
+
ctx.data.on("session.reasoning.started", (event) => {
|
|
4413
|
+
const data = event?.data ?? {};
|
|
4414
|
+
const part = {
|
|
4415
|
+
messageID: data.assistantMessageID,
|
|
4416
|
+
type: "part-open",
|
|
4417
|
+
timeStart: event?.created,
|
|
4418
|
+
raw: event
|
|
4419
|
+
};
|
|
4420
|
+
handlers.onPartUpdated(part);
|
|
4421
|
+
})
|
|
4422
|
+
);
|
|
4423
|
+
off.push(
|
|
4424
|
+
ctx.data.on("session.reasoning.delta", (event) => {
|
|
4425
|
+
const data = event?.data ?? {};
|
|
4426
|
+
const part = {
|
|
4427
|
+
messageID: data.assistantMessageID,
|
|
4428
|
+
type: "reasoning-delta",
|
|
4429
|
+
timeStart: event?.created,
|
|
4430
|
+
raw: event
|
|
4431
|
+
};
|
|
4432
|
+
handlers.onPartUpdated(part);
|
|
4433
|
+
})
|
|
4434
|
+
);
|
|
4435
|
+
off.push(
|
|
4436
|
+
ctx.data.on("session.text.delta", (event) => {
|
|
4437
|
+
const data = event?.data ?? {};
|
|
4438
|
+
const part = {
|
|
4439
|
+
messageID: data.assistantMessageID,
|
|
4440
|
+
type: "text-delta",
|
|
4441
|
+
timeStart: event?.created,
|
|
4442
|
+
raw: event
|
|
4443
|
+
};
|
|
4444
|
+
handlers.onPartUpdated(part);
|
|
4445
|
+
})
|
|
4446
|
+
);
|
|
4447
|
+
off.push(
|
|
4448
|
+
ctx.data.on("session.text.started", (event) => {
|
|
4449
|
+
const data = event?.data ?? {};
|
|
4450
|
+
const part = {
|
|
4451
|
+
messageID: data.assistantMessageID,
|
|
4452
|
+
type: "part-open",
|
|
4453
|
+
timeStart: event?.created ?? data.time?.start,
|
|
4454
|
+
raw: event
|
|
4455
|
+
};
|
|
4456
|
+
handlers.onPartUpdated(part);
|
|
4457
|
+
})
|
|
4458
|
+
);
|
|
4459
|
+
off.push(
|
|
4460
|
+
ctx.data.on("session.message.content.updated", () => {
|
|
4461
|
+
handlers.onInvalidate();
|
|
4462
|
+
})
|
|
4463
|
+
);
|
|
4464
|
+
off.push(
|
|
4465
|
+
ctx.data.on("session.idle", () => {
|
|
4466
|
+
markUsageCacheDirty();
|
|
4467
|
+
handlers.onInvalidate();
|
|
4468
|
+
})
|
|
4469
|
+
);
|
|
4470
|
+
const disposeFn = () => {
|
|
4471
|
+
for (const fn of off) {
|
|
4472
|
+
try {
|
|
4473
|
+
fn();
|
|
4474
|
+
} catch {
|
|
4475
|
+
}
|
|
4476
|
+
}
|
|
4477
|
+
};
|
|
4478
|
+
return disposeFn;
|
|
4479
|
+
},
|
|
4480
|
+
registerSidebar(render) {
|
|
4481
|
+
const offPanel = ctx.ui.slot({
|
|
4482
|
+
append: "sidebar.content",
|
|
4483
|
+
render: (input) => render({ sessionID: input?.sessionID ?? "" })
|
|
4484
|
+
});
|
|
4485
|
+
const offVehicle = ctx.ui.slot({
|
|
4486
|
+
append: "prompt.footer",
|
|
4487
|
+
render: () => {
|
|
4488
|
+
mountKeymapLayer?.();
|
|
4489
|
+
return null;
|
|
4490
|
+
}
|
|
4491
|
+
});
|
|
4492
|
+
return () => {
|
|
4493
|
+
if (typeof offPanel === "function") offPanel();
|
|
4494
|
+
if (typeof offVehicle === "function") offVehicle();
|
|
4495
|
+
};
|
|
4496
|
+
},
|
|
4497
|
+
registerCommands(specs) {
|
|
4498
|
+
let mountFailed = false;
|
|
4499
|
+
const mount = () => {
|
|
4500
|
+
try {
|
|
4501
|
+
ctx.keymap.layer(() => ({
|
|
4502
|
+
mode: "global",
|
|
4503
|
+
commands: specs.map((spec) => ({
|
|
4504
|
+
id: spec.id,
|
|
4505
|
+
title: spec.title,
|
|
4506
|
+
description: spec.description,
|
|
4507
|
+
group: spec.category ?? "Stats",
|
|
4508
|
+
palette: true,
|
|
4509
|
+
slash: spec.slash ? { name: spec.slash } : void 0,
|
|
4510
|
+
run: () => {
|
|
4511
|
+
void spec.run();
|
|
4512
|
+
}
|
|
4513
|
+
}))
|
|
4514
|
+
}));
|
|
4515
|
+
} catch (err) {
|
|
4516
|
+
if (!mountFailed) {
|
|
4517
|
+
mountFailed = true;
|
|
4518
|
+
try {
|
|
4519
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4520
|
+
ctx.ui?.toast?.show({ message: `TokenWatch commands unavailable: ${message}`, variant: "error" });
|
|
4521
|
+
} catch {
|
|
4522
|
+
}
|
|
4523
|
+
}
|
|
4524
|
+
}
|
|
4525
|
+
};
|
|
4526
|
+
mountKeymapLayer = mount;
|
|
4527
|
+
return () => {
|
|
4528
|
+
if (mountKeymapLayer === mount) mountKeymapLayer = void 0;
|
|
4529
|
+
};
|
|
4530
|
+
},
|
|
4531
|
+
notify(message, variant = "info") {
|
|
4532
|
+
try {
|
|
4533
|
+
ctx.ui?.toast?.show({ message, variant });
|
|
4534
|
+
} catch {
|
|
4535
|
+
}
|
|
4536
|
+
},
|
|
4537
|
+
async alert(input) {
|
|
4538
|
+
try {
|
|
4539
|
+
await ctx.ui.dialog.alert(input);
|
|
4540
|
+
} catch {
|
|
4541
|
+
}
|
|
4542
|
+
},
|
|
4543
|
+
async select(input) {
|
|
4544
|
+
try {
|
|
4545
|
+
return await ctx.ui.dialog.select({
|
|
4546
|
+
title: input.title,
|
|
4547
|
+
options: input.options
|
|
4548
|
+
});
|
|
4549
|
+
} catch {
|
|
4550
|
+
return void 0;
|
|
4551
|
+
}
|
|
4552
|
+
},
|
|
4553
|
+
onDispose(fn) {
|
|
4554
|
+
dispose.add(fn);
|
|
4555
|
+
},
|
|
4556
|
+
sessionMessages(sessionID) {
|
|
4557
|
+
try {
|
|
4558
|
+
return ctx.data.session.message.list(sessionID) ?? [];
|
|
4559
|
+
} catch {
|
|
4560
|
+
return [];
|
|
4561
|
+
}
|
|
4562
|
+
},
|
|
4563
|
+
messageParts(messageID, sessionID) {
|
|
4564
|
+
try {
|
|
4565
|
+
if (sessionID) {
|
|
4566
|
+
const message = ctx.data.session.message.get(sessionID, messageID);
|
|
4567
|
+
if (message) return normalizeParts(message);
|
|
4568
|
+
}
|
|
4569
|
+
for (const session of ctx.data.session.list()) {
|
|
4570
|
+
const messages = ctx.data.session.message.list(session.id) ?? [];
|
|
4571
|
+
for (const message of messages) {
|
|
4572
|
+
if (message?.id === messageID) return normalizeParts(message);
|
|
4573
|
+
}
|
|
4574
|
+
}
|
|
4575
|
+
} catch {
|
|
4576
|
+
}
|
|
4577
|
+
return [];
|
|
4578
|
+
},
|
|
4579
|
+
appConfig() {
|
|
4580
|
+
try {
|
|
4581
|
+
const agents = ctx.data.location.agent.list() ?? [];
|
|
4582
|
+
return { agent: Object.fromEntries(agents.map((a) => [a?.name ?? a?.id ?? "default", a])) };
|
|
4583
|
+
} catch {
|
|
4584
|
+
return {};
|
|
4585
|
+
}
|
|
4586
|
+
},
|
|
4587
|
+
onPartUpdated(handler) {
|
|
4588
|
+
try {
|
|
4589
|
+
return ctx.data.on("session.message.content.updated", () => handler());
|
|
4590
|
+
} catch {
|
|
4591
|
+
return () => {
|
|
4592
|
+
};
|
|
4593
|
+
}
|
|
4594
|
+
},
|
|
4595
|
+
dataSource: clientScanDataSource
|
|
4596
|
+
};
|
|
4597
|
+
}
|
|
4598
|
+
|
|
4599
|
+
// src/host/command-actions.ts
|
|
4600
|
+
import { writeFileSync as writeFileSync5 } from "node:fs";
|
|
4601
|
+
import { join as join6 } from "node:path";
|
|
4602
|
+
async function fetchUsage(host, filters) {
|
|
4603
|
+
if (host.dataSource.needsFirstRunNotice && host.dataSource.isCold()) {
|
|
4604
|
+
host.notify(t("noticeFirstScan"), "info");
|
|
4605
|
+
}
|
|
4606
|
+
return host.dataSource.getUsageReport(filters);
|
|
4607
|
+
}
|
|
4608
|
+
function errMsg(err) {
|
|
4609
|
+
return `${t("toastError")}: ${err instanceof Error ? err.message : String(err)}`;
|
|
4610
|
+
}
|
|
4611
|
+
function stamp() {
|
|
4612
|
+
const d = /* @__PURE__ */ new Date();
|
|
4613
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
4614
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
4615
|
+
}
|
|
4616
|
+
function createCommandActions(host) {
|
|
4617
|
+
return {
|
|
4618
|
+
async htmlReport(filters = {}, presetTag) {
|
|
4619
|
+
try {
|
|
4620
|
+
const usage = await fetchUsage(host, filters);
|
|
4621
|
+
const data = buildCombinedData(usage);
|
|
4622
|
+
const filePath = writeHtmlReport(data, getRangeSlug(filters, presetTag));
|
|
4623
|
+
host.notify(`${t("toastReportSaved")}: ${filePath}`, "success");
|
|
4624
|
+
openInBrowser(filePath);
|
|
4625
|
+
} catch (err) {
|
|
4626
|
+
host.notify(errMsg(err), "error");
|
|
4627
|
+
}
|
|
4628
|
+
},
|
|
4629
|
+
async jsonExport() {
|
|
4630
|
+
try {
|
|
4631
|
+
const usage = await fetchUsage(host, {});
|
|
4632
|
+
const filePath = join6(ensureReportDir(), `tokenwatch-${stamp()}.json`);
|
|
4633
|
+
writeFileSync5(filePath, JSON.stringify(usage, null, 2), "utf-8");
|
|
4634
|
+
host.notify(`${t("toastJsonSaved")}: ${filePath}`, "success");
|
|
4635
|
+
} catch (err) {
|
|
4636
|
+
host.notify(errMsg(err), "error");
|
|
4637
|
+
}
|
|
4638
|
+
},
|
|
4639
|
+
async textReport() {
|
|
4640
|
+
try {
|
|
4641
|
+
const usage = await fetchUsage(host, {});
|
|
4642
|
+
const filePath = join6(ensureReportDir(), `tokenwatch-${stamp()}.md`);
|
|
4643
|
+
writeFileSync5(filePath, formatUsageReport(usage), "utf-8");
|
|
4644
|
+
host.notify(`${t("toastReportSaved")}: ${filePath}`, "success");
|
|
4645
|
+
} catch (err) {
|
|
4646
|
+
host.notify(errMsg(err), "error");
|
|
4647
|
+
}
|
|
4648
|
+
},
|
|
4649
|
+
config: () => loadConfig(host.store, host.appConfig()),
|
|
4650
|
+
toggle: (key) => toggleSidebarSetting(host.store, key),
|
|
4651
|
+
setLanguage(language) {
|
|
4652
|
+
const cfg = setLanguageSetting(host.store, language);
|
|
4653
|
+
setLanguage(language);
|
|
4654
|
+
return cfg;
|
|
4655
|
+
}
|
|
4656
|
+
};
|
|
4657
|
+
}
|
|
4658
|
+
function rangePresets() {
|
|
4659
|
+
const today = stamp();
|
|
4660
|
+
const daysAgo = (n) => {
|
|
4661
|
+
const d = /* @__PURE__ */ new Date();
|
|
4662
|
+
d.setDate(d.getDate() - n);
|
|
4663
|
+
return localDateStr(d);
|
|
4664
|
+
};
|
|
4665
|
+
return [
|
|
4666
|
+
{ id: "all", label: t("menuAll"), tag: "all", filters: {} },
|
|
4667
|
+
{ id: "today", label: t("menuToday"), tag: "today", filters: { startDate: today, endDate: today } },
|
|
4668
|
+
{ id: "7d", label: t("menu7d"), tag: "7d", filters: { startDate: daysAgo(6) } },
|
|
4669
|
+
{ id: "30d", label: t("menu30d"), tag: "30d", filters: { startDate: daysAgo(29) } }
|
|
4670
|
+
];
|
|
4671
|
+
}
|
|
4672
|
+
|
|
4673
|
+
// src/host/v2/commands.ts
|
|
4674
|
+
var TOGGLE_KEYS = [
|
|
4675
|
+
"showPerformance",
|
|
4676
|
+
"showPricing",
|
|
4677
|
+
"showTokenDistribution",
|
|
4678
|
+
"showTrend"
|
|
4679
|
+
];
|
|
4680
|
+
var TOGGLE_LABELS = {
|
|
4681
|
+
showPerformance: () => t("showPerformance"),
|
|
4682
|
+
showPricing: () => t("showPricing"),
|
|
4683
|
+
showTokenDistribution: () => t("showTokenDistribution"),
|
|
4684
|
+
showTrend: () => t("showTrend")
|
|
4685
|
+
};
|
|
4686
|
+
async function showUsageMenu2(host) {
|
|
4687
|
+
const actions = createCommandActions(host);
|
|
4688
|
+
setLanguage(actions.config().language);
|
|
4689
|
+
const choice = await host.select({
|
|
4690
|
+
title: t("panelTitle"),
|
|
4691
|
+
options: [
|
|
4692
|
+
{ title: t("cmdTitleHtml"), value: "html", description: t("cmdDescHtml") },
|
|
4693
|
+
{ title: t("cmdTitleJson"), value: "json", description: t("cmdDescJson") },
|
|
4694
|
+
{ title: t("cmdTitleText"), value: "text", description: t("cmdDescText") },
|
|
4695
|
+
{ title: t("cmdTitleSettings"), value: "settings", description: t("cmdDescSettings") }
|
|
4696
|
+
]
|
|
4697
|
+
});
|
|
4698
|
+
if (choice === void 0) return;
|
|
4699
|
+
switch (choice) {
|
|
4700
|
+
case "html": {
|
|
4701
|
+
const presets = rangePresets();
|
|
4702
|
+
const picked = await host.select({
|
|
4703
|
+
title: t("cmdTitleHtml"),
|
|
4704
|
+
options: presets.map((p) => ({ title: p.label, value: p.id }))
|
|
4705
|
+
});
|
|
4706
|
+
if (picked === void 0) return;
|
|
4707
|
+
const preset = presets.find((p) => p.id === picked);
|
|
4708
|
+
if (preset) await actions.htmlReport(preset.filters, preset.tag);
|
|
4709
|
+
return;
|
|
4710
|
+
}
|
|
4711
|
+
case "json":
|
|
4712
|
+
return actions.jsonExport();
|
|
4713
|
+
case "text":
|
|
4714
|
+
return actions.textReport();
|
|
4715
|
+
case "settings":
|
|
4716
|
+
return showSettingsMenu(host);
|
|
4717
|
+
}
|
|
4718
|
+
}
|
|
4719
|
+
async function showSettingsMenu(host) {
|
|
4720
|
+
const actions = createCommandActions(host);
|
|
4721
|
+
const options = [
|
|
4722
|
+
...TOGGLE_KEYS.map((key) => ({
|
|
4723
|
+
title: `${actions.config().sidebar[key] ? "\u2713 " : " "}${TOGGLE_LABELS[key]()}`,
|
|
4724
|
+
value: key
|
|
4725
|
+
})),
|
|
4726
|
+
{ title: t("settingsLanguage"), value: "language" },
|
|
4727
|
+
{ title: t("done"), value: "done" }
|
|
4728
|
+
];
|
|
4729
|
+
const choice = await host.select({ title: t("settingsTitle"), options });
|
|
4730
|
+
if (choice === void 0 || choice === "done") return;
|
|
4731
|
+
if (choice === "language") {
|
|
4732
|
+
const current = actions.config().language;
|
|
4733
|
+
const lang = await host.select({
|
|
4734
|
+
title: t("settingsLanguage"),
|
|
4735
|
+
options: [
|
|
4736
|
+
{ title: `${current === "auto" ? "\u2713 " : " "}${t("langAuto")}`, value: "auto" },
|
|
4737
|
+
{ title: `${current === "zh" ? "\u2713 " : " "}\u4E2D\u6587`, value: "zh" },
|
|
4738
|
+
{ title: `${current === "en" ? "\u2713 " : " "}English`, value: "en" }
|
|
4739
|
+
]
|
|
4740
|
+
});
|
|
4741
|
+
if (lang !== void 0) actions.setLanguage(lang);
|
|
4742
|
+
return;
|
|
4743
|
+
}
|
|
4744
|
+
actions.toggle(choice);
|
|
4745
|
+
await showSettingsMenu(host);
|
|
4746
|
+
}
|
|
4747
|
+
function registerV2Commands(host) {
|
|
4748
|
+
const specs = [
|
|
4749
|
+
{
|
|
4750
|
+
id: "tokenwatch.usage",
|
|
4751
|
+
title: "TokenWatch",
|
|
4752
|
+
description: "Token usage reports, export, and settings",
|
|
4753
|
+
category: "Stats",
|
|
4754
|
+
slash: "usage",
|
|
4755
|
+
run: () => showUsageMenu2(host)
|
|
4756
|
+
}
|
|
4757
|
+
];
|
|
4758
|
+
host.registerCommands?.(specs);
|
|
4759
|
+
}
|
|
4760
|
+
|
|
4761
|
+
// src/tui.tsx
|
|
4762
|
+
var tui = async (api) => {
|
|
4763
|
+
const host = createV1Adapter(api);
|
|
4764
|
+
startTokenWatch(host);
|
|
4765
|
+
await registerCommands(api);
|
|
4766
|
+
};
|
|
4767
|
+
var setup = (ctx) => {
|
|
4768
|
+
try {
|
|
4769
|
+
const dispose = /* @__PURE__ */ new Set();
|
|
4770
|
+
const host = createV2Adapter(ctx, dispose);
|
|
4771
|
+
startTokenWatch(host);
|
|
4772
|
+
registerV2Commands(host);
|
|
4773
|
+
return () => {
|
|
4774
|
+
for (const fn of dispose) {
|
|
4775
|
+
try {
|
|
4776
|
+
fn();
|
|
4777
|
+
} catch {
|
|
4778
|
+
}
|
|
4779
|
+
}
|
|
4780
|
+
};
|
|
4781
|
+
} catch (err) {
|
|
4782
|
+
const message = err instanceof Error ? (err.stack ?? err.message).split("\n")[0] : String(err);
|
|
4783
|
+
try {
|
|
4784
|
+
ctx?.ui?.toast?.show({
|
|
4785
|
+
message: `TokenWatch setup failed: ${message}`,
|
|
4786
|
+
variant: "error"
|
|
4787
|
+
});
|
|
4788
|
+
} catch {
|
|
4789
|
+
}
|
|
4790
|
+
return () => {
|
|
4791
|
+
};
|
|
4792
|
+
}
|
|
4793
|
+
};
|
|
4794
|
+
var plugin = {
|
|
4795
|
+
id: "opencode-tokenwatch",
|
|
4796
|
+
tui,
|
|
4797
|
+
setup
|
|
4798
|
+
};
|
|
4799
|
+
var tui_default = plugin;
|
|
4800
|
+
export {
|
|
4801
|
+
tui_default as default
|
|
4802
|
+
};
|