lumencode 1.1.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.
- package/README.md +41 -0
- package/hooks/claude-post-tool-batch.js +51 -0
- package/hooks/codex-hook.js +56 -0
- package/hooks/init-steps.js +10 -0
- package/hooks/install-codex.js +9 -0
- package/hooks/install.js +14 -0
- package/hooks/opencode-hook.js +45 -0
- package/hooks/post-tool-use.js +42 -0
- package/index.js +236 -22
- package/lib/aggregate.js +27 -9
- package/lib/attribution.js +13 -0
- package/lib/capture-recorder.js +141 -0
- package/lib/config.js +26 -2
- package/lib/git-attribution-candidates.js +37 -0
- package/lib/git-attribution-options.js +105 -0
- package/lib/git-paths.js +41 -0
- package/lib/git.js +581 -129
- package/lib/hooks-manager.js +379 -0
- package/lib/line-blame.js +140 -0
- package/lib/parser.js +40 -18
- package/lib/parsers/base.js +69 -67
- package/lib/parsers/claude.js +51 -53
- package/lib/parsers/codex.js +21 -9
- package/lib/parsers/index.js +153 -151
- package/lib/parsers/opencode.js +28 -20
- package/lib/report.js +3 -3
- package/lib/server.js +242 -29
- package/lib/step-schema.js +217 -0
- package/lib/step-tracker.js +323 -0
- package/package.json +8 -2
- package/public/api.js +21 -0
- package/public/app.js +127 -2
- package/public/config.js +2 -0
- package/public/git-insights.js +19 -0
- package/public/index.html +69 -0
- package/public/style.css +85 -1
package/lib/parsers/index.js
CHANGED
|
@@ -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
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
filteredBreakdown[t] =
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
+
}
|
package/lib/parsers/opencode.js
CHANGED
|
@@ -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 数据库文件大小上限(
|
|
7
|
-
const MAX_DB_SIZE =
|
|
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
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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 -
|
|
137
|
-
output: Math.max(0, current.output -
|
|
138
|
-
cacheRead: Math.max(0, current.cacheRead -
|
|
139
|
-
cacheWrite: Math.max(0, current.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
|
-
|
|
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 (
|
|
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' ? '' :
|
|
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), '']);
|