lumencode 1.3.0 → 1.3.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 +8 -8
- package/lib/server.js +104 -93
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
</p>
|
|
12
12
|
|
|
13
13
|
<p align="center">
|
|
14
|
-
|
|
14
|
+
<b>AI 编码助手使用分析</b> —— 精确到每一行代码的 AI 归因 · 三工具统一 · 智能周报生成
|
|
15
15
|
</p>
|
|
16
16
|
|
|
17
17
|
<p align="center">
|
|
@@ -30,12 +30,12 @@
|
|
|
30
30
|
|
|
31
31
|
| 场景 | 用 lumencode 解决 |
|
|
32
32
|
|------|----------------------|
|
|
33
|
-
|
|
|
34
|
-
| **证明 AI ROI** |
|
|
35
|
-
|
|
|
36
|
-
|
|
|
37
|
-
|
|
|
38
|
-
| **追踪 AI 成本** |
|
|
33
|
+
| **精确量化 AI 贡献** | 不是模糊的"大概写了不少",而是精确到「4,200 行代码中 3,180 行由 AI 辅助完成」。**每一行都有数可查。** |
|
|
34
|
+
| **证明 AI ROI** | 周报自动生成:「本周 AI 辅助 12 个提交,节省约 8 小时编码时间,Token 花费 $18.5」。**老板一看就懂。** |
|
|
35
|
+
| **写周报/月报** | 选周期 → 点「工作汇报 → 复制」→ 粘贴飞书/钉钉。**3 秒搞定。** |
|
|
36
|
+
| **按项目汇报** | 多项目并行时,选择单个项目生成独立汇报,方便向不同项目负责人对齐 |
|
|
37
|
+
| **对齐 Sprint 周期** | 支持自定义起止日期,不再被日/周/月固定周期限制 |
|
|
38
|
+
| **追踪 AI 成本** | 600+ 模型内置定价(含 GLM、Kimi、Qwen、DeepSeek),自动算出等效 API 花销 |
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
@@ -63,8 +63,8 @@ npx lumencode serve
|
|
|
63
63
|
|
|
64
64
|
| 亮点 | 说明 |
|
|
65
65
|
|------|------|
|
|
66
|
+
| 🎯 **行级 AI 归因** | 通过 hook 步骤追踪,精确识别每一行代码的 AI 参与度。不是"这个提交 AI 帮忙了",而是"这行代码是 AI 写的" |
|
|
66
67
|
| 🌐 **三工具统一** | Claude Code / Codex / OpenCode 数据全自动汇总,左侧标签一键切换 |
|
|
67
|
-
| 🤖 **AI 贡献度量化** | 识别 `Co-Authored-By: Claude` 等签名,多层归因引擎量化 AI 在你代码中的实际占比 |
|
|
68
68
|
| 📝 **自然语言工作汇报** | 详报/简报一键生成,支持标准 Markdown / 飞书 / 钉钉三种格式,每个板块附诊断解读 |
|
|
69
69
|
| 📂 **按项目独立汇报** | 右侧面板选择项目,生成该项目的独立工作汇报(commits + AI 交互量 + 热点文件) |
|
|
70
70
|
| 📅 **自定义时间范围** | 除日/周/月外,支持选择任意起止日期,方便对齐 Sprint 周期 |
|
package/lib/server.js
CHANGED
|
@@ -7,7 +7,7 @@ import { generateWorkReport, generateFeishuCard } from './report.js';
|
|
|
7
7
|
import { collectAllRecords, filterRecordsByPeriod, groupBySessions, computeUsageStats, computeTrendData, computePrevPeriodRange } from './aggregate.js';
|
|
8
8
|
import { normalizeProjectPath } from './aggregate.js';
|
|
9
9
|
import { invalidateFileCache } from './cache.js';
|
|
10
|
-
import { invalidateGitCache, getGitStatsForMultipleReposAsync, finalizeGitStats } from './git.js';
|
|
10
|
+
import { invalidateGitCache, getGitStatsForMultipleReposAsync, finalizeGitStats, computeAIContribution, computeCommitTypes, computeFileHotspots } from './git.js';
|
|
11
11
|
import { identifyBillingBlocks } from './blocks.js';
|
|
12
12
|
import { detectAvailableTools, parseAllEnabledTools } from './parsers/index.js';
|
|
13
13
|
import { isAssistantRecord, getInputTokens, getOutputTokens } from './record-utils.js';
|
|
@@ -89,9 +89,9 @@ export function startServer(config, effectiveIncludeProjects, buildReportData, c
|
|
|
89
89
|
const REPORT_CACHE_TTL = 30_000; // 30s
|
|
90
90
|
const REPORT_CACHE_MAX_SIZE = 50;
|
|
91
91
|
|
|
92
|
-
function getReportCacheKey(period, date, tool, customStart, customEnd
|
|
93
|
-
return `${period}|${date}|${tool || 'all'}|${customStart || ''}|${customEnd || ''}
|
|
94
|
-
}
|
|
92
|
+
function getReportCacheKey(period, date, tool, customStart, customEnd) {
|
|
93
|
+
return `${period}|${date}|${tool || 'all'}|${customStart || ''}|${customEnd || ''}`;
|
|
94
|
+
}
|
|
95
95
|
|
|
96
96
|
function getCachedReport(cacheKey) {
|
|
97
97
|
const cached = _reportCache.get(cacheKey);
|
|
@@ -166,20 +166,80 @@ export function startServer(config, effectiveIncludeProjects, buildReportData, c
|
|
|
166
166
|
return null;
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
-
async function getOrParse(config, includeProjects) {
|
|
170
|
-
const cached = getCachedParse(config, includeProjects);
|
|
171
|
-
if (cached) return cached;
|
|
172
|
-
const result = await parseAllEnabledTools(config, {
|
|
173
|
-
excludeProjects: config.excludeProjects,
|
|
169
|
+
async function getOrParse(config, includeProjects) {
|
|
170
|
+
const cached = getCachedParse(config, includeProjects);
|
|
171
|
+
if (cached) return cached;
|
|
172
|
+
const result = await parseAllEnabledTools(config, {
|
|
173
|
+
excludeProjects: config.excludeProjects,
|
|
174
174
|
includeProjects,
|
|
175
175
|
});
|
|
176
176
|
_parsedCache = result;
|
|
177
177
|
_parsedCacheKey = `${config.claudeDir}|${includeProjects?.join(',') || ''}`;
|
|
178
|
-
_parsedCacheExpire = Date.now() + PARSED_CACHE_TTL;
|
|
179
|
-
return result;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
|
|
178
|
+
_parsedCacheExpire = Date.now() + PARSED_CACHE_TTL;
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function deriveProjectGitStats(gitStats, project, start, end, attributionOptions) {
|
|
183
|
+
if (!gitStats?.commitList?.length || !project) return null;
|
|
184
|
+
const windowEnd = end + 'T23:59:59';
|
|
185
|
+
const commitList = gitStats.commitList.filter(c => {
|
|
186
|
+
return getProjectBaseName(c.repo) === project && (c.date || '') >= start && (c.date || '') <= windowEnd;
|
|
187
|
+
});
|
|
188
|
+
if (commitList.length === 0) return null;
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
commits: commitList.length,
|
|
192
|
+
filesChanged: new Set(commitList.flatMap(c => (c.files || []).map(f => f.path))).size,
|
|
193
|
+
linesAdded: commitList.reduce((s, c) => s + (c.linesAdded || 0), 0),
|
|
194
|
+
linesDeleted: commitList.reduce((s, c) => s + (c.linesDeleted || 0), 0),
|
|
195
|
+
commitList,
|
|
196
|
+
commitTypes: computeCommitTypes(commitList),
|
|
197
|
+
fileHotspots: computeFileHotspots(commitList, 10),
|
|
198
|
+
aiContribution: computeAIContribution(commitList, null, attributionOptions),
|
|
199
|
+
attributionSummary: gitStats.attributionSummary,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function prepareJsonReportData(baseData, tool) {
|
|
204
|
+
const data = { ...baseData };
|
|
205
|
+
|
|
206
|
+
if (tool !== 'all' && data.gitStats?.aiContributionByTool) {
|
|
207
|
+
const toolAi = data.gitStats.aiContributionByTool[tool];
|
|
208
|
+
if (toolAi) {
|
|
209
|
+
data.gitStats = { ...data.gitStats, aiContribution: toolAi };
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (data.usageStats?.models) {
|
|
214
|
+
const modelEntries = Object.entries(data.usageStats.models)
|
|
215
|
+
.sort((a, b) => b[1].cost - a[1].cost);
|
|
216
|
+
const totalCost = modelEntries.reduce((s, [, d]) => s + (d.cost || 0), 0);
|
|
217
|
+
const cacheRead = data.usageStats.cacheRead || 0;
|
|
218
|
+
const cacheCreate = data.usageStats.cacheCreate || 0;
|
|
219
|
+
let cacheSaving = 0;
|
|
220
|
+
if (totalCost > 0 && (cacheRead + cacheCreate) > 0) {
|
|
221
|
+
const totalInput = data.usageStats.inputTokens || 1;
|
|
222
|
+
const avgInputCostPerToken = totalCost / (totalInput + data.usageStats.outputTokens + cacheRead + cacheCreate || 1);
|
|
223
|
+
cacheSaving = Math.round(cacheRead * avgInputCostPerToken * 0.9 * 100) / 100;
|
|
224
|
+
}
|
|
225
|
+
data.costBreakdown = {
|
|
226
|
+
models: modelEntries.map(([name, d]) => ({
|
|
227
|
+
name,
|
|
228
|
+
cost: d.cost || 0,
|
|
229
|
+
mode: d.costMode || 'unknown',
|
|
230
|
+
requests: d.count,
|
|
231
|
+
inputTokens: d.inputTokens,
|
|
232
|
+
outputTokens: d.outputTokens,
|
|
233
|
+
})),
|
|
234
|
+
cacheSaving,
|
|
235
|
+
total: Math.round(totalCost * 100) / 100,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return data;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const server = createServer(async (req, res) => {
|
|
183
243
|
const url = new URL(req.url, `http://localhost:${PORT}`);
|
|
184
244
|
|
|
185
245
|
// 安全响应头
|
|
@@ -312,21 +372,26 @@ export function startServer(config, effectiveIncludeProjects, buildReportData, c
|
|
|
312
372
|
|
|
313
373
|
try {
|
|
314
374
|
// 查询结果缓存:相同条件直接返回缓存
|
|
315
|
-
const reportCacheKey = getReportCacheKey(period, date, tool, customStart, customEnd
|
|
316
|
-
let data = getCachedReport(reportCacheKey);
|
|
317
|
-
if (data) {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
'
|
|
321
|
-
'
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
if (!data) {
|
|
375
|
+
const reportCacheKey = getReportCacheKey(period, date, tool, customStart, customEnd);
|
|
376
|
+
let data = getCachedReport(reportCacheKey);
|
|
377
|
+
if (data && format !== 'work') {
|
|
378
|
+
const responseData = prepareJsonReportData(data, tool);
|
|
379
|
+
res.writeHead(200, {
|
|
380
|
+
'Content-Type': 'application/json',
|
|
381
|
+
'Access-Control-Allow-Origin': 'http://localhost:' + PORT,
|
|
382
|
+
'X-Cache': 'HIT',
|
|
383
|
+
});
|
|
384
|
+
res.end(JSON.stringify(responseData));
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
let parsed = null;
|
|
389
|
+
if (!data) {
|
|
390
|
+
parsed = await getOrParse(config, includeProjects);
|
|
391
|
+
data = await buildReportData(period, date, config, includeProjects, tool, parsed, { customStart, customEnd });
|
|
392
|
+
if (data) setCachedReport(reportCacheKey, data);
|
|
393
|
+
}
|
|
394
|
+
if (!data) {
|
|
330
395
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
331
396
|
res.end(JSON.stringify({
|
|
332
397
|
error: '未找到数据',
|
|
@@ -352,10 +417,11 @@ export function startServer(config, effectiveIncludeProjects, buildReportData, c
|
|
|
352
417
|
let projUsageStats = data.usageStats;
|
|
353
418
|
let projGitStats = data.gitStats;
|
|
354
419
|
let projPrevStats = data.prevStats;
|
|
355
|
-
let projectName = '';
|
|
356
|
-
|
|
357
|
-
if (project) {
|
|
358
|
-
|
|
420
|
+
let projectName = '';
|
|
421
|
+
|
|
422
|
+
if (project) {
|
|
423
|
+
if (!parsed) parsed = await getOrParse(config, includeProjects);
|
|
424
|
+
const { records: allRecords } = parsed;
|
|
359
425
|
const toolRecords = tool !== 'all' ? allRecords.filter(r => r.tool === tool) : allRecords;
|
|
360
426
|
// basename 匹配
|
|
361
427
|
const projRecords = toolRecords.filter(r => {
|
|
@@ -375,25 +441,8 @@ export function startServer(config, effectiveIncludeProjects, buildReportData, c
|
|
|
375
441
|
projPrevStats = prevProjFiltered.length > 0 ? computeUsageStats(prevProjFiltered, config.scenarioKeywords, config.costMode) : null;
|
|
376
442
|
|
|
377
443
|
// 单项目 Git 统计
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
if (matchedRepo) {
|
|
381
|
-
try {
|
|
382
|
-
const { getGitStatsAsync } = await import('./git.js');
|
|
383
|
-
const sessions = groupBySessions(projFiltered);
|
|
384
|
-
const { finalizeGitStats, getGitStatsForMultipleReposAsync } = await import('./git.js');
|
|
385
|
-
let repoGit = await getGitStatsForMultipleReposAsync([matchedRepo], pStart, pEnd + 'T23:59:59');
|
|
386
|
-
repoGit = await finalizeGitStats(repoGit, sessions, {
|
|
387
|
-
attribution: config.aiAttribution,
|
|
388
|
-
stepTracking: config.stepTracking,
|
|
389
|
-
});
|
|
390
|
-
projGitStats = repoGit;
|
|
391
|
-
} catch (e) { console.warn("[server] error", e.message); }
|
|
392
|
-
}
|
|
393
|
-
} else {
|
|
394
|
-
projGitStats = null;
|
|
395
|
-
}
|
|
396
|
-
}
|
|
444
|
+
projGitStats = deriveProjectGitStats(data.gitStats, project, pStart, pEnd, config.aiAttribution);
|
|
445
|
+
}
|
|
397
446
|
|
|
398
447
|
const markdown = generateWorkReport(projUsageStats, projGitStats, period, data.start, data.end, projPrevStats, { level, platform, tool, projectName });
|
|
399
448
|
res.writeHead(200, {
|
|
@@ -404,52 +453,14 @@ export function startServer(config, effectiveIncludeProjects, buildReportData, c
|
|
|
404
453
|
return;
|
|
405
454
|
}
|
|
406
455
|
|
|
407
|
-
|
|
408
|
-
if (tool !== 'all' && data.gitStats?.aiContributionByTool) {
|
|
409
|
-
const toolAi = data.gitStats.aiContributionByTool[tool];
|
|
410
|
-
if (toolAi) {
|
|
411
|
-
data.gitStats.aiContribution = toolAi;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
456
|
+
const responseData = prepareJsonReportData(data, tool);
|
|
414
457
|
|
|
415
|
-
|
|
416
|
-
if (data.usageStats?.models) {
|
|
417
|
-
const modelEntries = Object.entries(data.usageStats.models)
|
|
418
|
-
.sort((a, b) => b[1].cost - a[1].cost);
|
|
419
|
-
const totalCost = modelEntries.reduce((s, [, d]) => s + (d.cost || 0), 0);
|
|
420
|
-
// 缓存节省 = (inputTokens - cacheRead) * avgInputRate - 已通过 cacheRead 低价体现
|
|
421
|
-
// 简化:缓存节省 = cacheRead * avgInputRate * (1 - cacheReadRate/inputRate)
|
|
422
|
-
const cacheRead = data.usageStats.cacheRead || 0;
|
|
423
|
-
const cacheCreate = data.usageStats.cacheCreate || 0;
|
|
424
|
-
let cacheSaving = 0;
|
|
425
|
-
if (totalCost > 0 && (cacheRead + cacheCreate) > 0) {
|
|
426
|
-
const totalInput = data.usageStats.inputTokens || 1;
|
|
427
|
-
const avgInputCostPerToken = totalCost / (totalInput + data.usageStats.outputTokens + cacheRead + cacheCreate || 1);
|
|
428
|
-
cacheSaving = Math.round(cacheRead * avgInputCostPerToken * 0.9 * 100) / 100;
|
|
429
|
-
}
|
|
430
|
-
data.costBreakdown = {
|
|
431
|
-
models: modelEntries.map(([name, d]) => ({
|
|
432
|
-
name,
|
|
433
|
-
cost: d.cost || 0,
|
|
434
|
-
mode: d.costMode || 'unknown',
|
|
435
|
-
requests: d.count,
|
|
436
|
-
inputTokens: d.inputTokens,
|
|
437
|
-
outputTokens: d.outputTokens,
|
|
438
|
-
})),
|
|
439
|
-
cacheSaving,
|
|
440
|
-
total: Math.round(totalCost * 100) / 100,
|
|
441
|
-
};
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
// 写入查询结果缓存
|
|
445
|
-
setCachedReport(reportCacheKey, data);
|
|
446
|
-
|
|
447
|
-
res.writeHead(200, {
|
|
458
|
+
res.writeHead(200, {
|
|
448
459
|
'Content-Type': 'application/json',
|
|
449
460
|
'Access-Control-Allow-Origin': 'http://localhost:' + PORT,
|
|
450
461
|
'X-Cache': 'MISS',
|
|
451
462
|
});
|
|
452
|
-
res.end(JSON.stringify(
|
|
463
|
+
res.end(JSON.stringify(responseData));
|
|
453
464
|
} catch (err) {
|
|
454
465
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
455
466
|
console.error('API error:', err.message);
|