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.
@@ -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
- export class OpencodeParser extends BaseParser {
10
- getInfo() {
11
- return {
12
- name: 'opencode',
13
- displayName: 'OpenCode',
14
- defaultDir: '~/.local/share/opencode',
15
- envVar: 'OPENCODE_DATA_DIR',
16
- };
17
- }
18
-
19
- async detect(config) {
20
- const dir = this.getDataDir(config);
21
- if (!dir) return false;
22
- try {
23
- return existsSync(join(dir, 'opencode.db'));
24
- } catch {
25
- return false;
26
- }
27
- }
28
-
29
- async parse(config, options = {}) {
30
- const dir = this.getDataDir(config);
31
- const records = [];
32
- if (!dir) return records;
33
-
34
- const dbPath = join(dir, 'opencode.db');
35
- if (!existsSync(dbPath)) return records;
36
-
37
- // 检查文件大小,避免大文件导致 Array buffer allocation failed
38
- const fileSize = statSync(dbPath).size;
39
- if (fileSize > MAX_DB_SIZE) {
40
- console.warn(`OpenCode: opencode.db 过大 (${(fileSize / 1024 / 1024).toFixed(0)}MB),跳过解析`);
41
- return records;
42
- }
43
-
44
- try {
45
- const initSqlJs = (await import('sql.js')).default;
46
- const SQL = await initSqlJs();
47
- const dbBuf = readFileSync(dbPath);
48
- const db = new SQL.Database(dbBuf);
49
-
50
- // 读取 session -> project 映射
51
- const sessionMap = {};
52
- // 不同版本 schema 不同,探测可用列
53
- let sessCols;
54
- try {
55
- const colInfo = db.exec("PRAGMA table_info(session)");
56
- sessCols = colInfo[0] ? colInfo[0].values.map(r => r[1]) : [];
57
- } catch { sessCols = []; }
58
- const hasPath = sessCols.includes('path');
59
- const hasDir = sessCols.includes('directory');
60
- const sessSelect = hasDir
61
- ? `SELECT id, directory${hasPath ? ', path' : ''} FROM session`
62
- : 'SELECT id FROM session';
63
- try {
64
- const sessRows = db.exec(sessSelect);
65
- if (sessRows[0]) {
66
- for (const row of sessRows[0].values) {
67
- const id = row[0];
68
- const dir = hasDir ? (row[1] || '') : '';
69
- const p = hasPath ? (row[2] || '') : '';
70
- sessionMap[id] = (p || dir || '').replace(/\\/g, '/');
71
- }
72
- }
73
- } catch {}
74
-
75
- // 读取所有 message
76
- const msgRows = db.exec(
77
- "SELECT id, session_id, time_created, data FROM message ORDER BY time_created"
78
- );
79
- if (!msgRows[0]) { db.close(); return records; }
80
-
81
- // 计算 delta tokens(message 中 tokens 是累计值)
82
- let lastTokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
83
-
84
- for (const [msgId, sessionId, timeCreated, dataStr] of msgRows[0].values) {
85
- let data;
86
- try { data = JSON.parse(dataStr); } catch { continue; }
87
-
88
- const role = data.role || '';
89
- const timestamp = new Date(timeCreated).toISOString();
90
- const project = sessionMap[sessionId] || '';
91
- const model = data.modelID || data.model?.modelID || '';
92
-
93
- // User messages for scenario classification
94
- if (role === 'user') {
95
- const text = this._extractUserText(db, msgId);
96
- if (text) {
97
- records.push(createUsageRecord({
98
- timestamp,
99
- tool: 'opencode',
100
- sessionId: sessionId || '',
101
- model: '',
102
- inputTokens: 0,
103
- outputTokens: 0,
104
- project,
105
- metadata: { type: 'user', text },
106
- }));
107
- }
108
- continue;
109
- }
110
-
111
- // Assistant messages with token usage
112
- if (role === 'assistant' && data.tokens) {
113
- const t = data.tokens;
114
- const current = {
115
- input: t.input || 0,
116
- output: t.output || 0,
117
- cacheRead: t.cache?.read || 0,
118
- cacheWrite: t.cache?.write || 0,
119
- };
120
-
121
- const delta = {
122
- input: Math.max(0, current.input - lastTokens.input),
123
- output: Math.max(0, current.output - lastTokens.output),
124
- cacheRead: Math.max(0, current.cacheRead - lastTokens.cacheRead),
125
- cacheWrite: Math.max(0, current.cacheWrite - lastTokens.cacheWrite),
126
- };
127
- lastTokens = current;
128
-
129
- // Collect tool calls from parts
130
- const toolCalls = this._extractToolCalls(db, msgId);
131
-
132
- if (delta.input > 0 || delta.output > 0) {
133
- records.push(createUsageRecord({
134
- timestamp,
135
- tool: 'opencode',
136
- sessionId: sessionId || '',
137
- model,
138
- inputTokens: delta.input,
139
- outputTokens: delta.output,
140
- cacheReadTokens: delta.cacheRead,
141
- cacheWriteTokens: delta.cacheWrite,
142
- costUSD: data.cost ?? null,
143
- project,
144
- metadata: {
145
- type: 'assistant',
146
- toolCalls,
147
- reasoningOutputTokens: t.reasoning || 0,
148
- },
149
- }));
150
- }
151
- }
152
- }
153
-
154
- db.close();
155
- } catch (err) {
156
- console.warn('OpenCode parse error:', err.message);
157
- }
158
-
159
- return records;
160
- }
161
-
162
- _extractUserText(db, msgId) {
163
- try {
164
- const rows = db.exec(
165
- "SELECT data FROM part WHERE message_id = ? AND json_extract(data, '$.type') = 'text'",
166
- [msgId]
167
- );
168
- if (rows[0]?.values?.length) {
169
- const parts = [];
170
- for (const [dataStr] of rows[0].values) {
171
- const d = JSON.parse(dataStr);
172
- if (d.text) parts.push(d.text);
173
- }
174
- return parts.join(' ').trim();
175
- }
176
- } catch {}
177
- return '';
178
- }
179
-
180
- _extractToolCalls(db, msgId) {
181
- const calls = [];
182
- try {
183
- const rows = db.exec(
184
- "SELECT data FROM part WHERE message_id = ? AND json_extract(data, '$.type') = 'tool'",
185
- [msgId]
186
- );
187
- if (rows[0]?.values) {
188
- for (const [dataStr] of rows[0].values) {
189
- const d = JSON.parse(dataStr);
190
- const name = d.name || d.tool || 'unknown';
191
- calls.push({ name });
192
- }
193
- }
194
- } catch {}
195
- return calls;
196
- }
197
-
198
- async getVersion(config) {
199
- const dir = this.getDataDir(config);
200
- if (!dir) return null;
201
- const dbPath = join(dir, 'opencode.db');
202
- if (!existsSync(dbPath)) return null;
203
- try {
204
- if (statSync(dbPath).size > MAX_DB_SIZE) return null;
205
- const initSqlJs = (await import('sql.js')).default;
206
- const SQL = await initSqlJs();
207
- const dbBuf = readFileSync(dbPath);
208
- const db = new SQL.Database(dbBuf);
209
- const rows = db.exec("SELECT value FROM settings WHERE key = 'version'");
210
- db.close();
211
- return rows[0]?.values?.[0]?.[0] || null;
212
- } catch {
213
- return null;
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
+ }
@@ -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
- if (r.type === 'assistant' && !r.tool) return true;
34
- return false;
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
- line += ` 其中 ${aiLinePct}% 代码变更有 AI 参与,${ai.aiCommits}/${git.commits} 提交使用 AI (${commitPct}%)。`;
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(`AI 参与 ${commitPct}% 的提交,人机协作比例适中`);
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 代码改写占比**\n${commitPct}%` } });
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
- lines.push(bullet(`${aiLinePct}% 代码变更有 AI 参与,${ai.aiCommits}/${gitData.commits} 提交使用 AI (${commitPct}%)`));
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} 项`);