lumencode 1.0.0 → 1.1.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/index.js +74 -63
- package/lib/git.js +51 -18
- package/lib/parsers/claude.js +321 -316
- package/lib/parsers/codex.js +360 -316
- package/lib/parsers/opencode.js +236 -216
- package/lib/record-utils.js +36 -35
- package/lib/report.js +41 -4
- package/lib/server.js +573 -523
- package/package.json +1 -1
- package/public/app.js +827 -809
- package/public/style.css +3 -2
package/lib/parsers/opencode.js
CHANGED
|
@@ -1,216 +1,236 @@
|
|
|
1
|
-
import { existsSync, readFileSync, statSync } from 'fs';
|
|
2
|
-
import { join } from 'path';
|
|
3
|
-
import { BaseParser } from './base.js';
|
|
4
|
-
import { createUsageRecord } from '../models/usage-record.js';
|
|
5
|
-
|
|
6
|
-
// SQLite 数据库文件大小上限(500MB),超过则跳过解析避免 OOM
|
|
7
|
-
const MAX_DB_SIZE = 500 * 1024 * 1024;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
}
|
|
1
|
+
import { existsSync, readFileSync, statSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { BaseParser } from './base.js';
|
|
4
|
+
import { createUsageRecord } from '../models/usage-record.js';
|
|
5
|
+
|
|
6
|
+
// SQLite 数据库文件大小上限(500MB),超过则跳过解析避免 OOM
|
|
7
|
+
const MAX_DB_SIZE = 500 * 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
// 增量解析缓存:基于 opencode.db 的 mtime
|
|
10
|
+
const _opencodeCache = {
|
|
11
|
+
dbPath: '',
|
|
12
|
+
mtimeMs: 0,
|
|
13
|
+
size: 0,
|
|
14
|
+
records: null,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export class OpencodeParser extends BaseParser {
|
|
18
|
+
getInfo() {
|
|
19
|
+
return {
|
|
20
|
+
name: 'opencode',
|
|
21
|
+
displayName: 'OpenCode',
|
|
22
|
+
defaultDir: '~/.local/share/opencode',
|
|
23
|
+
envVar: 'OPENCODE_DATA_DIR',
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async detect(config) {
|
|
28
|
+
const dir = this.getDataDir(config);
|
|
29
|
+
if (!dir) return false;
|
|
30
|
+
try {
|
|
31
|
+
return existsSync(join(dir, 'opencode.db'));
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async parse(config, options = {}) {
|
|
38
|
+
const dir = this.getDataDir(config);
|
|
39
|
+
const records = [];
|
|
40
|
+
if (!dir) return records;
|
|
41
|
+
|
|
42
|
+
const dbPath = join(dir, 'opencode.db');
|
|
43
|
+
if (!existsSync(dbPath)) return records;
|
|
44
|
+
|
|
45
|
+
// 检查文件大小,避免大文件导致 Array buffer allocation failed
|
|
46
|
+
const fileSize = statSync(dbPath).size;
|
|
47
|
+
if (fileSize > MAX_DB_SIZE) {
|
|
48
|
+
console.warn(`OpenCode: opencode.db 过大 (${(fileSize / 1024 / 1024).toFixed(0)}MB),跳过解析`);
|
|
49
|
+
return records;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 增量缓存:如果 db 文件未变化,直接返回缓存
|
|
53
|
+
const { mtimeMs } = statSync(dbPath);
|
|
54
|
+
if (_opencodeCache.dbPath === dbPath && _opencodeCache.mtimeMs === mtimeMs && _opencodeCache.size === fileSize && _opencodeCache.records) {
|
|
55
|
+
return _opencodeCache.records;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const initSqlJs = (await import('sql.js')).default;
|
|
60
|
+
const SQL = await initSqlJs();
|
|
61
|
+
const dbBuf = readFileSync(dbPath);
|
|
62
|
+
const db = new SQL.Database(dbBuf);
|
|
63
|
+
|
|
64
|
+
// 读取 session -> project 映射
|
|
65
|
+
const sessionMap = {};
|
|
66
|
+
// 不同版本 schema 不同,探测可用列
|
|
67
|
+
let sessCols;
|
|
68
|
+
try {
|
|
69
|
+
const colInfo = db.exec("PRAGMA table_info(session)");
|
|
70
|
+
sessCols = colInfo[0] ? colInfo[0].values.map(r => r[1]) : [];
|
|
71
|
+
} catch { sessCols = []; }
|
|
72
|
+
const hasPath = sessCols.includes('path');
|
|
73
|
+
const hasDir = sessCols.includes('directory');
|
|
74
|
+
const sessSelect = hasDir
|
|
75
|
+
? `SELECT id, directory${hasPath ? ', path' : ''} FROM session`
|
|
76
|
+
: 'SELECT id FROM session';
|
|
77
|
+
try {
|
|
78
|
+
const sessRows = db.exec(sessSelect);
|
|
79
|
+
if (sessRows[0]) {
|
|
80
|
+
for (const row of sessRows[0].values) {
|
|
81
|
+
const id = row[0];
|
|
82
|
+
const dir = hasDir ? (row[1] || '') : '';
|
|
83
|
+
const p = hasPath ? (row[2] || '') : '';
|
|
84
|
+
sessionMap[id] = (p || dir || '').replace(/\\/g, '/');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
} catch {}
|
|
88
|
+
|
|
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; }
|
|
94
|
+
|
|
95
|
+
// 计算 delta tokens(message 中 tokens 是累计值)
|
|
96
|
+
let lastTokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
97
|
+
|
|
98
|
+
for (const [msgId, sessionId, timeCreated, dataStr] of msgRows[0].values) {
|
|
99
|
+
let data;
|
|
100
|
+
try { data = JSON.parse(dataStr); } catch { continue; }
|
|
101
|
+
|
|
102
|
+
const role = data.role || '';
|
|
103
|
+
const timestamp = new Date(timeCreated).toISOString();
|
|
104
|
+
const project = sessionMap[sessionId] || '';
|
|
105
|
+
const model = data.modelID || data.model?.modelID || '';
|
|
106
|
+
|
|
107
|
+
// User messages for scenario classification
|
|
108
|
+
if (role === 'user') {
|
|
109
|
+
const text = this._extractUserText(db, msgId);
|
|
110
|
+
if (text) {
|
|
111
|
+
records.push(createUsageRecord({
|
|
112
|
+
timestamp,
|
|
113
|
+
tool: 'opencode',
|
|
114
|
+
sessionId: sessionId || '',
|
|
115
|
+
model: '',
|
|
116
|
+
inputTokens: 0,
|
|
117
|
+
outputTokens: 0,
|
|
118
|
+
project,
|
|
119
|
+
metadata: { type: 'user', text },
|
|
120
|
+
}));
|
|
121
|
+
}
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Assistant messages with token usage
|
|
126
|
+
if (role === 'assistant' && data.tokens) {
|
|
127
|
+
const t = data.tokens;
|
|
128
|
+
const current = {
|
|
129
|
+
input: t.input || 0,
|
|
130
|
+
output: t.output || 0,
|
|
131
|
+
cacheRead: t.cache?.read || 0,
|
|
132
|
+
cacheWrite: t.cache?.write || 0,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
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),
|
|
140
|
+
};
|
|
141
|
+
lastTokens = current;
|
|
142
|
+
|
|
143
|
+
// Collect tool calls from parts
|
|
144
|
+
const toolCalls = this._extractToolCalls(db, msgId);
|
|
145
|
+
|
|
146
|
+
if (delta.input > 0 || delta.output > 0) {
|
|
147
|
+
records.push(createUsageRecord({
|
|
148
|
+
timestamp,
|
|
149
|
+
tool: 'opencode',
|
|
150
|
+
sessionId: sessionId || '',
|
|
151
|
+
model,
|
|
152
|
+
inputTokens: delta.input,
|
|
153
|
+
outputTokens: delta.output,
|
|
154
|
+
cacheReadTokens: delta.cacheRead,
|
|
155
|
+
cacheWriteTokens: delta.cacheWrite,
|
|
156
|
+
costUSD: data.cost ?? null,
|
|
157
|
+
project,
|
|
158
|
+
metadata: {
|
|
159
|
+
type: 'assistant',
|
|
160
|
+
toolCalls,
|
|
161
|
+
reasoningOutputTokens: t.reasoning || 0,
|
|
162
|
+
},
|
|
163
|
+
}));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
db.close();
|
|
169
|
+
|
|
170
|
+
// 更新增量缓存
|
|
171
|
+
_opencodeCache.dbPath = dbPath;
|
|
172
|
+
_opencodeCache.mtimeMs = mtimeMs;
|
|
173
|
+
_opencodeCache.size = fileSize;
|
|
174
|
+
_opencodeCache.records = records;
|
|
175
|
+
} catch (err) {
|
|
176
|
+
console.warn('OpenCode parse error:', err.message);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return records;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
_extractUserText(db, msgId) {
|
|
183
|
+
try {
|
|
184
|
+
const rows = db.exec(
|
|
185
|
+
"SELECT data FROM part WHERE message_id = ? AND json_extract(data, '$.type') = 'text'",
|
|
186
|
+
[msgId]
|
|
187
|
+
);
|
|
188
|
+
if (rows[0]?.values?.length) {
|
|
189
|
+
const parts = [];
|
|
190
|
+
for (const [dataStr] of rows[0].values) {
|
|
191
|
+
const d = JSON.parse(dataStr);
|
|
192
|
+
if (d.text) parts.push(d.text);
|
|
193
|
+
}
|
|
194
|
+
return parts.join(' ').trim();
|
|
195
|
+
}
|
|
196
|
+
} catch {}
|
|
197
|
+
return '';
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
_extractToolCalls(db, msgId) {
|
|
201
|
+
const calls = [];
|
|
202
|
+
try {
|
|
203
|
+
const rows = db.exec(
|
|
204
|
+
"SELECT data FROM part WHERE message_id = ? AND json_extract(data, '$.type') = 'tool'",
|
|
205
|
+
[msgId]
|
|
206
|
+
);
|
|
207
|
+
if (rows[0]?.values) {
|
|
208
|
+
for (const [dataStr] of rows[0].values) {
|
|
209
|
+
const d = JSON.parse(dataStr);
|
|
210
|
+
const name = d.name || d.tool || 'unknown';
|
|
211
|
+
calls.push({ name });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
} catch {}
|
|
215
|
+
return calls;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async getVersion(config) {
|
|
219
|
+
const dir = this.getDataDir(config);
|
|
220
|
+
if (!dir) return null;
|
|
221
|
+
const dbPath = join(dir, 'opencode.db');
|
|
222
|
+
if (!existsSync(dbPath)) return null;
|
|
223
|
+
try {
|
|
224
|
+
if (statSync(dbPath).size > MAX_DB_SIZE) return null;
|
|
225
|
+
const initSqlJs = (await import('sql.js')).default;
|
|
226
|
+
const SQL = await initSqlJs();
|
|
227
|
+
const dbBuf = readFileSync(dbPath);
|
|
228
|
+
const db = new SQL.Database(dbBuf);
|
|
229
|
+
const rows = db.exec("SELECT value FROM settings WHERE key = 'version'");
|
|
230
|
+
db.close();
|
|
231
|
+
return rows[0]?.values?.[0]?.[0] || null;
|
|
232
|
+
} catch {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
package/lib/record-utils.js
CHANGED
|
@@ -1,35 +1,36 @@
|
|
|
1
|
-
// UsageRecord 兼容辅助函数
|
|
2
|
-
// 统一处理新格式(inputTokens/outputTokens)和旧格式(tokens.input/tokens.output)
|
|
3
|
-
|
|
4
|
-
export function getInputTokens(r) {
|
|
5
|
-
if (r.inputTokens !== undefined) return r.inputTokens;
|
|
6
|
-
return r.tokens?.input || 0;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function getOutputTokens(r) {
|
|
10
|
-
if (r.outputTokens !== undefined) return r.outputTokens;
|
|
11
|
-
return r.tokens?.output || 0;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function getCacheRead(r) {
|
|
15
|
-
if (r.cacheReadTokens !== undefined) return r.cacheReadTokens;
|
|
16
|
-
return r.tokens?.cacheRead || 0;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function getCacheCreate(r) {
|
|
20
|
-
if (r.cacheWriteTokens !== undefined) return r.cacheWriteTokens;
|
|
21
|
-
return r.tokens?.cacheCreate || 0;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function getModel(r) {
|
|
25
|
-
return r.model || '';
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function isAssistantRecord(r) {
|
|
29
|
-
if (r.metadata?.type === 'assistant') return true;
|
|
30
|
-
if (r.metadata?.type === 'user') return false;
|
|
31
|
-
if (r.tool === 'codex') return true;
|
|
32
|
-
if (r.tool === 'opencode' && r.metadata?.role !== 'user') return true;
|
|
33
|
-
|
|
34
|
-
return
|
|
35
|
-
|
|
1
|
+
// UsageRecord 兼容辅助函数
|
|
2
|
+
// 统一处理新格式(inputTokens/outputTokens)和旧格式(tokens.input/tokens.output)
|
|
3
|
+
|
|
4
|
+
export function getInputTokens(r) {
|
|
5
|
+
if (r.inputTokens !== undefined) return r.inputTokens;
|
|
6
|
+
return r.tokens?.input || 0;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getOutputTokens(r) {
|
|
10
|
+
if (r.outputTokens !== undefined) return r.outputTokens;
|
|
11
|
+
return r.tokens?.output || 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getCacheRead(r) {
|
|
15
|
+
if (r.cacheReadTokens !== undefined) return r.cacheReadTokens;
|
|
16
|
+
return r.tokens?.cacheRead || 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getCacheCreate(r) {
|
|
20
|
+
if (r.cacheWriteTokens !== undefined) return r.cacheWriteTokens;
|
|
21
|
+
return r.tokens?.cacheCreate || 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getModel(r) {
|
|
25
|
+
return r.model || '';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function isAssistantRecord(r) {
|
|
29
|
+
if (r.metadata?.type === 'assistant') return true;
|
|
30
|
+
if (r.metadata?.type === 'user') return false;
|
|
31
|
+
if (r.tool === 'codex') return true;
|
|
32
|
+
if (r.tool === 'opencode' && r.metadata?.role !== 'user') return true;
|
|
33
|
+
// 兼容 Claude Code 新版日志:type 可能统一为 'user',用 role 区分 user/assistant
|
|
34
|
+
if (!r.tool && (r.type === 'assistant' || r.role === 'assistant')) return true;
|
|
35
|
+
return false;
|
|
36
|
+
}
|
package/lib/report.js
CHANGED
|
@@ -144,7 +144,17 @@ function buildGitNarrative(git, periodName) {
|
|
|
144
144
|
const totalLines = ai.totalLinesChanged || 1;
|
|
145
145
|
const aiLinePct = Math.round((ai.aiLinesChanged / totalLines) * 100);
|
|
146
146
|
const commitPct = Math.round((ai.aiCommitRatio ?? (ai.aiCommits / git.commits)) * 100);
|
|
147
|
-
|
|
147
|
+
const possibleCommitPct = ai.possibleAICommits > 0 ? Math.round((ai.possibleAICommits / git.commits) * 100) : 0;
|
|
148
|
+
const weightedPct = Math.round((ai.weightedAILineRatio || 0) * 100);
|
|
149
|
+
line += ` 高/中置信 AI 提交 ${ai.aiCommits}/${git.commits} (${commitPct}%),`;
|
|
150
|
+
if (ai.possibleAICommits > 0) {
|
|
151
|
+
line += `可能 AI 提交 ${ai.possibleAICommits} (${possibleCommitPct}%),`;
|
|
152
|
+
}
|
|
153
|
+
line += `AI 代码改写占比 ${aiLinePct}%`;
|
|
154
|
+
if (weightedPct > aiLinePct) {
|
|
155
|
+
line += `,加权影响力 ${weightedPct}%`;
|
|
156
|
+
}
|
|
157
|
+
line += '。';
|
|
148
158
|
}
|
|
149
159
|
|
|
150
160
|
return line;
|
|
@@ -433,10 +443,12 @@ function buildGitInsight(git) {
|
|
|
433
443
|
const ai = git.aiContribution;
|
|
434
444
|
const ratio = ai.aiCommitRatio ?? (ai.aiCommits ? ai.aiCommits / git.commits : 0);
|
|
435
445
|
const commitPct = Math.round(ratio * 100);
|
|
446
|
+
const possiblePct = Math.round((ai.possibleAICommitRatio || 0) * 100);
|
|
436
447
|
if (!isNaN(commitPct)) {
|
|
437
448
|
if (commitPct > 80) insights.push('AI 参与度极高,核心代码产出几乎全程 AI 辅助');
|
|
438
449
|
else if (commitPct > 50) insights.push('AI 参与度较高,超过半数提交有 AI 辅助');
|
|
439
|
-
else if (commitPct > 0) insights.push(
|
|
450
|
+
else if (commitPct > 0) insights.push(`高/中置信 AI 参与 ${commitPct}% 的提交,人机协作比例适中`);
|
|
451
|
+
else if (possiblePct > 0) insights.push(`无高/中置信 AI 提交,但 ${possiblePct}% 提交可能受 AI 影响`);
|
|
440
452
|
}
|
|
441
453
|
}
|
|
442
454
|
const netLines = git.linesAdded - git.linesDeleted;
|
|
@@ -602,6 +614,10 @@ export function generateReport(usageData, gitData, period, startDate, endDate) {
|
|
|
602
614
|
gitTable.addRow(['高置信提交', String(ai.highConfidenceCommits), '']);
|
|
603
615
|
gitTable.addRow(['AI 命中文件新增行', String(ai.aiFileLinesAdded), '']);
|
|
604
616
|
gitTable.addRow(['AI 命中文件删除行', String(ai.aiFileLinesDeleted), '']);
|
|
617
|
+
if (ai.possibleAICommits > 0) {
|
|
618
|
+
const possiblePct = Math.round((ai.possibleAICommits / gitData.commits) * 100);
|
|
619
|
+
gitTable.addRow(['可能 AI 提交', `${ai.possibleAICommits}/${gitData.commits}`, `${possiblePct}%`]);
|
|
620
|
+
}
|
|
605
621
|
gitTable.addRow(['低置信关联提交', String(ai.lowConfidenceCommits), '']);
|
|
606
622
|
}
|
|
607
623
|
lines.push(gitTable.render());
|
|
@@ -900,7 +916,11 @@ export function generateFeishuCard(usageData, gitData, period, startDate, endDat
|
|
|
900
916
|
if (gitData.aiContribution) {
|
|
901
917
|
const ai = gitData.aiContribution;
|
|
902
918
|
const commitPct = Math.round((ai.aiCommitRatio ?? (ai.aiCommits / gitData.commits)) * 100);
|
|
903
|
-
fields.push({ is_short: true, text: { tag: 'lark_md', content: `**AI
|
|
919
|
+
fields.push({ is_short: true, text: { tag: 'lark_md', content: `**AI 提交**\n${ai.aiCommits}/${gitData.commits} (${commitPct}%)` } });
|
|
920
|
+
if (ai.possibleAICommits > 0) {
|
|
921
|
+
const possiblePct = Math.round((ai.possibleAICommits / gitData.commits) * 100);
|
|
922
|
+
fields.push({ is_short: true, text: { tag: 'lark_md', content: `**可能 AI**\n${ai.possibleAICommits} (${possiblePct}%)` } });
|
|
923
|
+
}
|
|
904
924
|
}
|
|
905
925
|
}
|
|
906
926
|
if (usageData.estimatedCost) {
|
|
@@ -1024,7 +1044,16 @@ export function generateBriefReport(usageData, gitData, period, startDate, endDa
|
|
|
1024
1044
|
const totalLines = ai.totalLinesChanged || 1;
|
|
1025
1045
|
const aiLinePct = Math.round((ai.aiLinesChanged / totalLines) * 100);
|
|
1026
1046
|
const commitPct = Math.round((ai.aiCommitRatio ?? (ai.aiCommits / gitData.commits)) * 100);
|
|
1027
|
-
|
|
1047
|
+
const possibleCommitPct = ai.possibleAICommits > 0 ? Math.round((ai.possibleAICommits / gitData.commits) * 100) : 0;
|
|
1048
|
+
const weightedPct = Math.round((ai.weightedAILineRatio || 0) * 100);
|
|
1049
|
+
let line = `${aiLinePct}% 代码变更有 AI 参与,${ai.aiCommits}/${gitData.commits} 提交使用 AI (${commitPct}%)`;
|
|
1050
|
+
if (ai.possibleAICommits > 0) {
|
|
1051
|
+
line += `,可能 AI 提交 ${ai.possibleAICommits} (${possibleCommitPct}%)`;
|
|
1052
|
+
}
|
|
1053
|
+
if (weightedPct > aiLinePct) {
|
|
1054
|
+
line += `,加权影响力 ${weightedPct}%`;
|
|
1055
|
+
}
|
|
1056
|
+
lines.push(bullet(line));
|
|
1028
1057
|
}
|
|
1029
1058
|
|
|
1030
1059
|
if (gitData.commitList?.length) {
|
|
@@ -1292,6 +1321,14 @@ export function generateWorkReport(usageData, gitData, period, startDate, endDat
|
|
|
1292
1321
|
const aiLinePct = Math.round(((gitData.aiContribution?.aiLineRatio ?? gitData.aiContribution?.aiRatio) || 0) * 100);
|
|
1293
1322
|
sectionLines.push(`- 高/中置信 AI 提交 **${totalAI}/${totalCommits}**(${aiLinePct}%),涉及 +${formatInt(aiDetail.totalAIFileAdded)}/-${formatInt(aiDetail.totalAIFileDeleted)} 行`);
|
|
1294
1323
|
|
|
1324
|
+
if (gitData.aiContribution?.possibleAICommits > 0) {
|
|
1325
|
+
const possiblePct = Math.round((gitData.aiContribution.possibleAICommits / totalCommits) * 100);
|
|
1326
|
+
sectionLines.push(`- 可能 AI 提交 **${gitData.aiContribution.possibleAICommits}/${totalCommits}**(${possiblePct}%)`);
|
|
1327
|
+
}
|
|
1328
|
+
if (gitData.aiContribution?.weightedAILineRatio > 0) {
|
|
1329
|
+
sectionLines.push(`- 加权 AI 影响力 **${Math.round(gitData.aiContribution.weightedAILineRatio * 100)}%**`);
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1295
1332
|
// 汇总统计(不列出具体 commit subject,工作汇报中无阅读价值)
|
|
1296
1333
|
const parts = [];
|
|
1297
1334
|
if (aiDetail.explicit.length > 0) parts.push(`显式标记 ${aiDetail.explicit.length} 项`);
|