codebuddy-stats 1.1.3 → 1.1.5
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 +3 -1
- package/dist/index.js +9 -6
- package/dist/lib/workspace-resolver.js +366 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
一个用于分析 CodeBuddy 系列产品使用成本的命令行工具,支持交互式 TUI 界面和纯文本输出。
|
|
4
4
|
|
|
5
|
+

|
|
6
|
+
|
|
5
7
|
## 功能特性
|
|
6
8
|
|
|
7
9
|
- **双数据源支持** - 同时支持 CodeBuddy Code 和 CodeBuddy IDE(VS Code 扩展)
|
|
@@ -68,7 +70,7 @@ cbs --help
|
|
|
68
70
|
按项目分类的费用统计,方便了解不同项目的 AI 使用成本。
|
|
69
71
|
|
|
70
72
|
工具会自动将项目标识解析为可读路径:
|
|
71
|
-
- **Code 模式**: 将 `Users-
|
|
73
|
+
- **Code 模式**: 将 `Users-foo-Documents-project-xxx` 还原为 `~/Documents/project/xxx`
|
|
72
74
|
- **IDE 模式**: 将 MD5 hash 映射为实际工作区路径
|
|
73
75
|
|
|
74
76
|
### Daily
|
package/dist/index.js
CHANGED
|
@@ -146,7 +146,7 @@ function renderOverview(box, data, width, note) {
|
|
|
146
146
|
content += '─'.repeat(summaryWidth) + '\n';
|
|
147
147
|
if (width >= 80) {
|
|
148
148
|
// 双列布局
|
|
149
|
-
content += `{green-fg}Total cost:{/green-fg}
|
|
149
|
+
content += `{green-fg}~Total cost:{/green-fg} ${formatCost(grandTotal.cost).padStart(12)} `;
|
|
150
150
|
content += `{green-fg}Active days:{/green-fg} ${String(activeDays).padStart(8)}\n`;
|
|
151
151
|
content += `{green-fg}Total tokens:{/green-fg} ${formatTokens(grandTotal.tokens).padStart(12)} `;
|
|
152
152
|
content += `{green-fg}Total requests:{/green-fg} ${formatNumber(grandTotal.requests).padStart(8)}\n`;
|
|
@@ -155,7 +155,7 @@ function renderOverview(box, data, width, note) {
|
|
|
155
155
|
}
|
|
156
156
|
else {
|
|
157
157
|
// 单列布局
|
|
158
|
-
content += `{green-fg}Total cost:{/green-fg}
|
|
158
|
+
content += `{green-fg}~Total cost:{/green-fg} ${formatCost(grandTotal.cost)}\n`;
|
|
159
159
|
content += `{green-fg}Total tokens:{/green-fg} ${formatTokens(grandTotal.tokens)}\n`;
|
|
160
160
|
content += `{green-fg}Total requests:{/green-fg} ${formatNumber(grandTotal.requests)}\n`;
|
|
161
161
|
content += `{green-fg}Active days:{/green-fg} ${activeDays}\n`;
|
|
@@ -188,7 +188,7 @@ function renderByModel(box, data, width, note) {
|
|
|
188
188
|
content +=
|
|
189
189
|
'{underline}' +
|
|
190
190
|
'Model'.padEnd(modelCol) +
|
|
191
|
-
'Cost'.padStart(12) +
|
|
191
|
+
'~Cost'.padStart(12) +
|
|
192
192
|
'Requests'.padStart(12) +
|
|
193
193
|
'Tokens'.padStart(12) +
|
|
194
194
|
'Avg/Req'.padStart(10) +
|
|
@@ -229,7 +229,7 @@ function renderByProject(box, data, width, note) {
|
|
|
229
229
|
content +=
|
|
230
230
|
'{underline}' +
|
|
231
231
|
'Project'.padEnd(projectCol) +
|
|
232
|
-
'Cost'.padStart(12) +
|
|
232
|
+
'~Cost'.padStart(12) +
|
|
233
233
|
'Requests'.padStart(12) +
|
|
234
234
|
'Tokens'.padStart(12) +
|
|
235
235
|
'{/underline}\n';
|
|
@@ -264,8 +264,9 @@ function renderDaily(box, data, scrollOffset = 0, width, note) {
|
|
|
264
264
|
const availableWidth = width - 6; // padding
|
|
265
265
|
const dateCol = 12;
|
|
266
266
|
const costCol = 12;
|
|
267
|
+
const tokensCol = 10;
|
|
267
268
|
const reqCol = 10;
|
|
268
|
-
const fixedCols = dateCol + costCol + reqCol;
|
|
269
|
+
const fixedCols = dateCol + costCol + tokensCol + reqCol;
|
|
269
270
|
const remainingWidth = availableWidth - fixedCols;
|
|
270
271
|
const modelCol = Math.max(15, Math.min(25, Math.floor(remainingWidth * 0.4)));
|
|
271
272
|
const projectCol = Math.max(20, remainingWidth - modelCol);
|
|
@@ -273,7 +274,8 @@ function renderDaily(box, data, scrollOffset = 0, width, note) {
|
|
|
273
274
|
content +=
|
|
274
275
|
'{underline}' +
|
|
275
276
|
'Date'.padEnd(dateCol) +
|
|
276
|
-
'Cost'.padStart(costCol) +
|
|
277
|
+
'~Cost'.padStart(costCol) +
|
|
278
|
+
'Tokens'.padStart(tokensCol) +
|
|
277
279
|
'Requests'.padStart(reqCol) +
|
|
278
280
|
'Top Model'.padStart(modelCol) +
|
|
279
281
|
'Top Project'.padStart(projectCol) +
|
|
@@ -304,6 +306,7 @@ function renderDaily(box, data, scrollOffset = 0, width, note) {
|
|
|
304
306
|
content +=
|
|
305
307
|
date.padEnd(dateCol) +
|
|
306
308
|
formatCost(daySummary.cost).padStart(costCol) +
|
|
309
|
+
formatTokens(daySummary.tokens).padStart(tokensCol) +
|
|
307
310
|
formatNumber(daySummary.requests).padStart(reqCol) +
|
|
308
311
|
truncate(topModel.id, modelCol - 1).padStart(modelCol) +
|
|
309
312
|
truncate(shortProject, projectCol - 1).padStart(projectCol) +
|
|
@@ -3,7 +3,7 @@ import fsSync from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import crypto from 'node:crypto';
|
|
6
|
-
import { getWorkspaceStorageDir } from './paths.js';
|
|
6
|
+
import { getIdeDataDir, getWorkspaceStorageDir } from './paths.js';
|
|
7
7
|
// 缓存已解析的 CodeBuddy Code 路径名
|
|
8
8
|
const codePathCache = new Map();
|
|
9
9
|
/**
|
|
@@ -86,38 +86,381 @@ function computePathHash(p) {
|
|
|
86
86
|
return crypto.createHash('md5').update(p).digest('hex');
|
|
87
87
|
}
|
|
88
88
|
/**
|
|
89
|
-
*
|
|
89
|
+
* 从 CodeBuddyIDE 的 file-tree 数据中反推出 workspace hash -> 真实路径
|
|
90
|
+
*
|
|
91
|
+
* 背景:Remote SSH 场景下,server 侧 `workspaceStorage/<hash>/workspace.json` 可能不存在,
|
|
92
|
+
* 但 CodeBuddyExtension 会把对话关联的文件路径写入 `file-tree.json`,可据此还原工作区根目录。
|
|
90
93
|
*/
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
|
|
94
|
+
async function loadWorkspaceMappingsFromIdeFileTree() {
|
|
95
|
+
const out = new Map();
|
|
96
|
+
const root = getIdeDataDir();
|
|
97
|
+
async function pathExists(p) {
|
|
98
|
+
try {
|
|
99
|
+
await fs.access(p);
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function collectFilePaths(node, acc, depth = 0) {
|
|
107
|
+
if (depth > 10)
|
|
108
|
+
return;
|
|
109
|
+
if (typeof node === 'string')
|
|
110
|
+
return;
|
|
111
|
+
if (Array.isArray(node)) {
|
|
112
|
+
for (const item of node)
|
|
113
|
+
collectFilePaths(item, acc, depth + 1);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (!node || typeof node !== 'object')
|
|
117
|
+
return;
|
|
118
|
+
for (const [k, v] of Object.entries(node)) {
|
|
119
|
+
if (k === 'filePath' && typeof v === 'string' && v) {
|
|
120
|
+
acc.push(v);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (k === 'createdEntries' && Array.isArray(v)) {
|
|
124
|
+
for (const e of v) {
|
|
125
|
+
if (typeof e === 'string' && e)
|
|
126
|
+
acc.push(e);
|
|
127
|
+
}
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
collectFilePaths(v, acc, depth + 1);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function resolveWorkspacePathFromFilePaths(hash, filePaths) {
|
|
134
|
+
for (const raw of filePaths) {
|
|
135
|
+
const p = extractPathFromUri(raw) || (path.isAbsolute(raw) ? raw : null);
|
|
136
|
+
if (!p)
|
|
137
|
+
continue;
|
|
138
|
+
let cur = p;
|
|
139
|
+
for (let i = 0; i < 50; i++) {
|
|
140
|
+
if (computePathHash(cur) === hash)
|
|
141
|
+
return cur;
|
|
142
|
+
const parent = path.dirname(cur);
|
|
143
|
+
if (parent === cur)
|
|
144
|
+
break;
|
|
145
|
+
cur = parent;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
// 扫描 `Data/*/CodeBuddyIDE/*/file-tree/*/<convId>/file-tree.json`
|
|
151
|
+
let level1 = [];
|
|
95
152
|
try {
|
|
96
|
-
|
|
153
|
+
level1 = await fs.readdir(root, { withFileTypes: true });
|
|
97
154
|
}
|
|
98
155
|
catch {
|
|
99
|
-
return
|
|
156
|
+
return out;
|
|
100
157
|
}
|
|
101
|
-
for (const
|
|
102
|
-
if (!
|
|
158
|
+
for (const dirent of level1) {
|
|
159
|
+
if (!dirent.isDirectory())
|
|
103
160
|
continue;
|
|
104
|
-
const
|
|
161
|
+
const codeBuddyIdeRoot = path.join(root, dirent.name, 'CodeBuddyIDE');
|
|
162
|
+
if (!(await pathExists(codeBuddyIdeRoot)))
|
|
163
|
+
continue;
|
|
164
|
+
// 兼容两种结构:
|
|
165
|
+
// 1) CodeBuddyIDE/file-tree
|
|
166
|
+
// 2) CodeBuddyIDE/<profile>/file-tree(当前主流)
|
|
167
|
+
const profileDirs = [];
|
|
168
|
+
if (await pathExists(path.join(codeBuddyIdeRoot, 'file-tree'))) {
|
|
169
|
+
profileDirs.push(codeBuddyIdeRoot);
|
|
170
|
+
}
|
|
171
|
+
let nested = [];
|
|
105
172
|
try {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
173
|
+
nested = await fs.readdir(codeBuddyIdeRoot, { withFileTypes: true });
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
nested = [];
|
|
177
|
+
}
|
|
178
|
+
for (const child of nested) {
|
|
179
|
+
if (!child.isDirectory())
|
|
110
180
|
continue;
|
|
111
|
-
const
|
|
112
|
-
if (
|
|
181
|
+
const profileDir = path.join(codeBuddyIdeRoot, child.name);
|
|
182
|
+
if (await pathExists(path.join(profileDir, 'file-tree'))) {
|
|
183
|
+
profileDirs.push(profileDir);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
for (const profileDir of profileDirs) {
|
|
187
|
+
const fileTreeDir = path.join(profileDir, 'file-tree');
|
|
188
|
+
let workspaces = [];
|
|
189
|
+
try {
|
|
190
|
+
workspaces = await fs.readdir(fileTreeDir, { withFileTypes: true });
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
113
193
|
continue;
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
|
|
194
|
+
}
|
|
195
|
+
for (const ws of workspaces) {
|
|
196
|
+
if (!ws.isDirectory())
|
|
197
|
+
continue;
|
|
198
|
+
const workspaceHash = ws.name;
|
|
199
|
+
if (!/^[a-f0-9]{32}$/.test(workspaceHash))
|
|
200
|
+
continue;
|
|
201
|
+
if (out.has(workspaceHash))
|
|
202
|
+
continue;
|
|
203
|
+
const workspaceDir = path.join(fileTreeDir, workspaceHash);
|
|
204
|
+
let convDirs = [];
|
|
205
|
+
try {
|
|
206
|
+
convDirs = await fs.readdir(workspaceDir, { withFileTypes: true });
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
let resolved = null;
|
|
212
|
+
for (const conv of convDirs) {
|
|
213
|
+
if (!conv.isDirectory())
|
|
214
|
+
continue;
|
|
215
|
+
const fileTreeJson = path.join(workspaceDir, conv.name, 'file-tree.json');
|
|
216
|
+
try {
|
|
217
|
+
const raw = await fs.readFile(fileTreeJson, 'utf8');
|
|
218
|
+
const parsed = JSON.parse(raw);
|
|
219
|
+
const paths = [];
|
|
220
|
+
collectFilePaths(parsed, paths);
|
|
221
|
+
resolved = resolveWorkspacePathFromFilePaths(workspaceHash, paths);
|
|
222
|
+
if (resolved)
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
// ignore
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (!resolved)
|
|
230
|
+
continue;
|
|
231
|
+
const folderUri = 'file://' + resolved;
|
|
232
|
+
out.set(workspaceHash, {
|
|
233
|
+
hash: workspaceHash,
|
|
234
|
+
folderUri,
|
|
235
|
+
displayPath: getDisplayPath(folderUri),
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return out;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* 从 CodeBuddyIDE 的 history/messages 中反推出 workspace hash -> 真实路径
|
|
244
|
+
*
|
|
245
|
+
* 场景:有些对话没有触发 file-tree 落盘,但 messages 里常包含 tool-result 的绝对路径。
|
|
246
|
+
*/
|
|
247
|
+
async function loadWorkspaceMappingsFromIdeHistory() {
|
|
248
|
+
const out = new Map();
|
|
249
|
+
const root = getIdeDataDir();
|
|
250
|
+
async function pathExists(p) {
|
|
251
|
+
try {
|
|
252
|
+
await fs.access(p);
|
|
253
|
+
return true;
|
|
117
254
|
}
|
|
118
255
|
catch {
|
|
119
|
-
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async function readFileHeadUtf8(filePath, bytes = 64 * 1024) {
|
|
260
|
+
const fh = await fs.open(filePath, 'r');
|
|
261
|
+
try {
|
|
262
|
+
const buf = Buffer.alloc(bytes);
|
|
263
|
+
const { bytesRead } = await fh.read(buf, 0, buf.length, 0);
|
|
264
|
+
return buf.toString('utf8', 0, bytesRead);
|
|
120
265
|
}
|
|
266
|
+
finally {
|
|
267
|
+
await fh.close();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function extractCandidatePathsFromText(text) {
|
|
271
|
+
const set = new Set();
|
|
272
|
+
const uriRe = /(vscode-remote:\/\/[^\s"']+|file:\/\/[^\s"']+)/g;
|
|
273
|
+
for (const m of text.matchAll(uriRe)) {
|
|
274
|
+
const v = m[1];
|
|
275
|
+
if (v)
|
|
276
|
+
set.add(v);
|
|
277
|
+
}
|
|
278
|
+
const absPathRe = /\/[A-Za-z0-9._~@%+=:,\-]+(?:\/[A-Za-z0-9._~@%+=:,\-]+)+/g;
|
|
279
|
+
for (const m of text.matchAll(absPathRe)) {
|
|
280
|
+
const v = m[0];
|
|
281
|
+
if (v)
|
|
282
|
+
set.add(v);
|
|
283
|
+
}
|
|
284
|
+
return [...set];
|
|
285
|
+
}
|
|
286
|
+
function resolveWorkspacePathFromCandidates(hash, candidates) {
|
|
287
|
+
for (const raw of candidates) {
|
|
288
|
+
const p = extractPathFromUri(raw) || (path.isAbsolute(raw) ? raw : null);
|
|
289
|
+
if (!p)
|
|
290
|
+
continue;
|
|
291
|
+
let cur = p;
|
|
292
|
+
for (let i = 0; i < 50; i++) {
|
|
293
|
+
if (computePathHash(cur) === hash)
|
|
294
|
+
return cur;
|
|
295
|
+
const parent = path.dirname(cur);
|
|
296
|
+
if (parent === cur)
|
|
297
|
+
break;
|
|
298
|
+
cur = parent;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
let level1 = [];
|
|
304
|
+
try {
|
|
305
|
+
level1 = await fs.readdir(root, { withFileTypes: true });
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
return out;
|
|
309
|
+
}
|
|
310
|
+
for (const dirent of level1) {
|
|
311
|
+
if (!dirent.isDirectory())
|
|
312
|
+
continue;
|
|
313
|
+
const codeBuddyIdeRoot = path.join(root, dirent.name, 'CodeBuddyIDE');
|
|
314
|
+
if (!(await pathExists(codeBuddyIdeRoot)))
|
|
315
|
+
continue;
|
|
316
|
+
const profileDirs = [];
|
|
317
|
+
if (await pathExists(path.join(codeBuddyIdeRoot, 'history'))) {
|
|
318
|
+
profileDirs.push(codeBuddyIdeRoot);
|
|
319
|
+
}
|
|
320
|
+
let nested = [];
|
|
321
|
+
try {
|
|
322
|
+
nested = await fs.readdir(codeBuddyIdeRoot, { withFileTypes: true });
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
nested = [];
|
|
326
|
+
}
|
|
327
|
+
for (const child of nested) {
|
|
328
|
+
if (!child.isDirectory())
|
|
329
|
+
continue;
|
|
330
|
+
const profileDir = path.join(codeBuddyIdeRoot, child.name);
|
|
331
|
+
if (await pathExists(path.join(profileDir, 'history'))) {
|
|
332
|
+
profileDirs.push(profileDir);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
for (const profileDir of profileDirs) {
|
|
336
|
+
const historyDir = path.join(profileDir, 'history');
|
|
337
|
+
let workspaces = [];
|
|
338
|
+
try {
|
|
339
|
+
workspaces = await fs.readdir(historyDir, { withFileTypes: true });
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
for (const ws of workspaces) {
|
|
345
|
+
if (!ws.isDirectory())
|
|
346
|
+
continue;
|
|
347
|
+
const workspaceHash = ws.name;
|
|
348
|
+
if (!/^[a-f0-9]{32}$/.test(workspaceHash))
|
|
349
|
+
continue;
|
|
350
|
+
if (out.has(workspaceHash))
|
|
351
|
+
continue;
|
|
352
|
+
const workspaceDir = path.join(historyDir, workspaceHash);
|
|
353
|
+
let convDirs = [];
|
|
354
|
+
try {
|
|
355
|
+
convDirs = await fs.readdir(workspaceDir, { withFileTypes: true });
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
let resolved = null;
|
|
361
|
+
for (const conv of convDirs) {
|
|
362
|
+
if (!conv.isDirectory())
|
|
363
|
+
continue;
|
|
364
|
+
const messagesDir = path.join(workspaceDir, conv.name, 'messages');
|
|
365
|
+
if (!(await pathExists(messagesDir)))
|
|
366
|
+
continue;
|
|
367
|
+
let msgFiles = [];
|
|
368
|
+
try {
|
|
369
|
+
msgFiles = await fs.readdir(messagesDir, { withFileTypes: true });
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
let tried = 0;
|
|
375
|
+
for (const msg of msgFiles) {
|
|
376
|
+
if (!msg.isFile() || !msg.name.endsWith('.json'))
|
|
377
|
+
continue;
|
|
378
|
+
const msgPath = path.join(messagesDir, msg.name);
|
|
379
|
+
tried += 1;
|
|
380
|
+
if (tried > 30)
|
|
381
|
+
break;
|
|
382
|
+
try {
|
|
383
|
+
const head = await readFileHeadUtf8(msgPath);
|
|
384
|
+
const candidates = extractCandidatePathsFromText(head);
|
|
385
|
+
resolved = resolveWorkspacePathFromCandidates(workspaceHash, candidates);
|
|
386
|
+
if (resolved)
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
// ignore
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (resolved)
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
if (!resolved)
|
|
397
|
+
continue;
|
|
398
|
+
const folderUri = 'file://' + resolved;
|
|
399
|
+
out.set(workspaceHash, {
|
|
400
|
+
hash: workspaceHash,
|
|
401
|
+
folderUri,
|
|
402
|
+
displayPath: getDisplayPath(folderUri),
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return out;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* 加载所有工作区映射
|
|
411
|
+
*/
|
|
412
|
+
export async function loadWorkspaceMappings() {
|
|
413
|
+
const mappings = new Map();
|
|
414
|
+
// 1) 尝试从客户端 workspaceStorage/workspace.json 解析
|
|
415
|
+
const storageDir = getWorkspaceStorageDir();
|
|
416
|
+
try {
|
|
417
|
+
const entries = await fs.readdir(storageDir, { withFileTypes: true });
|
|
418
|
+
for (const entry of entries) {
|
|
419
|
+
if (!entry.isDirectory())
|
|
420
|
+
continue;
|
|
421
|
+
const workspaceJsonPath = path.join(storageDir, entry.name, 'workspace.json');
|
|
422
|
+
try {
|
|
423
|
+
const content = await fs.readFile(workspaceJsonPath, 'utf8');
|
|
424
|
+
const data = JSON.parse(content);
|
|
425
|
+
const folderUri = data.folder;
|
|
426
|
+
if (!folderUri)
|
|
427
|
+
continue;
|
|
428
|
+
const extractedPath = extractPathFromUri(folderUri);
|
|
429
|
+
if (!extractedPath)
|
|
430
|
+
continue;
|
|
431
|
+
const hash = computePathHash(extractedPath);
|
|
432
|
+
const displayPath = getDisplayPath(folderUri);
|
|
433
|
+
mappings.set(hash, { hash, folderUri, displayPath });
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
// ignore
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
// ignore
|
|
442
|
+
}
|
|
443
|
+
// 2) Remote SSH 等场景的兜底:从 CodeBuddyIDE file-tree 反推
|
|
444
|
+
try {
|
|
445
|
+
const ideMappings = await loadWorkspaceMappingsFromIdeFileTree();
|
|
446
|
+
for (const [hash, mapping] of ideMappings) {
|
|
447
|
+
if (!mappings.has(hash))
|
|
448
|
+
mappings.set(hash, mapping);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
// ignore
|
|
453
|
+
}
|
|
454
|
+
// 3) 进一步兜底:从 CodeBuddyIDE history/messages 反推(tool-result 常带绝对路径)
|
|
455
|
+
try {
|
|
456
|
+
const ideMappings = await loadWorkspaceMappingsFromIdeHistory();
|
|
457
|
+
for (const [hash, mapping] of ideMappings) {
|
|
458
|
+
if (!mappings.has(hash))
|
|
459
|
+
mappings.set(hash, mapping);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
// ignore
|
|
121
464
|
}
|
|
122
465
|
return mappings;
|
|
123
466
|
}
|
|
@@ -137,8 +480,8 @@ function pathExistsSync(p) {
|
|
|
137
480
|
* 尝试将 CodeBuddy Code 的项目名(路径中 / 替换为 -)还原为真实路径
|
|
138
481
|
* 使用回溯搜索,因为目录名本身可能包含 -
|
|
139
482
|
*
|
|
140
|
-
* 例如: "Users-
|
|
141
|
-
* -> "/Users/
|
|
483
|
+
* 例如: "Users-foo-Documents-project-codebudy-cost-analyzer"
|
|
484
|
+
* -> "/Users/foo/Documents/project/codebudy-cost-analyzer"
|
|
142
485
|
*/
|
|
143
486
|
function tryResolveCodePath(name) {
|
|
144
487
|
// 检查缓存
|