codebuddy-stats 1.3.1 → 1.3.3
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/dist/index.js +40 -13
- package/dist/lib/code-usage.worker.js +132 -0
- package/dist/lib/data-loader.js +300 -18
- package/dist/lib/pricing.js +18 -0
- package/dist/views/by-project.js +1 -1
- package/dist/views/daily-detail.js +1 -1
- package/dist/views/daily.js +20 -12
- package/dist/views/monthly-detail.js +81 -44
- package/dist/views/monthly.js +34 -32
- package/dist/views/overview.js +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import blessed from 'blessed';
|
|
6
|
+
import { performance } from 'node:perf_hooks';
|
|
6
7
|
import { loadUsageData } from './lib/data-loader.js';
|
|
7
8
|
import { resolveProjectName } from './lib/workspace-resolver.js';
|
|
8
9
|
import { compareByCostThenTokens, formatCost, formatNumber, formatPercent, formatTokens, truncate } from './lib/utils.js';
|
|
@@ -72,7 +73,7 @@ function printTextReport(data) {
|
|
|
72
73
|
console.log(`\nTop model: ${topModel.id} (${formatCost(topModel.cost)})`);
|
|
73
74
|
}
|
|
74
75
|
if (topProject) {
|
|
75
|
-
const shortName = resolveProjectName(topProject.name, data.workspaceMappings);
|
|
76
|
+
const shortName = data.projectDisplayNames?.[topProject.name] ?? resolveProjectName(topProject.name, data.workspaceMappings);
|
|
76
77
|
console.log(`Top project: ${shortName}`);
|
|
77
78
|
console.log(` (${formatCost(topProject.cost)})`);
|
|
78
79
|
}
|
|
@@ -86,7 +87,7 @@ function printTextReport(data) {
|
|
|
86
87
|
for (const [project, stats] of Object.entries(projectTotals)
|
|
87
88
|
.sort((a, b) => b[1].cost - a[1].cost)
|
|
88
89
|
.slice(0, 10)) {
|
|
89
|
-
const shortName = resolveProjectName(project, data.workspaceMappings);
|
|
90
|
+
const shortName = data.projectDisplayNames?.[project] ?? resolveProjectName(project, data.workspaceMappings);
|
|
90
91
|
console.log(` ${truncate(shortName, 40)}: ${formatCost(stats.cost)}`); // eslint-disable-line no-console
|
|
91
92
|
}
|
|
92
93
|
console.log('\n' + '='.repeat(50) + '\n');
|
|
@@ -185,8 +186,14 @@ async function main() {
|
|
|
185
186
|
content += ' {gray-fg}(Tab view, s source){/gray-fg}';
|
|
186
187
|
tabBar.setContent(content);
|
|
187
188
|
}
|
|
189
|
+
// profiling(默认关闭;建议配合 --no-tui 使用以免打乱终端 UI)
|
|
190
|
+
const prof = process.env.CBS_PROFILE === '1';
|
|
191
|
+
const profTui = process.env.CBS_PROFILE_TUI === '1';
|
|
192
|
+
let lastRenderMs = 0;
|
|
193
|
+
let lastRenderTab = '';
|
|
188
194
|
// 更新内容
|
|
189
195
|
function updateContent() {
|
|
196
|
+
const t0 = prof ? performance.now() : 0;
|
|
190
197
|
const width = Number(screen.width) || 80;
|
|
191
198
|
const note = state.currentSource === 'code'
|
|
192
199
|
? `针对 CodeBuddy Code < 2.20.0 版本产生的数据,由于没有请求级别的 model ID,用量是基于当前 CodeBuddy Code 设置的 model ID(${state.data.defaultModelId})计算价格的`
|
|
@@ -225,7 +232,7 @@ async function main() {
|
|
|
225
232
|
const dailyMaxOffset = Math.max(0, Object.keys(state.data.dailySummary).length - state.dailyPageSize);
|
|
226
233
|
state.dailyScrollOffset = Math.min(state.dailyScrollOffset, dailyMaxOffset);
|
|
227
234
|
state.dailySelectedIndex = Math.min(state.dailySelectedIndex, Math.max(0, Object.keys(state.data.dailySummary).length - 1));
|
|
228
|
-
const sortedMonths = getSortedMonthsFromDailyData(state.data.dailyData);
|
|
235
|
+
const sortedMonths = state.data.sortedMonths ?? getSortedMonthsFromDailyData(state.data.dailyData);
|
|
229
236
|
// 如果当前在 Monthly detail 但该月份已不存在,回到列表
|
|
230
237
|
if (state.monthlyDetailMonth && !sortedMonths.includes(state.monthlyDetailMonth)) {
|
|
231
238
|
state.monthlyDetailMonth = null;
|
|
@@ -243,27 +250,39 @@ async function main() {
|
|
|
243
250
|
}
|
|
244
251
|
// Monthly detail 的滚动上限
|
|
245
252
|
if (state.monthlyDetailMonth) {
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
for (const projectName of Object.keys(dayData ?? {}))
|
|
251
|
-
projects.add(projectName);
|
|
253
|
+
const cachedMonthProjects = state.data.monthlyByProject?.[state.monthlyDetailMonth];
|
|
254
|
+
let projectCount = 0;
|
|
255
|
+
if (cachedMonthProjects) {
|
|
256
|
+
projectCount = Object.keys(cachedMonthProjects).length;
|
|
252
257
|
}
|
|
253
|
-
|
|
258
|
+
else {
|
|
259
|
+
const projects = new Set();
|
|
260
|
+
for (const [date, dayData] of Object.entries(state.data.dailyData)) {
|
|
261
|
+
if (!date.startsWith(state.monthlyDetailMonth))
|
|
262
|
+
continue;
|
|
263
|
+
for (const projectName of Object.keys(dayData ?? {}))
|
|
264
|
+
projects.add(projectName);
|
|
265
|
+
}
|
|
266
|
+
projectCount = projects.size;
|
|
267
|
+
}
|
|
268
|
+
const maxOffset = Math.max(0, projectCount - state.monthlyDetailPageSize);
|
|
254
269
|
state.monthlyDetailScrollOffset = Math.min(state.monthlyDetailScrollOffset, maxOffset);
|
|
255
270
|
}
|
|
256
271
|
switch (state.currentTab) {
|
|
257
272
|
case 0:
|
|
273
|
+
lastRenderTab = 'overview';
|
|
258
274
|
renderOverview(contentBox, state.data, width, innerHeight, note);
|
|
259
275
|
break;
|
|
260
276
|
case 1:
|
|
277
|
+
lastRenderTab = 'by-model';
|
|
261
278
|
renderByModel(contentBox, state.data, state.modelScrollOffset, width, note, state.modelPageSize);
|
|
262
279
|
break;
|
|
263
280
|
case 2:
|
|
281
|
+
lastRenderTab = 'by-project';
|
|
264
282
|
renderByProject(contentBox, state.data, state.projectScrollOffset, width, note, state.projectPageSize);
|
|
265
283
|
break;
|
|
266
284
|
case 3:
|
|
285
|
+
lastRenderTab = state.dailyDetailDate ? 'daily-detail' : 'daily';
|
|
267
286
|
if (state.dailyDetailDate) {
|
|
268
287
|
renderDailyDetail(contentBox, state.data, state.dailyDetailDate, state.dailyDetailScrollOffset, width, state.dailyDetailPageSize);
|
|
269
288
|
}
|
|
@@ -272,6 +291,7 @@ async function main() {
|
|
|
272
291
|
}
|
|
273
292
|
break;
|
|
274
293
|
case 4:
|
|
294
|
+
lastRenderTab = state.monthlyDetailMonth ? 'monthly-detail' : 'monthly';
|
|
275
295
|
if (state.monthlyDetailMonth) {
|
|
276
296
|
renderMonthlyDetail(contentBox, state.data, state.monthlyDetailMonth, state.monthlyDetailScrollOffset, width, state.monthlyDetailPageSize);
|
|
277
297
|
}
|
|
@@ -280,6 +300,12 @@ async function main() {
|
|
|
280
300
|
}
|
|
281
301
|
break;
|
|
282
302
|
}
|
|
303
|
+
if (prof) {
|
|
304
|
+
lastRenderMs = performance.now() - t0;
|
|
305
|
+
if (profTui) {
|
|
306
|
+
console.error(`[cbs] render tab=${lastRenderTab} ${lastRenderMs.toFixed(1)}ms`); // eslint-disable-line no-console
|
|
307
|
+
}
|
|
308
|
+
}
|
|
283
309
|
}
|
|
284
310
|
// 更新状态栏
|
|
285
311
|
function updateStatusBar() {
|
|
@@ -291,9 +317,10 @@ async function main() {
|
|
|
291
317
|
const reservedForRight = rightContent.length + 2; // 版本号 + 两侧空格
|
|
292
318
|
const availableForLeft = width - reservedForRight;
|
|
293
319
|
let leftContent;
|
|
294
|
-
const
|
|
295
|
-
const
|
|
296
|
-
const
|
|
320
|
+
const perfInfo = prof ? ` | render:${lastRenderTab} ${Math.round(lastRenderMs)}ms` : '';
|
|
321
|
+
const fullContent = ` ${daysInfo} | Source: ${sourceInfo} | Total: ${formatCost(state.data.grandTotal.cost)}${perfInfo} | q quit, Tab view, s source, r refresh`;
|
|
322
|
+
const mediumContent = ` ${daysInfo} | ${sourceInfo} | ${formatCost(state.data.grandTotal.cost)}${perfInfo} | q/Tab/s/r`;
|
|
323
|
+
const shortContent = ` ${sourceInfo} | ${formatCost(state.data.grandTotal.cost)}${perfInfo} | q/Tab/s/r`;
|
|
297
324
|
const minContent = ` ${formatCost(state.data.grandTotal.cost)}`;
|
|
298
325
|
if (fullContent.length <= availableForLeft) {
|
|
299
326
|
leftContent = fullContent;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import fsSync from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createInterface } from 'node:readline';
|
|
5
|
+
import { parentPort, workerData } from 'node:worker_threads';
|
|
6
|
+
import { DEFAULT_MODEL_ID, getPricingForModel, tokensToCost } from './pricing.js';
|
|
7
|
+
function getProjectName(filePath) {
|
|
8
|
+
const parts = filePath.split(path.sep);
|
|
9
|
+
const projectsIndex = parts.lastIndexOf('projects');
|
|
10
|
+
if (projectsIndex !== -1 && projectsIndex < parts.length - 1) {
|
|
11
|
+
return parts[projectsIndex + 1] ?? 'unknown-project';
|
|
12
|
+
}
|
|
13
|
+
return 'unknown-project';
|
|
14
|
+
}
|
|
15
|
+
function extractUsageStats(usage) {
|
|
16
|
+
const promptTokens = usage.prompt_tokens ?? 0;
|
|
17
|
+
const completionTokens = usage.completion_tokens ?? 0;
|
|
18
|
+
const totalTokens = usage.total_tokens ?? promptTokens + completionTokens;
|
|
19
|
+
const cacheHitTokens = usage.prompt_cache_hit_tokens ?? 0;
|
|
20
|
+
const cacheMissTokens = usage.prompt_cache_miss_tokens ?? (cacheHitTokens > 0 ? Math.max(promptTokens - cacheHitTokens, 0) : 0);
|
|
21
|
+
const cacheWriteTokens = usage.cache_creation_input_tokens ?? 0;
|
|
22
|
+
return {
|
|
23
|
+
promptTokens,
|
|
24
|
+
completionTokens,
|
|
25
|
+
totalTokens,
|
|
26
|
+
cacheHitTokens,
|
|
27
|
+
cacheMissTokens,
|
|
28
|
+
cacheWriteTokens,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function computeUsageCost(usage, modelId) {
|
|
32
|
+
const stats = extractUsageStats(usage);
|
|
33
|
+
const pricing = getPricingForModel(modelId);
|
|
34
|
+
let inputCost = 0;
|
|
35
|
+
if (stats.cacheHitTokens || stats.cacheMissTokens || stats.cacheWriteTokens) {
|
|
36
|
+
inputCost += tokensToCost(stats.cacheHitTokens, pricing.cacheRead);
|
|
37
|
+
inputCost += tokensToCost(stats.cacheMissTokens, pricing.prompt);
|
|
38
|
+
inputCost += tokensToCost(stats.cacheWriteTokens, pricing.cacheWrite);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
inputCost += tokensToCost(stats.promptTokens, pricing.prompt);
|
|
42
|
+
}
|
|
43
|
+
const outputCost = tokensToCost(stats.completionTokens, pricing.completion);
|
|
44
|
+
return { cost: inputCost + outputCost, stats };
|
|
45
|
+
}
|
|
46
|
+
function ensureDailyModelStats(dailyData, date, project, modelId) {
|
|
47
|
+
dailyData[date] ??= {};
|
|
48
|
+
dailyData[date][project] ??= {};
|
|
49
|
+
dailyData[date][project][modelId] ??= {
|
|
50
|
+
cost: 0,
|
|
51
|
+
promptTokens: 0,
|
|
52
|
+
completionTokens: 0,
|
|
53
|
+
totalTokens: 0,
|
|
54
|
+
cacheHitTokens: 0,
|
|
55
|
+
cacheMissTokens: 0,
|
|
56
|
+
cacheWriteTokens: 0,
|
|
57
|
+
requests: 0,
|
|
58
|
+
};
|
|
59
|
+
return dailyData[date][project][modelId];
|
|
60
|
+
}
|
|
61
|
+
async function main() {
|
|
62
|
+
const input = workerData;
|
|
63
|
+
const files = Array.isArray(input?.files) ? input.files : [];
|
|
64
|
+
const defaultModelId = typeof input?.defaultModelId === 'string' && input.defaultModelId ? input.defaultModelId : DEFAULT_MODEL_ID;
|
|
65
|
+
const minDate = typeof input?.minDate === 'string' ? input.minDate : null;
|
|
66
|
+
const dailyData = {};
|
|
67
|
+
const modelTotals = {};
|
|
68
|
+
const projectTotals = {};
|
|
69
|
+
const grandTotal = { cost: 0, tokens: 0, requests: 0, cacheHitTokens: 0, cacheMissTokens: 0 };
|
|
70
|
+
for (const filePath of files) {
|
|
71
|
+
const projectName = getProjectName(filePath);
|
|
72
|
+
let fileStream;
|
|
73
|
+
try {
|
|
74
|
+
fileStream = fsSync.createReadStream(filePath);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const rl = createInterface({ input: fileStream, crlfDelay: Number.POSITIVE_INFINITY });
|
|
80
|
+
for await (const line of rl) {
|
|
81
|
+
try {
|
|
82
|
+
const record = JSON.parse(line);
|
|
83
|
+
const usage = record?.providerData?.rawUsage;
|
|
84
|
+
const timestamp = record?.timestamp;
|
|
85
|
+
if (!usage || timestamp == null)
|
|
86
|
+
continue;
|
|
87
|
+
const dateObj = new Date(timestamp);
|
|
88
|
+
if (Number.isNaN(dateObj.getTime()))
|
|
89
|
+
continue;
|
|
90
|
+
const date = dateObj.toISOString().split('T')[0];
|
|
91
|
+
if (!date)
|
|
92
|
+
continue;
|
|
93
|
+
if (minDate && date < minDate)
|
|
94
|
+
continue;
|
|
95
|
+
const recordModelId = record?.providerData?.model;
|
|
96
|
+
const modelFromRecord = typeof recordModelId === 'string' ? recordModelId : null;
|
|
97
|
+
const usedModelId = modelFromRecord || defaultModelId;
|
|
98
|
+
const { cost, stats: usageStats } = computeUsageCost(usage, usedModelId);
|
|
99
|
+
const dayStats = ensureDailyModelStats(dailyData, date, projectName, usedModelId);
|
|
100
|
+
dayStats.cost += cost;
|
|
101
|
+
dayStats.promptTokens += usageStats.promptTokens;
|
|
102
|
+
dayStats.completionTokens += usageStats.completionTokens;
|
|
103
|
+
dayStats.totalTokens += usageStats.totalTokens;
|
|
104
|
+
dayStats.cacheHitTokens += usageStats.cacheHitTokens;
|
|
105
|
+
dayStats.cacheMissTokens += usageStats.cacheMissTokens;
|
|
106
|
+
dayStats.cacheWriteTokens += usageStats.cacheWriteTokens;
|
|
107
|
+
dayStats.requests += 1;
|
|
108
|
+
modelTotals[usedModelId] ??= { cost: 0, tokens: 0, requests: 0 };
|
|
109
|
+
modelTotals[usedModelId].cost += cost;
|
|
110
|
+
modelTotals[usedModelId].tokens += usageStats.totalTokens;
|
|
111
|
+
modelTotals[usedModelId].requests += 1;
|
|
112
|
+
projectTotals[projectName] ??= { cost: 0, tokens: 0, requests: 0 };
|
|
113
|
+
projectTotals[projectName].cost += cost;
|
|
114
|
+
projectTotals[projectName].tokens += usageStats.totalTokens;
|
|
115
|
+
projectTotals[projectName].requests += 1;
|
|
116
|
+
grandTotal.cost += cost;
|
|
117
|
+
grandTotal.tokens += usageStats.totalTokens;
|
|
118
|
+
grandTotal.requests += 1;
|
|
119
|
+
grandTotal.cacheHitTokens += usageStats.cacheHitTokens;
|
|
120
|
+
grandTotal.cacheMissTokens += usageStats.cacheMissTokens;
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// ignore
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
parentPort?.postMessage({ dailyData, modelTotals, projectTotals, grandTotal });
|
|
128
|
+
}
|
|
129
|
+
main().catch(err => {
|
|
130
|
+
// 让主线程感知 worker 失败
|
|
131
|
+
throw err;
|
|
132
|
+
});
|
package/dist/lib/data-loader.js
CHANGED
|
@@ -1,12 +1,33 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import fsSync from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
import { performance } from 'node:perf_hooks';
|
|
4
7
|
import { createInterface } from 'node:readline';
|
|
8
|
+
import { Worker } from 'node:worker_threads';
|
|
5
9
|
import { getIdeDataDir, getProjectsDir, getSettingsPath } from './paths.js';
|
|
6
10
|
import { DEFAULT_MODEL_ID, getPricingForModel, tokensToCost } from './pricing.js';
|
|
7
11
|
import { compareByCostThenTokens } from './utils.js';
|
|
8
|
-
import { loadWorkspaceMappings } from './workspace-resolver.js';
|
|
12
|
+
import { loadWorkspaceMappings, resolveProjectName } from './workspace-resolver.js';
|
|
9
13
|
export const BASE_DIR = getProjectsDir();
|
|
14
|
+
const memoryCache = new Map();
|
|
15
|
+
function getCacheKey(source, days) {
|
|
16
|
+
return `${source}:${days ?? 'all'}`;
|
|
17
|
+
}
|
|
18
|
+
function cacheGet(source, days) {
|
|
19
|
+
return memoryCache.get(getCacheKey(source, days));
|
|
20
|
+
}
|
|
21
|
+
function cacheSet(source, days, entry) {
|
|
22
|
+
const key = getCacheKey(source, days);
|
|
23
|
+
memoryCache.set(key, entry);
|
|
24
|
+
// 简单上限:只保留最近 4 个 key
|
|
25
|
+
if (memoryCache.size > 4) {
|
|
26
|
+
const firstKey = memoryCache.keys().next().value;
|
|
27
|
+
if (firstKey)
|
|
28
|
+
memoryCache.delete(firstKey);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
10
31
|
async function loadModelFromSettings() {
|
|
11
32
|
try {
|
|
12
33
|
const settingsPath = getSettingsPath();
|
|
@@ -116,24 +137,159 @@ function ensureDailyModelStats(dailyData, date, project, modelId) {
|
|
|
116
137
|
};
|
|
117
138
|
return dailyData[date][project][modelId];
|
|
118
139
|
}
|
|
140
|
+
function mergeSummaryTotals(target, src) {
|
|
141
|
+
for (const [k, v] of Object.entries(src)) {
|
|
142
|
+
target[k] ??= { cost: 0, tokens: 0, requests: 0 };
|
|
143
|
+
target[k].cost += v.cost;
|
|
144
|
+
target[k].tokens += v.tokens;
|
|
145
|
+
target[k].requests += v.requests;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function mergeGrandTotal(target, src) {
|
|
149
|
+
target.cost += src.cost;
|
|
150
|
+
target.tokens += src.tokens;
|
|
151
|
+
target.requests += src.requests;
|
|
152
|
+
target.cacheHitTokens += src.cacheHitTokens;
|
|
153
|
+
target.cacheMissTokens += src.cacheMissTokens;
|
|
154
|
+
}
|
|
155
|
+
function mergeDailyData(target, src) {
|
|
156
|
+
for (const [date, byProject] of Object.entries(src)) {
|
|
157
|
+
target[date] ??= {};
|
|
158
|
+
for (const [project, byModel] of Object.entries(byProject ?? {})) {
|
|
159
|
+
target[date][project] ??= {};
|
|
160
|
+
for (const [modelId, s] of Object.entries(byModel ?? {})) {
|
|
161
|
+
const t = ensureDailyModelStats(target, date, project, modelId);
|
|
162
|
+
t.cost += s.cost;
|
|
163
|
+
t.promptTokens += s.promptTokens;
|
|
164
|
+
t.completionTokens += s.completionTokens;
|
|
165
|
+
t.totalTokens += s.totalTokens;
|
|
166
|
+
t.cacheHitTokens += s.cacheHitTokens;
|
|
167
|
+
t.cacheMissTokens += s.cacheMissTokens;
|
|
168
|
+
t.cacheWriteTokens += s.cacheWriteTokens;
|
|
169
|
+
t.requests += s.requests;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function chunkArray(arr, chunks) {
|
|
175
|
+
const n = Math.max(1, Math.floor(chunks));
|
|
176
|
+
const out = Array.from({ length: n }, () => []);
|
|
177
|
+
for (let i = 0; i < arr.length; i++)
|
|
178
|
+
out[i % n].push(arr[i]);
|
|
179
|
+
return out.filter(a => a.length > 0);
|
|
180
|
+
}
|
|
181
|
+
async function parseCodeUsageWithWorkers(files, defaultModelId, minDate, workerCount) {
|
|
182
|
+
const chunks = chunkArray(files, Math.min(workerCount, files.length));
|
|
183
|
+
const workerUrl = new URL('./code-usage.worker.js', import.meta.url);
|
|
184
|
+
const results = await Promise.all(chunks.map(chunk => new Promise((resolve, reject) => {
|
|
185
|
+
const worker = new Worker(workerUrl, {
|
|
186
|
+
workerData: {
|
|
187
|
+
files: chunk,
|
|
188
|
+
defaultModelId,
|
|
189
|
+
minDate,
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
worker.once('message', msg => resolve(msg));
|
|
193
|
+
worker.once('error', err => reject(err));
|
|
194
|
+
worker.once('exit', code => {
|
|
195
|
+
if (code !== 0)
|
|
196
|
+
reject(new Error(`worker exited with code ${code}`));
|
|
197
|
+
});
|
|
198
|
+
})));
|
|
199
|
+
const merged = {
|
|
200
|
+
dailyData: {},
|
|
201
|
+
modelTotals: {},
|
|
202
|
+
projectTotals: {},
|
|
203
|
+
grandTotal: { cost: 0, tokens: 0, requests: 0, cacheHitTokens: 0, cacheMissTokens: 0 },
|
|
204
|
+
};
|
|
205
|
+
for (const r of results) {
|
|
206
|
+
mergeDailyData(merged.dailyData, r.dailyData);
|
|
207
|
+
mergeSummaryTotals(merged.modelTotals, r.modelTotals);
|
|
208
|
+
mergeSummaryTotals(merged.projectTotals, r.projectTotals);
|
|
209
|
+
mergeGrandTotal(merged.grandTotal, r.grandTotal);
|
|
210
|
+
}
|
|
211
|
+
return merged;
|
|
212
|
+
}
|
|
119
213
|
function finalizeAnalysis(defaultModelId, dailyData, modelTotals, projectTotals, grandTotal, workspaceMappings) {
|
|
214
|
+
const prof = process.env.CBS_PROFILE === '1';
|
|
215
|
+
const profLog = prof && (process.env.CBS_PROFILE_TUI === '1' || process.argv.includes('--no-tui'));
|
|
216
|
+
const t0 = prof ? performance.now() : 0;
|
|
120
217
|
const dailySummary = {};
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
218
|
+
const dailyTops = {};
|
|
219
|
+
const monthlyAgg = {};
|
|
220
|
+
const monthlyByProject = {};
|
|
221
|
+
// 一次性遍历 dailyData:生成 dailySummary + dailyTops + monthly 聚合
|
|
222
|
+
for (const [date, dayData] of Object.entries(dailyData)) {
|
|
223
|
+
const daySummary = { cost: 0, tokens: 0, requests: 0 };
|
|
224
|
+
let topModelId = '-';
|
|
225
|
+
let topModelCost = 0;
|
|
226
|
+
let topProjectName = '-';
|
|
227
|
+
let topProjectCost = 0;
|
|
228
|
+
const month = date.slice(0, 7);
|
|
229
|
+
if (month) {
|
|
230
|
+
monthlyAgg[month] ??= {
|
|
231
|
+
cost: 0,
|
|
232
|
+
tokens: 0,
|
|
233
|
+
requests: 0,
|
|
234
|
+
cacheHitTokens: 0,
|
|
235
|
+
cacheMissTokens: 0,
|
|
236
|
+
modelCost: {},
|
|
237
|
+
projectCost: {},
|
|
238
|
+
};
|
|
239
|
+
monthlyByProject[month] ??= {};
|
|
240
|
+
}
|
|
241
|
+
for (const [projectName, models] of Object.entries(dayData ?? {})) {
|
|
242
|
+
let projectCost = 0;
|
|
243
|
+
if (month) {
|
|
244
|
+
monthlyByProject[month][projectName] ??= { cost: 0, tokens: 0, requests: 0, modelCost: {} };
|
|
245
|
+
}
|
|
246
|
+
for (const [modelId, stats] of Object.entries(models ?? {})) {
|
|
247
|
+
const cost = stats.cost;
|
|
248
|
+
const tokens = stats.totalTokens;
|
|
249
|
+
const requests = stats.requests;
|
|
250
|
+
daySummary.cost += cost;
|
|
251
|
+
daySummary.tokens += tokens;
|
|
252
|
+
daySummary.requests += requests;
|
|
253
|
+
projectCost += cost;
|
|
254
|
+
if (cost > topModelCost) {
|
|
255
|
+
topModelCost = cost;
|
|
256
|
+
topModelId = modelId;
|
|
257
|
+
}
|
|
258
|
+
if (month) {
|
|
259
|
+
const mAgg = monthlyAgg[month];
|
|
260
|
+
mAgg.cost += cost;
|
|
261
|
+
mAgg.tokens += tokens;
|
|
262
|
+
mAgg.requests += requests;
|
|
263
|
+
mAgg.cacheHitTokens += stats.cacheHitTokens;
|
|
264
|
+
mAgg.cacheMissTokens += stats.cacheMissTokens;
|
|
265
|
+
mAgg.modelCost[modelId] = (mAgg.modelCost[modelId] ?? 0) + cost;
|
|
266
|
+
mAgg.projectCost[projectName] = (mAgg.projectCost[projectName] ?? 0) + cost;
|
|
267
|
+
const pAgg = monthlyByProject[month][projectName];
|
|
268
|
+
pAgg.cost += cost;
|
|
269
|
+
pAgg.tokens += tokens;
|
|
270
|
+
pAgg.requests += requests;
|
|
271
|
+
pAgg.modelCost[modelId] = (pAgg.modelCost[modelId] ?? 0) + cost;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (projectCost > topProjectCost) {
|
|
275
|
+
topProjectCost = projectCost;
|
|
276
|
+
topProjectName = projectName;
|
|
128
277
|
}
|
|
129
278
|
}
|
|
279
|
+
dailySummary[date] = daySummary;
|
|
280
|
+
dailyTops[date] = { topModelId, topModelCost, topProjectName, topProjectCost };
|
|
130
281
|
}
|
|
282
|
+
const projectDisplayNames = {};
|
|
283
|
+
for (const projectName of Object.keys(projectTotals)) {
|
|
284
|
+
projectDisplayNames[projectName] = resolveProjectName(projectName, workspaceMappings);
|
|
285
|
+
}
|
|
286
|
+
const sortedMonths = Object.keys(monthlyAgg).sort().reverse();
|
|
131
287
|
const topModelEntry = Object.entries(modelTotals).sort(compareByCostThenTokens)[0];
|
|
132
288
|
const topProjectEntry = Object.entries(projectTotals).sort((a, b) => b[1].cost - a[1].cost)[0];
|
|
133
289
|
const cacheHitRate = grandTotal.cacheHitTokens + grandTotal.cacheMissTokens > 0
|
|
134
290
|
? grandTotal.cacheHitTokens / (grandTotal.cacheHitTokens + grandTotal.cacheMissTokens)
|
|
135
291
|
: 0;
|
|
136
|
-
|
|
292
|
+
const out = {
|
|
137
293
|
defaultModelId,
|
|
138
294
|
dailyData,
|
|
139
295
|
dailySummary,
|
|
@@ -145,22 +301,94 @@ function finalizeAnalysis(defaultModelId, dailyData, modelTotals, projectTotals,
|
|
|
145
301
|
cacheHitRate,
|
|
146
302
|
activeDays: Object.keys(dailyData).length,
|
|
147
303
|
workspaceMappings,
|
|
304
|
+
projectDisplayNames,
|
|
305
|
+
dailyTops,
|
|
306
|
+
monthlyAgg,
|
|
307
|
+
monthlyByProject,
|
|
308
|
+
sortedMonths,
|
|
148
309
|
};
|
|
310
|
+
if (profLog) {
|
|
311
|
+
const ms = performance.now() - t0;
|
|
312
|
+
console.error(`[cbs] finalizeAnalysis ${ms.toFixed(1)}ms (days=${Object.keys(dailyData).length}, projects=${Object.keys(projectTotals).length}, models=${Object.keys(modelTotals).length})`); // eslint-disable-line no-console
|
|
313
|
+
}
|
|
314
|
+
return out;
|
|
149
315
|
}
|
|
150
316
|
/**
|
|
151
317
|
* 加载所有用量数据
|
|
152
318
|
*/
|
|
153
319
|
export async function loadUsageData(options = {}) {
|
|
154
320
|
const source = options.source ?? 'code';
|
|
155
|
-
|
|
156
|
-
|
|
321
|
+
const prof = process.env.CBS_PROFILE === '1';
|
|
322
|
+
const profLog = prof && (process.env.CBS_PROFILE_TUI === '1' || process.argv.includes('--no-tui'));
|
|
323
|
+
const t0 = prof ? performance.now() : 0;
|
|
324
|
+
const data = source === 'ide' ? await loadIdeUsageData(options) : await loadCodeUsageData(options);
|
|
325
|
+
if (profLog) {
|
|
326
|
+
const ms = performance.now() - t0;
|
|
327
|
+
const daysInfo = options.days ? `days=${options.days}` : 'days=all';
|
|
328
|
+
// 用 stderr 避免影响 TUI 输出(建议配合 --no-tui 使用)
|
|
329
|
+
console.error(`[cbs] loadUsageData source=${source} ${daysInfo} ${ms.toFixed(1)}ms`); // eslint-disable-line no-console
|
|
157
330
|
}
|
|
158
|
-
return
|
|
331
|
+
return data;
|
|
159
332
|
}
|
|
160
333
|
async function loadCodeUsageData(options = {}) {
|
|
334
|
+
const prof = process.env.CBS_PROFILE === '1';
|
|
335
|
+
const profLog = prof && (process.env.CBS_PROFILE_TUI === '1' || process.argv.includes('--no-tui'));
|
|
336
|
+
const t0 = prof ? performance.now() : 0;
|
|
337
|
+
const workerCountEnv = Number.parseInt(process.env.CBS_WORKERS ?? '', 10);
|
|
338
|
+
const workerCount = Number.isFinite(workerCountEnv)
|
|
339
|
+
? Math.max(1, workerCountEnv)
|
|
340
|
+
: Math.min(4, Math.max(1, os.cpus().length - 1));
|
|
341
|
+
const useWorkersDefault = workerCount > 1;
|
|
342
|
+
const useWorkers = useWorkersDefault &&
|
|
343
|
+
// tsx/dev 场景下没有编译后的 worker 文件,自动回退
|
|
344
|
+
!import.meta.url.endsWith('.ts');
|
|
161
345
|
const defaultModelId = await loadModelFromSettings();
|
|
162
346
|
const jsonlFiles = await findJsonlFiles(BASE_DIR);
|
|
163
347
|
const minDate = computeMinDate(options.days);
|
|
348
|
+
// 同进程内存缓存(不落盘):refresh 时如果源文件未变,直接复用结果
|
|
349
|
+
const sortedFiles = [...jsonlFiles].sort();
|
|
350
|
+
const hash = createHash('sha1');
|
|
351
|
+
hash.update(`code|defaultModelId=${defaultModelId}|minDate=${minDate ?? ''}|files=${sortedFiles.length}\n`);
|
|
352
|
+
const nonEmptyFiles = [];
|
|
353
|
+
for (const filePath of sortedFiles) {
|
|
354
|
+
try {
|
|
355
|
+
const st = await fs.stat(filePath);
|
|
356
|
+
hash.update(`${filePath}|${st.size}|${st.mtimeMs}\n`);
|
|
357
|
+
if (st.size > 0)
|
|
358
|
+
nonEmptyFiles.push(filePath);
|
|
359
|
+
}
|
|
360
|
+
catch {
|
|
361
|
+
// 忽略暂时不可访问的文件
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
const fingerprint = hash.digest('hex');
|
|
365
|
+
const cached = cacheGet('code', options.days);
|
|
366
|
+
if (cached?.fingerprint === fingerprint) {
|
|
367
|
+
if (profLog) {
|
|
368
|
+
const ms = performance.now() - t0;
|
|
369
|
+
console.error(`[cbs] loadCodeUsageData cacheHit ${ms.toFixed(1)}ms (files=${sortedFiles.length})`); // eslint-disable-line no-console
|
|
370
|
+
}
|
|
371
|
+
return cached.data;
|
|
372
|
+
}
|
|
373
|
+
// 并行解析(不落盘):多核并行 JSON.parse
|
|
374
|
+
if (useWorkers && nonEmptyFiles.length > 1) {
|
|
375
|
+
try {
|
|
376
|
+
const parsed = await parseCodeUsageWithWorkers(nonEmptyFiles, defaultModelId, minDate, workerCount);
|
|
377
|
+
const data = finalizeAnalysis(defaultModelId, parsed.dailyData, parsed.modelTotals, parsed.projectTotals, parsed.grandTotal);
|
|
378
|
+
cacheSet('code', options.days, { fingerprint, data });
|
|
379
|
+
if (profLog) {
|
|
380
|
+
const ms = performance.now() - t0;
|
|
381
|
+
console.error(`[cbs] loadCodeUsageData workers=${Math.min(workerCount, nonEmptyFiles.length)} ${ms.toFixed(1)}ms (files=${sortedFiles.length})`); // eslint-disable-line no-console
|
|
382
|
+
}
|
|
383
|
+
return data;
|
|
384
|
+
}
|
|
385
|
+
catch (err) {
|
|
386
|
+
if (profLog) {
|
|
387
|
+
console.error(`[cbs] loadCodeUsageData workersFailed; fallback to single-thread: ${err?.message ?? String(err)}`); // eslint-disable-line no-console
|
|
388
|
+
}
|
|
389
|
+
// fallback: 单线程解析
|
|
390
|
+
}
|
|
391
|
+
}
|
|
164
392
|
const dailyData = {};
|
|
165
393
|
const modelTotals = {};
|
|
166
394
|
const projectTotals = {};
|
|
@@ -171,10 +399,7 @@ async function loadCodeUsageData(options = {}) {
|
|
|
171
399
|
cacheHitTokens: 0,
|
|
172
400
|
cacheMissTokens: 0,
|
|
173
401
|
};
|
|
174
|
-
for (const filePath of
|
|
175
|
-
const fileStat = await fs.stat(filePath);
|
|
176
|
-
if (fileStat.size === 0)
|
|
177
|
-
continue;
|
|
402
|
+
for (const filePath of nonEmptyFiles) {
|
|
178
403
|
const fileStream = fsSync.createReadStream(filePath);
|
|
179
404
|
const rl = createInterface({
|
|
180
405
|
input: fileStream,
|
|
@@ -228,7 +453,13 @@ async function loadCodeUsageData(options = {}) {
|
|
|
228
453
|
}
|
|
229
454
|
}
|
|
230
455
|
}
|
|
231
|
-
|
|
456
|
+
const data = finalizeAnalysis(defaultModelId, dailyData, modelTotals, projectTotals, grandTotal);
|
|
457
|
+
cacheSet('code', options.days, { fingerprint, data });
|
|
458
|
+
if (profLog) {
|
|
459
|
+
const ms = performance.now() - t0;
|
|
460
|
+
console.error(`[cbs] loadCodeUsageData ${ms.toFixed(1)}ms (files=${sortedFiles.length})`); // eslint-disable-line no-console
|
|
461
|
+
}
|
|
462
|
+
return data;
|
|
232
463
|
}
|
|
233
464
|
async function findIdeHistoryDirs() {
|
|
234
465
|
const root = getIdeDataDir();
|
|
@@ -329,6 +560,9 @@ async function inferIdeModelIdForRequest(conversationDir, request, messageModelC
|
|
|
329
560
|
return null;
|
|
330
561
|
}
|
|
331
562
|
async function loadIdeUsageData(options = {}) {
|
|
563
|
+
const prof = process.env.CBS_PROFILE === '1';
|
|
564
|
+
const profLog = prof && (process.env.CBS_PROFILE_TUI === '1' || process.argv.includes('--no-tui'));
|
|
565
|
+
const t0 = prof ? performance.now() : 0;
|
|
332
566
|
const defaultModelId = await loadModelFromSettings();
|
|
333
567
|
const minDate = computeMinDate(options.days);
|
|
334
568
|
// 加载工作区映射
|
|
@@ -344,6 +578,48 @@ async function loadIdeUsageData(options = {}) {
|
|
|
344
578
|
cacheMissTokens: 0,
|
|
345
579
|
};
|
|
346
580
|
const historyDirs = await findIdeHistoryDirs();
|
|
581
|
+
// 同进程内存缓存(不落盘):refresh 时如果 IDE 索引未变,直接复用结果
|
|
582
|
+
const sortedHistoryDirs = [...historyDirs].sort();
|
|
583
|
+
const ideHash = createHash('sha1');
|
|
584
|
+
ideHash.update(`ide|defaultModelId=${defaultModelId}|minDate=${minDate ?? ''}|historyDirs=${sortedHistoryDirs.length}\n`);
|
|
585
|
+
const mappingKeys = [...workspaceMappings.keys()].sort();
|
|
586
|
+
ideHash.update(`mappings=${mappingKeys.length}\n`);
|
|
587
|
+
for (const k of mappingKeys) {
|
|
588
|
+
const v = workspaceMappings.get(k);
|
|
589
|
+
ideHash.update(`${k}|${v?.displayPath ?? ''}\n`);
|
|
590
|
+
}
|
|
591
|
+
for (const historyDir of sortedHistoryDirs) {
|
|
592
|
+
ideHash.update(`dir=${historyDir}\n`);
|
|
593
|
+
let workspaces = [];
|
|
594
|
+
try {
|
|
595
|
+
workspaces = await fs.readdir(historyDir, { withFileTypes: true });
|
|
596
|
+
}
|
|
597
|
+
catch {
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
for (const ws of workspaces) {
|
|
601
|
+
if (!ws.isDirectory())
|
|
602
|
+
continue;
|
|
603
|
+
const workspaceDir = path.join(historyDir, ws.name);
|
|
604
|
+
const workspaceIndexPath = path.join(workspaceDir, 'index.json');
|
|
605
|
+
try {
|
|
606
|
+
const st = await fs.stat(workspaceIndexPath);
|
|
607
|
+
ideHash.update(`${ws.name}|${st.size}|${st.mtimeMs}\n`);
|
|
608
|
+
}
|
|
609
|
+
catch {
|
|
610
|
+
// ignore
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
const fingerprint = ideHash.digest('hex');
|
|
615
|
+
const cached = cacheGet('ide', options.days);
|
|
616
|
+
if (cached?.fingerprint === fingerprint) {
|
|
617
|
+
if (profLog) {
|
|
618
|
+
const ms = performance.now() - t0;
|
|
619
|
+
console.error(`[cbs] loadIdeUsageData cacheHit ${ms.toFixed(1)}ms (historyDirs=${sortedHistoryDirs.length})`); // eslint-disable-line no-console
|
|
620
|
+
}
|
|
621
|
+
return cached.data;
|
|
622
|
+
}
|
|
347
623
|
const messageModelCache = new Map();
|
|
348
624
|
for (const historyDir of historyDirs) {
|
|
349
625
|
let workspaces = [];
|
|
@@ -434,5 +710,11 @@ async function loadIdeUsageData(options = {}) {
|
|
|
434
710
|
}
|
|
435
711
|
}
|
|
436
712
|
}
|
|
437
|
-
|
|
713
|
+
const data = finalizeAnalysis(defaultModelId, dailyData, modelTotals, projectTotals, grandTotal, workspaceMappings);
|
|
714
|
+
cacheSet('ide', options.days, { fingerprint, data });
|
|
715
|
+
if (profLog) {
|
|
716
|
+
const ms = performance.now() - t0;
|
|
717
|
+
console.error(`[cbs] loadIdeUsageData ${ms.toFixed(1)}ms (historyDirs=${historyDirs.length})`); // eslint-disable-line no-console
|
|
718
|
+
}
|
|
719
|
+
return data;
|
|
438
720
|
}
|
package/dist/lib/pricing.js
CHANGED
|
@@ -31,6 +31,24 @@ export const MODEL_PRICING = {
|
|
|
31
31
|
"gpt-5.1-codex-mini": createPricing(0.25, 0.025, 2.0),
|
|
32
32
|
"gpt-5-codex": createPricing(1.25, 0.125, 10.0),
|
|
33
33
|
// Claude 系列
|
|
34
|
+
"claude-opus-4.6": {
|
|
35
|
+
prompt: [
|
|
36
|
+
{ limit: 200_000, pricePerMTok: 5.0 },
|
|
37
|
+
{ limit: Number.POSITIVE_INFINITY, pricePerMTok: 10.0 },
|
|
38
|
+
],
|
|
39
|
+
completion: [
|
|
40
|
+
{ limit: 200_000, pricePerMTok: 25.0 },
|
|
41
|
+
{ limit: Number.POSITIVE_INFINITY, pricePerMTok: 37.5 },
|
|
42
|
+
],
|
|
43
|
+
cacheRead: [
|
|
44
|
+
{ limit: 200_000, pricePerMTok: 0.5 },
|
|
45
|
+
{ limit: Number.POSITIVE_INFINITY, pricePerMTok: 1.0 },
|
|
46
|
+
],
|
|
47
|
+
cacheWrite: [
|
|
48
|
+
{ limit: 200_000, pricePerMTok: 6.25 },
|
|
49
|
+
{ limit: Number.POSITIVE_INFINITY, pricePerMTok: 12.5 },
|
|
50
|
+
],
|
|
51
|
+
},
|
|
34
52
|
"claude-opus-4.5": createPricing(5.0, 0.5, 25.0, 10.0),
|
|
35
53
|
"claude-haiku-4.5": createPricing(1.0, 0.1, 5.0, 1.25),
|
|
36
54
|
"claude-4.5": {
|
package/dist/views/by-project.js
CHANGED
|
@@ -20,7 +20,7 @@ export function renderByProject(box, data, scrollOffset = 0, width, note, pageSi
|
|
|
20
20
|
const visibleProjects = sorted.slice(scrollOffset, scrollOffset + safePageSize);
|
|
21
21
|
for (const [projectName, stats] of visibleProjects) {
|
|
22
22
|
// 简化项目名
|
|
23
|
-
const shortName = resolveProjectName(projectName, data.workspaceMappings);
|
|
23
|
+
const shortName = data.projectDisplayNames?.[projectName] ?? resolveProjectName(projectName, data.workspaceMappings);
|
|
24
24
|
content +=
|
|
25
25
|
truncate(shortName, projectCol - 1).padEnd(projectCol) +
|
|
26
26
|
formatCost(stats.cost).padStart(12) +
|
|
@@ -10,7 +10,7 @@ export function renderDailyDetail(box, data, date, scrollOffset = 0, width, page
|
|
|
10
10
|
}
|
|
11
11
|
const projectDetails = [];
|
|
12
12
|
for (const [projectName, models] of Object.entries(dayData)) {
|
|
13
|
-
const shortName = resolveProjectName(projectName, data.workspaceMappings);
|
|
13
|
+
const shortName = data.projectDisplayNames?.[projectName] ?? resolveProjectName(projectName, data.workspaceMappings);
|
|
14
14
|
const modelList = [];
|
|
15
15
|
let totalCost = 0;
|
|
16
16
|
let totalTokens = 0;
|
package/dist/views/daily.js
CHANGED
|
@@ -31,23 +31,31 @@ export function renderDaily(box, data, scrollOffset = 0, selectedIndex = 0, widt
|
|
|
31
31
|
const dayData = dailyData[date];
|
|
32
32
|
if (!daySummary || !dayData)
|
|
33
33
|
continue;
|
|
34
|
-
// 找出当天 top model 和 project
|
|
34
|
+
// 找出当天 top model 和 project(优先使用预计算缓存)
|
|
35
35
|
let topModel = { id: '-', cost: 0 };
|
|
36
36
|
let topProject = { name: '-', cost: 0 };
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
37
|
+
const cachedTop = data.dailyTops?.[date];
|
|
38
|
+
if (cachedTop) {
|
|
39
|
+
topModel = { id: cachedTop.topModelId || '-', cost: cachedTop.topModelCost || 0 };
|
|
40
|
+
topProject = { name: cachedTop.topProjectName || '-', cost: cachedTop.topProjectCost || 0 };
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
for (const [project, models] of Object.entries(dayData)) {
|
|
44
|
+
let projectCost = 0;
|
|
45
|
+
for (const [model, stats] of Object.entries(models)) {
|
|
46
|
+
const modelStats = stats;
|
|
47
|
+
const c = Number(modelStats.cost ?? 0);
|
|
48
|
+
projectCost += c;
|
|
49
|
+
if (c > topModel.cost) {
|
|
50
|
+
topModel = { id: model, cost: c };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (projectCost > topProject.cost) {
|
|
54
|
+
topProject = { name: project, cost: projectCost };
|
|
44
55
|
}
|
|
45
|
-
}
|
|
46
|
-
if (projectCost > topProject.cost) {
|
|
47
|
-
topProject = { name: project, cost: projectCost };
|
|
48
56
|
}
|
|
49
57
|
}
|
|
50
|
-
const shortProject = resolveProjectName(topProject.name, data.workspaceMappings);
|
|
58
|
+
const shortProject = data.projectDisplayNames?.[topProject.name] ?? resolveProjectName(topProject.name, data.workspaceMappings);
|
|
51
59
|
const isSelected = scrollOffset + i === selectedIndex;
|
|
52
60
|
const rowContent = date.padEnd(dateCol) +
|
|
53
61
|
formatCost(daySummary.cost).padStart(costCol) +
|
|
@@ -1,62 +1,99 @@
|
|
|
1
1
|
import { resolveProjectName } from '../lib/workspace-resolver.js';
|
|
2
2
|
import { formatCost, formatNumber, formatPercent, formatTokens, truncate } from '../lib/utils.js';
|
|
3
3
|
export function renderMonthlyDetail(box, data, month, scrollOffset = 0, width, pageSize) {
|
|
4
|
-
|
|
4
|
+
let projects = [];
|
|
5
5
|
let totalCost = 0;
|
|
6
6
|
let totalTokens = 0;
|
|
7
7
|
let totalRequests = 0;
|
|
8
|
-
let
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
let cacheHitRate = 0;
|
|
9
|
+
const cachedMonthAgg = data.monthlyAgg?.[month];
|
|
10
|
+
const cachedByProject = data.monthlyByProject?.[month];
|
|
11
|
+
if (cachedMonthAgg && cachedByProject) {
|
|
12
|
+
totalCost = cachedMonthAgg.cost;
|
|
13
|
+
totalTokens = cachedMonthAgg.tokens;
|
|
14
|
+
totalRequests = cachedMonthAgg.requests;
|
|
15
|
+
cacheHitRate =
|
|
16
|
+
cachedMonthAgg.cacheHitTokens + cachedMonthAgg.cacheMissTokens > 0
|
|
17
|
+
? cachedMonthAgg.cacheHitTokens / (cachedMonthAgg.cacheHitTokens + cachedMonthAgg.cacheMissTokens)
|
|
18
|
+
: 0;
|
|
19
|
+
projects = Object.entries(cachedByProject)
|
|
20
|
+
.map(([projectName, agg]) => {
|
|
21
|
+
let topModelId = '-';
|
|
22
|
+
let topModelCost = 0;
|
|
23
|
+
for (const [modelId, c] of Object.entries(agg.modelCost)) {
|
|
24
|
+
if (c > topModelCost) {
|
|
25
|
+
topModelCost = c;
|
|
26
|
+
topModelId = modelId;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const shortName = data.projectDisplayNames?.[projectName] ?? resolveProjectName(projectName, data.workspaceMappings);
|
|
30
|
+
return {
|
|
15
31
|
name: projectName,
|
|
16
|
-
shortName
|
|
17
|
-
cost:
|
|
18
|
-
tokens:
|
|
19
|
-
requests:
|
|
20
|
-
modelCost:
|
|
32
|
+
shortName,
|
|
33
|
+
cost: agg.cost,
|
|
34
|
+
tokens: agg.tokens,
|
|
35
|
+
requests: agg.requests,
|
|
36
|
+
modelCost: agg.modelCost,
|
|
37
|
+
topModelId,
|
|
21
38
|
};
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const s = stats;
|
|
25
|
-
const cost = Number(s.cost ?? 0);
|
|
26
|
-
const tokens = Number(s.totalTokens ?? 0);
|
|
27
|
-
const requests = Number(s.requests ?? 0);
|
|
28
|
-
const cacheHit = Number(s.cacheHitTokens ?? 0);
|
|
29
|
-
const cacheMiss = Number(s.cacheMissTokens ?? 0);
|
|
30
|
-
p.cost += cost;
|
|
31
|
-
p.tokens += tokens;
|
|
32
|
-
p.requests += requests;
|
|
33
|
-
p.modelCost[modelId] = (p.modelCost[modelId] ?? 0) + cost;
|
|
34
|
-
totalCost += cost;
|
|
35
|
-
totalTokens += tokens;
|
|
36
|
-
totalRequests += requests;
|
|
37
|
-
cacheHitTokens += cacheHit;
|
|
38
|
-
cacheMissTokens += cacheMiss;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
39
|
+
})
|
|
40
|
+
.sort((a, b) => b.cost - a.cost);
|
|
41
41
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
let
|
|
45
|
-
let
|
|
46
|
-
for (const [
|
|
47
|
-
if (
|
|
48
|
-
|
|
49
|
-
|
|
42
|
+
else {
|
|
43
|
+
const aggByProject = {};
|
|
44
|
+
let cacheHitTokens = 0;
|
|
45
|
+
let cacheMissTokens = 0;
|
|
46
|
+
for (const [date, dayData] of Object.entries(data.dailyData)) {
|
|
47
|
+
if (!date.startsWith(month))
|
|
48
|
+
continue;
|
|
49
|
+
for (const [projectName, models] of Object.entries(dayData ?? {})) {
|
|
50
|
+
aggByProject[projectName] ??= {
|
|
51
|
+
name: projectName,
|
|
52
|
+
shortName: data.projectDisplayNames?.[projectName] ?? resolveProjectName(projectName, data.workspaceMappings),
|
|
53
|
+
cost: 0,
|
|
54
|
+
tokens: 0,
|
|
55
|
+
requests: 0,
|
|
56
|
+
modelCost: {},
|
|
57
|
+
};
|
|
58
|
+
const p = aggByProject[projectName];
|
|
59
|
+
for (const [modelId, stats] of Object.entries(models ?? {})) {
|
|
60
|
+
const s = stats;
|
|
61
|
+
const cost = Number(s.cost ?? 0);
|
|
62
|
+
const tokens = Number(s.totalTokens ?? 0);
|
|
63
|
+
const requests = Number(s.requests ?? 0);
|
|
64
|
+
const cacheHit = Number(s.cacheHitTokens ?? 0);
|
|
65
|
+
const cacheMiss = Number(s.cacheMissTokens ?? 0);
|
|
66
|
+
p.cost += cost;
|
|
67
|
+
p.tokens += tokens;
|
|
68
|
+
p.requests += requests;
|
|
69
|
+
p.modelCost[modelId] = (p.modelCost[modelId] ?? 0) + cost;
|
|
70
|
+
totalCost += cost;
|
|
71
|
+
totalTokens += tokens;
|
|
72
|
+
totalRequests += requests;
|
|
73
|
+
cacheHitTokens += cacheHit;
|
|
74
|
+
cacheMissTokens += cacheMiss;
|
|
75
|
+
}
|
|
50
76
|
}
|
|
51
77
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
78
|
+
projects = Object.values(aggByProject)
|
|
79
|
+
.map(p => {
|
|
80
|
+
let topModelId = '-';
|
|
81
|
+
let topModelCost = 0;
|
|
82
|
+
for (const [modelId, c] of Object.entries(p.modelCost)) {
|
|
83
|
+
if (c > topModelCost) {
|
|
84
|
+
topModelCost = c;
|
|
85
|
+
topModelId = modelId;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return { ...p, topModelId };
|
|
89
|
+
})
|
|
90
|
+
.sort((a, b) => b.cost - a.cost);
|
|
91
|
+
cacheHitRate = cacheHitTokens + cacheMissTokens > 0 ? cacheHitTokens / (cacheHitTokens + cacheMissTokens) : 0;
|
|
92
|
+
}
|
|
55
93
|
if (!projects.length) {
|
|
56
94
|
box.setContent(`{bold}${month}{/bold}\n\nNo data available for this month.`);
|
|
57
95
|
return;
|
|
58
96
|
}
|
|
59
|
-
const cacheHitRate = cacheHitTokens + cacheMissTokens > 0 ? cacheHitTokens / (cacheHitTokens + cacheMissTokens) : 0;
|
|
60
97
|
// 根据宽度计算列宽
|
|
61
98
|
const availableWidth = width - 6; // padding
|
|
62
99
|
const costCol = 12;
|
package/dist/views/monthly.js
CHANGED
|
@@ -1,40 +1,42 @@
|
|
|
1
1
|
import { resolveProjectName } from '../lib/workspace-resolver.js';
|
|
2
2
|
import { formatCost, formatNumber, formatTokens, truncate } from '../lib/utils.js';
|
|
3
3
|
export function renderMonthly(box, data, scrollOffset = 0, selectedIndex = 0, width, note, pageSize) {
|
|
4
|
-
const aggByMonth = {};
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
for (const [
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
4
|
+
const aggByMonth = data.monthlyAgg ?? {};
|
|
5
|
+
if (!data.monthlyAgg) {
|
|
6
|
+
for (const [date, dayData] of Object.entries(data.dailyData)) {
|
|
7
|
+
const month = date.slice(0, 7);
|
|
8
|
+
if (!month)
|
|
9
|
+
continue;
|
|
10
|
+
aggByMonth[month] ??= {
|
|
11
|
+
cost: 0,
|
|
12
|
+
tokens: 0,
|
|
13
|
+
requests: 0,
|
|
14
|
+
cacheHitTokens: 0,
|
|
15
|
+
cacheMissTokens: 0,
|
|
16
|
+
modelCost: {},
|
|
17
|
+
projectCost: {},
|
|
18
|
+
};
|
|
19
|
+
const monthAgg = aggByMonth[month];
|
|
20
|
+
for (const [projectName, models] of Object.entries(dayData ?? {})) {
|
|
21
|
+
for (const [modelId, stats] of Object.entries(models ?? {})) {
|
|
22
|
+
const s = stats;
|
|
23
|
+
const cost = Number(s.cost ?? 0);
|
|
24
|
+
const tokens = Number(s.totalTokens ?? 0);
|
|
25
|
+
const requests = Number(s.requests ?? 0);
|
|
26
|
+
const cacheHit = Number(s.cacheHitTokens ?? 0);
|
|
27
|
+
const cacheMiss = Number(s.cacheMissTokens ?? 0);
|
|
28
|
+
monthAgg.cost += cost;
|
|
29
|
+
monthAgg.tokens += tokens;
|
|
30
|
+
monthAgg.requests += requests;
|
|
31
|
+
monthAgg.cacheHitTokens += cacheHit;
|
|
32
|
+
monthAgg.cacheMissTokens += cacheMiss;
|
|
33
|
+
monthAgg.modelCost[modelId] = (monthAgg.modelCost[modelId] ?? 0) + cost;
|
|
34
|
+
monthAgg.projectCost[projectName] = (monthAgg.projectCost[projectName] ?? 0) + cost;
|
|
35
|
+
}
|
|
34
36
|
}
|
|
35
37
|
}
|
|
36
38
|
}
|
|
37
|
-
const sortedMonths = Object.keys(aggByMonth).sort().reverse();
|
|
39
|
+
const sortedMonths = data.sortedMonths ?? Object.keys(aggByMonth).sort().reverse();
|
|
38
40
|
// 根据宽度计算列宽
|
|
39
41
|
const availableWidth = width - 6; // padding
|
|
40
42
|
const monthCol = 9;
|
|
@@ -73,7 +75,7 @@ export function renderMonthly(box, data, scrollOffset = 0, selectedIndex = 0, wi
|
|
|
73
75
|
if (c > topProject.cost)
|
|
74
76
|
topProject = { name: projectName, cost: c };
|
|
75
77
|
}
|
|
76
|
-
const shortProject = resolveProjectName(topProject.name, data.workspaceMappings);
|
|
78
|
+
const shortProject = data.projectDisplayNames?.[topProject.name] ?? resolveProjectName(topProject.name, data.workspaceMappings);
|
|
77
79
|
const isSelected = scrollOffset + i === selectedIndex;
|
|
78
80
|
const rowContent = month.padEnd(monthCol) +
|
|
79
81
|
formatCost(m.cost).padStart(costCol) +
|
package/dist/views/overview.js
CHANGED
|
@@ -170,7 +170,7 @@ export function renderOverview(box, data, width, height, note) {
|
|
|
170
170
|
}
|
|
171
171
|
if (topProject) {
|
|
172
172
|
const label = '{cyan-fg}Top project:{/cyan-fg} ';
|
|
173
|
-
const shortName = resolveProjectName(topProject.name, data.workspaceMappings);
|
|
173
|
+
const shortName = data.projectDisplayNames?.[topProject.name] ?? resolveProjectName(topProject.name, data.workspaceMappings);
|
|
174
174
|
const tail = `(${formatCost(topProject.cost)})`;
|
|
175
175
|
const maxNameLen = Math.max(4, maxW - visibleLen(label) - visibleLen(tail) - 2);
|
|
176
176
|
lines.push(label + truncate(shortName, maxNameLen) + ' ' + tail);
|