codebuddy-stats 1.0.0 → 1.1.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/.github/workflows/publish.yml +41 -0
- package/README.md +120 -0
- package/dist/index.js +76 -18
- package/dist/index.js.map +1 -1
- package/dist/lib/data-loader.js +289 -56
- package/dist/lib/data-loader.js.map +1 -1
- package/dist/lib/paths.js +31 -7
- package/dist/lib/paths.js.map +1 -1
- package/dist/lib/workspace-resolver.js +211 -0
- package/dist/lib/workspace-resolver.js.map +1 -0
- package/package.json +1 -1
- package/src/index.ts +86 -20
- package/src/lib/data-loader.ts +346 -67
- package/src/lib/paths.ts +33 -8
- package/src/lib/workspace-resolver.ts +235 -0
- package/index.js +0 -16
package/src/lib/data-loader.ts
CHANGED
|
@@ -3,8 +3,9 @@ import fsSync from 'node:fs'
|
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { createInterface } from 'node:readline'
|
|
5
5
|
|
|
6
|
-
import { getProjectsDir, getSettingsPath } from './paths.js'
|
|
6
|
+
import { getIdeDataDir, getProjectsDir, getSettingsPath } from './paths.js'
|
|
7
7
|
import { DEFAULT_MODEL_ID, getPricingForModel, tokensToCost } from './pricing.js'
|
|
8
|
+
import { loadWorkspaceMappings, type WorkspaceMapping } from './workspace-resolver.js'
|
|
8
9
|
|
|
9
10
|
export const BASE_DIR = getProjectsDir()
|
|
10
11
|
|
|
@@ -55,10 +56,15 @@ export interface AnalysisData {
|
|
|
55
56
|
topProject: (SummaryStats & { name: string }) | null
|
|
56
57
|
cacheHitRate: number
|
|
57
58
|
activeDays: number
|
|
59
|
+
/** 工作区 hash -> 路径映射(仅 IDE source 有效) */
|
|
60
|
+
workspaceMappings?: Map<string, import('./workspace-resolver.js').WorkspaceMapping>
|
|
58
61
|
}
|
|
59
62
|
|
|
63
|
+
export type UsageSource = 'code' | 'ide'
|
|
64
|
+
|
|
60
65
|
export interface LoadUsageOptions {
|
|
61
66
|
days?: number | null
|
|
67
|
+
source?: UsageSource
|
|
62
68
|
}
|
|
63
69
|
|
|
64
70
|
interface SettingsFile {
|
|
@@ -133,10 +139,7 @@ function extractUsageStats(usage: RawUsage): UsageStats {
|
|
|
133
139
|
}
|
|
134
140
|
}
|
|
135
141
|
|
|
136
|
-
function computeUsageCost(
|
|
137
|
-
usage: RawUsage,
|
|
138
|
-
modelId: string | null | undefined
|
|
139
|
-
): { cost: number; stats: UsageStats; modelId: string } {
|
|
142
|
+
function computeUsageCost(usage: RawUsage, modelId: string): { cost: number; stats: UsageStats } {
|
|
140
143
|
const stats = extractUsageStats(usage)
|
|
141
144
|
const pricing = getPricingForModel(modelId)
|
|
142
145
|
|
|
@@ -151,35 +154,111 @@ function computeUsageCost(
|
|
|
151
154
|
|
|
152
155
|
const outputCost = tokensToCost(stats.completionTokens, pricing.completion)
|
|
153
156
|
|
|
154
|
-
return { cost: inputCost + outputCost, stats
|
|
157
|
+
return { cost: inputCost + outputCost, stats }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function computeMinDate(days?: number | null): string | null {
|
|
161
|
+
if (!days) return null
|
|
162
|
+
const d = new Date()
|
|
163
|
+
d.setDate(d.getDate() - days + 1)
|
|
164
|
+
d.setHours(0, 0, 0, 0)
|
|
165
|
+
return d.toISOString().split('T')[0] ?? null
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function toISODateString(value: unknown): string | null {
|
|
169
|
+
if (typeof value !== 'string') return null
|
|
170
|
+
const d = new Date(value)
|
|
171
|
+
if (Number.isNaN(d.getTime())) return null
|
|
172
|
+
return d.toISOString().split('T')[0] ?? null
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function pathExists(p: string): Promise<boolean> {
|
|
176
|
+
try {
|
|
177
|
+
await fs.access(p)
|
|
178
|
+
return true
|
|
179
|
+
} catch {
|
|
180
|
+
return false
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function ensureDailyModelStats(dailyData: DailyData, date: string, project: string, modelId: string): DailyModelStats {
|
|
185
|
+
dailyData[date] ??= {}
|
|
186
|
+
dailyData[date]![project] ??= {}
|
|
187
|
+
dailyData[date]![project]![modelId] ??= {
|
|
188
|
+
cost: 0,
|
|
189
|
+
promptTokens: 0,
|
|
190
|
+
completionTokens: 0,
|
|
191
|
+
totalTokens: 0,
|
|
192
|
+
cacheHitTokens: 0,
|
|
193
|
+
cacheMissTokens: 0,
|
|
194
|
+
cacheWriteTokens: 0,
|
|
195
|
+
requests: 0,
|
|
196
|
+
}
|
|
197
|
+
return dailyData[date]![project]![modelId]!
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function finalizeAnalysis(
|
|
201
|
+
defaultModelId: string,
|
|
202
|
+
dailyData: DailyData,
|
|
203
|
+
modelTotals: Record<string, SummaryStats>,
|
|
204
|
+
projectTotals: Record<string, SummaryStats>,
|
|
205
|
+
grandTotal: GrandTotal,
|
|
206
|
+
workspaceMappings?: Map<string, WorkspaceMapping>
|
|
207
|
+
): AnalysisData {
|
|
208
|
+
const dailySummary: Record<string, SummaryStats> = {}
|
|
209
|
+
for (const date of Object.keys(dailyData)) {
|
|
210
|
+
dailySummary[date] = { cost: 0, tokens: 0, requests: 0 }
|
|
211
|
+
for (const project of Object.values(dailyData[date] ?? {})) {
|
|
212
|
+
for (const model of Object.values(project ?? {})) {
|
|
213
|
+
dailySummary[date]!.cost += model.cost
|
|
214
|
+
dailySummary[date]!.tokens += model.totalTokens
|
|
215
|
+
dailySummary[date]!.requests += model.requests
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const topModelEntry = Object.entries(modelTotals).sort((a, b) => b[1].cost - a[1].cost)[0]
|
|
221
|
+
const topProjectEntry = Object.entries(projectTotals).sort((a, b) => b[1].cost - a[1].cost)[0]
|
|
222
|
+
|
|
223
|
+
const cacheHitRate =
|
|
224
|
+
grandTotal.cacheHitTokens + grandTotal.cacheMissTokens > 0
|
|
225
|
+
? grandTotal.cacheHitTokens / (grandTotal.cacheHitTokens + grandTotal.cacheMissTokens)
|
|
226
|
+
: 0
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
defaultModelId,
|
|
230
|
+
dailyData,
|
|
231
|
+
dailySummary,
|
|
232
|
+
modelTotals,
|
|
233
|
+
projectTotals,
|
|
234
|
+
grandTotal,
|
|
235
|
+
topModel: topModelEntry ? { id: topModelEntry[0], ...topModelEntry[1] } : null,
|
|
236
|
+
topProject: topProjectEntry ? { name: topProjectEntry[0], ...topProjectEntry[1] } : null,
|
|
237
|
+
cacheHitRate,
|
|
238
|
+
activeDays: Object.keys(dailyData).length,
|
|
239
|
+
workspaceMappings,
|
|
240
|
+
}
|
|
155
241
|
}
|
|
156
242
|
|
|
157
243
|
/**
|
|
158
244
|
* 加载所有用量数据
|
|
159
245
|
*/
|
|
160
246
|
export async function loadUsageData(options: LoadUsageOptions = {}): Promise<AnalysisData> {
|
|
247
|
+
const source: UsageSource = options.source ?? 'code'
|
|
248
|
+
if (source === 'ide') {
|
|
249
|
+
return loadIdeUsageData(options)
|
|
250
|
+
}
|
|
251
|
+
return loadCodeUsageData(options)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function loadCodeUsageData(options: LoadUsageOptions = {}): Promise<AnalysisData> {
|
|
161
255
|
const defaultModelId = await loadModelFromSettings()
|
|
162
256
|
const jsonlFiles = await findJsonlFiles(BASE_DIR)
|
|
257
|
+
const minDate = computeMinDate(options.days)
|
|
163
258
|
|
|
164
|
-
// 计算日期过滤范围
|
|
165
|
-
let minDate: string | null = null
|
|
166
|
-
if (options.days) {
|
|
167
|
-
const d = new Date()
|
|
168
|
-
d.setDate(d.getDate() - options.days + 1)
|
|
169
|
-
d.setHours(0, 0, 0, 0)
|
|
170
|
-
minDate = d.toISOString().split('T')[0] ?? null
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// 按日期 -> 项目 -> 模型 组织的数据
|
|
174
259
|
const dailyData: DailyData = {}
|
|
175
|
-
|
|
176
|
-
// 按模型汇总
|
|
177
260
|
const modelTotals: Record<string, SummaryStats> = {}
|
|
178
|
-
|
|
179
|
-
// 按项目汇总
|
|
180
261
|
const projectTotals: Record<string, SummaryStats> = {}
|
|
181
|
-
|
|
182
|
-
// 总计
|
|
183
262
|
const grandTotal: GrandTotal = {
|
|
184
263
|
cost: 0,
|
|
185
264
|
tokens: 0,
|
|
@@ -213,27 +292,15 @@ export async function loadUsageData(options: LoadUsageOptions = {}): Promise<Ana
|
|
|
213
292
|
const date = dateObj.toISOString().split('T')[0]
|
|
214
293
|
if (!date) continue
|
|
215
294
|
|
|
216
|
-
// 日期过滤
|
|
217
295
|
if (minDate && date < minDate) continue
|
|
218
296
|
|
|
219
297
|
const recordModelId = record?.providerData?.model
|
|
220
|
-
const
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
dailyData[date] ??= {}
|
|
224
|
-
dailyData[date]![projectName] ??= {}
|
|
225
|
-
dailyData[date]![projectName]![usedModelId] ??= {
|
|
226
|
-
cost: 0,
|
|
227
|
-
promptTokens: 0,
|
|
228
|
-
completionTokens: 0,
|
|
229
|
-
totalTokens: 0,
|
|
230
|
-
cacheHitTokens: 0,
|
|
231
|
-
cacheMissTokens: 0,
|
|
232
|
-
cacheWriteTokens: 0,
|
|
233
|
-
requests: 0,
|
|
234
|
-
}
|
|
298
|
+
const modelFromRecord = typeof recordModelId === 'string' ? recordModelId : null
|
|
299
|
+
const usedModelId = modelFromRecord || defaultModelId
|
|
235
300
|
|
|
236
|
-
const
|
|
301
|
+
const { cost, stats: usageStats } = computeUsageCost(usage, usedModelId)
|
|
302
|
+
|
|
303
|
+
const dayStats = ensureDailyModelStats(dailyData, date, projectName, usedModelId)
|
|
237
304
|
dayStats.cost += cost
|
|
238
305
|
dayStats.promptTokens += usageStats.promptTokens
|
|
239
306
|
dayStats.completionTokens += usageStats.completionTokens
|
|
@@ -264,39 +331,251 @@ export async function loadUsageData(options: LoadUsageOptions = {}): Promise<Ana
|
|
|
264
331
|
}
|
|
265
332
|
}
|
|
266
333
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
334
|
+
return finalizeAnalysis(defaultModelId, dailyData, modelTotals, projectTotals, grandTotal)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
interface IdeConversationMeta {
|
|
338
|
+
id?: unknown
|
|
339
|
+
createdAt?: unknown
|
|
340
|
+
lastMessageAt?: unknown
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
interface IdeRequestUsage {
|
|
344
|
+
inputTokens?: unknown
|
|
345
|
+
outputTokens?: unknown
|
|
346
|
+
totalTokens?: unknown
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
interface IdeRequest {
|
|
350
|
+
messages?: unknown
|
|
351
|
+
usage?: IdeRequestUsage
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
interface IdeConversationIndex {
|
|
355
|
+
requests?: unknown
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function findIdeHistoryDirs(): Promise<string[]> {
|
|
359
|
+
const root = getIdeDataDir()
|
|
360
|
+
const out = new Set<string>()
|
|
361
|
+
|
|
362
|
+
let level1: fsSync.Dirent[] = []
|
|
363
|
+
try {
|
|
364
|
+
level1 = await fs.readdir(root, { withFileTypes: true })
|
|
365
|
+
} catch {
|
|
366
|
+
return []
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
for (const dirent of level1) {
|
|
370
|
+
if (!dirent.isDirectory()) continue
|
|
371
|
+
const codeBuddyIdeDir = path.join(root, dirent.name, 'CodeBuddyIDE')
|
|
372
|
+
if (!(await pathExists(codeBuddyIdeDir))) continue
|
|
373
|
+
|
|
374
|
+
const directHistory = path.join(codeBuddyIdeDir, 'history')
|
|
375
|
+
if (await pathExists(directHistory)) {
|
|
376
|
+
out.add(directHistory)
|
|
377
|
+
continue
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
let nested: fsSync.Dirent[] = []
|
|
381
|
+
try {
|
|
382
|
+
nested = await fs.readdir(codeBuddyIdeDir, { withFileTypes: true })
|
|
383
|
+
} catch {
|
|
384
|
+
continue
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
for (const child of nested) {
|
|
388
|
+
if (!child.isDirectory()) continue
|
|
389
|
+
const nestedHistory = path.join(codeBuddyIdeDir, child.name, 'history')
|
|
390
|
+
if (await pathExists(nestedHistory)) {
|
|
391
|
+
out.add(nestedHistory)
|
|
276
392
|
}
|
|
277
393
|
}
|
|
278
394
|
}
|
|
279
395
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
Object.entries(projectTotals).sort((a, b) => b[1].cost - a[1].cost)[0]
|
|
396
|
+
return [...out]
|
|
397
|
+
}
|
|
283
398
|
|
|
284
|
-
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
: 0
|
|
399
|
+
async function readJsonFile(filePath: string): Promise<unknown> {
|
|
400
|
+
const raw = await fs.readFile(filePath, 'utf8')
|
|
401
|
+
return JSON.parse(raw) as unknown
|
|
402
|
+
}
|
|
289
403
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
404
|
+
async function readFileHeadUtf8(filePath: string, bytes = 64 * 1024): Promise<string> {
|
|
405
|
+
const fh = await fs.open(filePath, 'r')
|
|
406
|
+
try {
|
|
407
|
+
const buf = Buffer.alloc(bytes)
|
|
408
|
+
const { bytesRead } = await fh.read(buf, 0, buf.length, 0)
|
|
409
|
+
return buf.toString('utf8', 0, bytesRead)
|
|
410
|
+
} finally {
|
|
411
|
+
await fh.close()
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function extractModelIdFromMessageHead(head: string): string | null {
|
|
416
|
+
// extra 是一个转义的 JSON 字符串,形如: "extra": "{\"modelId\":\"gpt-5.1\",...}"
|
|
417
|
+
// 正则需要匹配包含转义字符的完整字符串
|
|
418
|
+
const extraMatch = head.match(/"extra"\s*:\s*"((?:[^"\\]|\\.)*)"/s)
|
|
419
|
+
if (!extraMatch?.[1]) return null
|
|
420
|
+
|
|
421
|
+
try {
|
|
422
|
+
// extraMatch[1] 是转义后的内容,需要先解析为字符串
|
|
423
|
+
const extraStr = JSON.parse(`"${extraMatch[1]}"`) as string
|
|
424
|
+
const extra = JSON.parse(extraStr) as { modelId?: unknown; modelName?: unknown }
|
|
425
|
+
if (typeof extra.modelId === 'string' && extra.modelId) return extra.modelId
|
|
426
|
+
if (typeof extra.modelName === 'string' && extra.modelName) return extra.modelName
|
|
427
|
+
return null
|
|
428
|
+
} catch {
|
|
429
|
+
return null
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
async function inferIdeModelIdForRequest(
|
|
434
|
+
conversationDir: string,
|
|
435
|
+
request: IdeRequest,
|
|
436
|
+
messageModelCache: Map<string, string>
|
|
437
|
+
): Promise<string | null> {
|
|
438
|
+
const messages = Array.isArray(request.messages) ? (request.messages as unknown[]) : []
|
|
439
|
+
|
|
440
|
+
for (let i = 0; i < Math.min(messages.length, 3); i++) {
|
|
441
|
+
const messageId = messages[i]
|
|
442
|
+
if (typeof messageId !== 'string' || !messageId) continue
|
|
443
|
+
|
|
444
|
+
const cached = messageModelCache.get(messageId)
|
|
445
|
+
if (cached) return cached
|
|
446
|
+
|
|
447
|
+
const msgPath = path.join(conversationDir, 'messages', `${messageId}.json`)
|
|
448
|
+
try {
|
|
449
|
+
const head = await readFileHeadUtf8(msgPath)
|
|
450
|
+
const modelId = extractModelIdFromMessageHead(head)
|
|
451
|
+
if (modelId) {
|
|
452
|
+
messageModelCache.set(messageId, modelId)
|
|
453
|
+
return modelId
|
|
454
|
+
}
|
|
455
|
+
} catch {
|
|
456
|
+
// ignore
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return null
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
async function loadIdeUsageData(options: LoadUsageOptions = {}): Promise<AnalysisData> {
|
|
464
|
+
const defaultModelId = await loadModelFromSettings()
|
|
465
|
+
const minDate = computeMinDate(options.days)
|
|
466
|
+
|
|
467
|
+
// 加载工作区映射
|
|
468
|
+
const workspaceMappings = await loadWorkspaceMappings()
|
|
469
|
+
|
|
470
|
+
const dailyData: DailyData = {}
|
|
471
|
+
const modelTotals: Record<string, SummaryStats> = {}
|
|
472
|
+
const projectTotals: Record<string, SummaryStats> = {}
|
|
473
|
+
const grandTotal: GrandTotal = {
|
|
474
|
+
cost: 0,
|
|
475
|
+
tokens: 0,
|
|
476
|
+
requests: 0,
|
|
477
|
+
cacheHitTokens: 0,
|
|
478
|
+
cacheMissTokens: 0,
|
|
301
479
|
}
|
|
480
|
+
|
|
481
|
+
const historyDirs = await findIdeHistoryDirs()
|
|
482
|
+
const messageModelCache = new Map<string, string>()
|
|
483
|
+
|
|
484
|
+
for (const historyDir of historyDirs) {
|
|
485
|
+
let workspaces: fsSync.Dirent[] = []
|
|
486
|
+
try {
|
|
487
|
+
workspaces = await fs.readdir(historyDir, { withFileTypes: true })
|
|
488
|
+
} catch {
|
|
489
|
+
continue
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
for (const ws of workspaces) {
|
|
493
|
+
if (!ws.isDirectory()) continue
|
|
494
|
+
const workspaceHash = ws.name
|
|
495
|
+
const workspaceDir = path.join(historyDir, workspaceHash)
|
|
496
|
+
const workspaceIndexPath = path.join(workspaceDir, 'index.json')
|
|
497
|
+
|
|
498
|
+
let convList: IdeConversationMeta[] = []
|
|
499
|
+
try {
|
|
500
|
+
const parsed = (await readJsonFile(workspaceIndexPath)) as unknown
|
|
501
|
+
if (Array.isArray(parsed)) {
|
|
502
|
+
convList = parsed as IdeConversationMeta[]
|
|
503
|
+
} else if (parsed && typeof parsed === 'object') {
|
|
504
|
+
const maybe = (parsed as any).conversations ?? (parsed as any).items ?? (parsed as any).list
|
|
505
|
+
if (Array.isArray(maybe)) convList = maybe as IdeConversationMeta[]
|
|
506
|
+
}
|
|
507
|
+
} catch {
|
|
508
|
+
continue
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
for (const conv of convList) {
|
|
512
|
+
const conversationId = typeof conv.id === 'string' ? conv.id : null
|
|
513
|
+
if (!conversationId) continue
|
|
514
|
+
|
|
515
|
+
const date = toISODateString(conv.lastMessageAt) ?? toISODateString(conv.createdAt)
|
|
516
|
+
if (!date) continue
|
|
517
|
+
if (minDate && date < minDate) continue
|
|
518
|
+
|
|
519
|
+
const conversationDir = path.join(workspaceDir, conversationId)
|
|
520
|
+
const convIndexPath = path.join(conversationDir, 'index.json')
|
|
521
|
+
|
|
522
|
+
let convIndex: IdeConversationIndex | null = null
|
|
523
|
+
try {
|
|
524
|
+
convIndex = (await readJsonFile(convIndexPath)) as IdeConversationIndex
|
|
525
|
+
} catch {
|
|
526
|
+
continue
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const requests = Array.isArray(convIndex?.requests) ? (convIndex!.requests as IdeRequest[]) : []
|
|
530
|
+
for (const req of requests) {
|
|
531
|
+
const usage = req?.usage
|
|
532
|
+
const inputTokens = typeof usage?.inputTokens === 'number' ? usage.inputTokens : Number(usage?.inputTokens ?? 0)
|
|
533
|
+
const outputTokens = typeof usage?.outputTokens === 'number' ? usage.outputTokens : Number(usage?.outputTokens ?? 0)
|
|
534
|
+
const totalTokens =
|
|
535
|
+
typeof usage?.totalTokens === 'number'
|
|
536
|
+
? usage.totalTokens
|
|
537
|
+
: Number.isFinite(Number(usage?.totalTokens))
|
|
538
|
+
? Number(usage?.totalTokens)
|
|
539
|
+
: inputTokens + outputTokens
|
|
540
|
+
|
|
541
|
+
if (!Number.isFinite(inputTokens) || !Number.isFinite(outputTokens) || !Number.isFinite(totalTokens)) continue
|
|
542
|
+
|
|
543
|
+
const inferredModelId = await inferIdeModelIdForRequest(conversationDir, req, messageModelCache)
|
|
544
|
+
const usedModelId = inferredModelId || defaultModelId
|
|
545
|
+
|
|
546
|
+
const rawUsage: RawUsage = {
|
|
547
|
+
prompt_tokens: Math.max(0, inputTokens),
|
|
548
|
+
completion_tokens: Math.max(0, outputTokens),
|
|
549
|
+
total_tokens: Math.max(0, totalTokens),
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const { cost, stats } = computeUsageCost(rawUsage, usedModelId)
|
|
553
|
+
|
|
554
|
+
const projectName = workspaceHash
|
|
555
|
+
const dayStats = ensureDailyModelStats(dailyData, date, projectName, usedModelId)
|
|
556
|
+
dayStats.cost += cost
|
|
557
|
+
dayStats.promptTokens += stats.promptTokens
|
|
558
|
+
dayStats.completionTokens += stats.completionTokens
|
|
559
|
+
dayStats.totalTokens += stats.totalTokens
|
|
560
|
+
dayStats.requests += 1
|
|
561
|
+
|
|
562
|
+
modelTotals[usedModelId] ??= { cost: 0, tokens: 0, requests: 0 }
|
|
563
|
+
modelTotals[usedModelId]!.cost += cost
|
|
564
|
+
modelTotals[usedModelId]!.tokens += stats.totalTokens
|
|
565
|
+
modelTotals[usedModelId]!.requests += 1
|
|
566
|
+
|
|
567
|
+
projectTotals[projectName] ??= { cost: 0, tokens: 0, requests: 0 }
|
|
568
|
+
projectTotals[projectName]!.cost += cost
|
|
569
|
+
projectTotals[projectName]!.tokens += stats.totalTokens
|
|
570
|
+
projectTotals[projectName]!.requests += 1
|
|
571
|
+
|
|
572
|
+
grandTotal.cost += cost
|
|
573
|
+
grandTotal.tokens += stats.totalTokens
|
|
574
|
+
grandTotal.requests += 1
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return finalizeAnalysis(defaultModelId, dailyData, modelTotals, projectTotals, grandTotal, workspaceMappings)
|
|
302
581
|
}
|
package/src/lib/paths.ts
CHANGED
|
@@ -22,6 +22,39 @@ export function getConfigDir(): string {
|
|
|
22
22
|
return path.join(os.homedir(), '.codebuddy')
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* 获取 CodeBuddy IDE (CodeBuddyExtension) 数据目录
|
|
27
|
+
* - macOS: ~/Library/Application Support/CodeBuddyExtension/Data
|
|
28
|
+
* - Windows: %APPDATA%/CodeBuddyExtension/Data
|
|
29
|
+
* - Linux: $XDG_CONFIG_HOME/CodeBuddyExtension/Data 或 ~/.config/CodeBuddyExtension/Data
|
|
30
|
+
*/
|
|
31
|
+
export function getIdeDataDir(): string {
|
|
32
|
+
if (process.platform === 'darwin') {
|
|
33
|
+
return path.join(os.homedir(), 'Library', 'Application Support', 'CodeBuddyExtension', 'Data')
|
|
34
|
+
}
|
|
35
|
+
if (process.platform === 'win32') {
|
|
36
|
+
const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming')
|
|
37
|
+
return path.join(appData, 'CodeBuddyExtension', 'Data')
|
|
38
|
+
}
|
|
39
|
+
const xdgConfigHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config')
|
|
40
|
+
return path.join(xdgConfigHome, 'CodeBuddyExtension', 'Data')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 获取 CodeBuddy IDE 的 workspaceStorage 目录
|
|
45
|
+
*/
|
|
46
|
+
export function getWorkspaceStorageDir(): string {
|
|
47
|
+
if (process.platform === 'darwin') {
|
|
48
|
+
return path.join(os.homedir(), 'Library', 'Application Support', 'CodeBuddy CN', 'User', 'workspaceStorage')
|
|
49
|
+
}
|
|
50
|
+
if (process.platform === 'win32') {
|
|
51
|
+
const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming')
|
|
52
|
+
return path.join(appData, 'CodeBuddy CN', 'User', 'workspaceStorage')
|
|
53
|
+
}
|
|
54
|
+
const configHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config')
|
|
55
|
+
return path.join(configHome, 'CodeBuddy CN', 'User', 'workspaceStorage')
|
|
56
|
+
}
|
|
57
|
+
|
|
25
58
|
/**
|
|
26
59
|
* 获取项目数据目录
|
|
27
60
|
*/
|
|
@@ -35,11 +68,3 @@ export function getProjectsDir(): string {
|
|
|
35
68
|
export function getSettingsPath(): string {
|
|
36
69
|
return path.join(getConfigDir(), 'settings.json')
|
|
37
70
|
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* 简化项目路径显示
|
|
41
|
-
* 保持原始名称不变
|
|
42
|
-
*/
|
|
43
|
-
export function shortenProjectName(name: string): string {
|
|
44
|
-
return name
|
|
45
|
-
}
|