opencode-tokenwatch 0.3.0 → 0.3.2
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 +21 -11
- package/README.md +21 -11
- package/dist/commands.jsx +14 -2
- package/dist/generate-usage-html.js +41 -21
- package/dist/i18n.js +2 -0
- package/dist/perf-tracker.d.ts +1 -0
- package/dist/perf-tracker.js +42 -0
- package/dist/sidebar.d.ts +2 -2
- package/dist/sidebar.jsx +323 -114
- package/dist/stats-store.d.ts +23 -0
- package/dist/stats-store.js +258 -0
- package/dist/tui.jsx +81 -26
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -8,18 +8,20 @@ Real-time token usage, cache analytics & performance dashboard plugin for OpenCo
|
|
|
8
8
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
11
|
-
- **Sidebar panel** — Session-level and per-model real-time stats (requests, tokens, cache,
|
|
12
|
-
- **
|
|
11
|
+
- **Sidebar panel** — Session-level and per-model real-time stats (requests, tokens, cache, cost)
|
|
12
|
+
- **Smart sorting** — Models sorted by most recent call time, current model always on top
|
|
13
|
+
- **Provider name truncation** — Long provider names auto-truncated (12 chars) to prevent layout overflow
|
|
14
|
+
- **Cache hit rate** — Inline color progress bars with trend indicators (↑/↓) and global weighted hit rate
|
|
13
15
|
- **Performance metrics** — TTFT / TPS / End-to-end latency + P50/P95/P99 latency percentiles
|
|
14
|
-
- **Token distribution** — 5-bucket role breakdown (system
|
|
15
|
-
- **Error rate
|
|
16
|
-
- **
|
|
16
|
+
- **Token distribution** — 5-bucket role breakdown (system / user / toolCall / toolResult / output + other fallback)
|
|
17
|
+
- **Error rate tracking** — Detects failed requests (empty token response) and computes real-time error rate
|
|
18
|
+
- **Invalid data filtering** — Full-chain defense: filters zero-token entries from perf-tracker, sidebar, and HTML reports
|
|
19
|
+
- **Cost display** — Per-model cost (requires provider billing data)
|
|
17
20
|
- **`/usage` command** — HTML Report → JSON Export → Text Report → Settings
|
|
18
|
-
- **HTML report** — Interactive ECharts dashboard:
|
|
21
|
+
- **HTML report** — Interactive ECharts dashboard: token distribution, performance percentiles, TPS horizontal ranking, error rate analysis — auto-opened in browser
|
|
22
|
+
- **Persistent stats** — Performance metrics (TPS/TTFT/latency) written to a dedicated JSON file that accumulates forever, unaffected by log rotation
|
|
19
23
|
- **Multi-level collapse** — Panel, models, and sub-blocks collapsible with persisted state
|
|
20
24
|
- **Language switching** — Auto-detect or manually switch between Chinese and English
|
|
21
|
-
- **Per-request tracking** — TTFT/TPS/latency logged to JSONL for report analysis
|
|
22
|
-
- **Adaptive coloring** — Colors auto-derived from theme
|
|
23
25
|
|
|
24
26
|
## Install
|
|
25
27
|
|
|
@@ -45,8 +47,8 @@ Add to `opencode.json` or `opencode.jsonc`:
|
|
|
45
47
|
"opencode-tokenwatch": {
|
|
46
48
|
"sidebar": {
|
|
47
49
|
"showPerformance": true,
|
|
48
|
-
"showPricing":
|
|
49
|
-
"showTokenDistribution":
|
|
50
|
+
"showPricing": true,
|
|
51
|
+
"showTokenDistribution": true,
|
|
50
52
|
"showTrend": true
|
|
51
53
|
},
|
|
52
54
|
"language": "auto"
|
|
@@ -58,7 +60,7 @@ Add to `opencode.json` or `opencode.jsonc`:
|
|
|
58
60
|
| Option | Type | Default | Description |
|
|
59
61
|
|--------|------|---------|-------------|
|
|
60
62
|
| `sidebar.showPerformance` | boolean | `true` | Show TTFT/TPS/latency |
|
|
61
|
-
| `sidebar.showPricing` | boolean | `true` | Show
|
|
63
|
+
| `sidebar.showPricing` | boolean | `true` | Show request cost |
|
|
62
64
|
| `sidebar.showTokenDistribution` | boolean | `true` | Show token distribution |
|
|
63
65
|
| `sidebar.showTrend` | boolean | `true` | Show trend indicator |
|
|
64
66
|
| `language` | `"auto"` / `"zh"` / `"en"` | `"auto"` | UI language |
|
|
@@ -74,6 +76,14 @@ In OpenCode TUI, run `/usage`:
|
|
|
74
76
|
- **Text Report** — Exports Markdown report to `~/.opencode/reports/`
|
|
75
77
|
- **Settings** — Toggle sidebar blocks, switch language
|
|
76
78
|
|
|
79
|
+
## Data Files
|
|
80
|
+
|
|
81
|
+
| File | Path | Description |
|
|
82
|
+
|------|------|-------------|
|
|
83
|
+
| JSONL log | `~/.opencode/tokenwatch.jsonl` | Raw per-request log, auto-rotated at 5 MB |
|
|
84
|
+
| Aggregated stats | `~/.opencode/tokenwatch-stats.json` | Persistent performance stats, accumulates forever |
|
|
85
|
+
| Report output | `~/.opencode/reports/` | HTML / JSON / Markdown reports |
|
|
86
|
+
|
|
77
87
|
## Requirements
|
|
78
88
|
|
|
79
89
|
- OpenCode CLI (with `opencode db` command)
|
package/README.md
CHANGED
|
@@ -8,18 +8,20 @@ OpenCode CLI 的实时 Token 用量统计、缓存分析与性能指标插件。
|
|
|
8
8
|
|
|
9
9
|
## 功能
|
|
10
10
|
|
|
11
|
-
- **侧边栏面板** — 会话级与按模型的实时统计(请求数、Token
|
|
12
|
-
-
|
|
11
|
+
- **侧边栏面板** — 会话级与按模型的实时统计(请求数、Token、缓存、成本)
|
|
12
|
+
- **智能排序** — 模型按最近调用时间降序排列,当前焦点模型始终在顶部
|
|
13
|
+
- **供应商名称截断** — 超长供应商名自动截断(12 字符),避免撑破布局
|
|
14
|
+
- **缓存命中率** — 模型行内彩色进度条,带趋势指示器(↑/↓),全局总计行显示**加权命中率**
|
|
13
15
|
- **性能指标** — TTFT / TPS / 端到端延迟 + P50/P95/P99 延迟分位数
|
|
14
|
-
- **Token 分布** — 5
|
|
16
|
+
- **Token 分布** — 5 桶角色分解(system / user / toolCall / toolResult / output + other 兜底)
|
|
15
17
|
- **错误率统计** — 识别并统计失败请求(空 Token 响应),实时计算错误率
|
|
16
|
-
-
|
|
18
|
+
- **无效数据过滤** — 全链路防御:从 perf-tracker 到 sidebar、HTML 报告均过滤全零 token 无效请求
|
|
19
|
+
- **成本展示** — 按模型显示 cost(需 provider 返回计费数据)
|
|
17
20
|
- **`/usage` 命令** — HTML 报告 → JSON 导出 → 文本报告 → 设置
|
|
18
|
-
- **HTML 报告** — 交互式 ECharts 仪表盘:Token
|
|
21
|
+
- **HTML 报告** — 交互式 ECharts 仪表盘:Token 分布、性能分位数、TPS 水平排名、错误率分析,自动在浏览器打开
|
|
22
|
+
- **持久化统计** — 性能指标(TPS/TTFT/延迟)写入独立 JSON 文件,永久累积,不受日志轮转影响
|
|
19
23
|
- **多级折叠** — 面板、模型、子区块均可折叠,状态持久化
|
|
20
|
-
- **语言切换** —
|
|
21
|
-
- **性能追踪** — 每次请求的 TTFT/TPS/延迟写入 JSONL,用于报告分析
|
|
22
|
-
- **自适应配色** — 从主题色自动衍生
|
|
24
|
+
- **语言切换** — 中英双语,跟随系统或手动切换
|
|
23
25
|
|
|
24
26
|
## 安装
|
|
25
27
|
|
|
@@ -45,8 +47,8 @@ npm install opencode-tokenwatch
|
|
|
45
47
|
"opencode-tokenwatch": {
|
|
46
48
|
"sidebar": {
|
|
47
49
|
"showPerformance": true,
|
|
48
|
-
"showPricing":
|
|
49
|
-
"showTokenDistribution":
|
|
50
|
+
"showPricing": true,
|
|
51
|
+
"showTokenDistribution": true,
|
|
50
52
|
"showTrend": true
|
|
51
53
|
},
|
|
52
54
|
"language": "auto"
|
|
@@ -58,7 +60,7 @@ npm install opencode-tokenwatch
|
|
|
58
60
|
| 配置项 | 类型 | 默认值 | 说明 |
|
|
59
61
|
|--------|------|--------|------|
|
|
60
62
|
| `sidebar.showPerformance` | boolean | `true` | 显示 TTFT/TPS/延迟 |
|
|
61
|
-
| `sidebar.showPricing` | boolean | `true` |
|
|
63
|
+
| `sidebar.showPricing` | boolean | `true` | 显示请求成本 |
|
|
62
64
|
| `sidebar.showTokenDistribution` | boolean | `true` | 显示 Token 分布 |
|
|
63
65
|
| `sidebar.showTrend` | boolean | `true` | 显示趋势指示器 |
|
|
64
66
|
| `language` | `"auto"` / `"zh"` / `"en"` | `"auto"` | 界面语言 |
|
|
@@ -74,6 +76,14 @@ npm install opencode-tokenwatch
|
|
|
74
76
|
- **文本报告** — 导出 Markdown 格式至 `~/.opencode/reports/`
|
|
75
77
|
- **设置** — 开关侧边栏显示项、切换语言
|
|
76
78
|
|
|
79
|
+
## 数据文件
|
|
80
|
+
|
|
81
|
+
| 文件 | 路径 | 说明 |
|
|
82
|
+
|------|------|------|
|
|
83
|
+
| JSONL 日志 | `~/.opencode/tokenwatch.jsonl` | 原始请求日志,超 5MB 自动轮转 |
|
|
84
|
+
| 聚合统计 | `~/.opencode/tokenwatch-stats.json` | 持久化性能统计,永久累积 |
|
|
85
|
+
| 报告输出 | `~/.opencode/reports/` | HTML / JSON / Markdown 报告 |
|
|
86
|
+
|
|
77
87
|
## 系统要求
|
|
78
88
|
|
|
79
89
|
- OpenCode CLI(支持 `opencode db` 命令)
|
package/dist/commands.jsx
CHANGED
|
@@ -3,6 +3,7 @@ import { formatUsageReport } from "./formatter.js";
|
|
|
3
3
|
import { generateUsageHtml } from "./generate-usage-html.js";
|
|
4
4
|
import { t, setLanguage } from "./i18n.js";
|
|
5
5
|
import { readLogs } from "./perf-tracker.js";
|
|
6
|
+
import { readPersistedStats } from "./stats-store.js";
|
|
6
7
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
7
8
|
import { join } from "node:path";
|
|
8
9
|
import { homedir } from "node:os";
|
|
@@ -60,6 +61,9 @@ function computePercentile(sortedArr, p) {
|
|
|
60
61
|
function aggregatePerfStats(logs) {
|
|
61
62
|
const map = new Map();
|
|
62
63
|
for (const entry of logs) {
|
|
64
|
+
// 过滤掉全零无效条目:token 均为 0 表示请求失败或未完成
|
|
65
|
+
if (entry.inputTokens + entry.outputTokens + entry.cacheReadTokens + entry.cacheWriteTokens === 0)
|
|
66
|
+
continue;
|
|
63
67
|
const key = entry.model;
|
|
64
68
|
let s = map.get(key);
|
|
65
69
|
if (!s) {
|
|
@@ -144,7 +148,11 @@ function aggregatePerfStats(logs) {
|
|
|
144
148
|
}
|
|
145
149
|
async function buildCombinedData(api, filters = {}) {
|
|
146
150
|
const report = await getUsageReport(filters);
|
|
147
|
-
|
|
151
|
+
// perfLogs: 仅用于 JSON 导出参考,保持适当窗口即可
|
|
152
|
+
const logs = readLogs(200);
|
|
153
|
+
// perfSummary: 使用持久化聚合统计,包含自插件安装以来的全量历史数据
|
|
154
|
+
// 不再受 readLogs 窗口限制,即使 JSONL 被轮转,历史指标也不会丢失
|
|
155
|
+
const perfSummary = readPersistedStats();
|
|
148
156
|
const now = new Date();
|
|
149
157
|
const pad = (n) => String(n).padStart(2, '0');
|
|
150
158
|
const meta = {
|
|
@@ -157,7 +165,7 @@ async function buildCombinedData(api, filters = {}) {
|
|
|
157
165
|
return {
|
|
158
166
|
...report,
|
|
159
167
|
perfLogs: logs,
|
|
160
|
-
perfSummary
|
|
168
|
+
perfSummary,
|
|
161
169
|
meta,
|
|
162
170
|
};
|
|
163
171
|
}
|
|
@@ -208,6 +216,10 @@ function showHtmlReportRangeMenu(api, dialog) {
|
|
|
208
216
|
]} flat={true}/>));
|
|
209
217
|
}
|
|
210
218
|
function showUsageMenu(api, dialog) {
|
|
219
|
+
try {
|
|
220
|
+
setLanguage(loadConfigFromStore(api).language);
|
|
221
|
+
}
|
|
222
|
+
catch { }
|
|
211
223
|
dialog.replace(() => (<api.ui.DialogSelect title={t("panelTitle")} placeholder="Select an action..." options={[
|
|
212
224
|
{
|
|
213
225
|
title: `${t("cmdTitleHtml")} ▸`,
|
|
@@ -23,7 +23,8 @@ function cacheHitRate(input, cacheRead) {
|
|
|
23
23
|
return cacheRead / (input + cacheRead);
|
|
24
24
|
}
|
|
25
25
|
function sortModelsByUsage(models) {
|
|
26
|
-
|
|
26
|
+
// 过滤掉 totalTokens=0 的无效模型条目(如会话失败导致全为 0 的记录)
|
|
27
|
+
return [...models].filter(m => m.totalTokens > 0).sort((a, b) => b.totalTokens - a.totalTokens);
|
|
27
28
|
}
|
|
28
29
|
function renderMeta(data) {
|
|
29
30
|
const m = data.meta;
|
|
@@ -33,11 +34,15 @@ function renderKpiCards(data) {
|
|
|
33
34
|
const s = data.summary;
|
|
34
35
|
const hitRate = cacheHitRate(s.inputTokens, s.cacheRead);
|
|
35
36
|
const hitRatePct = fmtPercent(hitRate);
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
let tpsSum = 0, tpsReqs = 0;
|
|
38
|
+
for (const p of data.perfSummary) {
|
|
39
|
+
if (p.avgTPS != null && p.avgTPS > 0) {
|
|
40
|
+
tpsSum += p.avgTPS * p.requestCount;
|
|
41
|
+
tpsReqs += p.requestCount;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const avgTpsRaw = tpsReqs > 0 ? tpsSum / tpsReqs : 0;
|
|
45
|
+
const avgTps = tpsReqs > 0 ? avgTpsRaw.toFixed(1) : '—';
|
|
41
46
|
const isHighCache = hitRate >= 0.5;
|
|
42
47
|
const errors = data.errors;
|
|
43
48
|
const errorRatePct = errors ? (errors.errorRate * 100).toFixed(1) + '%' : '—';
|
|
@@ -217,10 +222,13 @@ function renderModelChart(chart) {
|
|
|
217
222
|
}`;
|
|
218
223
|
}
|
|
219
224
|
function renderScatterChartInit(data) {
|
|
220
|
-
|
|
225
|
+
// 过滤掉全零无效条目:无请求或无任何 token 的模型
|
|
226
|
+
const validPerf = data.perfSummary.filter(p => p.requestCount > 0 &&
|
|
227
|
+
(p.totalInput + p.totalOutput + p.totalCacheRead + p.totalCacheWrite) > 0);
|
|
228
|
+
if (validPerf.length === 0)
|
|
221
229
|
return "";
|
|
222
230
|
// 按 TPS 降序排列(null TPS 放末尾),让最快的模型显示在最上方
|
|
223
|
-
const sorted = [...
|
|
231
|
+
const sorted = [...validPerf].sort((a, b) => {
|
|
224
232
|
if (a.avgTPS == null && b.avgTPS == null)
|
|
225
233
|
return 0;
|
|
226
234
|
if (a.avgTPS == null)
|
|
@@ -280,7 +288,7 @@ function initScatterChart() {
|
|
|
280
288
|
'Requests: ' + effReq[i];
|
|
281
289
|
}
|
|
282
290
|
},
|
|
283
|
-
grid: { left: 20, right:
|
|
291
|
+
grid: { left: 20, right: 280, bottom: 30, top: 20, containLabel: true },
|
|
284
292
|
xAxis: {
|
|
285
293
|
type: 'value',
|
|
286
294
|
name: 'Avg TPS (tokens / sec)',
|
|
@@ -295,7 +303,7 @@ function initScatterChart() {
|
|
|
295
303
|
axisLabel: {
|
|
296
304
|
color: '#E0E0F0',
|
|
297
305
|
fontSize: 11,
|
|
298
|
-
formatter: function(v) { return v.length >
|
|
306
|
+
formatter: function(v) { return v.length > 50 ? v.slice(0, 48) + '\u2026' : v; }
|
|
299
307
|
},
|
|
300
308
|
axisLine: { show: false },
|
|
301
309
|
axisTick: { show: false }
|
|
@@ -334,14 +342,22 @@ function renderProviderCards(data) {
|
|
|
334
342
|
return data.providers.map(p => {
|
|
335
343
|
const modelCount = data.models.filter(m => m.provider === p.provider).length;
|
|
336
344
|
const perfItems = data.perfSummary.filter(ps => ps.providerID === p.provider);
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
+
let ttftSum = 0, ttftReqs = 0;
|
|
346
|
+
for (const x of perfItems) {
|
|
347
|
+
if (x.avgTTFT != null && x.avgTTFT > 0) {
|
|
348
|
+
ttftSum += x.avgTTFT * x.requestCount;
|
|
349
|
+
ttftReqs += x.requestCount;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
const avgTtft = ttftReqs > 0 ? ttftSum / ttftReqs : null;
|
|
353
|
+
let tpsSum = 0, tpsReqs = 0;
|
|
354
|
+
for (const x of perfItems) {
|
|
355
|
+
if (x.avgTPS != null && x.avgTPS > 0) {
|
|
356
|
+
tpsSum += x.avgTPS * x.requestCount;
|
|
357
|
+
tpsReqs += x.requestCount;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const avgTps = tpsReqs > 0 ? tpsSum / tpsReqs : null;
|
|
345
361
|
return `
|
|
346
362
|
<div class="provider-card" style="border-color:${providerBorderColor(p.provider)}">
|
|
347
363
|
<div class="provider-name">${p.provider}</div>
|
|
@@ -399,10 +415,13 @@ function renderDataTable(data) {
|
|
|
399
415
|
}
|
|
400
416
|
/** 渲染 P50/P95/P99 延迟分位数表格 */
|
|
401
417
|
function renderPerfPercentileTable(data) {
|
|
402
|
-
|
|
418
|
+
// 过滤掉无效记录:无请求或 token 全为 0 的模型
|
|
419
|
+
const validPerf = data.perfSummary.filter(p => p.requestCount > 0 &&
|
|
420
|
+
(p.totalInput + p.totalOutput + p.totalCacheRead + p.totalCacheWrite) > 0);
|
|
421
|
+
if (validPerf.length === 0)
|
|
403
422
|
return '';
|
|
404
423
|
const fmtMs = (v) => v != null ? v.toFixed(0) + 'ms' : '—';
|
|
405
|
-
const rows =
|
|
424
|
+
const rows = validPerf.map(p => {
|
|
406
425
|
const hitColor = p.cacheHitRate != null && p.cacheHitRate >= 85 ? 'var(--cache)'
|
|
407
426
|
: p.cacheHitRate != null && p.cacheHitRate >= 70 ? 'var(--tps)' : 'var(--output)';
|
|
408
427
|
return `<tr>
|
|
@@ -636,7 +655,8 @@ export function generateUsageHtml(data) {
|
|
|
636
655
|
const errorStatsStr = renderErrorStatsSection(data);
|
|
637
656
|
const dailyChartJs = renderDailyTrendInit(data);
|
|
638
657
|
const heatmapJs = renderHeatmapInit(data);
|
|
639
|
-
const hasPerf = data.perfSummary.
|
|
658
|
+
const hasPerf = data.perfSummary.some(p => p.requestCount > 0 &&
|
|
659
|
+
(p.totalInput + p.totalOutput + p.totalCacheRead + p.totalCacheWrite) > 0);
|
|
640
660
|
const jsonData = JSON.stringify(data);
|
|
641
661
|
return `<!DOCTYPE html>
|
|
642
662
|
<html lang="en">
|
package/dist/i18n.js
CHANGED
|
@@ -14,6 +14,7 @@ const zh = {
|
|
|
14
14
|
trendUp: "↑",
|
|
15
15
|
trendDown: "↓",
|
|
16
16
|
cache: "缓存",
|
|
17
|
+
lat: "延迟",
|
|
17
18
|
performance: "性能",
|
|
18
19
|
pricing: "Pricing",
|
|
19
20
|
tokenDistribution: "Token分布",
|
|
@@ -86,6 +87,7 @@ const en = {
|
|
|
86
87
|
trendUp: "↑",
|
|
87
88
|
trendDown: "↓",
|
|
88
89
|
cache: "Cache",
|
|
90
|
+
lat: "Lat",
|
|
89
91
|
performance: "Performance",
|
|
90
92
|
pricing: "Pricing",
|
|
91
93
|
tokenDistribution: "Token Distribution",
|
package/dist/perf-tracker.d.ts
CHANGED
|
@@ -54,6 +54,7 @@ declare class PerfTracker {
|
|
|
54
54
|
getSessionStats(): SessionPerfStats;
|
|
55
55
|
readLogs(last?: number): LogEntry[];
|
|
56
56
|
reset(): void;
|
|
57
|
+
loadSession(sessionID: string): void;
|
|
57
58
|
}
|
|
58
59
|
export declare function createPerfTracker(): PerfTracker;
|
|
59
60
|
export type { PartEvent, PerfTracker };
|
package/dist/perf-tracker.js
CHANGED
|
@@ -2,6 +2,7 @@ import { appendFileSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { existsSync, statSync } from "node:fs";
|
|
5
|
+
import { updatePersistedStats } from "./stats-store.js";
|
|
5
6
|
const LOG_PATH = join(homedir(), ".opencode", "tokenwatch.jsonl");
|
|
6
7
|
class PerfTracker {
|
|
7
8
|
firstPartTimes = new Map();
|
|
@@ -42,6 +43,11 @@ class PerfTracker {
|
|
|
42
43
|
const cacheRead = tokens?.cache?.read ?? 0;
|
|
43
44
|
const cacheWrite = tokens?.cache?.write ?? 0;
|
|
44
45
|
const cost = info.cost ?? 0;
|
|
46
|
+
// 过滤全零 token 的失败请求,不写入日志和统计,防止污染数据
|
|
47
|
+
if (inputTokens + outputTokens + cacheRead + cacheWrite === 0) {
|
|
48
|
+
this.firstPartTimes.delete(messageID);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
45
51
|
const firstPart = this.firstPartTimes.get(messageID) ?? null;
|
|
46
52
|
const latencyMs = completed - created;
|
|
47
53
|
const ttftMs = firstPart !== null ? firstPart - created : null;
|
|
@@ -76,6 +82,7 @@ class PerfTracker {
|
|
|
76
82
|
try {
|
|
77
83
|
// Risk fix: JSONL 日志轮转保护,防止长期使用后文件无限增长
|
|
78
84
|
// 超过 5MB 时截断,保留最新 2000 行
|
|
85
|
+
// 注意:轮转前先调用 updatePersistedStats,确保被轮转行的数据已持久化
|
|
79
86
|
const MAX_SIZE = 5 * 1024 * 1024; // 5 MB
|
|
80
87
|
const KEEP_LINES = 2000;
|
|
81
88
|
if (existsSync(LOG_PATH) && statSync(LOG_PATH).size > MAX_SIZE) {
|
|
@@ -87,6 +94,9 @@ class PerfTracker {
|
|
|
87
94
|
catch {
|
|
88
95
|
// Silently fail — logging is non-critical
|
|
89
96
|
}
|
|
97
|
+
// 无论 JSONL 写入是否成功,都尝试更新持久化聚合统计
|
|
98
|
+
// 这样即使日志被轮转,历史统计数据也永不丢失
|
|
99
|
+
updatePersistedStats(entry);
|
|
90
100
|
}
|
|
91
101
|
handleMessageRemoved(event) {
|
|
92
102
|
const mid = event.properties?.messageID ?? "";
|
|
@@ -247,6 +257,38 @@ class PerfTracker {
|
|
|
247
257
|
this.ttftSamples.clear();
|
|
248
258
|
this.latencySamples.clear();
|
|
249
259
|
}
|
|
260
|
+
loadSession(sessionID) {
|
|
261
|
+
this.firstPartTimes.clear();
|
|
262
|
+
this.statsMap.clear();
|
|
263
|
+
this.ttftSamples.clear();
|
|
264
|
+
this.latencySamples.clear();
|
|
265
|
+
if (!sessionID)
|
|
266
|
+
return;
|
|
267
|
+
try {
|
|
268
|
+
if (!existsSync(LOG_PATH))
|
|
269
|
+
return;
|
|
270
|
+
const content = readFileSync(LOG_PATH, "utf-8").trim();
|
|
271
|
+
if (!content)
|
|
272
|
+
return;
|
|
273
|
+
const lines = content.split("\n");
|
|
274
|
+
for (const line of lines) {
|
|
275
|
+
if (!line)
|
|
276
|
+
continue;
|
|
277
|
+
try {
|
|
278
|
+
const entry = JSON.parse(line);
|
|
279
|
+
if (entry.sessionID === sessionID) {
|
|
280
|
+
this.updateStats(entry.model, entry);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
// Skip malformed lines
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
// Non-critical loading failure
|
|
290
|
+
}
|
|
291
|
+
}
|
|
250
292
|
}
|
|
251
293
|
export function createPerfTracker() {
|
|
252
294
|
return new PerfTracker();
|
package/dist/sidebar.d.ts
CHANGED
|
@@ -15,8 +15,8 @@ interface TokenWatchPanelProps {
|
|
|
15
15
|
api: TuiPluginApi;
|
|
16
16
|
theme: TuiTheme;
|
|
17
17
|
perfTracker: PerfTracker;
|
|
18
|
-
messages: readonly any[];
|
|
19
|
-
allTokenMessages: TokenMessage[];
|
|
18
|
+
messages: () => readonly any[];
|
|
19
|
+
allTokenMessages: () => TokenMessage[];
|
|
20
20
|
}
|
|
21
21
|
export declare function TokenWatchPanel(props: TokenWatchPanelProps): any;
|
|
22
22
|
export {};
|