lumencode 1.3.0 → 1.3.2
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 +8 -8
- package/index.js +348 -343
- package/lib/aggregate.js +58 -3
- package/lib/config.js +21 -8
- package/lib/path-utils.js +18 -0
- package/lib/report.js +969 -53
- package/lib/scenario.js +29 -4
- package/lib/server.js +304 -278
- package/package.json +1 -1
- package/public/app.js +232 -17
- package/public/config.js +1 -0
- package/public/export.js +11 -7
- package/public/index.html +77 -16
- package/public/style.css +248 -1
- package/public/utils.js +218 -0
package/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { loadConfig, initConfig, getConfigPath } from './lib/config.js';
|
|
3
|
-
import { collectAllRecords, computeUsageStats, filterRecordsByPeriod, normalizeProjectPath, computeTrendData, computePrevPeriodRange, groupBySessions } from './lib/aggregate.js';
|
|
4
|
-
import { getGitStatsForMultipleReposAsync, invalidateGitCache, finalizeGitStats, computeCommitTypes, computeFileHotspots } from './lib/git.js';
|
|
5
|
-
import { invalidateFileCache } from './lib/cache.js';
|
|
6
|
-
import { generateReport, generateWorkReport } from './lib/report.js';
|
|
7
|
-
import { startServer } from './lib/server.js';
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { loadConfig, initConfig, getConfigPath } from './lib/config.js';
|
|
3
|
+
import { collectAllRecords, computeUsageStats, filterRecordsByPeriod, normalizeProjectPath, computeTrendData, computePrevPeriodRange, groupBySessions } from './lib/aggregate.js';
|
|
4
|
+
import { getGitStatsForMultipleReposAsync, invalidateGitCache, finalizeGitStats, computeCommitTypes, computeFileHotspots } from './lib/git.js';
|
|
5
|
+
import { invalidateFileCache } from './lib/cache.js';
|
|
6
|
+
import { generateReport, generateWorkReport, generateBossReport } from './lib/report.js';
|
|
7
|
+
import { startServer } from './lib/server.js';
|
|
8
8
|
import { detectClaudeDir, deriveProjectPaths } from './lib/parser.js';
|
|
9
9
|
import { identifyBillingBlocks } from './lib/blocks.js';
|
|
10
10
|
import { registerParser, parseAllEnabledTools, detectAvailableTools } from './lib/parsers/index.js';
|
|
@@ -15,12 +15,12 @@ import { initPricing, preloadUnknownPricing } from './lib/pricing-loader.js';
|
|
|
15
15
|
import { createInterface } from 'readline';
|
|
16
16
|
import { stdin as input, stdout as output } from 'process';
|
|
17
17
|
import { enableHooks, disableHooks, getHooksStatus, HOOK_TOOLS, initStepTracking } from './lib/hooks-manager.js';
|
|
18
|
-
|
|
19
|
-
// 注册所有解析器
|
|
20
|
-
registerParser(ClaudeParser);
|
|
21
|
-
registerParser(CodexParser);
|
|
22
|
-
registerParser(OpencodeParser);
|
|
23
|
-
|
|
18
|
+
|
|
19
|
+
// 注册所有解析器
|
|
20
|
+
registerParser(ClaudeParser);
|
|
21
|
+
registerParser(CodexParser);
|
|
22
|
+
registerParser(OpencodeParser);
|
|
23
|
+
|
|
24
24
|
const args = process.argv.slice(2);
|
|
25
25
|
const command = args[0];
|
|
26
26
|
|
|
@@ -184,270 +184,272 @@ async function handleHooksCommand() {
|
|
|
184
184
|
|
|
185
185
|
console.log('未知 hooks 命令。用法: node index.js hooks status|enable|disable|init [claude,codex] [--yes]');
|
|
186
186
|
}
|
|
187
|
-
|
|
188
|
-
function loadCliConfig() {
|
|
189
|
-
let config = loadConfig();
|
|
190
|
-
|
|
191
|
-
// 零配置:自动检测 claudeDir
|
|
192
|
-
if (!config.claudeDir || config.claudeDir === '') {
|
|
193
|
-
config.claudeDir = detectClaudeDir() || config.claudeDir;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// 零配置:自动推导项目路径(从 cwd 字段)
|
|
197
|
-
if ((!config.repos || config.repos.length === 0) && config.claudeDir) {
|
|
198
|
-
try {
|
|
199
|
-
const derived = deriveProjectPaths(config.claudeDir, config.excludeProjects || []);
|
|
200
|
-
if (derived.length > 0) {
|
|
201
|
-
config._autoRepos = derived;
|
|
202
|
-
}
|
|
203
|
-
} catch {}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// 日期参数
|
|
207
|
-
let dateArg = (() => { const n = new Date(); return `${n.getFullYear()}-${String(n.getMonth()+1).padStart(2,'0')}-${String(n.getDate()).padStart(2,'0')}`; })();
|
|
208
|
-
const skipArgs = new Set();
|
|
209
|
-
for (let i = 0; i < args.length; i++) {
|
|
210
|
-
if (args[i] === '--projects' || args[i] === '--start' || args[i] === '--end') {
|
|
211
|
-
skipArgs.add(i);
|
|
212
|
-
skipArgs.add(i + 1);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
for (let i = 2; i < args.length; i++) {
|
|
216
|
-
if (skipArgs.has(i)) continue;
|
|
217
|
-
if (!args[i].startsWith('--')) {
|
|
218
|
-
dateArg = args[i];
|
|
219
|
-
break;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// --projects 参数
|
|
224
|
-
let includeProjects = null;
|
|
225
|
-
const projectsIdx = args.indexOf('--projects');
|
|
226
|
-
if (projectsIdx !== -1 && args[projectsIdx + 1]) {
|
|
227
|
-
includeProjects = args[projectsIdx + 1].split(',').map(p => p.trim());
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// 推导 includeProjects
|
|
231
|
-
let effectiveIncludeProjects = includeProjects;
|
|
232
|
-
if (!effectiveIncludeProjects && config.repos && config.repos.length > 0) {
|
|
233
|
-
effectiveIncludeProjects = config.repos.map(r => normalizeProjectPath(r));
|
|
234
|
-
} else if (!effectiveIncludeProjects && config._autoRepos && config._autoRepos.length > 0) {
|
|
235
|
-
effectiveIncludeProjects = config._autoRepos.map(r => normalizeProjectPath(r));
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// 自动推导的 repos 也用于 Git 统计
|
|
239
|
-
if ((!config.repos || config.repos.length === 0) && config._autoRepos) {
|
|
240
|
-
config.repos = config._autoRepos;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const configPath = getConfigPath();
|
|
244
|
-
return { config, dateArg, effectiveIncludeProjects, configPath };
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
async function buildReportData(period, dateArg, config, effectiveIncludeProjects, tool = 'all', preParsed = null, options = {}) {
|
|
248
|
-
// 使用预解析结果或全量解析
|
|
249
|
-
let records, toolBreakdown;
|
|
250
|
-
if (preParsed) {
|
|
251
|
-
({ records, toolBreakdown } = preParsed);
|
|
252
|
-
} else {
|
|
253
|
-
({ records, toolBreakdown } = await parseAllEnabledTools(config, {
|
|
254
|
-
excludeProjects: config.excludeProjects,
|
|
255
|
-
includeProjects: effectiveIncludeProjects,
|
|
256
|
-
}));
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
if (records.length === 0) {
|
|
260
|
-
return null;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// 预加载未知模型定价
|
|
264
|
-
await preloadUnknownPricing(records);
|
|
265
|
-
|
|
266
|
-
// 按工具过滤
|
|
267
|
-
const toolRecords = tool !== 'all' ? records.filter(r => r.tool === tool) : records;
|
|
268
|
-
if (toolRecords.length === 0) {
|
|
269
|
-
return null;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const { filtered, start, end } = filterRecordsByPeriod(toolRecords, period, dateArg, { customStart: options.customStart, customEnd: options.customEnd });
|
|
273
|
-
const reposConfigured = !!(config.repos && config.repos.length > 0);
|
|
274
|
-
|
|
275
|
-
// ── 第一层并发:三个独立的同步计算 ──
|
|
276
|
-
const [usageStats, sessions, billingBlocks] = [
|
|
277
|
-
computeUsageStats(filtered, config.scenarioKeywords, config.costMode),
|
|
278
|
-
groupBySessions(filtered),
|
|
279
|
-
identifyBillingBlocks(filtered, config.blockQuota ? 5 : 5, config.costMode),
|
|
280
|
-
];
|
|
281
|
-
|
|
282
|
-
// ── 第二层并发:gitStats(async) + trendData + prevStats ──
|
|
283
|
-
const gitStatsPromise = (async () => {
|
|
284
|
-
if (!reposConfigured) return null;
|
|
285
|
-
const coveredBases = new Set(filtered.map(r => {
|
|
286
|
-
const p = r.project || '';
|
|
287
|
-
return p.replace(/\\/g, '/').replace(/\/$/, '').split('/').pop();
|
|
288
|
-
}).filter(Boolean));
|
|
289
|
-
let toolRepos = config.repos.filter(r => coveredBases.has(r.replace(/\\/g, '/').replace(/\/$/, '').split('/').pop()));
|
|
290
|
-
if (toolRepos.length === 0) toolRepos = config.repos;
|
|
291
|
-
if (toolRepos.length === 0) return null;
|
|
292
|
-
const extendedEnd = new Date(end);
|
|
293
|
-
extendedEnd.setDate(extendedEnd.getDate() + 2);
|
|
294
|
-
const extendedEndStr = extendedEnd.toISOString().slice(0, 10) + 'T23:59:59';
|
|
295
|
-
let gs = await getGitStatsForMultipleReposAsync(toolRepos, start, extendedEndStr);
|
|
187
|
+
|
|
188
|
+
function loadCliConfig() {
|
|
189
|
+
let config = loadConfig();
|
|
190
|
+
|
|
191
|
+
// 零配置:自动检测 claudeDir
|
|
192
|
+
if (!config.claudeDir || config.claudeDir === '') {
|
|
193
|
+
config.claudeDir = detectClaudeDir() || config.claudeDir;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 零配置:自动推导项目路径(从 cwd 字段)
|
|
197
|
+
if ((!config.repos || config.repos.length === 0) && config.claudeDir) {
|
|
198
|
+
try {
|
|
199
|
+
const derived = deriveProjectPaths(config.claudeDir, config.excludeProjects || []);
|
|
200
|
+
if (derived.length > 0) {
|
|
201
|
+
config._autoRepos = derived;
|
|
202
|
+
}
|
|
203
|
+
} catch {}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 日期参数
|
|
207
|
+
let dateArg = (() => { const n = new Date(); return `${n.getFullYear()}-${String(n.getMonth()+1).padStart(2,'0')}-${String(n.getDate()).padStart(2,'0')}`; })();
|
|
208
|
+
const skipArgs = new Set();
|
|
209
|
+
for (let i = 0; i < args.length; i++) {
|
|
210
|
+
if (args[i] === '--projects' || args[i] === '--start' || args[i] === '--end') {
|
|
211
|
+
skipArgs.add(i);
|
|
212
|
+
skipArgs.add(i + 1);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
for (let i = 2; i < args.length; i++) {
|
|
216
|
+
if (skipArgs.has(i)) continue;
|
|
217
|
+
if (!args[i].startsWith('--')) {
|
|
218
|
+
dateArg = args[i];
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// --projects 参数
|
|
224
|
+
let includeProjects = null;
|
|
225
|
+
const projectsIdx = args.indexOf('--projects');
|
|
226
|
+
if (projectsIdx !== -1 && args[projectsIdx + 1]) {
|
|
227
|
+
includeProjects = args[projectsIdx + 1].split(',').map(p => p.trim());
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// 推导 includeProjects
|
|
231
|
+
let effectiveIncludeProjects = includeProjects;
|
|
232
|
+
if (!effectiveIncludeProjects && config.repos && config.repos.length > 0) {
|
|
233
|
+
effectiveIncludeProjects = config.repos.map(r => normalizeProjectPath(r));
|
|
234
|
+
} else if (!effectiveIncludeProjects && config._autoRepos && config._autoRepos.length > 0) {
|
|
235
|
+
effectiveIncludeProjects = config._autoRepos.map(r => normalizeProjectPath(r));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 自动推导的 repos 也用于 Git 统计
|
|
239
|
+
if ((!config.repos || config.repos.length === 0) && config._autoRepos) {
|
|
240
|
+
config.repos = config._autoRepos;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const configPath = getConfigPath();
|
|
244
|
+
return { config, dateArg, effectiveIncludeProjects, configPath };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function buildReportData(period, dateArg, config, effectiveIncludeProjects, tool = 'all', preParsed = null, options = {}) {
|
|
248
|
+
// 使用预解析结果或全量解析
|
|
249
|
+
let records, toolBreakdown;
|
|
250
|
+
if (preParsed) {
|
|
251
|
+
({ records, toolBreakdown } = preParsed);
|
|
252
|
+
} else {
|
|
253
|
+
({ records, toolBreakdown } = await parseAllEnabledTools(config, {
|
|
254
|
+
excludeProjects: config.excludeProjects,
|
|
255
|
+
includeProjects: effectiveIncludeProjects,
|
|
256
|
+
}));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (records.length === 0) {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// 预加载未知模型定价
|
|
264
|
+
await preloadUnknownPricing(records);
|
|
265
|
+
|
|
266
|
+
// 按工具过滤
|
|
267
|
+
const toolRecords = tool !== 'all' ? records.filter(r => r.tool === tool) : records;
|
|
268
|
+
if (toolRecords.length === 0) {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const { filtered, start, end } = filterRecordsByPeriod(toolRecords, period, dateArg, { customStart: options.customStart, customEnd: options.customEnd });
|
|
273
|
+
const reposConfigured = !!(config.repos && config.repos.length > 0);
|
|
274
|
+
|
|
275
|
+
// ── 第一层并发:三个独立的同步计算 ──
|
|
276
|
+
const [usageStats, sessions, billingBlocks] = [
|
|
277
|
+
computeUsageStats(filtered, config.scenarioKeywords, config.costMode),
|
|
278
|
+
groupBySessions(filtered),
|
|
279
|
+
identifyBillingBlocks(filtered, config.blockQuota ? 5 : 5, config.costMode),
|
|
280
|
+
];
|
|
281
|
+
|
|
282
|
+
// ── 第二层并发:gitStats(async) + trendData + prevStats ──
|
|
283
|
+
const gitStatsPromise = (async () => {
|
|
284
|
+
if (!reposConfigured) return null;
|
|
285
|
+
const coveredBases = new Set(filtered.map(r => {
|
|
286
|
+
const p = r.project || '';
|
|
287
|
+
return p.replace(/\\/g, '/').replace(/\/$/, '').split('/').pop();
|
|
288
|
+
}).filter(Boolean));
|
|
289
|
+
let toolRepos = config.repos.filter(r => coveredBases.has(r.replace(/\\/g, '/').replace(/\/$/, '').split('/').pop()));
|
|
290
|
+
if (toolRepos.length === 0) toolRepos = config.repos;
|
|
291
|
+
if (toolRepos.length === 0) return null;
|
|
292
|
+
const extendedEnd = new Date(end);
|
|
293
|
+
extendedEnd.setDate(extendedEnd.getDate() + 2);
|
|
294
|
+
const extendedEndStr = extendedEnd.toISOString().slice(0, 10) + 'T23:59:59';
|
|
295
|
+
let gs = await getGitStatsForMultipleReposAsync(toolRepos, start, extendedEndStr);
|
|
296
296
|
gs = await finalizeGitStats(gs, sessions, {
|
|
297
297
|
attribution: config.aiAttribution,
|
|
298
298
|
stepTracking: config.stepTracking,
|
|
299
299
|
});
|
|
300
|
-
if (gs.commitList) {
|
|
301
|
-
const windowStart = start;
|
|
302
|
-
const windowEnd = end + 'T23:59:59';
|
|
303
|
-
const inWindow = gs.commitList.filter(c => (c.date || '') >= windowStart && (c.date || '') <= windowEnd);
|
|
304
|
-
gs.commits = inWindow.length;
|
|
305
|
-
gs.linesAdded = inWindow.reduce((s, c) => s + (c.linesAdded || 0), 0);
|
|
306
|
-
gs.linesDeleted = inWindow.reduce((s, c) => s + (c.linesDeleted || 0), 0);
|
|
307
|
-
gs.filesChanged = new Set(inWindow.flatMap(c => (c.files || []).map(f => f.path))).size;
|
|
308
|
-
gs.commitTypes = computeCommitTypes(inWindow);
|
|
309
|
-
gs.fileHotspots = computeFileHotspots(inWindow, 10);
|
|
310
|
-
}
|
|
311
|
-
return gs;
|
|
312
|
-
})();
|
|
313
|
-
|
|
314
|
-
const trendDataPromise = Promise.resolve(computeTrendData(toolRecords, period, dateArg));
|
|
315
|
-
|
|
316
|
-
const prevStatsPromise = (async () => {
|
|
317
|
-
const prevRange = computePrevPeriodRange(period, dateArg, { customStart: options.customStart, customEnd: options.customEnd });
|
|
318
|
-
const prevFiltered = toolRecords.filter(r => {
|
|
319
|
-
if (!r.timestamp) return false;
|
|
320
|
-
const date = r.timestamp.slice(0, 10);
|
|
321
|
-
return date >= prevRange.start && date <= prevRange.end;
|
|
322
|
-
});
|
|
323
|
-
return prevFiltered.length > 0 ? computeUsageStats(prevFiltered, config.scenarioKeywords, config.costMode) : null;
|
|
324
|
-
})();
|
|
325
|
-
|
|
326
|
-
const [gitStats, trendData, prevStats] = await Promise.all([gitStatsPromise, trendDataPromise, prevStatsPromise]);
|
|
327
|
-
|
|
328
|
-
// ── 第三层:依赖 usageStats 的同步派生 ──
|
|
329
|
-
const slimSessions = sessions.map(s => ({
|
|
330
|
-
id: s.id,
|
|
331
|
-
project: s.project,
|
|
332
|
-
startTime: s.startTime,
|
|
333
|
-
endTime: s.endTime,
|
|
334
|
-
requests: s.requests,
|
|
335
|
-
commits: s.commits || [],
|
|
336
|
-
}));
|
|
337
|
-
|
|
338
|
-
const statsTB = usageStats.toolBreakdown || {};
|
|
339
|
-
const mergedBreakdown = {};
|
|
340
|
-
for (const [name, base] of Object.entries(toolBreakdown)) {
|
|
341
|
-
const s = statsTB[name] || {};
|
|
342
|
-
mergedBreakdown[name] = {
|
|
343
|
-
inputTokens: s.inputTokens || 0,
|
|
344
|
-
outputTokens: s.outputTokens || 0,
|
|
345
|
-
cacheRead: s.cacheRead || 0,
|
|
346
|
-
cacheCreate: s.cacheCreate || 0,
|
|
347
|
-
count: s.count || 0,
|
|
348
|
-
sessionCount: base.sessionCount || 0,
|
|
349
|
-
};
|
|
350
|
-
}
|
|
351
|
-
for (const [name, data] of Object.entries(statsTB)) {
|
|
352
|
-
if (!mergedBreakdown[name]) {
|
|
353
|
-
mergedBreakdown[name] = {
|
|
354
|
-
inputTokens: data.inputTokens || 0,
|
|
355
|
-
outputTokens: data.outputTokens || 0,
|
|
356
|
-
cacheRead: data.cacheRead || 0,
|
|
357
|
-
cacheCreate: data.cacheCreate || 0,
|
|
358
|
-
count: data.count || 0,
|
|
359
|
-
sessionCount: 0,
|
|
360
|
-
};
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// ── 第四层:projectDetails(从 commitList 按 repo 分组派生,无需再次 git 调用)──
|
|
365
|
-
const projectDetails = {};
|
|
366
|
-
const projEntries = Object.entries(usageStats.projects || {}).sort((a, b) => b[1].requests - a[1].requests);
|
|
367
|
-
if (reposConfigured && gitStats?.commitList?.length) {
|
|
368
|
-
const windowEnd = end + 'T23:59:59';
|
|
369
|
-
const inWindow = gitStats.commitList.filter(c => (c.date || '') >= start && (c.date || '') <= windowEnd);
|
|
370
|
-
const repoGroups = new Map();
|
|
371
|
-
for (const c of inWindow) {
|
|
372
|
-
const base = (c.repo || '').replace(/\\/g, '/').replace(/\/$/, '').split('/').pop();
|
|
373
|
-
if (!base) continue;
|
|
374
|
-
if (!repoGroups.has(base)) repoGroups.set(base, []);
|
|
375
|
-
repoGroups.get(base).push(c);
|
|
376
|
-
}
|
|
377
|
-
for (const [projName, projStats] of projEntries) {
|
|
378
|
-
const repoCommits = repoGroups.get(projName) || [];
|
|
379
|
-
if (repoCommits.length === 0) {
|
|
380
|
-
projectDetails[projName] = { usage: projStats, git: null, topCommits: [] };
|
|
381
|
-
continue;
|
|
382
|
-
}
|
|
383
|
-
const uniqueFiles = new Set();
|
|
384
|
-
let linesAdded = 0, linesDeleted = 0;
|
|
385
|
-
for (const c of repoCommits) {
|
|
386
|
-
linesAdded += c.linesAdded || 0;
|
|
387
|
-
linesDeleted += c.linesDeleted || 0;
|
|
388
|
-
for (const f of c.files || []) uniqueFiles.add(f.path);
|
|
389
|
-
}
|
|
390
|
-
const topCommits = repoCommits
|
|
391
|
-
.filter(c => c.type === 'feat' || c.type === 'fix')
|
|
392
|
-
.slice(0, 5)
|
|
393
|
-
.map(c => ({ type: c.type, subject: c.subject, scope: c.scope }));
|
|
394
|
-
projectDetails[projName] = {
|
|
395
|
-
usage: projStats,
|
|
396
|
-
git: {
|
|
397
|
-
commits: repoCommits.length, linesAdded, linesDeleted,
|
|
398
|
-
filesChanged: uniqueFiles.size,
|
|
399
|
-
fileHotspots: computeFileHotspots(repoCommits, 5),
|
|
400
|
-
},
|
|
401
|
-
topCommits,
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
} else {
|
|
405
|
-
for (const [projName, projStats] of projEntries) {
|
|
406
|
-
projectDetails[projName] = { usage: projStats, git: null, topCommits: [] };
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
// 工具检测诊断:记录每个工具的检测状态和数据目录
|
|
411
|
-
const diagnostics = {};
|
|
412
|
-
try {
|
|
413
|
-
const availableTools = await detectAvailableTools(config);
|
|
414
|
-
for (const t of availableTools) {
|
|
415
|
-
diagnostics[t.name] = { detected: t.detected, dataDir: t.dataDir || null };
|
|
416
|
-
}
|
|
417
|
-
} catch {}
|
|
418
|
-
|
|
419
|
-
return { usageStats, gitStats, reposConfigured, sessions: slimSessions, start, end, trendData, prevStats, billingBlocks, toolBreakdown: mergedBreakdown, projectDetails, _diagnostics: diagnostics };
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
if (!command || command === 'help' || command === '--help') {
|
|
423
|
-
console.log(`
|
|
424
|
-
用法: lumencode <命令> [周期] [日期] [选项]
|
|
425
|
-
|
|
426
|
-
命令:
|
|
427
|
-
report 生成使用报告(默认命令)
|
|
428
|
-
serve 启动 Web 服务(默认端口 4567)
|
|
429
|
-
init 初始化配置文件
|
|
430
|
-
help 显示帮助信息
|
|
431
|
-
|
|
432
|
-
周期:
|
|
433
|
-
daily 日报(默认)
|
|
434
|
-
weekly 周报
|
|
435
|
-
monthly 月报
|
|
436
|
-
|
|
437
|
-
日期:
|
|
438
|
-
指定报告的参考日期,格式 YYYY-MM-DD(默认今天)
|
|
439
|
-
|
|
440
|
-
选项:
|
|
441
|
-
--projects 只统计指定项目,多个项目用逗号分隔
|
|
442
|
-
--work 输出工作汇报版本(Markdown 格式)
|
|
443
|
-
--
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
lumencode report daily
|
|
448
|
-
lumencode report
|
|
449
|
-
lumencode report
|
|
300
|
+
if (gs.commitList) {
|
|
301
|
+
const windowStart = start;
|
|
302
|
+
const windowEnd = end + 'T23:59:59';
|
|
303
|
+
const inWindow = gs.commitList.filter(c => (c.date || '') >= windowStart && (c.date || '') <= windowEnd);
|
|
304
|
+
gs.commits = inWindow.length;
|
|
305
|
+
gs.linesAdded = inWindow.reduce((s, c) => s + (c.linesAdded || 0), 0);
|
|
306
|
+
gs.linesDeleted = inWindow.reduce((s, c) => s + (c.linesDeleted || 0), 0);
|
|
307
|
+
gs.filesChanged = new Set(inWindow.flatMap(c => (c.files || []).map(f => f.path))).size;
|
|
308
|
+
gs.commitTypes = computeCommitTypes(inWindow);
|
|
309
|
+
gs.fileHotspots = computeFileHotspots(inWindow, 10);
|
|
310
|
+
}
|
|
311
|
+
return gs;
|
|
312
|
+
})();
|
|
313
|
+
|
|
314
|
+
const trendDataPromise = Promise.resolve(computeTrendData(toolRecords, period, dateArg));
|
|
315
|
+
|
|
316
|
+
const prevStatsPromise = (async () => {
|
|
317
|
+
const prevRange = computePrevPeriodRange(period, dateArg, { customStart: options.customStart, customEnd: options.customEnd });
|
|
318
|
+
const prevFiltered = toolRecords.filter(r => {
|
|
319
|
+
if (!r.timestamp) return false;
|
|
320
|
+
const date = r.timestamp.slice(0, 10);
|
|
321
|
+
return date >= prevRange.start && date <= prevRange.end;
|
|
322
|
+
});
|
|
323
|
+
return prevFiltered.length > 0 ? computeUsageStats(prevFiltered, config.scenarioKeywords, config.costMode) : null;
|
|
324
|
+
})();
|
|
325
|
+
|
|
326
|
+
const [gitStats, trendData, prevStats] = await Promise.all([gitStatsPromise, trendDataPromise, prevStatsPromise]);
|
|
327
|
+
|
|
328
|
+
// ── 第三层:依赖 usageStats 的同步派生 ──
|
|
329
|
+
const slimSessions = sessions.map(s => ({
|
|
330
|
+
id: s.id,
|
|
331
|
+
project: s.project,
|
|
332
|
+
startTime: s.startTime,
|
|
333
|
+
endTime: s.endTime,
|
|
334
|
+
requests: s.requests,
|
|
335
|
+
commits: s.commits || [],
|
|
336
|
+
}));
|
|
337
|
+
|
|
338
|
+
const statsTB = usageStats.toolBreakdown || {};
|
|
339
|
+
const mergedBreakdown = {};
|
|
340
|
+
for (const [name, base] of Object.entries(toolBreakdown)) {
|
|
341
|
+
const s = statsTB[name] || {};
|
|
342
|
+
mergedBreakdown[name] = {
|
|
343
|
+
inputTokens: s.inputTokens || 0,
|
|
344
|
+
outputTokens: s.outputTokens || 0,
|
|
345
|
+
cacheRead: s.cacheRead || 0,
|
|
346
|
+
cacheCreate: s.cacheCreate || 0,
|
|
347
|
+
count: s.count || 0,
|
|
348
|
+
sessionCount: base.sessionCount || 0,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
for (const [name, data] of Object.entries(statsTB)) {
|
|
352
|
+
if (!mergedBreakdown[name]) {
|
|
353
|
+
mergedBreakdown[name] = {
|
|
354
|
+
inputTokens: data.inputTokens || 0,
|
|
355
|
+
outputTokens: data.outputTokens || 0,
|
|
356
|
+
cacheRead: data.cacheRead || 0,
|
|
357
|
+
cacheCreate: data.cacheCreate || 0,
|
|
358
|
+
count: data.count || 0,
|
|
359
|
+
sessionCount: 0,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ── 第四层:projectDetails(从 commitList 按 repo 分组派生,无需再次 git 调用)──
|
|
365
|
+
const projectDetails = {};
|
|
366
|
+
const projEntries = Object.entries(usageStats.projects || {}).sort((a, b) => b[1].requests - a[1].requests);
|
|
367
|
+
if (reposConfigured && gitStats?.commitList?.length) {
|
|
368
|
+
const windowEnd = end + 'T23:59:59';
|
|
369
|
+
const inWindow = gitStats.commitList.filter(c => (c.date || '') >= start && (c.date || '') <= windowEnd);
|
|
370
|
+
const repoGroups = new Map();
|
|
371
|
+
for (const c of inWindow) {
|
|
372
|
+
const base = (c.repo || '').replace(/\\/g, '/').replace(/\/$/, '').split('/').pop();
|
|
373
|
+
if (!base) continue;
|
|
374
|
+
if (!repoGroups.has(base)) repoGroups.set(base, []);
|
|
375
|
+
repoGroups.get(base).push(c);
|
|
376
|
+
}
|
|
377
|
+
for (const [projName, projStats] of projEntries) {
|
|
378
|
+
const repoCommits = repoGroups.get(projName) || [];
|
|
379
|
+
if (repoCommits.length === 0) {
|
|
380
|
+
projectDetails[projName] = { usage: projStats, git: null, topCommits: [] };
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
const uniqueFiles = new Set();
|
|
384
|
+
let linesAdded = 0, linesDeleted = 0;
|
|
385
|
+
for (const c of repoCommits) {
|
|
386
|
+
linesAdded += c.linesAdded || 0;
|
|
387
|
+
linesDeleted += c.linesDeleted || 0;
|
|
388
|
+
for (const f of c.files || []) uniqueFiles.add(f.path);
|
|
389
|
+
}
|
|
390
|
+
const topCommits = repoCommits
|
|
391
|
+
.filter(c => c.type === 'feat' || c.type === 'fix')
|
|
392
|
+
.slice(0, 5)
|
|
393
|
+
.map(c => ({ type: c.type, subject: c.subject, scope: c.scope }));
|
|
394
|
+
projectDetails[projName] = {
|
|
395
|
+
usage: projStats,
|
|
396
|
+
git: {
|
|
397
|
+
commits: repoCommits.length, linesAdded, linesDeleted,
|
|
398
|
+
filesChanged: uniqueFiles.size,
|
|
399
|
+
fileHotspots: computeFileHotspots(repoCommits, 5),
|
|
400
|
+
},
|
|
401
|
+
topCommits,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
} else {
|
|
405
|
+
for (const [projName, projStats] of projEntries) {
|
|
406
|
+
projectDetails[projName] = { usage: projStats, git: null, topCommits: [] };
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// 工具检测诊断:记录每个工具的检测状态和数据目录
|
|
411
|
+
const diagnostics = {};
|
|
412
|
+
try {
|
|
413
|
+
const availableTools = await detectAvailableTools(config);
|
|
414
|
+
for (const t of availableTools) {
|
|
415
|
+
diagnostics[t.name] = { detected: t.detected, dataDir: t.dataDir || null };
|
|
416
|
+
}
|
|
417
|
+
} catch {}
|
|
418
|
+
|
|
419
|
+
return { usageStats, gitStats, reposConfigured, sessions: slimSessions, start, end, trendData, prevStats, billingBlocks, toolBreakdown: mergedBreakdown, projectDetails, _diagnostics: diagnostics };
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (!command || command === 'help' || command === '--help') {
|
|
423
|
+
console.log(`
|
|
424
|
+
用法: lumencode <命令> [周期] [日期] [选项]
|
|
425
|
+
|
|
426
|
+
命令:
|
|
427
|
+
report 生成使用报告(默认命令)
|
|
428
|
+
serve 启动 Web 服务(默认端口 4567)
|
|
429
|
+
init 初始化配置文件
|
|
430
|
+
help 显示帮助信息
|
|
431
|
+
|
|
432
|
+
周期:
|
|
433
|
+
daily 日报(默认)
|
|
434
|
+
weekly 周报
|
|
435
|
+
monthly 月报
|
|
436
|
+
|
|
437
|
+
日期:
|
|
438
|
+
指定报告的参考日期,格式 YYYY-MM-DD(默认今天)
|
|
439
|
+
|
|
440
|
+
选项:
|
|
441
|
+
--projects 只统计指定项目,多个项目用逗号分隔
|
|
442
|
+
--work 输出工作汇报版本(Markdown 格式)
|
|
443
|
+
--boss 输出 Boss 报告(给领导看的版本,凸显工作成果)
|
|
444
|
+
--brief 配合 --work 使用,输出简报(3-5 句话)
|
|
445
|
+
|
|
446
|
+
示例:
|
|
447
|
+
lumencode report daily 2026-05-15
|
|
448
|
+
lumencode report daily --projects D://fzwork
|
|
449
|
+
lumencode report weekly 2026-05-15 --projects D://fzwork,E://play/idea
|
|
450
|
+
lumencode report daily --work
|
|
450
451
|
lumencode report daily --work --brief
|
|
452
|
+
lumencode report weekly --boss
|
|
451
453
|
node index.js serve
|
|
452
454
|
node index.js init
|
|
453
455
|
node index.js hooks status
|
|
@@ -457,14 +459,14 @@ if (!command || command === 'help' || command === '--help') {
|
|
|
457
459
|
node index.js hooks:install
|
|
458
460
|
node index.js hooks:install-claude
|
|
459
461
|
node index.js hooks:install-codex
|
|
460
|
-
|
|
461
|
-
零配置:
|
|
462
|
-
首次运行自动检测 Claude 日志目录和项目路径,无需手动配置。
|
|
463
|
-
如需自定义,运行 lumencode init 或在 Web 模式下点击设置。
|
|
464
|
-
`);
|
|
465
|
-
process.exit(0);
|
|
466
|
-
}
|
|
467
|
-
|
|
462
|
+
|
|
463
|
+
零配置:
|
|
464
|
+
首次运行自动检测 Claude 日志目录和项目路径,无需手动配置。
|
|
465
|
+
如需自定义,运行 lumencode init 或在 Web 模式下点击设置。
|
|
466
|
+
`);
|
|
467
|
+
process.exit(0);
|
|
468
|
+
}
|
|
469
|
+
|
|
468
470
|
if (command === 'init') {
|
|
469
471
|
initConfig(args[1]);
|
|
470
472
|
process.exit(0);
|
|
@@ -491,70 +493,73 @@ if (command === 'hooks:install-codex') {
|
|
|
491
493
|
}
|
|
492
494
|
|
|
493
495
|
if (command === 'serve') {
|
|
494
|
-
const { config, effectiveIncludeProjects, configPath } = loadCliConfig();
|
|
495
|
-
startServer(config, effectiveIncludeProjects, buildReportData, configPath);
|
|
496
|
-
} else {
|
|
497
|
-
// report command (default)
|
|
498
|
-
const period = args[1] || 'daily';
|
|
499
|
-
const isWorkMode = args.includes('--work');
|
|
500
|
-
const
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
console.log(
|
|
515
|
-
console.log(`
|
|
516
|
-
console.log(
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
const
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
usageStats
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
496
|
+
const { config, effectiveIncludeProjects, configPath } = loadCliConfig();
|
|
497
|
+
startServer(config, effectiveIncludeProjects, buildReportData, configPath);
|
|
498
|
+
} else {
|
|
499
|
+
// report command (default)
|
|
500
|
+
const period = args[1] || 'daily';
|
|
501
|
+
const isWorkMode = args.includes('--work');
|
|
502
|
+
const isBossMode = args.includes('--boss');
|
|
503
|
+
const isBrief = args.includes('--brief');
|
|
504
|
+
const { config, dateArg, effectiveIncludeProjects } = loadCliConfig();
|
|
505
|
+
|
|
506
|
+
console.log('正在扫描 AI 编码助手日志...');
|
|
507
|
+
const { records, toolBreakdown } = await parseAllEnabledTools(config, {
|
|
508
|
+
excludeProjects: config.excludeProjects,
|
|
509
|
+
includeProjects: effectiveIncludeProjects,
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
// 预加载未知模型定价
|
|
513
|
+
await preloadUnknownPricing(records);
|
|
514
|
+
|
|
515
|
+
if (records.length === 0) {
|
|
516
|
+
console.log('未找到任何会话记录。可能原因:');
|
|
517
|
+
console.log(` 1. 日志目录不存在或路径错误`);
|
|
518
|
+
console.log(` 2. 该目录下没有可解析的数据`);
|
|
519
|
+
console.log('请运行 lumencode init 创建配置文件,或在 Web 模式下点击设置按钮配置。');
|
|
520
|
+
process.exit(1);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const projectSet = new Set(records.map(r => r.project).filter(Boolean));
|
|
524
|
+
const toolNames = Object.keys(toolBreakdown || {});
|
|
525
|
+
console.log(`已加载 ${records.length} 条记录,${projectSet.size} 个项目,工具: ${toolNames.join(', ')}`);
|
|
526
|
+
|
|
527
|
+
const { filtered, start, end } = filterRecordsByPeriod(records, period, dateArg);
|
|
528
|
+
console.log(`筛选 ${period} 数据: ${start} ~ ${end},共 ${filtered.length} 条记录`);
|
|
529
|
+
|
|
530
|
+
const usageStats = computeUsageStats(filtered, config.scenarioKeywords, config.costMode);
|
|
531
|
+
usageStats.toolBreakdown = toolBreakdown;
|
|
532
|
+
|
|
533
|
+
let gitStats = null;
|
|
534
|
+
if (config.repos && config.repos.length > 0) {
|
|
535
|
+
console.log('正在统计 Git 指标...');
|
|
536
|
+
const sessions = groupBySessions(filtered);
|
|
537
|
+
gitStats = await getGitStatsForMultipleReposAsync(config.repos, start, end + 'T23:59:59');
|
|
535
538
|
gitStats = await finalizeGitStats(gitStats, sessions, {
|
|
536
539
|
attribution: config.aiAttribution,
|
|
537
540
|
stepTracking: config.stepTracking,
|
|
538
541
|
});
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
// 上一周期数据(用于工作汇报环比)
|
|
542
|
-
const prevRange = computePrevPeriodRange(period, dateArg);
|
|
543
|
-
const prevFiltered = records.filter(r => {
|
|
544
|
-
if (!r.timestamp) return false;
|
|
545
|
-
const date = r.timestamp.slice(0, 10);
|
|
546
|
-
return date >= prevRange.start && date <= prevRange.end;
|
|
547
|
-
});
|
|
548
|
-
const prevStats = prevFiltered.length > 0 ? computeUsageStats(prevFiltered, config.scenarioKeywords, config.costMode) : null;
|
|
549
|
-
|
|
550
|
-
const report =
|
|
551
|
-
?
|
|
552
|
-
:
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
return
|
|
560
|
-
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// 上一周期数据(用于工作汇报环比)
|
|
545
|
+
const prevRange = computePrevPeriodRange(period, dateArg);
|
|
546
|
+
const prevFiltered = records.filter(r => {
|
|
547
|
+
if (!r.timestamp) return false;
|
|
548
|
+
const date = r.timestamp.slice(0, 10);
|
|
549
|
+
return date >= prevRange.start && date <= prevRange.end;
|
|
550
|
+
});
|
|
551
|
+
const prevStats = prevFiltered.length > 0 ? computeUsageStats(prevFiltered, config.scenarioKeywords, config.costMode) : null;
|
|
552
|
+
|
|
553
|
+
const report = isBossMode
|
|
554
|
+
? generateBossReport(usageStats, gitStats, period, start, end, prevStats)
|
|
555
|
+
: isWorkMode
|
|
556
|
+
? generateWorkReport(usageStats, gitStats, period, start, end, prevStats, { level: isBrief ? 'brief' : 'detailed' })
|
|
557
|
+
: generateReport(usageStats, gitStats, period, start, end);
|
|
558
|
+
console.log(report);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function fmtNum(n) {
|
|
562
|
+
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M';
|
|
563
|
+
if (n >= 1_000) return (n / 1_000).toFixed(1) + 'K';
|
|
564
|
+
return String(n);
|
|
565
|
+
}
|