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/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] = (stats.tools[tc.name] || 0) + 1;
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', 'add', 'refactor', '重构', '组件'],
23
- testing: ['测试', 'test', 'spec', '覆盖率', 'coverage', '单元测试', 'unit test', 'jest', 'vitest', 'mocha'],
24
- debugging: ['修复', 'bug', 'debug', 'fix', '报错', '错误', '异常', 'error', 'issue', '问题', '排查', '堆栈'],
25
- documentation: ['文档', 'doc', 'readme', 'md', '注释', 'comment', '说明', '指南', 'guide'],
26
- review: ['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
+ }