ai-metrics-mcp-server 1.0.9

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.
@@ -0,0 +1,957 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execSync } from 'child_process';
4
+ import os from 'os';
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+ import crypto from 'crypto';
8
+
9
+ // 尝试加载 axios,支持降级模式
10
+ let axios = null;
11
+ let DEPENDENCY_AVAILABLE = true;
12
+
13
+ // 配置
14
+ const API_ENDPOINT = 'http://data-flow.jd.com/api/ai-metrics/code-metrics';
15
+ const USER_ID = process.env.AI_METRICS_USER_ID || '';
16
+ const DEBUG_MODE = process.env.AI_METRICS_DEBUG === 'true';
17
+
18
+ // 日志文件配置
19
+ const LOG_DIR = path.join(os.homedir(), '.ai-metrics-cache', 'debug-logs');
20
+ const LOG_FILE = path.join(LOG_DIR, `git-collector-${Date.now()}.log`);
21
+
22
+ /**
23
+ * 初始化日志目录
24
+ */
25
+ function initLogDir() {
26
+ try {
27
+ if (!fs.existsSync(LOG_DIR)) {
28
+ fs.mkdirSync(LOG_DIR, { recursive: true });
29
+ }
30
+ } catch (error) {
31
+ console.error('Failed to create log directory:', error);
32
+ }
33
+ }
34
+
35
+ /**
36
+ * 记录日志到文件和控制台
37
+ */
38
+ function log(level, message, data = null) {
39
+ const timestamp = getLocalTime();
40
+ const prefix = `[${timestamp}] [${level}]`;
41
+ const logMessage = data ? `${prefix} ${message} ${JSON.stringify(data)}` : `${prefix} ${message}`;
42
+
43
+ // 输出到控制台
44
+ if (level === 'ERROR') {
45
+ console.error(logMessage);
46
+ } else if (level === 'WARN') {
47
+ console.warn(logMessage);
48
+ } else if (DEBUG_MODE) {
49
+ console.log(logMessage);
50
+ }
51
+
52
+ // 记录到文件
53
+ try {
54
+ fs.appendFileSync(LOG_FILE, logMessage + '\n');
55
+ } catch (error) {
56
+ // 忽略日志写入失败
57
+ }
58
+ }
59
+
60
+ initLogDir();
61
+ log('INFO', `Git Collector started - DEBUG_MODE: ${DEBUG_MODE}`);
62
+ log('INFO', `LOG_FILE: ${LOG_FILE}`);
63
+
64
+ // 获取commit hash
65
+ const commitHash = process.argv[2];
66
+
67
+ if (!commitHash) {
68
+ log('ERROR', 'Commit hash is required');
69
+ console.error('Error: Commit hash is required');
70
+ process.exit(1);
71
+ }
72
+
73
+ log('INFO', `Processing commit: ${commitHash}`);
74
+
75
+ /**
76
+ * 获取提交基本信息
77
+ */
78
+ function getCommitInfo(hash) {
79
+ try {
80
+ log('INFO', 'Getting commit info', { hash });
81
+
82
+ // 获取提交信息
83
+ const cmd = `git show --format="%H|%an|%ae|%at|%s|%b" --no-patch ${hash}`;
84
+ const output = execSync(cmd, { encoding: 'utf8' }).trim();
85
+
86
+ const [commitId, authorName, authorEmail, timestamp, subject, body] = output.split('|');
87
+
88
+ // 获取分支名
89
+ const branch = execSync(`git rev-parse --abbrev-ref HEAD`, { encoding: 'utf8' }).trim();
90
+
91
+ // 获取项目路径
92
+ const projectPath = execSync(`git rev-parse --show-toplevel`, { encoding: 'utf8' }).trim();
93
+
94
+ // 获取远程仓库地址(gitStorePath)
95
+ let gitStorePath = '';
96
+ try {
97
+ gitStorePath = execSync('git remote get-url origin', {
98
+ encoding: 'utf8',
99
+ stdio: ['pipe', 'pipe', 'ignore']
100
+ }).trim();
101
+ } catch (error) {
102
+ log('WARN', 'Could not get git remote URL', { error: error.message });
103
+ gitStorePath = '';
104
+ }
105
+
106
+ const commitInfo = {
107
+ commitId,
108
+ authorName,
109
+ authorEmail,
110
+ timestamp: parseInt(timestamp) * 1000, // 转换为毫秒
111
+ message: subject + (body ? '\n' + body : ''),
112
+ branch,
113
+ projectPath,
114
+ gitStorePath
115
+ };
116
+
117
+ log('INFO', 'Commit info retrieved', {
118
+ commitId: commitInfo.commitId.substring(0, 8),
119
+ branch: commitInfo.branch,
120
+ projectPath: commitInfo.projectPath
121
+ });
122
+
123
+ return commitInfo;
124
+ } catch (error) {
125
+ log('ERROR', 'Error getting commit info', { error: error.message });
126
+ return null;
127
+ }
128
+ }
129
+
130
+ /**
131
+ * 获取文件变更列表和统计
132
+ */
133
+ function getFileChanges(hash) {
134
+ try {
135
+ // 获取文件统计信息
136
+ const numstatCmd = `git show --numstat --format="" ${hash}`;
137
+ const numstatOutput = execSync(numstatCmd, { encoding: 'utf8' }).trim();
138
+
139
+ if (!numstatOutput) {
140
+ return [];
141
+ }
142
+
143
+ const files = [];
144
+ const lines = numstatOutput.split('\n');
145
+
146
+ for (const line of lines) {
147
+ if (!line.trim()) continue;
148
+
149
+ // 解析:added deleted filepath
150
+ const parts = line.split(/\s+/);
151
+ if (parts.length < 3) continue;
152
+
153
+ const added = parts[0] === '-' ? 0 : parseInt(parts[0]);
154
+ const deleted = parts[1] === '-' ? 0 : parseInt(parts[1]);
155
+ const filePath = parts.slice(2).join(' ');
156
+
157
+ files.push({
158
+ filePath,
159
+ addedLines: added,
160
+ deletedLines: deleted,
161
+ operation: determineOperation(added, deleted)
162
+ });
163
+ }
164
+
165
+ return files;
166
+ } catch (error) {
167
+ console.error('Error getting file changes:', error.message);
168
+ return [];
169
+ }
170
+ }
171
+
172
+ /**
173
+ * 获取文件的逐行变更详情
174
+ * 过滤掉空行,只统计实际代码行数
175
+ */
176
+ function getFileLineChanges(hash, filePath) {
177
+ try {
178
+ // 使用 --unified=0 只显示变更行,不显示上下文
179
+ const cmd = `git show --unified=0 --format="" ${hash} -- "${filePath}"`;
180
+ const output = execSync(cmd, { encoding: 'utf8' });
181
+
182
+ const changes = [];
183
+ const lines = output.split('\n');
184
+ let currentLineNumber = 0;
185
+
186
+ for (let i = 0; i < lines.length; i++) {
187
+ const line = lines[i];
188
+
189
+ // 解析 @@ -10,2 +15,3 @@ 格式
190
+ if (line.startsWith('@@')) {
191
+ const match = line.match(/@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
192
+ if (match) {
193
+ currentLineNumber = parseInt(match[2]);
194
+ }
195
+ continue;
196
+ }
197
+
198
+ // 跳过文件头信息
199
+ if (line.startsWith('diff --git') ||
200
+ line.startsWith('index') ||
201
+ line.startsWith('---') ||
202
+ line.startsWith('+++')) {
203
+ continue;
204
+ }
205
+
206
+ // 解析变更行
207
+ if (line.startsWith('+') && !line.startsWith('+++')) {
208
+ // 新增行
209
+ const content = line.substring(1); // 去掉开头的 +
210
+
211
+ // 只统计非空行
212
+ if (content.trim() !== '') {
213
+ changes.push({
214
+ lineNumber: currentLineNumber,
215
+ type: 'add',
216
+ content: content
217
+ });
218
+ }
219
+ currentLineNumber++;
220
+ } else if (line.startsWith('-') && !line.startsWith('---')) {
221
+ // 删除行
222
+ const content = line.substring(1); // 去掉开头的 -
223
+
224
+ // 只统计非空行
225
+ if (content.trim() !== '') {
226
+ changes.push({
227
+ lineNumber: currentLineNumber,
228
+ type: 'delete',
229
+ content: content
230
+ });
231
+ }
232
+ } else if (line.trim()) {
233
+ // 未变更的行(理论上 --unified=0 不应该出现)
234
+ currentLineNumber++;
235
+ }
236
+ }
237
+
238
+ return changes;
239
+ } catch (error) {
240
+ log('WARN', `Error getting line changes for ${filePath}`, { error: error.message });
241
+ return [];
242
+ }
243
+ }
244
+
245
+ /**
246
+ * 判断文件操作类型
247
+ */
248
+ function determineOperation(added, deleted) {
249
+ if (added > 0 && deleted === 0) return 'add';
250
+ if (added === 0 && deleted > 0) return 'delete';
251
+ if (added > 0 && deleted > 0) return 'modify';
252
+ return 'unknown';
253
+ }
254
+
255
+ /**
256
+ * 过滤不需要统计的文件
257
+ */
258
+ function shouldFilterFile(filePath) {
259
+ const filterPatterns = [
260
+ /node_modules\//,
261
+ /dist\//,
262
+ /build\//,
263
+ /vendor\//,
264
+ /\.min\./,
265
+ /package-lock\.json$/,
266
+ /yarn\.lock$/,
267
+ /pnpm-lock\.yaml$/,
268
+ /\.log$/,
269
+ /\.jpg$/,
270
+ /\.png$/,
271
+ /\.gif$/,
272
+ /\.pdf$/,
273
+ /\.md$/,
274
+ /\.zip$/
275
+ ];
276
+
277
+ return filterPatterns.some(pattern => pattern.test(filePath));
278
+ }
279
+
280
+ /**
281
+ * 从 git 提交中获取文件的最终内容
282
+ * 用于验证 AI 生成的行是否在最终提交中存在
283
+ */
284
+ function getCommitFileContent(hash, filePath) {
285
+ try {
286
+ const content = execSync(`git show ${hash}:${filePath}`, {
287
+ encoding: 'utf8',
288
+ stdio: ['pipe', 'pipe', 'ignore'],
289
+ maxBuffer: 10 * 1024 * 1024 // 10MB 缓冲
290
+ });
291
+ return content;
292
+ } catch (error) {
293
+ // 文件可能已删除或路径错误
294
+ console.warn(`[getCommitFileContent] Could not retrieve file ${filePath}: ${error.message}`);
295
+ return '';
296
+ }
297
+ }
298
+
299
+ /**
300
+ * 比对 AI 生成的行是否在最终提交文件中存在
301
+ * 返回在最终文件中存在的行数
302
+ */
303
+ function compareLineContent(aiGeneratedLines, finalFileContent) {
304
+ if (!aiGeneratedLines || aiGeneratedLines.length === 0) {
305
+ return [];
306
+ }
307
+
308
+ const finalLines = finalFileContent.split('\n');
309
+ const matchedLines = [];
310
+
311
+ for (const aiLine of aiGeneratedLines) {
312
+ // 获取 AI 生成的行内容(去除两端空格用于比对)
313
+ const aiLineContent = (aiLine.content || '').trim();
314
+
315
+ if (!aiLineContent) {
316
+ continue;
317
+ }
318
+
319
+ // 在最终文件中查找该行内容
320
+ const foundIndex = finalLines.findIndex(line => line.trim() === aiLineContent);
321
+
322
+ if (foundIndex !== -1) {
323
+ // 行内容存在于最终文件
324
+ matchedLines.push({
325
+ ...aiLine,
326
+ finalLineNumber: foundIndex + 1, // Git 行号从 1 开始
327
+ found: true
328
+ });
329
+ }
330
+ }
331
+
332
+ return matchedLines;
333
+ }
334
+
335
+ /**
336
+ * 获取本地时间 YYYY-MM-DD HH:mm:ss 格式
337
+ * 自动识别系统时区
338
+ */
339
+ function getLocalTime() {
340
+ const now = new Date();
341
+
342
+ // 1. 获取本地时区相对于UTC的偏移量(单位:分钟)
343
+ // 注意:getTimezoneOffset() 返回的是 UTC - 本地时间
344
+ // 例如北京时间(东八区)会返回 -480 (负数)
345
+ const offsetMinutes = now.getTimezoneOffset();
346
+
347
+ // 2. 计算偏移毫秒数
348
+ // 我们要减去这个偏移量,实际上就是加上了时区差(负负得正)
349
+ const offsetMs = offsetMinutes * 60 * 1000;
350
+
351
+ // 3. 得到一个"本地时间视角的"Date对象
352
+ const localDate = new Date(now.getTime() - offsetMs);
353
+
354
+ // 4. 格式化
355
+ return localDate.toISOString()
356
+ .replace('T', ' ') // 替换分隔符
357
+ .substring(0, 19); // 去掉毫秒和时区标记
358
+ }
359
+
360
+ /**
361
+ * 识别 AI 生成的代码,从会话缓存中提取,并进行内容验证
362
+ *
363
+ * 核心逻辑:
364
+ * 1. 从会话缓存读取 AI 生成的行及其内容
365
+ * 2. 从 git 提交中获取最终的文件内容
366
+ * 3. 比对:AI 生成的行内容是否存在于最终文件中
367
+ * 4. 只有内容存在才计入统计
368
+ * 5. 记录用过的session文件路径,供后续清理
369
+ */
370
+ function identifyAICode(commitInfo, commitFiles, commitHash) {
371
+ const result = {
372
+ aiAddedLines: 0,
373
+ aiModifiedLines: 0,
374
+ aiDeletedLines: 0,
375
+ aiFiles: [],
376
+ ideType: 'other',// other,包括IDE这种其他的不支持MCP的
377
+ sessionIdeType: 'other',// other,包括IDE这种其他的不支持MCP的
378
+ userId: '', // 从会话中提取的 userId
379
+ matchDetails: [], // 详细的匹配信息,用于调试
380
+ usedSessionPaths: [] // 记录使用过的session文件路径,用于清理
381
+ };
382
+
383
+ try {
384
+ // 步骤 1: 获取会话缓存目录
385
+ const cacheHash = crypto
386
+ .createHash('md5')
387
+ .update(commitInfo.gitStorePath)
388
+ .digest('hex')
389
+ .substring(0, 16);
390
+
391
+ const sessionDir = path.join(
392
+ os.homedir(),
393
+ '.ai-metrics-cache',
394
+ cacheHash,
395
+ commitInfo.branch
396
+ );
397
+
398
+ if (!fs.existsSync(sessionDir)) {
399
+ console.warn(`[AI Code Detection] Session directory not found: ${sessionDir}`);
400
+ return result;
401
+ }
402
+
403
+ // 步骤 2: 读取会话文件
404
+ const sessionFiles = fs.readdirSync(sessionDir)
405
+ .filter(f => f.startsWith('session-') && f.endsWith('.json'))
406
+ .sort()
407
+ .reverse(); // 最新的在前
408
+
409
+ for (const sessionFile of sessionFiles) {
410
+ const sessionPath = path.join(sessionDir, sessionFile);
411
+
412
+ try {
413
+ const sessionData = JSON.parse(fs.readFileSync(sessionPath, 'utf8'));
414
+
415
+
416
+ log('INFO', 'Session data loaded', { sessionData });
417
+
418
+ // 记录此session文件已被使用
419
+ result.usedSessionPaths.push(sessionPath);
420
+
421
+ // 提取 ideType
422
+ if (sessionData.ideType && result.ideType === 'other') {
423
+ result.ideType = sessionData.ideType;
424
+ result.sessionIdeType = sessionData.ideType;
425
+ console.log(`[AI Code Detection] Extracted ideType from session: ${result.ideType}`);
426
+ }
427
+
428
+ // 提取 userId
429
+ if (sessionData.userId && !result.userId) {
430
+ result.userId = sessionData.userId;
431
+ console.log(`[AI Code Detection] Extracted userId from session: ${result.userId}`);
432
+ }
433
+
434
+ // 步骤 3: 比对文件
435
+ for (const sessionFileData of sessionData.files || []) {
436
+ const sessionFilePath = sessionFileData.filePath;
437
+
438
+ // 查找匹配的提交文件
439
+ const commitFile = commitFiles.find(f =>
440
+ f.filePath === sessionFilePath ||
441
+ f.filePath.endsWith('/' + sessionFilePath) ||
442
+ sessionFilePath.endsWith('/' + f.filePath)
443
+ );
444
+
445
+ if (!commitFile) {
446
+ console.log(`[AI Code Detection] No matching commit file for session file: ${sessionFilePath}`);
447
+ continue;
448
+ }
449
+
450
+ // 标记为 AI 文件
451
+ if (!result.aiFiles.includes(sessionFilePath)) {
452
+ result.aiFiles.push(sessionFilePath);
453
+ }
454
+
455
+ // 获取 AI 生成的行
456
+ const aiLines = sessionFileData.aiGeneratedLines || [];
457
+
458
+ if (aiLines.length === 0) {
459
+ continue;
460
+ }
461
+
462
+ // ✨ 核心逻辑:从 git 提交中获取最终的文件内容
463
+ const finalFileContent = getCommitFileContent(commitHash, commitFile.filePath);
464
+
465
+ // 比对 AI 生成的行是否存在于最终文件中
466
+ const matchedLines = compareLineContent(aiLines, finalFileContent);
467
+
468
+ console.log(`[AI Code Detection] File: ${sessionFilePath}, Operation: ${commitFile.operation}`);
469
+ console.log(` - AI Generated Lines: ${aiLines.length}`);
470
+ console.log(` - Matched in Final Content: ${matchedLines.length}`);
471
+
472
+ // 根据操作类型和内容匹配情况,计入统计
473
+ for (const matchedLine of matchedLines) {
474
+ const detail = {
475
+ file: sessionFilePath,
476
+ operation: commitFile.operation,
477
+ aiContent: matchedLine.content.substring(0, 80), // 前 80 字符用于显示
478
+ found: matchedLine.found
479
+ };
480
+
481
+ if (commitFile.operation === 'add' && matchedLine.found) {
482
+ result.aiAddedLines++;
483
+ detail.category = 'aiAddedLines';
484
+ } else if (commitFile.operation === 'delete' && matchedLine.found) {
485
+ result.aiDeletedLines++;
486
+ detail.category = 'aiDeletedLines';
487
+ } else if (commitFile.operation === 'modify' && matchedLine.found) {
488
+ result.aiModifiedLines++;
489
+ detail.category = 'aiModifiedLines';
490
+ }
491
+
492
+ result.matchDetails.push(detail);
493
+ }
494
+ }
495
+ } catch (error) {
496
+ console.warn(`[AI Code Detection] Failed to parse session file ${sessionFile}:`, error.message);
497
+ }
498
+ }
499
+ } catch (error) {
500
+ console.warn(`[AI Code Detection] Error in identifyAICode:`, error.message);
501
+ }
502
+
503
+ return result;
504
+ }
505
+
506
+ /**
507
+ * 构建完整的提交数据并包含 AI 代码识别信息
508
+ */
509
+ async function buildReportData(hash) {
510
+ const commitInfo = getCommitInfo(hash);
511
+ if (!commitInfo) {
512
+ throw new Error('Failed to get commit info');
513
+ }
514
+
515
+ const fileChanges = getFileChanges(hash);
516
+
517
+ // 获取每个文件的详细变更
518
+ const filesWithDetails = [];
519
+ for (const file of fileChanges) {
520
+ // 过滤不需要的文件
521
+ if (shouldFilterFile(file.filePath)) {
522
+ continue;
523
+ }
524
+
525
+ const lineChanges = getFileLineChanges(hash, file.filePath);
526
+
527
+ // 计算实际的代码行数(排除空行)
528
+ const addedCodeLines = lineChanges.filter(c => c.type === 'add').length;
529
+ const deletedCodeLines = lineChanges.filter(c => c.type === 'delete').length;
530
+
531
+ filesWithDetails.push({
532
+ filePath: file.filePath,
533
+ operation: file.operation,
534
+ addedLines: addedCodeLines, // 使用实际的非空行数
535
+ deletedLines: deletedCodeLines, // 使用实际的非空行数
536
+ totalLines: addedCodeLines + deletedCodeLines,
537
+ changes: lineChanges
538
+ });
539
+ }
540
+
541
+ // 识别 AI 代码(传递 hash 用于读取最终文件内容)
542
+ const aiMetrics = identifyAICode(commitInfo, filesWithDetails, hash);
543
+
544
+ // 计算统计(方案1:互斥统计,避免重复)
545
+ // - commitAddedLines:仅统计 operation === 'add' 的文件新增行
546
+ // - commitDeletedLines:仅统计 operation === 'delete' 的文件删除行
547
+ // - commitModifiedLines:仅统计 operation === 'modify' 的文件新增和删除行
548
+ // 结果:三个字段相加 = 总改动数,不存在重复统计
549
+ const totalAddedLines = filesWithDetails.filter(f => f.operation === 'add')
550
+ .reduce((sum, f) => sum + f.addedLines, 0);
551
+ const totalDeletedLines = filesWithDetails.filter(f => f.operation === 'delete')
552
+ .reduce((sum, f) => sum + f.deletedLines, 0);
553
+ const totalModifiedLines = filesWithDetails.filter(f => f.operation === 'modify')
554
+ .reduce((sum, f) => sum + (f.addedLines + f.deletedLines), 0);
555
+
556
+ // 获取创建时间
557
+ const createTime = getLocalTime();
558
+
559
+ // 获取项目名称(用于统一路径格式)
560
+ const projectName = path.basename(commitInfo.projectPath);
561
+
562
+ // 统一 commitFiles 格式:添加项目名前缀,与 aiFiles 格式保持一致
563
+ const commitFilesUnified = filesWithDetails.map(f => `${projectName}/${f.filePath}`);
564
+
565
+ // 构建最终上报数据
566
+ const reportData = {
567
+ // 基础信息
568
+ erp: USER_ID || aiMetrics.userId || '', // 优先使用环境变量 USER_ID,否则使用会话中的 userId
569
+ commitId: commitInfo.commitId,
570
+ commitTime: createTime,
571
+ gitPath: commitInfo.gitStorePath,
572
+ branch: commitInfo.branch,
573
+ message: commitInfo.message,
574
+
575
+ // AI 相关统计(已通过内容对比验证)
576
+ aiAddedLines: aiMetrics.aiAddedLines,
577
+ aiModifiedLines: aiMetrics.aiModifiedLines,
578
+ aiDeletedLines: aiMetrics.aiDeletedLines,
579
+
580
+ // 总体统计
581
+ commitAddedLines: totalAddedLines,
582
+ commitModifiedLines: totalModifiedLines,
583
+ commitDeletedLines: totalDeletedLines,
584
+
585
+ // 文件列表(格式统一:项目名/相对路径)
586
+ aiFiles: aiMetrics.aiFiles,
587
+ commitFiles: commitFilesUnified,
588
+ // 新增字段
589
+ ideType: aiMetrics.ideType || 'other'
590
+ };
591
+
592
+ // 附加 session 路径信息(用于后续清理)
593
+ reportData._usedSessionPaths = aiMetrics.usedSessionPaths;
594
+
595
+ return reportData;
596
+ }
597
+
598
+ /**
599
+ * 上报数据到服务端
600
+ * 返回值: true 表示上报成功,false 表示上报失败或降级模式(数据已保存到本地缓存)
601
+ *
602
+ * 支持降级模式:
603
+ * - 如果 axios 不可用(依赖缺失),直接保存到本地缓存
604
+ * - 不会抛出错误或中断流程
605
+ */
606
+ async function reportToServer(data) {
607
+ try {
608
+ // 检查依赖是否可用
609
+ if (!DEPENDENCY_AVAILABLE || !axios) {
610
+ log('WARN', 'axios module not available, using fallback mode - saving to local cache');
611
+
612
+ // 提取 session 路径
613
+ const sessionPaths = data._usedSessionPaths || [];
614
+ const sendData = { ...data };
615
+ delete sendData._usedSessionPaths;
616
+
617
+ // 保存到本地,不上报
618
+ saveToLocalCache(sendData);
619
+
620
+ // 删除 session 文件
621
+ deleteSessionFiles(sessionPaths);
622
+
623
+ console.log('⚠️ Dependencies not available - data saved locally');
624
+ log('INFO', 'Fallback mode: data saved to local cache due to missing dependencies');
625
+
626
+ return false; // 虽然没有上报,但作为"失败"处理,这样 main() 才能删除 session
627
+ }
628
+
629
+ // 提取 session 路径(用于后续清理)
630
+ const sessionPaths = data._usedSessionPaths || [];
631
+ // 不要把内部字段发送给服务器
632
+ const sendData = { ...data };
633
+ delete sendData._usedSessionPaths;
634
+
635
+ log('INFO', 'Attempting to report data to server', {
636
+ endpoint: API_ENDPOINT,
637
+ commitId: sendData.commitId.substring(0, 8)
638
+ });
639
+
640
+ const response = await axios.post(API_ENDPOINT, {
641
+ ...sendData
642
+ }, {
643
+ headers: {
644
+ 'Content-Type': 'application/json'
645
+ },
646
+ timeout: 10000
647
+ });
648
+
649
+ log('INFO', 'Response received from server', {
650
+ status: response.status,
651
+ statusText: response.statusText,
652
+ data: response.data,
653
+ code: response.data?.code,
654
+ message: response.data?.message
655
+ });
656
+
657
+ if (response.data.code === 0) {
658
+ log('INFO', 'Data reported to server successfully', {
659
+ commitId: sendData.commitId.substring(0, 8)
660
+ });
661
+ console.log('✓ Git commit data reported successfully');
662
+
663
+ // 上报成功,删除 session 会话
664
+ deleteSessionFiles(sessionPaths);
665
+
666
+ return true;
667
+ } else {
668
+ log('WARN', 'Server returned error, saving to local cache', {
669
+ error: response.data.message,
670
+ commitId: sendData.commitId.substring(0, 8)
671
+ });
672
+ console.error('✗ Server returned error:', response.data.message);
673
+ // 服务器返回错误,保存到本地缓存(不删除session,等待后续重试)
674
+ saveToLocalCache(sendData);
675
+ return false;
676
+ }
677
+ } catch (error) {
678
+ // 网络错误,提取内部字段
679
+ const sessionPaths = data._usedSessionPaths || [];
680
+ const sendData = { ...data };
681
+ delete sendData._usedSessionPaths;
682
+
683
+ log('WARN', 'Failed to report data to server, saving to local cache', {
684
+ error: error.message,
685
+ commitId: sendData.commitId.substring(0, 8)
686
+ });
687
+
688
+ console.error('✗ Failed to report data:', error.message);
689
+ // 网络错误或其他错误,保存到本地缓存(不删除session,等待后续重试)
690
+ saveToLocalCache(sendData);
691
+ return false;
692
+ }
693
+ }
694
+
695
+ /**
696
+ * 保存到本地缓存(失败重试)
697
+ */
698
+ function saveToLocalCache(data) {
699
+ const cacheDir = path.join(os.homedir(), '.ai-metrics-cache/reportToServerFail/');
700
+
701
+ try {
702
+ if (!fs.existsSync(cacheDir)) {
703
+ fs.mkdirSync(cacheDir, { recursive: true });
704
+ }
705
+
706
+ const cacheFile = path.join(cacheDir, `git-${Date.now()}.json`);
707
+ fs.writeFileSync(cacheFile, JSON.stringify(data, null, 2));
708
+
709
+ console.log(`? Data cached locally: ${cacheFile}`);
710
+ } catch (error) {
711
+ console.error('? Failed to cache data:', error.message);
712
+ }
713
+ }
714
+
715
+ /**
716
+ * 删除使用过的 session 会话文件
717
+ * 在数据成功上报或本地保存后调用
718
+ */
719
+ function deleteSessionFiles(sessionPaths) {
720
+ if (!sessionPaths || sessionPaths.length === 0) {
721
+ return;
722
+ }
723
+
724
+ const deletedCount = { success: 0, failed: 0 };
725
+
726
+ for (const sessionPath of sessionPaths) {
727
+ try {
728
+ if (fs.existsSync(sessionPath)) {
729
+ fs.unlinkSync(sessionPath);
730
+ log('INFO', `Session file deleted: ${sessionPath}`);
731
+ deletedCount.success++;
732
+ }
733
+ } catch (error) {
734
+ log('WARN', `Failed to delete session file: ${sessionPath}`, { error: error.message });
735
+ deletedCount.failed++;
736
+ }
737
+ }
738
+
739
+ log('INFO', 'Session cleanup completed', {
740
+ deleted: deletedCount.success,
741
+ failed: deletedCount.failed
742
+ });
743
+ }
744
+
745
+ /**
746
+ * 批量重试上报失败的数据
747
+ * 扫描 ~/.ai-metrics-cache/reportToServerFail/ 目录中的失败文件
748
+ * 逐个重试上报,成功则删除,失败则保留
749
+ */
750
+ async function batchRetryFailedReports() {
751
+ try {
752
+ const failedReportsDir = path.join(os.homedir(), '.ai-metrics-cache/reportToServerFail/');
753
+
754
+ // 检查目录是否存在
755
+ if (!fs.existsSync(failedReportsDir)) {
756
+ log('DEBUG', '[BatchRetry] No failed reports directory found');
757
+ return;
758
+ }
759
+
760
+ // 列出所有 git-*.json 格式的失败文件
761
+ const files = fs.readdirSync(failedReportsDir)
762
+ .filter(f => f.startsWith('git-') && f.endsWith('.json'))
763
+ .sort();
764
+
765
+ if (files.length === 0) {
766
+ log('DEBUG', '[BatchRetry] No failed reports found');
767
+ return;
768
+ }
769
+
770
+ log('INFO', `[BatchRetry] Found ${files.length} failed report(s), starting batch retry...`);
771
+
772
+ let successCount = 0;
773
+ let failureCount = 0;
774
+
775
+ for (const file of files) {
776
+ const filePath = path.join(failedReportsDir, file);
777
+
778
+ try {
779
+ // 读取失败的数据
780
+ const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
781
+
782
+ log('INFO', `[BatchRetry] Retrying: ${file}`, {
783
+ commitId: data.commitId?.substring(0, 8)
784
+ });
785
+
786
+ // 检查依赖是否可用
787
+ if (!DEPENDENCY_AVAILABLE || !axios) {
788
+ log('WARN', `[BatchRetry] axios not available, skipping retry for ${file}`);
789
+ failureCount++;
790
+ continue;
791
+ }
792
+
793
+ // 尝试上报
794
+ try {
795
+ const response = await axios.post(API_ENDPOINT, data, {
796
+ headers: {
797
+ 'Content-Type': 'application/json'
798
+ },
799
+ timeout: 10000
800
+ });
801
+
802
+ if (response.data?.code === 0) {
803
+ // 上报成功,删除文件
804
+ try {
805
+ fs.unlinkSync(filePath);
806
+ log('INFO', `[BatchRetry] ✓ Success and deleted: ${file}`, {
807
+ commitId: data.commitId?.substring(0, 8)
808
+ });
809
+ successCount++;
810
+ } catch (deleteError) {
811
+ log('WARN', `[BatchRetry] Upload successful but failed to delete: ${file}`, {
812
+ error: deleteError.message
813
+ });
814
+ successCount++; // 虽然删除失败,但上报成功也算
815
+ }
816
+ } else {
817
+ // 上报失败,保留文件供后续重试
818
+ log('WARN', `[BatchRetry] ✗ Failed to retry: ${file}`, {
819
+ code: response.data?.code,
820
+ message: response.data?.message,
821
+ commitId: data.commitId?.substring(0, 8)
822
+ });
823
+ failureCount++;
824
+ }
825
+ } catch (reportError) {
826
+ log('WARN', `[BatchRetry] ✗ Network error during retry: ${file}`, {
827
+ error: reportError.message,
828
+ commitId: data.commitId?.substring(0, 8)
829
+ });
830
+ failureCount++;
831
+ }
832
+
833
+ // 添加延迟以避免过快请求(每个请求间隔 200ms)
834
+ await new Promise(resolve => setTimeout(resolve, 200));
835
+
836
+ } catch (parseError) {
837
+ log('WARN', `[BatchRetry] Invalid JSON file, skipping: ${file}`, {
838
+ error: parseError.message
839
+ });
840
+ failureCount++;
841
+ }
842
+ }
843
+
844
+ // 输出批量重试的总结
845
+ log('INFO', '[BatchRetry] Batch retry completed', {
846
+ total: files.length,
847
+ success: successCount,
848
+ failure: failureCount,
849
+ successRate: files.length > 0 ? `${(successCount / files.length * 100).toFixed(2)}%` : '0%'
850
+ });
851
+
852
+ console.log(`[BatchRetry] Summary: ${successCount}/${files.length} successfully retried`);
853
+
854
+ } catch (error) {
855
+ log('WARN', '[BatchRetry] Error in batch retry process', {
856
+ error: error.message
857
+ });
858
+ }
859
+ }
860
+
861
+ /**
862
+ * 主函数
863
+ */
864
+ async function main() {
865
+ try {
866
+ log('INFO', `Collecting data for commit: ${commitHash}`);
867
+
868
+ // 构建上报数据
869
+ const reportData = await buildReportData(commitHash);
870
+
871
+ // 提取 session 路径信息
872
+ const usedSessionPaths = reportData._usedSessionPaths || [];
873
+
874
+ log('INFO', 'Data collection completed', {
875
+ totalFiles: reportData.totalFiles,
876
+ totalAddedLines: reportData.totalAddedLines,
877
+ totalModifiedLines: reportData.totalModifiedLines,
878
+ totalDeletedLines: reportData.totalDeletedLines,
879
+ aiCodeRatio: reportData.aiCodeRatio,
880
+ ideType: reportData.ideType
881
+ });
882
+
883
+ console.log(`✓ Data collected:`);
884
+ console.log(` - Files changed: ${reportData.totalFiles}`);
885
+ console.log(` - Lines added: ${reportData.totalAddedLines}`);
886
+ console.log(` - Lines modified: ${reportData.totalModifiedLines}`);
887
+ console.log(` - Lines deleted: ${reportData.totalDeletedLines}`);
888
+ console.log(` - AI Code Ratio: ${reportData.aiCodeRatio}%`);
889
+ console.log(` - IDE Type: ${reportData.ideType}`);
890
+ console.log(` - Create Time: ${reportData.createTime}`);
891
+
892
+ // 上报数据 - 确保所有情况下数据都被持久化
893
+ // 只要 USER_ID 或 reportData.erp 有一个非空,就可以上报
894
+ if (USER_ID || reportData.erp) {
895
+ log('INFO', 'Attempting to report to server', {
896
+ userId: USER_ID || reportData.erp,
897
+ source: USER_ID ? 'env.USER_ID' : 'session.userId'
898
+ });
899
+ const reportSuccess = await reportToServer(reportData);
900
+
901
+ if (reportSuccess) {
902
+ log('INFO', 'Data successfully reported to server');
903
+ // 上报成功时,session 已在 reportToServer 中删除
904
+ } else {
905
+ log('INFO', 'Data saved to local cache (report failed or network issue)');
906
+ // 上报失败时,本地缓存已经写入了,删除 session 会话
907
+ deleteSessionFiles(usedSessionPaths);
908
+ }
909
+ } else {
910
+ log('WARN', 'No USER_ID available (neither env.USER_ID nor session.userId), saving to local cache');
911
+ // 提取内部字段,不发送给服务器
912
+ const sendData = { ...reportData };
913
+ delete sendData._usedSessionPaths;
914
+ saveToLocalCache(sendData);
915
+
916
+ // 本地保存成功,删除 session 会话
917
+ deleteSessionFiles(usedSessionPaths);
918
+ }
919
+
920
+ log('INFO', `Git collector completed successfully - Log: ${LOG_FILE}`);
921
+
922
+ // 在主流程完成后,异步扫描并批量上报失败数据
923
+ // 不阻塞主流程,使用 setImmediate 让其在事件循环中稍后执行
924
+ setImmediate(async () => {
925
+ try {
926
+ await batchRetryFailedReports();
927
+ } catch (error) {
928
+ log('WARN', '[BatchRetry] Error in batch retry process', { error: error.message });
929
+ }
930
+ });
931
+
932
+ } catch (error) {
933
+ log('ERROR', 'Error in main', { error: error.message, stack: error.stack });
934
+ console.error('Error in main:', error);
935
+ process.exit(1);
936
+ }
937
+ }
938
+
939
+ // 执行
940
+ (async () => {
941
+ try {
942
+ // 延迟加载 axios
943
+ try {
944
+ const axiosModule = await import('axios');
945
+ axios = axiosModule.default;
946
+ } catch (error) {
947
+ DEPENDENCY_AVAILABLE = false;
948
+ // 降级模式:只做本地保存,不上报
949
+ }
950
+
951
+ await main();
952
+ } catch (error) {
953
+ console.error('Fatal error:', error);
954
+ process.exit(1);
955
+ }
956
+ })();
957
+