lumencode 1.3.1 → 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 +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 +331 -316
- 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/lib/aggregate.js
CHANGED
|
@@ -238,6 +238,8 @@ export function computeUsageStats(records, scenarioKeywords, costMode = 'auto')
|
|
|
238
238
|
projects: {},
|
|
239
239
|
dailyStats: {},
|
|
240
240
|
toolBreakdown: {}, // 新增:各工具数据分布
|
|
241
|
+
skills: {},
|
|
242
|
+
mcpTools: {},
|
|
241
243
|
};
|
|
242
244
|
|
|
243
245
|
for (const r of records) {
|
|
@@ -308,10 +310,51 @@ export function computeUsageStats(records, scenarioKeywords, costMode = 'auto')
|
|
|
308
310
|
}
|
|
309
311
|
}
|
|
310
312
|
|
|
311
|
-
// Tools (toolCalls) -
|
|
313
|
+
// Tools (toolCalls) - calls: 总调用次数, uses: 使用次数(同一 record 内同名工具只算一次)
|
|
312
314
|
const toolCalls = r.metadata?.toolCalls || r.toolCalls || [];
|
|
313
315
|
for (const tc of toolCalls) {
|
|
314
|
-
stats.tools[tc.name]
|
|
316
|
+
if (!stats.tools[tc.name]) stats.tools[tc.name] = { calls: 0, uses: 0 };
|
|
317
|
+
stats.tools[tc.name].calls++;
|
|
318
|
+
if (projectName && stats.projects[projectName]) {
|
|
319
|
+
if (!stats.projects[projectName].tools) stats.projects[projectName].tools = {};
|
|
320
|
+
if (!stats.projects[projectName].tools[tc.name]) stats.projects[projectName].tools[tc.name] = { calls: 0, uses: 0 };
|
|
321
|
+
stats.projects[projectName].tools[tc.name].calls++;
|
|
322
|
+
}
|
|
323
|
+
// Skill 细分采集
|
|
324
|
+
if (tc.name === 'Skill' && tc.input?.skill) {
|
|
325
|
+
const sk = tc.input.skill;
|
|
326
|
+
if (!stats.skills[sk]) stats.skills[sk] = { calls: 0, uses: 0 };
|
|
327
|
+
stats.skills[sk].calls++;
|
|
328
|
+
}
|
|
329
|
+
// MCP 细分采集
|
|
330
|
+
if (tc.name.startsWith('mcp__')) {
|
|
331
|
+
if (!stats.mcpTools[tc.name]) stats.mcpTools[tc.name] = { calls: 0, uses: 0 };
|
|
332
|
+
stats.mcpTools[tc.name].calls++;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
const uniqueToolNames = new Set(toolCalls.map(tc => tc.name));
|
|
336
|
+
for (const name of uniqueToolNames) {
|
|
337
|
+
if (!stats.tools[name]) stats.tools[name] = { calls: 0, uses: 0 };
|
|
338
|
+
stats.tools[name].uses++;
|
|
339
|
+
if (projectName && stats.projects[projectName] && stats.projects[projectName].tools[name]) {
|
|
340
|
+
stats.projects[projectName].tools[name].uses++;
|
|
341
|
+
}
|
|
342
|
+
// 同步更新 skills / mcpTools 的 uses
|
|
343
|
+
if (name === 'Skill') {
|
|
344
|
+
const skillNames = new Set(
|
|
345
|
+
toolCalls
|
|
346
|
+
.filter(tc => tc.name === 'Skill' && tc.input?.skill)
|
|
347
|
+
.map(tc => tc.input.skill)
|
|
348
|
+
);
|
|
349
|
+
for (const sk of skillNames) {
|
|
350
|
+
if (!stats.skills[sk]) stats.skills[sk] = { calls: 0, uses: 0 };
|
|
351
|
+
stats.skills[sk].uses++;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (name.startsWith('mcp__')) {
|
|
355
|
+
if (!stats.mcpTools[name]) stats.mcpTools[name] = { calls: 0, uses: 0 };
|
|
356
|
+
stats.mcpTools[name].uses++;
|
|
357
|
+
}
|
|
315
358
|
}
|
|
316
359
|
|
|
317
360
|
// Daily stats
|
|
@@ -406,6 +449,7 @@ export function computeUsageStats(records, scenarioKeywords, costMode = 'auto')
|
|
|
406
449
|
cacheCreate: p.cacheCreate || 0,
|
|
407
450
|
estimatedCost: p.estimatedCost || 0,
|
|
408
451
|
models: p.models || {},
|
|
452
|
+
tools: p.tools || {},
|
|
409
453
|
};
|
|
410
454
|
}
|
|
411
455
|
|
|
@@ -488,12 +532,23 @@ export function computeTrendData(allRecords, period, refDate) {
|
|
|
488
532
|
if (!r.timestamp) continue;
|
|
489
533
|
const date = r.timestamp.slice(0, 10);
|
|
490
534
|
if (date < trendStart || date > trendEnd) continue;
|
|
491
|
-
if (!dailyStats[date]) dailyStats[date] = { requests: 0, inputTokens: 0, outputTokens: 0 };
|
|
535
|
+
if (!dailyStats[date]) dailyStats[date] = { requests: 0, inputTokens: 0, outputTokens: 0, tools: {} };
|
|
492
536
|
if (isAssistantRecord(r)) {
|
|
493
537
|
dailyStats[date].requests++;
|
|
494
538
|
dailyStats[date].inputTokens += getInputTokens(r);
|
|
495
539
|
dailyStats[date].outputTokens += getOutputTokens(r);
|
|
496
540
|
}
|
|
541
|
+
const toolCalls = r.metadata?.toolCalls || r.toolCalls || [];
|
|
542
|
+
for (const tc of toolCalls) {
|
|
543
|
+
if (tc.name === 'Skill' || tc.name?.startsWith('mcp__')) continue;
|
|
544
|
+
if (!dailyStats[date].tools[tc.name]) dailyStats[date].tools[tc.name] = { calls: 0, uses: 0 };
|
|
545
|
+
dailyStats[date].tools[tc.name].calls++;
|
|
546
|
+
}
|
|
547
|
+
const uniqueNames = new Set(toolCalls.map(tc => tc.name).filter(n => n !== 'Skill' && !n?.startsWith('mcp__')));
|
|
548
|
+
for (const name of uniqueNames) {
|
|
549
|
+
if (!dailyStats[date].tools[name]) dailyStats[date].tools[name] = { calls: 0, uses: 0 };
|
|
550
|
+
dailyStats[date].tools[name].uses++;
|
|
551
|
+
}
|
|
497
552
|
}
|
|
498
553
|
|
|
499
554
|
return { dailyStats, start: trendStart, end: trendEnd };
|
package/lib/config.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
|
2
2
|
import { join, dirname } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
4
|
import { DEFAULT_ATTRIBUTION_OPTIONS } from './git-attribution-options.js';
|
|
5
|
+
import { parseRepoPaths } from './path-utils.js';
|
|
5
6
|
|
|
6
7
|
const CONFIG_LOCATIONS = [
|
|
7
8
|
join(homedir(), '.lumencode.json'),
|
|
@@ -19,12 +20,13 @@ const DEFAULT_CONFIG = {
|
|
|
19
20
|
blockQuota: null, // 5h 计费窗口 token 上限(Max Pro=1000000, Max=450000 等),null=不限
|
|
20
21
|
costMode: 'auto', // 'auto' | 'calculate' | 'display'
|
|
21
22
|
scenarioKeywords: {
|
|
22
|
-
coding: ['实现', '功能', '开发', '添加', '修改代码', 'implement', 'feature', '
|
|
23
|
-
testing: ['测试', 'test', 'spec', '覆盖率', 'coverage', '单元测试', 'unit test', 'jest', 'vitest', 'mocha'],
|
|
24
|
-
debugging: ['修复', 'bug', 'debug', 'fix', '报错', '错误', '异常', 'error', '
|
|
25
|
-
documentation: ['文档', '
|
|
26
|
-
review: ['review', '审查', '
|
|
27
|
-
planning: ['计划', 'plan', '设计', '架构', '方案', 'design', 'architect'],
|
|
23
|
+
coding: ['实现', '功能', '开发', '添加', '修改代码', 'implement', 'feature', '组件', 'component', '编写', 'write code'],
|
|
24
|
+
testing: ['测试', 'test', 'spec', '覆盖率', 'coverage', '单元测试', 'unit test', 'jest', 'vitest', 'mocha', 'cypress', 'playwright'],
|
|
25
|
+
debugging: ['修复', 'bug', 'debug', 'fix', '报错', '错误', '异常', 'error', '排查', '堆栈', 'trace', 'stack trace', 'crash'],
|
|
26
|
+
documentation: ['文档', 'readme.md', '注释', '说明', '指南', 'guide', 'wiki', '手册', 'api doc'],
|
|
27
|
+
review: ['review', '审查', '代码审查', '/review', 'pr', 'pull request', 'approve', 'approval', 'reject', '走查', '代码走查'],
|
|
28
|
+
planning: ['计划', 'plan', '设计', '架构', '方案', 'design', 'architect', 'roadmap', '规划'],
|
|
29
|
+
refactoring: ['重构', 'refactor', '重写', 'rewrite', '清理代码', 'clean up', '简化', 'simplify', '提取', 'extract'],
|
|
28
30
|
},
|
|
29
31
|
aiAttribution: DEFAULT_ATTRIBUTION_OPTIONS,
|
|
30
32
|
stepTracking: {
|
|
@@ -51,6 +53,17 @@ function deepMerge(target, source) {
|
|
|
51
53
|
return result;
|
|
52
54
|
}
|
|
53
55
|
|
|
56
|
+
function normalizeConfig(config) {
|
|
57
|
+
// 支持 repos / excludeProjects 为字符串(逗号或换行分隔)
|
|
58
|
+
if (config.repos !== undefined) {
|
|
59
|
+
config.repos = parseRepoPaths(config.repos);
|
|
60
|
+
}
|
|
61
|
+
if (config.excludeProjects !== undefined) {
|
|
62
|
+
config.excludeProjects = parseRepoPaths(config.excludeProjects);
|
|
63
|
+
}
|
|
64
|
+
return config;
|
|
65
|
+
}
|
|
66
|
+
|
|
54
67
|
export function loadConfig(configPath) {
|
|
55
68
|
let config = { ...DEFAULT_CONFIG };
|
|
56
69
|
|
|
@@ -60,7 +73,7 @@ export function loadConfig(configPath) {
|
|
|
60
73
|
try {
|
|
61
74
|
const userConfig = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
62
75
|
config = deepMerge(config, userConfig);
|
|
63
|
-
return config;
|
|
76
|
+
return normalizeConfig(config);
|
|
64
77
|
} catch (e) {
|
|
65
78
|
console.error(`配置文件读取失败: ${configPath}`, e.message);
|
|
66
79
|
// 文件存在但解析失败,返回默认值
|
|
@@ -75,7 +88,7 @@ export function loadConfig(configPath) {
|
|
|
75
88
|
try {
|
|
76
89
|
const userConfig = JSON.parse(readFileSync(p, 'utf-8'));
|
|
77
90
|
config = deepMerge(config, userConfig);
|
|
78
|
-
return config;
|
|
91
|
+
return normalizeConfig(config);
|
|
79
92
|
} catch (e) {
|
|
80
93
|
console.error(`配置文件读取失败: ${p}`, e.message);
|
|
81
94
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 解析仓库路径字符串,支持多种分隔符:
|
|
3
|
+
* - 英文逗号 ,
|
|
4
|
+
* - 中文逗号 ,
|
|
5
|
+
* - 换行 \n / \r\n
|
|
6
|
+
* - 多余空白自动 trim
|
|
7
|
+
*
|
|
8
|
+
* @param {string|Array} input - 路径字符串或已解析的数组
|
|
9
|
+
* @returns {string[]} 解析后的路径数组
|
|
10
|
+
*/
|
|
11
|
+
export function parseRepoPaths(input) {
|
|
12
|
+
if (Array.isArray(input)) return input.map(s => String(s || '').trim()).filter(Boolean);
|
|
13
|
+
if (typeof input !== 'string') return [];
|
|
14
|
+
return input
|
|
15
|
+
.split(/[,,\n\r]+/)
|
|
16
|
+
.map(s => s.trim())
|
|
17
|
+
.filter(Boolean);
|
|
18
|
+
}
|