codebuddy-stats 1.0.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/README.md +120 -0
- package/dist/index.js +76 -19
- package/dist/lib/data-loader.js +289 -57
- package/dist/lib/paths.js +31 -8
- package/dist/lib/pricing.js +0 -1
- package/dist/lib/utils.js +0 -1
- package/dist/lib/workspace-resolver.js +210 -0
- package/package.json +4 -1
- 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/index.js +0 -16
- package/src/index.ts +0 -549
- package/src/lib/data-loader.ts +0 -302
- package/src/lib/paths.ts +0 -45
- package/src/lib/pricing.ts +0 -128
- package/src/lib/utils.ts +0 -61
- package/tsconfig.json +0 -25
package/dist/lib/data-loader.js
CHANGED
|
@@ -2,8 +2,9 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import fsSync from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { createInterface } from 'node:readline';
|
|
5
|
-
import { getProjectsDir, getSettingsPath } from './paths.js';
|
|
5
|
+
import { getIdeDataDir, getProjectsDir, getSettingsPath } from './paths.js';
|
|
6
6
|
import { DEFAULT_MODEL_ID, getPricingForModel, tokensToCost } from './pricing.js';
|
|
7
|
+
import { loadWorkspaceMappings } from './workspace-resolver.js';
|
|
7
8
|
export const BASE_DIR = getProjectsDir();
|
|
8
9
|
async function loadModelFromSettings() {
|
|
9
10
|
try {
|
|
@@ -72,29 +73,96 @@ function computeUsageCost(usage, modelId) {
|
|
|
72
73
|
inputCost += tokensToCost(stats.promptTokens, pricing.prompt);
|
|
73
74
|
}
|
|
74
75
|
const outputCost = tokensToCost(stats.completionTokens, pricing.completion);
|
|
75
|
-
return { cost: inputCost + outputCost, stats
|
|
76
|
+
return { cost: inputCost + outputCost, stats };
|
|
77
|
+
}
|
|
78
|
+
function computeMinDate(days) {
|
|
79
|
+
if (!days)
|
|
80
|
+
return null;
|
|
81
|
+
const d = new Date();
|
|
82
|
+
d.setDate(d.getDate() - days + 1);
|
|
83
|
+
d.setHours(0, 0, 0, 0);
|
|
84
|
+
return d.toISOString().split('T')[0] ?? null;
|
|
85
|
+
}
|
|
86
|
+
function toISODateString(value) {
|
|
87
|
+
if (typeof value !== 'string')
|
|
88
|
+
return null;
|
|
89
|
+
const d = new Date(value);
|
|
90
|
+
if (Number.isNaN(d.getTime()))
|
|
91
|
+
return null;
|
|
92
|
+
return d.toISOString().split('T')[0] ?? null;
|
|
93
|
+
}
|
|
94
|
+
async function pathExists(p) {
|
|
95
|
+
try {
|
|
96
|
+
await fs.access(p);
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function ensureDailyModelStats(dailyData, date, project, modelId) {
|
|
104
|
+
dailyData[date] ??= {};
|
|
105
|
+
dailyData[date][project] ??= {};
|
|
106
|
+
dailyData[date][project][modelId] ??= {
|
|
107
|
+
cost: 0,
|
|
108
|
+
promptTokens: 0,
|
|
109
|
+
completionTokens: 0,
|
|
110
|
+
totalTokens: 0,
|
|
111
|
+
cacheHitTokens: 0,
|
|
112
|
+
cacheMissTokens: 0,
|
|
113
|
+
cacheWriteTokens: 0,
|
|
114
|
+
requests: 0,
|
|
115
|
+
};
|
|
116
|
+
return dailyData[date][project][modelId];
|
|
117
|
+
}
|
|
118
|
+
function finalizeAnalysis(defaultModelId, dailyData, modelTotals, projectTotals, grandTotal, workspaceMappings) {
|
|
119
|
+
const dailySummary = {};
|
|
120
|
+
for (const date of Object.keys(dailyData)) {
|
|
121
|
+
dailySummary[date] = { cost: 0, tokens: 0, requests: 0 };
|
|
122
|
+
for (const project of Object.values(dailyData[date] ?? {})) {
|
|
123
|
+
for (const model of Object.values(project ?? {})) {
|
|
124
|
+
dailySummary[date].cost += model.cost;
|
|
125
|
+
dailySummary[date].tokens += model.totalTokens;
|
|
126
|
+
dailySummary[date].requests += model.requests;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const topModelEntry = Object.entries(modelTotals).sort((a, b) => b[1].cost - a[1].cost)[0];
|
|
131
|
+
const topProjectEntry = Object.entries(projectTotals).sort((a, b) => b[1].cost - a[1].cost)[0];
|
|
132
|
+
const cacheHitRate = grandTotal.cacheHitTokens + grandTotal.cacheMissTokens > 0
|
|
133
|
+
? grandTotal.cacheHitTokens / (grandTotal.cacheHitTokens + grandTotal.cacheMissTokens)
|
|
134
|
+
: 0;
|
|
135
|
+
return {
|
|
136
|
+
defaultModelId,
|
|
137
|
+
dailyData,
|
|
138
|
+
dailySummary,
|
|
139
|
+
modelTotals,
|
|
140
|
+
projectTotals,
|
|
141
|
+
grandTotal,
|
|
142
|
+
topModel: topModelEntry ? { id: topModelEntry[0], ...topModelEntry[1] } : null,
|
|
143
|
+
topProject: topProjectEntry ? { name: topProjectEntry[0], ...topProjectEntry[1] } : null,
|
|
144
|
+
cacheHitRate,
|
|
145
|
+
activeDays: Object.keys(dailyData).length,
|
|
146
|
+
workspaceMappings,
|
|
147
|
+
};
|
|
76
148
|
}
|
|
77
149
|
/**
|
|
78
150
|
* 加载所有用量数据
|
|
79
151
|
*/
|
|
80
152
|
export async function loadUsageData(options = {}) {
|
|
153
|
+
const source = options.source ?? 'code';
|
|
154
|
+
if (source === 'ide') {
|
|
155
|
+
return loadIdeUsageData(options);
|
|
156
|
+
}
|
|
157
|
+
return loadCodeUsageData(options);
|
|
158
|
+
}
|
|
159
|
+
async function loadCodeUsageData(options = {}) {
|
|
81
160
|
const defaultModelId = await loadModelFromSettings();
|
|
82
161
|
const jsonlFiles = await findJsonlFiles(BASE_DIR);
|
|
83
|
-
|
|
84
|
-
let minDate = null;
|
|
85
|
-
if (options.days) {
|
|
86
|
-
const d = new Date();
|
|
87
|
-
d.setDate(d.getDate() - options.days + 1);
|
|
88
|
-
d.setHours(0, 0, 0, 0);
|
|
89
|
-
minDate = d.toISOString().split('T')[0] ?? null;
|
|
90
|
-
}
|
|
91
|
-
// 按日期 -> 项目 -> 模型 组织的数据
|
|
162
|
+
const minDate = computeMinDate(options.days);
|
|
92
163
|
const dailyData = {};
|
|
93
|
-
// 按模型汇总
|
|
94
164
|
const modelTotals = {};
|
|
95
|
-
// 按项目汇总
|
|
96
165
|
const projectTotals = {};
|
|
97
|
-
// 总计
|
|
98
166
|
const grandTotal = {
|
|
99
167
|
cost: 0,
|
|
100
168
|
tokens: 0,
|
|
@@ -125,25 +193,13 @@ export async function loadUsageData(options = {}) {
|
|
|
125
193
|
const date = dateObj.toISOString().split('T')[0];
|
|
126
194
|
if (!date)
|
|
127
195
|
continue;
|
|
128
|
-
// 日期过滤
|
|
129
196
|
if (minDate && date < minDate)
|
|
130
197
|
continue;
|
|
131
198
|
const recordModelId = record?.providerData?.model;
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
dailyData
|
|
136
|
-
dailyData[date][projectName][usedModelId] ??= {
|
|
137
|
-
cost: 0,
|
|
138
|
-
promptTokens: 0,
|
|
139
|
-
completionTokens: 0,
|
|
140
|
-
totalTokens: 0,
|
|
141
|
-
cacheHitTokens: 0,
|
|
142
|
-
cacheMissTokens: 0,
|
|
143
|
-
cacheWriteTokens: 0,
|
|
144
|
-
requests: 0,
|
|
145
|
-
};
|
|
146
|
-
const dayStats = dailyData[date][projectName][usedModelId];
|
|
199
|
+
const modelFromRecord = typeof recordModelId === 'string' ? recordModelId : null;
|
|
200
|
+
const usedModelId = modelFromRecord || defaultModelId;
|
|
201
|
+
const { cost, stats: usageStats } = computeUsageCost(usage, usedModelId);
|
|
202
|
+
const dayStats = ensureDailyModelStats(dailyData, date, projectName, usedModelId);
|
|
147
203
|
dayStats.cost += cost;
|
|
148
204
|
dayStats.promptTokens += usageStats.promptTokens;
|
|
149
205
|
dayStats.completionTokens += usageStats.completionTokens;
|
|
@@ -171,35 +227,211 @@ export async function loadUsageData(options = {}) {
|
|
|
171
227
|
}
|
|
172
228
|
}
|
|
173
229
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
230
|
+
return finalizeAnalysis(defaultModelId, dailyData, modelTotals, projectTotals, grandTotal);
|
|
231
|
+
}
|
|
232
|
+
async function findIdeHistoryDirs() {
|
|
233
|
+
const root = getIdeDataDir();
|
|
234
|
+
const out = new Set();
|
|
235
|
+
let level1 = [];
|
|
236
|
+
try {
|
|
237
|
+
level1 = await fs.readdir(root, { withFileTypes: true });
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
return [];
|
|
241
|
+
}
|
|
242
|
+
for (const dirent of level1) {
|
|
243
|
+
if (!dirent.isDirectory())
|
|
244
|
+
continue;
|
|
245
|
+
const codeBuddyIdeDir = path.join(root, dirent.name, 'CodeBuddyIDE');
|
|
246
|
+
if (!(await pathExists(codeBuddyIdeDir)))
|
|
247
|
+
continue;
|
|
248
|
+
const directHistory = path.join(codeBuddyIdeDir, 'history');
|
|
249
|
+
if (await pathExists(directHistory)) {
|
|
250
|
+
out.add(directHistory);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
let nested = [];
|
|
254
|
+
try {
|
|
255
|
+
nested = await fs.readdir(codeBuddyIdeDir, { withFileTypes: true });
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
for (const child of nested) {
|
|
261
|
+
if (!child.isDirectory())
|
|
262
|
+
continue;
|
|
263
|
+
const nestedHistory = path.join(codeBuddyIdeDir, child.name, 'history');
|
|
264
|
+
if (await pathExists(nestedHistory)) {
|
|
265
|
+
out.add(nestedHistory);
|
|
183
266
|
}
|
|
184
267
|
}
|
|
185
268
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
269
|
+
return [...out];
|
|
270
|
+
}
|
|
271
|
+
async function readJsonFile(filePath) {
|
|
272
|
+
const raw = await fs.readFile(filePath, 'utf8');
|
|
273
|
+
return JSON.parse(raw);
|
|
274
|
+
}
|
|
275
|
+
async function readFileHeadUtf8(filePath, bytes = 64 * 1024) {
|
|
276
|
+
const fh = await fs.open(filePath, 'r');
|
|
277
|
+
try {
|
|
278
|
+
const buf = Buffer.alloc(bytes);
|
|
279
|
+
const { bytesRead } = await fh.read(buf, 0, buf.length, 0);
|
|
280
|
+
return buf.toString('utf8', 0, bytesRead);
|
|
281
|
+
}
|
|
282
|
+
finally {
|
|
283
|
+
await fh.close();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
function extractModelIdFromMessageHead(head) {
|
|
287
|
+
// extra 是一个转义的 JSON 字符串,形如: "extra": "{\"modelId\":\"gpt-5.1\",...}"
|
|
288
|
+
// 正则需要匹配包含转义字符的完整字符串
|
|
289
|
+
const extraMatch = head.match(/"extra"\s*:\s*"((?:[^"\\]|\\.)*)"/s);
|
|
290
|
+
if (!extraMatch?.[1])
|
|
291
|
+
return null;
|
|
292
|
+
try {
|
|
293
|
+
// extraMatch[1] 是转义后的内容,需要先解析为字符串
|
|
294
|
+
const extraStr = JSON.parse(`"${extraMatch[1]}"`);
|
|
295
|
+
const extra = JSON.parse(extraStr);
|
|
296
|
+
if (typeof extra.modelId === 'string' && extra.modelId)
|
|
297
|
+
return extra.modelId;
|
|
298
|
+
if (typeof extra.modelName === 'string' && extra.modelName)
|
|
299
|
+
return extra.modelName;
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
async function inferIdeModelIdForRequest(conversationDir, request, messageModelCache) {
|
|
307
|
+
const messages = Array.isArray(request.messages) ? request.messages : [];
|
|
308
|
+
for (let i = 0; i < Math.min(messages.length, 3); i++) {
|
|
309
|
+
const messageId = messages[i];
|
|
310
|
+
if (typeof messageId !== 'string' || !messageId)
|
|
311
|
+
continue;
|
|
312
|
+
const cached = messageModelCache.get(messageId);
|
|
313
|
+
if (cached)
|
|
314
|
+
return cached;
|
|
315
|
+
const msgPath = path.join(conversationDir, 'messages', `${messageId}.json`);
|
|
316
|
+
try {
|
|
317
|
+
const head = await readFileHeadUtf8(msgPath);
|
|
318
|
+
const modelId = extractModelIdFromMessageHead(head);
|
|
319
|
+
if (modelId) {
|
|
320
|
+
messageModelCache.set(messageId, modelId);
|
|
321
|
+
return modelId;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
// ignore
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
async function loadIdeUsageData(options = {}) {
|
|
331
|
+
const defaultModelId = await loadModelFromSettings();
|
|
332
|
+
const minDate = computeMinDate(options.days);
|
|
333
|
+
// 加载工作区映射
|
|
334
|
+
const workspaceMappings = await loadWorkspaceMappings();
|
|
335
|
+
const dailyData = {};
|
|
336
|
+
const modelTotals = {};
|
|
337
|
+
const projectTotals = {};
|
|
338
|
+
const grandTotal = {
|
|
339
|
+
cost: 0,
|
|
340
|
+
tokens: 0,
|
|
341
|
+
requests: 0,
|
|
342
|
+
cacheHitTokens: 0,
|
|
343
|
+
cacheMissTokens: 0,
|
|
203
344
|
};
|
|
345
|
+
const historyDirs = await findIdeHistoryDirs();
|
|
346
|
+
const messageModelCache = new Map();
|
|
347
|
+
for (const historyDir of historyDirs) {
|
|
348
|
+
let workspaces = [];
|
|
349
|
+
try {
|
|
350
|
+
workspaces = await fs.readdir(historyDir, { withFileTypes: true });
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
for (const ws of workspaces) {
|
|
356
|
+
if (!ws.isDirectory())
|
|
357
|
+
continue;
|
|
358
|
+
const workspaceHash = ws.name;
|
|
359
|
+
const workspaceDir = path.join(historyDir, workspaceHash);
|
|
360
|
+
const workspaceIndexPath = path.join(workspaceDir, 'index.json');
|
|
361
|
+
let convList = [];
|
|
362
|
+
try {
|
|
363
|
+
const parsed = (await readJsonFile(workspaceIndexPath));
|
|
364
|
+
if (Array.isArray(parsed)) {
|
|
365
|
+
convList = parsed;
|
|
366
|
+
}
|
|
367
|
+
else if (parsed && typeof parsed === 'object') {
|
|
368
|
+
const maybe = parsed.conversations ?? parsed.items ?? parsed.list;
|
|
369
|
+
if (Array.isArray(maybe))
|
|
370
|
+
convList = maybe;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
for (const conv of convList) {
|
|
377
|
+
const conversationId = typeof conv.id === 'string' ? conv.id : null;
|
|
378
|
+
if (!conversationId)
|
|
379
|
+
continue;
|
|
380
|
+
const date = toISODateString(conv.lastMessageAt) ?? toISODateString(conv.createdAt);
|
|
381
|
+
if (!date)
|
|
382
|
+
continue;
|
|
383
|
+
if (minDate && date < minDate)
|
|
384
|
+
continue;
|
|
385
|
+
const conversationDir = path.join(workspaceDir, conversationId);
|
|
386
|
+
const convIndexPath = path.join(conversationDir, 'index.json');
|
|
387
|
+
let convIndex = null;
|
|
388
|
+
try {
|
|
389
|
+
convIndex = (await readJsonFile(convIndexPath));
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
const requests = Array.isArray(convIndex?.requests) ? convIndex.requests : [];
|
|
395
|
+
for (const req of requests) {
|
|
396
|
+
const usage = req?.usage;
|
|
397
|
+
const inputTokens = typeof usage?.inputTokens === 'number' ? usage.inputTokens : Number(usage?.inputTokens ?? 0);
|
|
398
|
+
const outputTokens = typeof usage?.outputTokens === 'number' ? usage.outputTokens : Number(usage?.outputTokens ?? 0);
|
|
399
|
+
const totalTokens = typeof usage?.totalTokens === 'number'
|
|
400
|
+
? usage.totalTokens
|
|
401
|
+
: Number.isFinite(Number(usage?.totalTokens))
|
|
402
|
+
? Number(usage?.totalTokens)
|
|
403
|
+
: inputTokens + outputTokens;
|
|
404
|
+
if (!Number.isFinite(inputTokens) || !Number.isFinite(outputTokens) || !Number.isFinite(totalTokens))
|
|
405
|
+
continue;
|
|
406
|
+
const inferredModelId = await inferIdeModelIdForRequest(conversationDir, req, messageModelCache);
|
|
407
|
+
const usedModelId = inferredModelId || defaultModelId;
|
|
408
|
+
const rawUsage = {
|
|
409
|
+
prompt_tokens: Math.max(0, inputTokens),
|
|
410
|
+
completion_tokens: Math.max(0, outputTokens),
|
|
411
|
+
total_tokens: Math.max(0, totalTokens),
|
|
412
|
+
};
|
|
413
|
+
const { cost, stats } = computeUsageCost(rawUsage, usedModelId);
|
|
414
|
+
const projectName = workspaceHash;
|
|
415
|
+
const dayStats = ensureDailyModelStats(dailyData, date, projectName, usedModelId);
|
|
416
|
+
dayStats.cost += cost;
|
|
417
|
+
dayStats.promptTokens += stats.promptTokens;
|
|
418
|
+
dayStats.completionTokens += stats.completionTokens;
|
|
419
|
+
dayStats.totalTokens += stats.totalTokens;
|
|
420
|
+
dayStats.requests += 1;
|
|
421
|
+
modelTotals[usedModelId] ??= { cost: 0, tokens: 0, requests: 0 };
|
|
422
|
+
modelTotals[usedModelId].cost += cost;
|
|
423
|
+
modelTotals[usedModelId].tokens += stats.totalTokens;
|
|
424
|
+
modelTotals[usedModelId].requests += 1;
|
|
425
|
+
projectTotals[projectName] ??= { cost: 0, tokens: 0, requests: 0 };
|
|
426
|
+
projectTotals[projectName].cost += cost;
|
|
427
|
+
projectTotals[projectName].tokens += stats.totalTokens;
|
|
428
|
+
projectTotals[projectName].requests += 1;
|
|
429
|
+
grandTotal.cost += cost;
|
|
430
|
+
grandTotal.tokens += stats.totalTokens;
|
|
431
|
+
grandTotal.requests += 1;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return finalizeAnalysis(defaultModelId, dailyData, modelTotals, projectTotals, grandTotal, workspaceMappings);
|
|
204
437
|
}
|
|
205
|
-
//# sourceMappingURL=data-loader.js.map
|
package/dist/lib/paths.js
CHANGED
|
@@ -20,6 +20,37 @@ export function getConfigDir() {
|
|
|
20
20
|
// macOS 和 Linux 默认
|
|
21
21
|
return path.join(os.homedir(), '.codebuddy');
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* 获取 CodeBuddy IDE (CodeBuddyExtension) 数据目录
|
|
25
|
+
* - macOS: ~/Library/Application Support/CodeBuddyExtension/Data
|
|
26
|
+
* - Windows: %APPDATA%/CodeBuddyExtension/Data
|
|
27
|
+
* - Linux: $XDG_CONFIG_HOME/CodeBuddyExtension/Data 或 ~/.config/CodeBuddyExtension/Data
|
|
28
|
+
*/
|
|
29
|
+
export function getIdeDataDir() {
|
|
30
|
+
if (process.platform === 'darwin') {
|
|
31
|
+
return path.join(os.homedir(), 'Library', 'Application Support', 'CodeBuddyExtension', 'Data');
|
|
32
|
+
}
|
|
33
|
+
if (process.platform === 'win32') {
|
|
34
|
+
const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
|
|
35
|
+
return path.join(appData, 'CodeBuddyExtension', 'Data');
|
|
36
|
+
}
|
|
37
|
+
const xdgConfigHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
38
|
+
return path.join(xdgConfigHome, 'CodeBuddyExtension', 'Data');
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* 获取 CodeBuddy IDE 的 workspaceStorage 目录
|
|
42
|
+
*/
|
|
43
|
+
export function getWorkspaceStorageDir() {
|
|
44
|
+
if (process.platform === 'darwin') {
|
|
45
|
+
return path.join(os.homedir(), 'Library', 'Application Support', 'CodeBuddy CN', 'User', 'workspaceStorage');
|
|
46
|
+
}
|
|
47
|
+
if (process.platform === 'win32') {
|
|
48
|
+
const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
|
|
49
|
+
return path.join(appData, 'CodeBuddy CN', 'User', 'workspaceStorage');
|
|
50
|
+
}
|
|
51
|
+
const configHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
52
|
+
return path.join(configHome, 'CodeBuddy CN', 'User', 'workspaceStorage');
|
|
53
|
+
}
|
|
23
54
|
/**
|
|
24
55
|
* 获取项目数据目录
|
|
25
56
|
*/
|
|
@@ -32,11 +63,3 @@ export function getProjectsDir() {
|
|
|
32
63
|
export function getSettingsPath() {
|
|
33
64
|
return path.join(getConfigDir(), 'settings.json');
|
|
34
65
|
}
|
|
35
|
-
/**
|
|
36
|
-
* 简化项目路径显示
|
|
37
|
-
* 保持原始名称不变
|
|
38
|
-
*/
|
|
39
|
-
export function shortenProjectName(name) {
|
|
40
|
-
return name;
|
|
41
|
-
}
|
|
42
|
-
//# sourceMappingURL=paths.js.map
|
package/dist/lib/pricing.js
CHANGED
package/dist/lib/utils.js
CHANGED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import fsSync from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import crypto from 'node:crypto';
|
|
6
|
+
import { getWorkspaceStorageDir } from './paths.js';
|
|
7
|
+
// 缓存已解析的 CodeBuddy Code 路径名
|
|
8
|
+
const codePathCache = new Map();
|
|
9
|
+
/**
|
|
10
|
+
* 从 folder URI 提取纯路径(用于计算 MD5)
|
|
11
|
+
*/
|
|
12
|
+
function extractPathFromUri(folderUri) {
|
|
13
|
+
// 处理本地文件路径: file:///path/to/folder
|
|
14
|
+
if (folderUri.startsWith('file://')) {
|
|
15
|
+
try {
|
|
16
|
+
const url = new URL(folderUri);
|
|
17
|
+
return decodeURIComponent(url.pathname);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return decodeURIComponent(folderUri.replace('file://', ''));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// 处理远程路径: vscode-remote://codebuddy-remote-ssh%2B.../path
|
|
24
|
+
if (folderUri.startsWith('vscode-remote://')) {
|
|
25
|
+
try {
|
|
26
|
+
const url = new URL(folderUri);
|
|
27
|
+
return decodeURIComponent(url.pathname);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
const match = folderUri.match(/vscode-remote:\/\/[^/]+(.+)$/);
|
|
31
|
+
if (match?.[1]) {
|
|
32
|
+
return decodeURIComponent(match[1]);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 格式化路径用于显示(简化 home 目录)
|
|
40
|
+
*/
|
|
41
|
+
function formatDisplayPath(p) {
|
|
42
|
+
const home = os.homedir();
|
|
43
|
+
if (p.startsWith(home)) {
|
|
44
|
+
return '~' + p.slice(home.length);
|
|
45
|
+
}
|
|
46
|
+
return p;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 从 folder URI 生成用于显示的友好路径
|
|
50
|
+
*/
|
|
51
|
+
function getDisplayPath(folderUri) {
|
|
52
|
+
// 本地路径
|
|
53
|
+
if (folderUri.startsWith('file://')) {
|
|
54
|
+
const p = extractPathFromUri(folderUri);
|
|
55
|
+
if (p) {
|
|
56
|
+
return formatDisplayPath(p);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// 远程路径
|
|
60
|
+
if (folderUri.startsWith('vscode-remote://')) {
|
|
61
|
+
const p = extractPathFromUri(folderUri);
|
|
62
|
+
if (p) {
|
|
63
|
+
const hostMatch = folderUri.match(/vscode-remote:\/\/codebuddy-remote-ssh%2B([^/]+)/);
|
|
64
|
+
if (hostMatch?.[1]) {
|
|
65
|
+
let host = decodeURIComponent(hostMatch[1]);
|
|
66
|
+
host = host.replace(/_x([0-9a-fA-F]{2})_/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
67
|
+
host = host.replace(/\\x([0-9a-fA-F]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
68
|
+
if (host.includes('@')) {
|
|
69
|
+
const parts = host.split('@');
|
|
70
|
+
host = parts[parts.length - 1]?.split(':')[0] || host;
|
|
71
|
+
}
|
|
72
|
+
if (host.length > 20) {
|
|
73
|
+
host = host.slice(0, 17) + '...';
|
|
74
|
+
}
|
|
75
|
+
return `[${host}]${p}`;
|
|
76
|
+
}
|
|
77
|
+
return `[remote]${p}`;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return folderUri;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* 计算路径的 MD5 hash(CodeBuddyExtension 使用纯路径计算)
|
|
84
|
+
*/
|
|
85
|
+
function computePathHash(p) {
|
|
86
|
+
return crypto.createHash('md5').update(p).digest('hex');
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* 加载所有工作区映射
|
|
90
|
+
*/
|
|
91
|
+
export async function loadWorkspaceMappings() {
|
|
92
|
+
const mappings = new Map();
|
|
93
|
+
const storageDir = getWorkspaceStorageDir();
|
|
94
|
+
let entries = [];
|
|
95
|
+
try {
|
|
96
|
+
entries = await fs.readdir(storageDir, { withFileTypes: true });
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return mappings;
|
|
100
|
+
}
|
|
101
|
+
for (const entry of entries) {
|
|
102
|
+
if (!entry.isDirectory())
|
|
103
|
+
continue;
|
|
104
|
+
const workspaceJsonPath = path.join(storageDir, entry.name, 'workspace.json');
|
|
105
|
+
try {
|
|
106
|
+
const content = await fs.readFile(workspaceJsonPath, 'utf8');
|
|
107
|
+
const data = JSON.parse(content);
|
|
108
|
+
const folderUri = data.folder;
|
|
109
|
+
if (!folderUri)
|
|
110
|
+
continue;
|
|
111
|
+
const extractedPath = extractPathFromUri(folderUri);
|
|
112
|
+
if (!extractedPath)
|
|
113
|
+
continue;
|
|
114
|
+
const hash = computePathHash(extractedPath);
|
|
115
|
+
const displayPath = getDisplayPath(folderUri);
|
|
116
|
+
mappings.set(hash, { hash, folderUri, displayPath });
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// 跳过无法读取的文件
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return mappings;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* 检查路径是否存在(同步版本,用于路径探测)
|
|
126
|
+
*/
|
|
127
|
+
function pathExistsSync(p) {
|
|
128
|
+
try {
|
|
129
|
+
fsSync.accessSync(p);
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* 尝试将 CodeBuddy Code 的项目名(路径中 / 替换为 -)还原为真实路径
|
|
138
|
+
* 使用回溯搜索,因为目录名本身可能包含 -
|
|
139
|
+
*
|
|
140
|
+
* 例如: "Users-anoti-Documents-project-codebudy-cost-analyzer"
|
|
141
|
+
* -> "/Users/anoti/Documents/project/codebudy-cost-analyzer"
|
|
142
|
+
*/
|
|
143
|
+
function tryResolveCodePath(name) {
|
|
144
|
+
// 检查缓存
|
|
145
|
+
const cached = codePathCache.get(name);
|
|
146
|
+
if (cached !== undefined) {
|
|
147
|
+
return cached || null;
|
|
148
|
+
}
|
|
149
|
+
const parts = name.split('-');
|
|
150
|
+
if (parts.length < 2) {
|
|
151
|
+
codePathCache.set(name, '');
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
// 回溯搜索:尝试不同的分割方式
|
|
155
|
+
function backtrack(index, currentPath) {
|
|
156
|
+
if (index >= parts.length) {
|
|
157
|
+
// 检查完整路径是否存在
|
|
158
|
+
if (pathExistsSync(currentPath)) {
|
|
159
|
+
return currentPath;
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
// 尝试从当前位置开始,合并不同数量的 parts
|
|
164
|
+
for (let end = index; end < parts.length; end++) {
|
|
165
|
+
const segment = parts.slice(index, end + 1).join('-');
|
|
166
|
+
const newPath = currentPath ? `${currentPath}/${segment}` : `/${segment}`;
|
|
167
|
+
// 如果这不是最后一段,检查目录是否存在
|
|
168
|
+
if (end < parts.length - 1) {
|
|
169
|
+
if (pathExistsSync(newPath)) {
|
|
170
|
+
const result = backtrack(end + 1, newPath);
|
|
171
|
+
if (result)
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
// 最后一段,检查完整路径
|
|
177
|
+
if (pathExistsSync(newPath)) {
|
|
178
|
+
return newPath;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
const result = backtrack(0, '');
|
|
185
|
+
codePathCache.set(name, result || '');
|
|
186
|
+
return result;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* 解析项目名称
|
|
190
|
+
* - MD5 hash (32位十六进制): 从 IDE workspaceMappings 查找
|
|
191
|
+
* - 路径格式 (包含 -): 尝试还原 CodeBuddy Code 的路径格式
|
|
192
|
+
*/
|
|
193
|
+
export function resolveProjectName(name, mappings) {
|
|
194
|
+
// IDE source: MD5 hash
|
|
195
|
+
if (mappings && /^[a-f0-9]{32}$/.test(name)) {
|
|
196
|
+
const mapping = mappings.get(name);
|
|
197
|
+
if (mapping) {
|
|
198
|
+
return mapping.displayPath;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// Code source: 路径中 / 替换为 - 的格式
|
|
202
|
+
// 特征:以大写字母开头(如 Users-、home-),包含 -
|
|
203
|
+
if (/^[A-Za-z]/.test(name) && name.includes('-')) {
|
|
204
|
+
const resolved = tryResolveCodePath(name);
|
|
205
|
+
if (resolved) {
|
|
206
|
+
return formatDisplayPath(resolved);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return name;
|
|
210
|
+
}
|
package/package.json
CHANGED