lumencode 1.2.0 → 1.3.0

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.
@@ -1,151 +1,153 @@
1
- import { BaseParser } from './base.js';
2
-
3
- // 解析器注册表 - 新增工具时在此注册
4
- const PARSER_REGISTRY = [];
5
-
6
- /**
7
- * 注册解析器类
8
- * @param {typeof BaseParser} ParserClass
9
- */
10
- export function registerParser(ParserClass) {
11
- if (!(ParserClass.prototype instanceof BaseParser)) {
12
- throw new Error('注册的解析器必须继承 BaseParser');
13
- }
14
- PARSER_REGISTRY.push(ParserClass);
15
- }
16
-
17
- /**
18
- * 获取所有已注册的解析器实例
19
- * @returns {BaseParser[]}
20
- */
21
- export function getAllParsers() {
22
- return PARSER_REGISTRY.map(P => new P());
23
- }
24
-
25
- /**
26
- * 检测哪些工具可用(数据目录存在)
27
- * @param {Object} config
28
- * @returns {Promise<Array<{name, displayName, detected, dataDir}>>}
29
- */
30
- export async function detectAvailableTools(config) {
31
- const results = [];
32
- for (const P of PARSER_REGISTRY) {
33
- const parser = new P();
34
- const info = parser.getInfo();
35
- const dataDir = parser.getDataDir(config);
36
- let detected = false;
37
- try {
38
- detected = await parser.detect(config);
39
- } catch {
40
- detected = false;
41
- }
42
- let version = null;
43
- try {
44
- version = await parser.getVersion(config);
45
- } catch {}
46
- results.push({
47
- name: info.name,
48
- displayName: info.displayName,
49
- detected,
50
- dataDir,
51
- version,
52
- });
53
- }
54
- return results;
55
- }
56
-
57
- /**
58
- * 获取用户启用的工具列表
59
- * @param {Object} config
60
- * @param {Array} availableTools - detectAvailableTools 的结果
61
- * @returns {Array<string>} 工具名称列表
62
- */
63
- export function getEnabledTools(config, availableTools) {
64
- if (config.enabledTools && config.enabledTools.length > 0) {
65
- return config.enabledTools;
66
- }
67
- // 默认启用所有检测到的工具
68
- return availableTools.filter(t => t.detected).map(t => t.name);
69
- }
70
-
71
- /**
72
- * 解析指定工具的数据
73
- * @param {string} toolName
74
- * @param {Object} config
75
- * @param {Object} options
76
- * @returns {Promise<Array>}
77
- */
78
- export async function parseTool(toolName, config, options = {}) {
79
- for (const P of PARSER_REGISTRY) {
80
- const parser = new P();
81
- if (parser.getInfo().name === toolName) {
82
- return parser.parse(config, options);
83
- }
84
- }
85
- throw new Error(`未找到工具解析器: ${toolName}`);
86
- }
87
-
88
- // 统一的项目名规范化:取路径最后一段作为项目名
89
- function normalizeProjectToBase(projectPath) {
90
- if (!projectPath) return '';
91
- const normalized = projectPath.replace(/\\/g, '/').replace(/\/$/, '');
92
- const parts = normalized.split('/');
93
- return parts[parts.length - 1] || '';
94
- }
95
-
96
- /**
97
- * 解析所有已启用工具的数据
98
- * @param {Object} config
99
- * @param {Object} options
100
- * @returns {Promise<{records: Array, toolBreakdown: Object}>}
101
- */
102
- export async function parseAllEnabledTools(config, options = {}) {
103
- const available = await detectAvailableTools(config);
104
- const enabled = getEnabledTools(config, available);
105
- let allRecords = [];
106
- const toolBreakdown = {};
107
-
108
- for (const toolName of enabled) {
109
- try {
110
- const records = await parseTool(toolName, config, options);
111
- allRecords.push(...records);
112
- toolBreakdown[toolName] = {
113
- recordCount: records.length,
114
- sessionCount: new Set(records.map(r => r.sessionId)).size,
115
- };
116
- } catch (err) {
117
- console.warn(`解析工具 ${toolName} 失败:`, err.message);
118
- toolBreakdown[toolName] = { recordCount: 0, sessionCount: 0, error: err.message };
119
- }
120
- }
121
-
122
- // 统一按 includeProjects 过滤
123
- // Claude 记录已在解析器中通过 encoded 目录名精确匹配,直接放行
124
- // Codex/OpenCode 记录通过 basename 匹配
125
- if (options.includeProjects && options.includeProjects.length > 0) {
126
- const allowedBases = new Set(options.includeProjects.map(p => normalizeProjectToBase(p)));
127
- allRecords = allRecords.filter(r => {
128
- if (r.tool === 'claude') return true;
129
- const base = normalizeProjectToBase(r.project);
130
- return !base || allowedBases.has(base);
131
- });
132
- }
133
-
134
- // 过滤后重新计算 toolBreakdown
135
- const toolGroups = {};
136
- for (const r of allRecords) {
137
- const t = r.tool || 'claude';
138
- if (!toolGroups[t]) toolGroups[t] = { recordCount: 0, sessions: new Set() };
139
- toolGroups[t].recordCount++;
140
- if (r.sessionId) toolGroups[t].sessions.add(r.sessionId);
141
- }
142
- const filteredBreakdown = {};
143
- for (const [t, g] of Object.entries(toolGroups)) {
144
- filteredBreakdown[t] = { recordCount: g.recordCount, sessionCount: g.sessions.size };
145
- }
146
-
147
- // 按时间戳排序
148
- allRecords.sort((a, b) => (a.timestamp || '').localeCompare(b.timestamp || ''));
149
-
150
- return { records: allRecords, toolBreakdown: filteredBreakdown };
151
- }
1
+ import { BaseParser } from './base.js';
2
+
3
+ // 解析器注册表 - 新增工具时在此注册
4
+ const PARSER_REGISTRY = [];
5
+
6
+ /**
7
+ * 注册解析器类
8
+ * @param {typeof BaseParser} ParserClass
9
+ */
10
+ export function registerParser(ParserClass) {
11
+ if (!(ParserClass.prototype instanceof BaseParser)) {
12
+ throw new Error('注册的解析器必须继承 BaseParser');
13
+ }
14
+ PARSER_REGISTRY.push(ParserClass);
15
+ }
16
+
17
+ /**
18
+ * 获取所有已注册的解析器实例
19
+ * @returns {BaseParser[]}
20
+ */
21
+ export function getAllParsers() {
22
+ return PARSER_REGISTRY.map(P => new P());
23
+ }
24
+
25
+ /**
26
+ * 检测哪些工具可用(数据目录存在)
27
+ * @param {Object} config
28
+ * @returns {Promise<Array<{name, displayName, detected, dataDir}>>}
29
+ */
30
+ export async function detectAvailableTools(config) {
31
+ const results = [];
32
+ for (const P of PARSER_REGISTRY) {
33
+ const parser = new P();
34
+ const info = parser.getInfo();
35
+ const dataDir = parser.getDataDir(config);
36
+ let detected = false;
37
+ try {
38
+ detected = await parser.detect(config);
39
+ } catch {
40
+ detected = false;
41
+ }
42
+ let version = null;
43
+ try {
44
+ version = await parser.getVersion(config);
45
+ } catch (e) { /* getVersion 失败不影响主流程 */ }
46
+ results.push({
47
+ name: info.name,
48
+ displayName: info.displayName,
49
+ detected,
50
+ dataDir,
51
+ version,
52
+ });
53
+ }
54
+ return results;
55
+ }
56
+
57
+ /**
58
+ * 获取用户启用的工具列表
59
+ * @param {Object} config
60
+ * @param {Array} availableTools - detectAvailableTools 的结果
61
+ * @returns {Array<string>} 工具名称列表
62
+ */
63
+ export function getEnabledTools(config, availableTools) {
64
+ if (config.enabledTools && config.enabledTools.length > 0) {
65
+ return config.enabledTools;
66
+ }
67
+ // 默认启用所有检测到的工具
68
+ return availableTools.filter(t => t.detected).map(t => t.name);
69
+ }
70
+
71
+ /**
72
+ * 解析指定工具的数据
73
+ * @param {string} toolName
74
+ * @param {Object} config
75
+ * @param {Object} options
76
+ * @returns {Promise<Array>}
77
+ */
78
+ export async function parseTool(toolName, config, options = {}) {
79
+ for (const P of PARSER_REGISTRY) {
80
+ const parser = new P();
81
+ if (parser.getInfo().name === toolName) {
82
+ return parser.parse(config, options);
83
+ }
84
+ }
85
+ throw new Error(`未找到工具解析器: ${toolName}`);
86
+ }
87
+
88
+ // 统一的项目名规范化:取路径最后一段作为项目名
89
+ function normalizeProjectToBase(projectPath) {
90
+ if (!projectPath) return '';
91
+ const normalized = projectPath.replace(/\\/g, '/').replace(/\/$/, '');
92
+ const parts = normalized.split('/');
93
+ return parts[parts.length - 1] || '';
94
+ }
95
+
96
+ /**
97
+ * 解析所有已启用工具的数据
98
+ * @param {Object} config
99
+ * @param {Object} options
100
+ * @returns {Promise<{records: Array, toolBreakdown: Object}>}
101
+ */
102
+ export async function parseAllEnabledTools(config, options = {}) {
103
+ const available = await detectAvailableTools(config);
104
+ const enabled = getEnabledTools(config, available);
105
+ let allRecords = [];
106
+ const toolBreakdown = {};
107
+
108
+ for (const toolName of enabled) {
109
+ try {
110
+ const records = await parseTool(toolName, config, options);
111
+ allRecords.push(...records);
112
+ toolBreakdown[toolName] = {
113
+ recordCount: records.length,
114
+ sessionCount: new Set(records.map(r => r.sessionId)).size,
115
+ };
116
+ } catch (err) {
117
+ console.warn(`解析工具 ${toolName} 失败:`, err.message);
118
+ toolBreakdown[toolName] = { recordCount: 0, sessionCount: 0, error: err.message };
119
+ }
120
+ }
121
+
122
+ // 统一按 includeProjects 过滤(所有工具按 basename 匹配)
123
+ // 阶段 1 已修复 Claude 项目名从 cwd 提取,basename 准确可用
124
+ if (options.includeProjects && options.includeProjects.length > 0) {
125
+ const allowedBases = new Set(options.includeProjects.map(p => normalizeProjectToBase(p)));
126
+ allRecords = allRecords.filter(r => {
127
+ const base = normalizeProjectToBase(r.project);
128
+ return !base || allowedBases.has(base);
129
+ });
130
+ }
131
+
132
+ // 过滤后重新计算 toolBreakdown,保留 0 记录的工具(诊断用)
133
+ const toolGroups = {};
134
+ for (const r of allRecords) {
135
+ const t = r.tool || 'claude';
136
+ if (!toolGroups[t]) toolGroups[t] = { recordCount: 0, sessions: new Set() };
137
+ toolGroups[t].recordCount++;
138
+ if (r.sessionId) toolGroups[t].sessions.add(r.sessionId);
139
+ }
140
+ const filteredBreakdown = {};
141
+ // 从初始 toolBreakdown 保留所有已启用工具(即使 0 记录)
142
+ for (const t of Object.keys(toolBreakdown)) {
143
+ const g = toolGroups[t];
144
+ filteredBreakdown[t] = g
145
+ ? { recordCount: g.recordCount, sessionCount: g.sessions.size }
146
+ : { recordCount: 0, sessionCount: 0 };
147
+ }
148
+
149
+ // 按时间戳排序
150
+ allRecords.sort((a, b) => (a.timestamp || '').localeCompare(b.timestamp || ''));
151
+
152
+ return { records: allRecords, toolBreakdown: filteredBreakdown };
153
+ }
@@ -3,8 +3,8 @@ import { join } from 'path';
3
3
  import { BaseParser } from './base.js';
4
4
  import { createUsageRecord } from '../models/usage-record.js';
5
5
 
6
- // SQLite 数据库文件大小上限(500MB),超过则跳过解析避免 OOM
7
- const MAX_DB_SIZE = 500 * 1024 * 1024;
6
+ // SQLite 数据库文件大小上限(100MB),超过则跳过解析避免 OOM
7
+ const MAX_DB_SIZE = 100 * 1024 * 1024;
8
8
 
9
9
  // 增量解析缓存:基于 opencode.db 的 mtime
10
10
  const _opencodeCache = {
@@ -55,11 +55,12 @@ export class OpencodeParser extends BaseParser {
55
55
  return _opencodeCache.records;
56
56
  }
57
57
 
58
+ let db;
58
59
  try {
59
60
  const initSqlJs = (await import('sql.js')).default;
60
61
  const SQL = await initSqlJs();
61
62
  const dbBuf = readFileSync(dbPath);
62
- const db = new SQL.Database(dbBuf);
63
+ db = new SQL.Database(dbBuf);
63
64
 
64
65
  // 读取 session -> project 映射
65
66
  const sessionMap = {};
@@ -84,16 +85,22 @@ export class OpencodeParser extends BaseParser {
84
85
  sessionMap[id] = (p || dir || '').replace(/\\/g, '/');
85
86
  }
86
87
  }
87
- } catch {}
88
+ } catch (e) { console.warn("[opencode] parse error", e.message); }
88
89
 
89
- // 读取所有 message
90
- const msgRows = db.exec(
91
- "SELECT id, session_id, time_created, data FROM message ORDER BY time_created"
92
- );
93
- if (!msgRows[0]) { db.close(); return records; }
90
+ // 读取所有 message(表可能不存在于精简 schema)
91
+ let msgRows;
92
+ try {
93
+ msgRows = db.exec(
94
+ "SELECT id, session_id, time_created, data FROM message ORDER BY time_created"
95
+ );
96
+ } catch (e) {
97
+ console.warn("[opencode] message 表不存在或查询失败:", e.message);
98
+ return records;
99
+ }
100
+ if (!msgRows[0]) return records;
94
101
 
95
- // 计算 delta tokens(message 中 tokens 是累计值)
96
- let lastTokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
102
+ // 计算 delta tokens(message 中 tokens 是按 session 累计的值)
103
+ const lastTokensBySession = {};
97
104
 
98
105
  for (const [msgId, sessionId, timeCreated, dataStr] of msgRows[0].values) {
99
106
  let data;
@@ -132,13 +139,14 @@ export class OpencodeParser extends BaseParser {
132
139
  cacheWrite: t.cache?.write || 0,
133
140
  };
134
141
 
142
+ const prev = lastTokensBySession[sessionId] || { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
135
143
  const delta = {
136
- input: Math.max(0, current.input - lastTokens.input),
137
- output: Math.max(0, current.output - lastTokens.output),
138
- cacheRead: Math.max(0, current.cacheRead - lastTokens.cacheRead),
139
- cacheWrite: Math.max(0, current.cacheWrite - lastTokens.cacheWrite),
144
+ input: Math.max(0, current.input - prev.input),
145
+ output: Math.max(0, current.output - prev.output),
146
+ cacheRead: Math.max(0, current.cacheRead - prev.cacheRead),
147
+ cacheWrite: Math.max(0, current.cacheWrite - prev.cacheWrite),
140
148
  };
141
- lastTokens = current;
149
+ lastTokensBySession[sessionId] = current;
142
150
 
143
151
  // Collect tool calls from parts
144
152
  const toolCalls = this._extractToolCalls(db, msgId);
@@ -165,8 +173,6 @@ export class OpencodeParser extends BaseParser {
165
173
  }
166
174
  }
167
175
 
168
- db.close();
169
-
170
176
  // 更新增量缓存
171
177
  _opencodeCache.dbPath = dbPath;
172
178
  _opencodeCache.mtimeMs = mtimeMs;
@@ -174,6 +180,8 @@ export class OpencodeParser extends BaseParser {
174
180
  _opencodeCache.records = records;
175
181
  } catch (err) {
176
182
  console.warn('OpenCode parse error:', err.message);
183
+ } finally {
184
+ if (db) db.close();
177
185
  }
178
186
 
179
187
  return records;
@@ -193,7 +201,7 @@ export class OpencodeParser extends BaseParser {
193
201
  }
194
202
  return parts.join(' ').trim();
195
203
  }
196
- } catch {}
204
+ } catch (e) { console.warn("[opencode] parse error", e.message); }
197
205
  return '';
198
206
  }
199
207
 
@@ -211,7 +219,7 @@ export class OpencodeParser extends BaseParser {
211
219
  calls.push({ name });
212
220
  }
213
221
  }
214
- } catch {}
222
+ } catch (e) { console.warn("[opencode] parse error", e.message); }
215
223
  return calls;
216
224
  }
217
225
 
package/lib/report.js CHANGED
@@ -46,7 +46,7 @@ function buildCoreNarrative(stats, periodName) {
46
46
  const reqStr = fmtN(stats.requestCount);
47
47
  const tokenStr = fmtToken(stats.totalTokens);
48
48
  const sessionStr = fmtN(stats.sessionCount);
49
- const costStr = stats.estimatedCost ? `$${stats.estimatedCost.toFixed(2)}` : null;
49
+ const costStr = stats.estimatedCost != null && stats.estimatedCost > 0 ? `$${stats.estimatedCost.toFixed(2)}` : null;
50
50
  const projCount = Object.keys(stats.projects).length;
51
51
 
52
52
  let line = `${periodName}共发起 ${reqStr} 次 AI 交互(${sessionStr} 个会话),消耗 ${tokenStr} Token`;
@@ -534,7 +534,7 @@ function fmtToken(n) {
534
534
  }
535
535
 
536
536
  function pctChange(curr, prev) {
537
- if (!prev || prev === 0) return curr > 0 ? 100 : null;
537
+ if (prev == null || prev === 0) return curr > 0 ? 100 : null;
538
538
  return Math.round((curr - prev) / prev * 100);
539
539
  }
540
540
 
@@ -578,7 +578,7 @@ export function generateReport(usageData, gitData, period, startDate, endDate) {
578
578
  overviewTable.addRow(['会话数', formatInt(totalSessions), '独立对话数']);
579
579
  overviewTable.addRow(['用户消息数', formatInt(totalUserMessages), '用户主动发出的消息']);
580
580
  overviewTable.addRow(['总请求数', formatInt(totalRequests), '含 assistant 响应']);
581
- overviewTable.addRow(['活跃天数', `${activeDays} 天`, period === 'daily' ? '' : `日均请求 ${avgPerDay} 次`]);
581
+ overviewTable.addRow(['活跃天数', `${activeDays} 天`, period === 'daily' ? '' : `活跃日均 ${avgPerDay} 次`]);
582
582
  overviewTable.addRow(['Token 总消耗', formatNumber(usageData.totalTokens), `≈ ${formatNumber(Math.round(usageData.totalTokens / (totalRequests || 1)))}/请求`]);
583
583
  overviewTable.addRow(['输入 Token', formatNumber(usageData.inputTokens), '']);
584
584
  overviewTable.addRow(['输出 Token', formatNumber(usageData.outputTokens), '']);