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/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
- --brief 配合 --work 使用,输出简报(3-5 句话)
444
-
445
- 示例:
446
- lumencode report daily 2026-05-15
447
- lumencode report daily --projects D://fzwork
448
- lumencode report weekly 2026-05-15 --projects D://fzwork,E://play/idea
449
- lumencode report daily --work
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 isBrief = args.includes('--brief');
501
- const { config, dateArg, effectiveIncludeProjects } = loadCliConfig();
502
-
503
- console.log('正在扫描 AI 编码助手日志...');
504
- const { records, toolBreakdown } = await parseAllEnabledTools(config, {
505
- excludeProjects: config.excludeProjects,
506
- includeProjects: effectiveIncludeProjects,
507
- });
508
-
509
- // 预加载未知模型定价
510
- await preloadUnknownPricing(records);
511
-
512
- if (records.length === 0) {
513
- console.log('未找到任何会话记录。可能原因:');
514
- console.log(` 1. 日志目录不存在或路径错误`);
515
- console.log(` 2. 该目录下没有可解析的数据`);
516
- console.log('请运行 lumencode init 创建配置文件,或在 Web 模式下点击设置按钮配置。');
517
- process.exit(1);
518
- }
519
-
520
- const projectSet = new Set(records.map(r => r.project).filter(Boolean));
521
- const toolNames = Object.keys(toolBreakdown || {});
522
- console.log(`已加载 ${records.length} 条记录,${projectSet.size} 个项目,工具: ${toolNames.join(', ')}`);
523
-
524
- const { filtered, start, end } = filterRecordsByPeriod(records, period, dateArg);
525
- console.log(`筛选 ${period} 数据: ${start} ~ ${end},共 ${filtered.length} 条记录`);
526
-
527
- const usageStats = computeUsageStats(filtered, config.scenarioKeywords, config.costMode);
528
- usageStats.toolBreakdown = toolBreakdown;
529
-
530
- let gitStats = null;
531
- if (config.repos && config.repos.length > 0) {
532
- console.log('正在统计 Git 指标...');
533
- const sessions = groupBySessions(filtered);
534
- gitStats = await getGitStatsForMultipleReposAsync(config.repos, start, end + 'T23:59:59');
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 = isWorkMode
551
- ? generateWorkReport(usageStats, gitStats, period, start, end, prevStats, { level: isBrief ? 'brief' : 'detailed' })
552
- : generateReport(usageStats, gitStats, period, start, end);
553
- console.log(report);
554
- }
555
-
556
- function fmtNum(n) {
557
- if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M';
558
- if (n >= 1_000) return (n / 1_000).toFixed(1) + 'K';
559
- return String(n);
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
+ }