codebuddy-stats 1.1.0 → 1.1.1
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 +0 -1
- package/dist/lib/data-loader.js +0 -1
- package/dist/lib/paths.js +0 -1
- package/dist/lib/pricing.js +0 -1
- package/dist/lib/utils.js +0 -1
- package/dist/lib/workspace-resolver.js +0 -1
- package/package.json +4 -1
- package/.github/workflows/publish.yml +0 -41
- package/dist/index.js.map +0 -1
- package/dist/lib/data-loader.js.map +0 -1
- package/dist/lib/paths.js.map +0 -1
- package/dist/lib/pricing.js.map +0 -1
- package/dist/lib/utils.js.map +0 -1
- package/dist/lib/workspace-resolver.js.map +0 -1
- package/src/index.ts +0 -615
- package/src/lib/data-loader.ts +0 -581
- package/src/lib/paths.ts +0 -70
- package/src/lib/pricing.ts +0 -128
- package/src/lib/utils.ts +0 -61
- package/src/lib/workspace-resolver.ts +0 -235
- package/tsconfig.json +0 -25
package/src/lib/data-loader.ts
DELETED
|
@@ -1,581 +0,0 @@
|
|
|
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
|
-
|
|
6
|
-
import { getIdeDataDir, getProjectsDir, getSettingsPath } from './paths.js'
|
|
7
|
-
import { DEFAULT_MODEL_ID, getPricingForModel, tokensToCost } from './pricing.js'
|
|
8
|
-
import { loadWorkspaceMappings, type WorkspaceMapping } from './workspace-resolver.js'
|
|
9
|
-
|
|
10
|
-
export const BASE_DIR = getProjectsDir()
|
|
11
|
-
|
|
12
|
-
export interface RawUsage {
|
|
13
|
-
prompt_tokens?: number
|
|
14
|
-
completion_tokens?: number
|
|
15
|
-
total_tokens?: number
|
|
16
|
-
prompt_cache_hit_tokens?: number
|
|
17
|
-
prompt_cache_miss_tokens?: number
|
|
18
|
-
cache_creation_input_tokens?: number
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface UsageStats {
|
|
22
|
-
promptTokens: number
|
|
23
|
-
completionTokens: number
|
|
24
|
-
totalTokens: number
|
|
25
|
-
cacheHitTokens: number
|
|
26
|
-
cacheMissTokens: number
|
|
27
|
-
cacheWriteTokens: number
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface DailyModelStats extends UsageStats {
|
|
31
|
-
cost: number
|
|
32
|
-
requests: number
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface SummaryStats {
|
|
36
|
-
cost: number
|
|
37
|
-
tokens: number
|
|
38
|
-
requests: number
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface GrandTotal extends SummaryStats {
|
|
42
|
-
cacheHitTokens: number
|
|
43
|
-
cacheMissTokens: number
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export type DailyData = Record<string, Record<string, Record<string, DailyModelStats>>>
|
|
47
|
-
|
|
48
|
-
export interface AnalysisData {
|
|
49
|
-
defaultModelId: string
|
|
50
|
-
dailyData: DailyData
|
|
51
|
-
dailySummary: Record<string, SummaryStats>
|
|
52
|
-
modelTotals: Record<string, SummaryStats>
|
|
53
|
-
projectTotals: Record<string, SummaryStats>
|
|
54
|
-
grandTotal: GrandTotal
|
|
55
|
-
topModel: (SummaryStats & { id: string }) | null
|
|
56
|
-
topProject: (SummaryStats & { name: string }) | null
|
|
57
|
-
cacheHitRate: number
|
|
58
|
-
activeDays: number
|
|
59
|
-
/** 工作区 hash -> 路径映射(仅 IDE source 有效) */
|
|
60
|
-
workspaceMappings?: Map<string, import('./workspace-resolver.js').WorkspaceMapping>
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export type UsageSource = 'code' | 'ide'
|
|
64
|
-
|
|
65
|
-
export interface LoadUsageOptions {
|
|
66
|
-
days?: number | null
|
|
67
|
-
source?: UsageSource
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
interface SettingsFile {
|
|
71
|
-
model?: unknown
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
interface JsonlRecord {
|
|
75
|
-
providerData?: {
|
|
76
|
-
rawUsage?: RawUsage
|
|
77
|
-
model?: unknown
|
|
78
|
-
}
|
|
79
|
-
timestamp?: unknown
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
async function loadModelFromSettings(): Promise<string> {
|
|
83
|
-
try {
|
|
84
|
-
const settingsPath = getSettingsPath()
|
|
85
|
-
const settingsRaw = await fs.readFile(settingsPath, 'utf8')
|
|
86
|
-
const settings = JSON.parse(settingsRaw) as SettingsFile
|
|
87
|
-
return typeof settings?.model === 'string' ? settings.model : DEFAULT_MODEL_ID
|
|
88
|
-
} catch {
|
|
89
|
-
return DEFAULT_MODEL_ID
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
async function findJsonlFiles(dir: string): Promise<string[]> {
|
|
94
|
-
try {
|
|
95
|
-
const dirents = await fs.readdir(dir, { withFileTypes: true })
|
|
96
|
-
const files = await Promise.all(
|
|
97
|
-
dirents.map(async dirent => {
|
|
98
|
-
const res = path.resolve(dir, dirent.name)
|
|
99
|
-
if (dirent.isDirectory()) {
|
|
100
|
-
return findJsonlFiles(res)
|
|
101
|
-
}
|
|
102
|
-
return res.endsWith('.jsonl') ? [res] : ([] as string[])
|
|
103
|
-
})
|
|
104
|
-
)
|
|
105
|
-
return files.flat()
|
|
106
|
-
} catch (error) {
|
|
107
|
-
if (typeof error === 'object' && error && 'code' in error && (error as any).code === 'ENOENT') {
|
|
108
|
-
return []
|
|
109
|
-
}
|
|
110
|
-
throw error
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function getProjectName(filePath: string): string {
|
|
115
|
-
const parts = filePath.split(path.sep)
|
|
116
|
-
const projectsIndex = parts.lastIndexOf('projects')
|
|
117
|
-
if (projectsIndex !== -1 && projectsIndex < parts.length - 1) {
|
|
118
|
-
return parts[projectsIndex + 1] ?? 'unknown-project'
|
|
119
|
-
}
|
|
120
|
-
return 'unknown-project'
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function extractUsageStats(usage: RawUsage): UsageStats {
|
|
124
|
-
const promptTokens = usage.prompt_tokens ?? 0
|
|
125
|
-
const completionTokens = usage.completion_tokens ?? 0
|
|
126
|
-
const totalTokens = usage.total_tokens ?? promptTokens + completionTokens
|
|
127
|
-
const cacheHitTokens = usage.prompt_cache_hit_tokens ?? 0
|
|
128
|
-
const cacheMissTokens =
|
|
129
|
-
usage.prompt_cache_miss_tokens ?? (cacheHitTokens > 0 ? Math.max(promptTokens - cacheHitTokens, 0) : 0)
|
|
130
|
-
const cacheWriteTokens = usage.cache_creation_input_tokens ?? 0
|
|
131
|
-
|
|
132
|
-
return {
|
|
133
|
-
promptTokens,
|
|
134
|
-
completionTokens,
|
|
135
|
-
totalTokens,
|
|
136
|
-
cacheHitTokens,
|
|
137
|
-
cacheMissTokens,
|
|
138
|
-
cacheWriteTokens,
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function computeUsageCost(usage: RawUsage, modelId: string): { cost: number; stats: UsageStats } {
|
|
143
|
-
const stats = extractUsageStats(usage)
|
|
144
|
-
const pricing = getPricingForModel(modelId)
|
|
145
|
-
|
|
146
|
-
let inputCost = 0
|
|
147
|
-
if (stats.cacheHitTokens || stats.cacheMissTokens || stats.cacheWriteTokens) {
|
|
148
|
-
inputCost += tokensToCost(stats.cacheHitTokens, pricing.cacheRead)
|
|
149
|
-
inputCost += tokensToCost(stats.cacheMissTokens, pricing.prompt)
|
|
150
|
-
inputCost += tokensToCost(stats.cacheWriteTokens, pricing.cacheWrite)
|
|
151
|
-
} else {
|
|
152
|
-
inputCost += tokensToCost(stats.promptTokens, pricing.prompt)
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const outputCost = tokensToCost(stats.completionTokens, pricing.completion)
|
|
156
|
-
|
|
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
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* 加载所有用量数据
|
|
245
|
-
*/
|
|
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> {
|
|
255
|
-
const defaultModelId = await loadModelFromSettings()
|
|
256
|
-
const jsonlFiles = await findJsonlFiles(BASE_DIR)
|
|
257
|
-
const minDate = computeMinDate(options.days)
|
|
258
|
-
|
|
259
|
-
const dailyData: DailyData = {}
|
|
260
|
-
const modelTotals: Record<string, SummaryStats> = {}
|
|
261
|
-
const projectTotals: Record<string, SummaryStats> = {}
|
|
262
|
-
const grandTotal: GrandTotal = {
|
|
263
|
-
cost: 0,
|
|
264
|
-
tokens: 0,
|
|
265
|
-
requests: 0,
|
|
266
|
-
cacheHitTokens: 0,
|
|
267
|
-
cacheMissTokens: 0,
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
for (const filePath of jsonlFiles) {
|
|
271
|
-
const fileStat = await fs.stat(filePath)
|
|
272
|
-
if (fileStat.size === 0) continue
|
|
273
|
-
|
|
274
|
-
const fileStream = fsSync.createReadStream(filePath)
|
|
275
|
-
const rl = createInterface({
|
|
276
|
-
input: fileStream,
|
|
277
|
-
crlfDelay: Number.POSITIVE_INFINITY,
|
|
278
|
-
})
|
|
279
|
-
|
|
280
|
-
const projectName = getProjectName(filePath)
|
|
281
|
-
|
|
282
|
-
for await (const line of rl) {
|
|
283
|
-
try {
|
|
284
|
-
const record = JSON.parse(line) as JsonlRecord
|
|
285
|
-
const usage = record?.providerData?.rawUsage
|
|
286
|
-
const timestamp = record?.timestamp
|
|
287
|
-
|
|
288
|
-
if (!usage || timestamp == null) continue
|
|
289
|
-
|
|
290
|
-
const dateObj = new Date(timestamp as any)
|
|
291
|
-
if (Number.isNaN(dateObj.getTime())) continue
|
|
292
|
-
const date = dateObj.toISOString().split('T')[0]
|
|
293
|
-
if (!date) continue
|
|
294
|
-
|
|
295
|
-
if (minDate && date < minDate) continue
|
|
296
|
-
|
|
297
|
-
const recordModelId = record?.providerData?.model
|
|
298
|
-
const modelFromRecord = typeof recordModelId === 'string' ? recordModelId : null
|
|
299
|
-
const usedModelId = modelFromRecord || defaultModelId
|
|
300
|
-
|
|
301
|
-
const { cost, stats: usageStats } = computeUsageCost(usage, usedModelId)
|
|
302
|
-
|
|
303
|
-
const dayStats = ensureDailyModelStats(dailyData, date, projectName, usedModelId)
|
|
304
|
-
dayStats.cost += cost
|
|
305
|
-
dayStats.promptTokens += usageStats.promptTokens
|
|
306
|
-
dayStats.completionTokens += usageStats.completionTokens
|
|
307
|
-
dayStats.totalTokens += usageStats.totalTokens
|
|
308
|
-
dayStats.cacheHitTokens += usageStats.cacheHitTokens
|
|
309
|
-
dayStats.cacheMissTokens += usageStats.cacheMissTokens
|
|
310
|
-
dayStats.cacheWriteTokens += usageStats.cacheWriteTokens
|
|
311
|
-
dayStats.requests += 1
|
|
312
|
-
|
|
313
|
-
modelTotals[usedModelId] ??= { cost: 0, tokens: 0, requests: 0 }
|
|
314
|
-
modelTotals[usedModelId]!.cost += cost
|
|
315
|
-
modelTotals[usedModelId]!.tokens += usageStats.totalTokens
|
|
316
|
-
modelTotals[usedModelId]!.requests += 1
|
|
317
|
-
|
|
318
|
-
projectTotals[projectName] ??= { cost: 0, tokens: 0, requests: 0 }
|
|
319
|
-
projectTotals[projectName]!.cost += cost
|
|
320
|
-
projectTotals[projectName]!.tokens += usageStats.totalTokens
|
|
321
|
-
projectTotals[projectName]!.requests += 1
|
|
322
|
-
|
|
323
|
-
grandTotal.cost += cost
|
|
324
|
-
grandTotal.tokens += usageStats.totalTokens
|
|
325
|
-
grandTotal.requests += 1
|
|
326
|
-
grandTotal.cacheHitTokens += usageStats.cacheHitTokens
|
|
327
|
-
grandTotal.cacheMissTokens += usageStats.cacheMissTokens
|
|
328
|
-
} catch {
|
|
329
|
-
// 忽略无法解析的行
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
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)
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
return [...out]
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
async function readJsonFile(filePath: string): Promise<unknown> {
|
|
400
|
-
const raw = await fs.readFile(filePath, 'utf8')
|
|
401
|
-
return JSON.parse(raw) as unknown
|
|
402
|
-
}
|
|
403
|
-
|
|
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,
|
|
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)
|
|
581
|
-
}
|
package/src/lib/paths.ts
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import os from 'node:os'
|
|
2
|
-
import path from 'node:path'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* 获取 CodeBuddy 配置目录
|
|
6
|
-
* - Windows: %APPDATA%/CodeBuddy
|
|
7
|
-
* - macOS: ~/.codebuddy
|
|
8
|
-
* - Linux: $XDG_CONFIG_HOME/codebuddy 或 ~/.codebuddy
|
|
9
|
-
*/
|
|
10
|
-
export function getConfigDir(): string {
|
|
11
|
-
if (process.platform === 'win32') {
|
|
12
|
-
const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming')
|
|
13
|
-
return path.join(appData, 'CodeBuddy')
|
|
14
|
-
}
|
|
15
|
-
if (process.platform === 'linux') {
|
|
16
|
-
const xdgConfigHome = process.env.XDG_CONFIG_HOME
|
|
17
|
-
if (xdgConfigHome) {
|
|
18
|
-
return path.join(xdgConfigHome, 'codebuddy')
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
// macOS 和 Linux 默认
|
|
22
|
-
return path.join(os.homedir(), '.codebuddy')
|
|
23
|
-
}
|
|
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
|
-
|
|
58
|
-
/**
|
|
59
|
-
* 获取项目数据目录
|
|
60
|
-
*/
|
|
61
|
-
export function getProjectsDir(): string {
|
|
62
|
-
return path.join(getConfigDir(), 'projects')
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* 获取设置文件路径
|
|
67
|
-
*/
|
|
68
|
-
export function getSettingsPath(): string {
|
|
69
|
-
return path.join(getConfigDir(), 'settings.json')
|
|
70
|
-
}
|