refactor: 确立独立子工程架构 (backend, admin, mpuniapp) 并同步更新 AGENTS.md 规范
这个提交包含在:
@@ -0,0 +1,168 @@
|
||||
const { loadProjectConfig, generateReviewPrompt, getAIReview, getChanges, addReviewComment, processReview } = require('../index');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// 输出环境变量
|
||||
console.log('测试环境变量:');
|
||||
console.log('GITLAB_TOKEN:', process.env.GITLAB_TOKEN ? '已设置' : '未设置');
|
||||
console.log('GITLAB_URL:', process.env.GITLAB_URL);
|
||||
console.log('DEEPSEEK_API_KEY:', process.env.DEEPSEEK_API_KEY ? '已设置' : '未设置');
|
||||
console.log('CI_PROJECT_ID:', process.env.CI_PROJECT_ID);
|
||||
console.log('CI_MERGE_REQUEST_IID:', process.env.CI_MERGE_REQUEST_IID);
|
||||
console.log('CI_COMMIT_SHA:', process.env.CI_COMMIT_SHA);
|
||||
console.log('CI_COMMIT_BRANCH:', process.env.CI_COMMIT_BRANCH);
|
||||
console.log('CI_PIPELINE_SOURCE:', process.env.CI_PIPELINE_SOURCE);
|
||||
console.log('CI_PROJECT_DIR:', process.env.CI_PROJECT_DIR);
|
||||
console.log('----------------------------------------');
|
||||
|
||||
describe('Code Review System', () => {
|
||||
// 测试配置文件加载
|
||||
describe('loadProjectConfig', () => {
|
||||
it('should load project configuration correctly', async () => {
|
||||
const config = loadProjectConfig();
|
||||
console.log('项目配置:', JSON.stringify(config, null, 2));
|
||||
expect(config).toBeDefined();
|
||||
expect(config.language).toBeDefined();
|
||||
expect(config.reviewGuidelines).toBeDefined();
|
||||
expect(config.reviewRules).toBeDefined();
|
||||
expect(config.ignoreFiles).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// 测试提示词生成
|
||||
describe('generateReviewPrompt', () => {
|
||||
it('should generate review prompt correctly', () => {
|
||||
const config = loadProjectConfig();
|
||||
const changes = [
|
||||
{
|
||||
file: 'test.js',
|
||||
diff: 'console.log("test");'
|
||||
}
|
||||
];
|
||||
const prompt = generateReviewPrompt(config, JSON.stringify(changes));
|
||||
console.log('评审提示词:', prompt);
|
||||
expect(prompt).toContain(config.language);
|
||||
expect(prompt).toContain(config.reviewGuidelines);
|
||||
expect(prompt).toContain('test.js');
|
||||
});
|
||||
});
|
||||
|
||||
// 测试获取代码变更
|
||||
describe('getChanges', () => {
|
||||
it('should get changes from merge request', async () => {
|
||||
const changes = await getChanges(
|
||||
process.env.CI_PROJECT_ID,
|
||||
'merge_request',
|
||||
process.env.CI_MERGE_REQUEST_IID
|
||||
);
|
||||
console.log('合并请求变更:', JSON.stringify(changes, null, 2));
|
||||
expect(Array.isArray(changes)).toBe(true);
|
||||
if (changes.length > 0) {
|
||||
expect(changes[0]).toHaveProperty('file');
|
||||
expect(changes[0]).toHaveProperty('diff');
|
||||
}
|
||||
}, 30000); // 设置超时时间为 30 秒
|
||||
|
||||
it('should get changes from push', async () => {
|
||||
const changes = await getChanges(
|
||||
process.env.CI_PROJECT_ID,
|
||||
'push',
|
||||
process.env.CI_COMMIT_SHA
|
||||
);
|
||||
console.log('推送变更:', JSON.stringify(changes, null, 2));
|
||||
expect(Array.isArray(changes)).toBe(true);
|
||||
if (changes.length > 0) {
|
||||
expect(changes[0]).toHaveProperty('file');
|
||||
expect(changes[0]).toHaveProperty('diff');
|
||||
}
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
// 测试 AI 评审
|
||||
describe('getAIReview', () => {
|
||||
it('should get AI review for changes', async () => {
|
||||
console.log('开始 AI 评审测试...');
|
||||
|
||||
console.log('加载项目配置...');
|
||||
const config = loadProjectConfig();
|
||||
console.log('项目配置加载完成');
|
||||
|
||||
console.log('获取代码变更...');
|
||||
const sourceType = process.env.CI_PIPELINE_SOURCE === 'merge_request_event' ? 'merge_request' : 'push';
|
||||
const sourceId = sourceType === 'merge_request' ? process.env.CI_MERGE_REQUEST_IID : process.env.CI_COMMIT_SHA;
|
||||
console.log(`变更来源: ${sourceType}, ID: ${sourceId}`);
|
||||
|
||||
const changes = await getChanges(
|
||||
process.env.CI_PROJECT_ID,
|
||||
sourceType,
|
||||
sourceId
|
||||
);
|
||||
console.log(`获取到 ${changes.length} 个文件变更`);
|
||||
changes.forEach(change => {
|
||||
console.log(`文件: ${change.file}`);
|
||||
console.log(`变更内容:\n${change.diff}`);
|
||||
});
|
||||
|
||||
console.log('生成评审提示词...');
|
||||
const prompt = generateReviewPrompt(config, JSON.stringify(changes));
|
||||
console.log('评审提示词生成完成');
|
||||
|
||||
console.log('调用 AI 进行评审...');
|
||||
const review = await getAIReview(prompt);
|
||||
console.log('AI 评审完成');
|
||||
console.log('评审结果:', review);
|
||||
|
||||
expect(typeof review).toBe('string');
|
||||
expect(review.length).toBeGreaterThan(0);
|
||||
console.log('AI 评审测试完成');
|
||||
}, 120000); // 设置测试超时时间为 120 秒
|
||||
});
|
||||
|
||||
// 测试添加评审评论
|
||||
describe('addReviewComment', () => {
|
||||
// it('should add review comment to merge request', async () => {
|
||||
// const review = 'Test review comment';
|
||||
// console.log('添加合并请求评论:', review);
|
||||
// await expect(addReviewComment(
|
||||
// process.env.CI_PROJECT_ID,
|
||||
// 'merge_request',
|
||||
// process.env.CI_MERGE_REQUEST_IID,
|
||||
// review
|
||||
// )).resolves.not.toThrow();
|
||||
// }, 30000);
|
||||
|
||||
it('should add review comment to commit', async () => {
|
||||
const review = 'Test review comment';
|
||||
console.log('添加提交评论:', review);
|
||||
await expect(addReviewComment(
|
||||
process.env.CI_PROJECT_ID,
|
||||
'push',
|
||||
process.env.CI_COMMIT_SHA,
|
||||
review
|
||||
)).resolves.not.toThrow();
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
// 测试完整评审流程
|
||||
describe('processReview', () => {
|
||||
// it('should process review for merge request', async () => {
|
||||
// console.log('开始处理合并请求评审...');
|
||||
// await expect(processReview(
|
||||
// process.env.CI_PROJECT_ID,
|
||||
// 'merge_request',
|
||||
// process.env.CI_MERGE_REQUEST_IID
|
||||
// )).resolves.not.toThrow();
|
||||
// console.log('合并请求评审完成');
|
||||
// }, 120000); // 设置超时时间为 120 秒
|
||||
|
||||
it('should process review for push', async () => {
|
||||
console.log('开始处理推送评审...');
|
||||
await expect(processReview(
|
||||
process.env.CI_PROJECT_ID,
|
||||
'push',
|
||||
process.env.CI_COMMIT_SHA
|
||||
)).resolves.not.toThrow();
|
||||
console.log('推送评审完成');
|
||||
}, 120000);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
// 设置测试环境变量
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
// GitLab 相关配置
|
||||
process.env.GITLAB_TOKEN = process.env.TEST_GITLAB_TOKEN;
|
||||
process.env.GITLAB_URL = process.env.TEST_GITLAB_URL || 'http://git.dcloud.io';
|
||||
|
||||
// DeepSeek API 配置
|
||||
process.env.DEEPSEEK_API_KEY = process.env.TEST_DEEPSEEK_API_KEY;
|
||||
|
||||
// 测试项目配置
|
||||
process.env.TEST_PROJECT_ID = '602';
|
||||
process.env.TEST_MERGE_REQUEST_IID = '1';
|
||||
process.env.TEST_COMMIT_SHA = 'f07f0dfe33e4099256bb23412d004502973c55c8';
|
||||
process.env.TEST_BRANCH = 'dev';
|
||||
process.env.TEST_PIPELINE_SOURCE = 'merge_request_event';
|
||||
process.env.CI_PROJECT_DIR = process.cwd();
|
||||
|
||||
// 添加新的配置
|
||||
process.env.CI_PROJECT_ID = process.env.TEST_PROJECT_ID;
|
||||
process.env.CI_MERGE_REQUEST_IID = process.env.TEST_MERGE_REQUEST_IID;
|
||||
process.env.CI_COMMIT_SHA = process.env.TEST_COMMIT_SHA;
|
||||
process.env.CI_COMMIT_BRANCH = process.env.TEST_BRANCH || 'dev';
|
||||
process.env.CI_PIPELINE_SOURCE = process.env.TEST_PIPELINE_SOURCE || 'push';
|
||||
process.env.CI_PROJECT_DIR = process.cwd() + '/../';
|
||||
@@ -0,0 +1,81 @@
|
||||
# uni-app x 示例项目代码评审配置文件
|
||||
|
||||
# 项目基本信息
|
||||
project:
|
||||
reviewGuidelines: |
|
||||
**请扮演一位经验丰富的 uni-app x 高级开发者,专注于对 Git Commit 进行代码评审。**
|
||||
|
||||
我将提供本次提交的以下信息:
|
||||
1. 提交日志(Commit Message)
|
||||
2. 代码变更内容(Diff)
|
||||
3. 变更涉及到的代码上下文(包含注释)
|
||||
|
||||
我的项目使用 **uni-app x 框架** 实现示例功能,使用 **jest 框架** 进行断言和截图测试。
|
||||
|
||||
---
|
||||
|
||||
**请基于这些信息,针对 本次提交引入的变更 进行代码评审。你的评审应聚焦于:**
|
||||
|
||||
### 1. 变更目的与实际代码一致性
|
||||
* 代码变更是否准确、完整地实现了提交日志中描述的目的或修复的问题?
|
||||
* 提交日志是否清晰地解释了本次变更的意图?
|
||||
|
||||
### 2. 代码质量 (针对变更部分)
|
||||
* **可读性与清晰度:**
|
||||
* 新添加或修改的代码是否容易理解?
|
||||
* 变量、函数、类、方法的命名是否符合规范且表达清晰?
|
||||
* 代码结构是否合理?
|
||||
* **潜在问题:**
|
||||
* 变更是否可能引入新的 bug 或意外行为?
|
||||
* 是否可能引入性能瓶颈(例如,在循环中执行昂贵的数据库操作,或者未优化 Eloquent 查询)?
|
||||
* 是否存在潜在的资源泄漏或错误处理不当?
|
||||
* **代码规范与最佳实践:**
|
||||
* 新代码是否遵循 UTS 编码标准和项目内部的编码规范?
|
||||
* 是否考虑到多端适配性(如 iOS、Android、H5、小程序等)?
|
||||
* 是否遵循了 uni-app x 的最佳实践(如组件化、状态管理等)?
|
||||
* 是否遵循了 jest 的最佳实践?
|
||||
* **安全性:**
|
||||
* 变更是否引入了新的安全风险?(特别关注用户输入处理、数据库交互、跨站脚本 XSS、跨站请求伪造 CSRF 等)
|
||||
* 对于敏感操作,是否确保了适当的授权和认证?
|
||||
* 涉及到手机号验证、邮箱验证、实人认证、三要素认证等付费验证时,需要增加合理的防刷机制。
|
||||
|
||||
### 3. 代码注释
|
||||
* 新添加的注释是否清晰、准确、简洁,能够有效解释代码意图或复杂部分?
|
||||
* 变更是否导致现有注释过时、不准确或产生误导?
|
||||
* 是否有需要添加但缺失的关键注释(例如,对公共方法或复杂逻辑的解释)?
|
||||
|
||||
### 4. 整体变更结构
|
||||
* 这次提交的变更范围是否合理(即,只包含了与提交目的相关的修改,没有混入其他不相关的改动)?
|
||||
* 如果变更跨越多个文件,它们之间的逻辑联系是否清晰?
|
||||
|
||||
### 5. 测试文件变更
|
||||
* 如果修改的是 .test.js 文件,需要验证测试用例是否合理、覆盖了相关功能,并且符合 jest 的最佳实践。
|
||||
* 如果测试中涉及到截图测试(program.screenshot) 且没有在 app webview 模式(通过 process.env.UNI_AUTOMATOR_APP_WEBVIEW == 'true' 判断)时跳过,则需要设置为截长图模式,即 program.screenshot 配置参数 fullPage: true。
|
||||
|
||||
---
|
||||
|
||||
**请以清晰的格式(如列表或分节)输出评审结果,并:**
|
||||
|
||||
* **引用** 提交日志来作为评审的起点。
|
||||
* **具体指出** 发现问题的代码位置(尽可能引用文件路径和相关的 diff 行号)。
|
||||
* **详细描述** 发现的问题是什么。
|
||||
* **提供具体、可操作的改进建议**。
|
||||
* **标注问题的建议严重程度**(例如:`严重`, `主要`, `次要`, `建议`)。
|
||||
* 如果基于本次变更看,代码质量良好且没有发现明显问题,也请简要说明。
|
||||
* 将总结内容放在最前面输出,总结内容需要包含"总体评分(优、良、中、差)"、"推荐级别(可上线、建议修改、必须修改)"、"总体评价",方便评审人员快速评估。
|
||||
* 回复时使用中文。
|
||||
|
||||
---
|
||||
|
||||
**以下是本次 Git Commit 的相关信息:**
|
||||
|
||||
# 忽略文件
|
||||
ignore:
|
||||
- "public/build/**"
|
||||
- "node_modules/**"
|
||||
- "*.lock"
|
||||
- "code-review/**"
|
||||
- ".env"
|
||||
- ".env.*"
|
||||
- "package-lock.json"
|
||||
- ".gitlab-ci.yml"
|
||||
587
mpuniapp/code-review/index.js
普通文件
587
mpuniapp/code-review/index.js
普通文件
@@ -0,0 +1,587 @@
|
||||
const { Gitlab } = require('@gitbeaker/rest');
|
||||
const axios = require('axios');
|
||||
const yaml = require('yaml');
|
||||
const fs = require('fs');
|
||||
const minimatch = require('minimatch');
|
||||
const { execSync } = require('child_process');
|
||||
require('dotenv').config();
|
||||
|
||||
// 阿里百炼 https://bailian.console.aliyun.com/
|
||||
const BAILIAN_API_KEY = process.env.BAILIAN_API_KEY;
|
||||
const BAILIAN_API_URL = 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions';
|
||||
|
||||
// 阿波罗AI https://api.ablai.top/personal
|
||||
const ABLAI_API_KEY = process.env.ABLAI_API_KEY;
|
||||
const ABLAI_API_URL = 'https://api.ablai.top/v1/chat/completions';
|
||||
|
||||
// Gemini CLI 配置
|
||||
const CODE_ASSIST_ENDPOINT = process.env.CODE_ASSIST_ENDPOINT || 'https://ww3.html5plus.org/gemini';
|
||||
const GOOGLE_CLOUD_ACCESS_TOKEN = process.env.GOOGLE_CLOUD_ACCESS_TOKEN;
|
||||
const GOOGLE_GENAI_USE_GCA = process.env.GOOGLE_GENAI_USE_GCA || 'true';
|
||||
const GEMINI_MODEL = process.env.GEMINI_MODEL || 'gemini-2.5-pro';
|
||||
|
||||
const GITLAB_TOKEN = process.env.GITLAB_TOKEN;
|
||||
const GITLAB_URL = process.env.CI_SERVER_URL || 'http://git.dcloud.io';
|
||||
|
||||
const api = new Gitlab({
|
||||
token: GITLAB_TOKEN,
|
||||
host: GITLAB_URL
|
||||
});
|
||||
|
||||
// AI 服务商配置
|
||||
const AI_PROVIDERS = {
|
||||
bailian: {
|
||||
name: '阿里百炼',
|
||||
apiKey: BAILIAN_API_KEY,
|
||||
apiUrl: BAILIAN_API_URL,
|
||||
envKey: 'BAILIAN_API_KEY'
|
||||
},
|
||||
ablai: {
|
||||
name: '阿波罗',
|
||||
apiKey: ABLAI_API_KEY,
|
||||
apiUrl: ABLAI_API_URL,
|
||||
envKey: 'ABLAI_API_KEY'
|
||||
},
|
||||
gemini_cli: {
|
||||
name: 'Gemini CLI',
|
||||
type: 'cli',
|
||||
accessToken: GOOGLE_CLOUD_ACCESS_TOKEN,
|
||||
endpoint: CODE_ASSIST_ENDPOINT,
|
||||
useGCA: GOOGLE_GENAI_USE_GCA,
|
||||
model: GEMINI_MODEL,
|
||||
envKey: 'GOOGLE_CLOUD_ACCESS_TOKEN'
|
||||
}
|
||||
};
|
||||
|
||||
// 检查提交是否已经被评审过
|
||||
async function isCommitReviewed(projectId, commitId) {
|
||||
try {
|
||||
const discussions = await api.CommitDiscussions.all(projectId, commitId);
|
||||
return discussions.some(discussion =>
|
||||
discussion.notes.some(note =>
|
||||
note.body.includes('🤖 AI 代码评审结果')
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(`检查提交 ${commitId} 评审状态时出错:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载项目配置
|
||||
function loadProjectConfig() {
|
||||
try {
|
||||
// 在 GitLab CI 环境中,工作目录是 /builds/username/project-name/
|
||||
const configPath = `${process.env.CI_PROJECT_DIR}/code-review/configs/code-review.yaml`;
|
||||
const configContent = fs.readFileSync(configPath, 'utf8');
|
||||
const config = yaml.parse(configContent);
|
||||
|
||||
if (!config || !config.project) {
|
||||
throw new Error('配置文件格式错误');
|
||||
}
|
||||
|
||||
return {
|
||||
reviewGuidelines: config.project.reviewGuidelines || '',
|
||||
ignoreFiles: config.ignore || [],
|
||||
aiModel: config.project.aiModel || process.env.AI_MODEL || "gemini-2.5-pro",
|
||||
provider: config.project.provider || process.env.AI_PROVIDER || 'ablai',
|
||||
maxTokens: config.project.maxTokens || parseInt(process.env.AI_MAX_TOKENS) || 5000
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error loading config:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 生成 AI 评审提示词
|
||||
function generateReviewPrompt(projectConfig, changes, commitInfo = null) {
|
||||
const { reviewGuidelines } = projectConfig;
|
||||
|
||||
// 格式化变更信息
|
||||
const formattedChanges = changes.map(change => {
|
||||
return `
|
||||
#### 文件路径:${change.file}
|
||||
##### 变更内容:
|
||||
${change.diff}
|
||||
${change.content ? `##### 文件完整内容:
|
||||
${change.content}` : ''}
|
||||
`;
|
||||
}).join('\n');
|
||||
|
||||
// 添加 commit 信息
|
||||
const commitInfoText = commitInfo ? `${commitInfo.message}` : '';
|
||||
|
||||
return `
|
||||
${reviewGuidelines}
|
||||
|
||||
### 提交日志 (Commit Message):
|
||||
${commitInfoText}
|
||||
|
||||
### 代码变更及上下文:
|
||||
${formattedChanges}
|
||||
`;
|
||||
}
|
||||
|
||||
// 添加重试函数 - 支持更多错误类型
|
||||
async function retryWithDelay(fn, maxRetries = 5, delay = 3000) {
|
||||
let lastError;
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
|
||||
// 判断是否应该重试
|
||||
const shouldRetry =
|
||||
// 5xx 服务器错误
|
||||
(error.response && error.response.status >= 500) ||
|
||||
// 网络错误
|
||||
error.code === 'ECONNRESET' ||
|
||||
error.code === 'ETIMEDOUT' ||
|
||||
error.code === 'ENOTFOUND' ||
|
||||
error.code === 'ECONNREFUSED' ||
|
||||
// Axios 超时
|
||||
error.code === 'ECONNABORTED' ||
|
||||
// 429 限流错误
|
||||
(error.response && error.response.status === 429);
|
||||
|
||||
if (shouldRetry && i < maxRetries - 1) {
|
||||
const errorMsg = error.response
|
||||
? `状态码: ${error.response.status}`
|
||||
: `错误: ${error.code || error.message}`;
|
||||
console.warn(`AI API 请求失败 (${errorMsg}),正在进行第 ${i + 1}/${maxRetries} 次重试...`);
|
||||
|
||||
// 如果是 429 限流,等待更长时间
|
||||
const retryDelay = error.response?.status === 429 ? delay * 2 : delay;
|
||||
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
||||
continue;
|
||||
}
|
||||
|
||||
// 不应该重试的错误,直接抛出
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
// 使用 Gemini CLI 进行评审 - 带重试机制
|
||||
async function getGeminiCLIReview(prompt, providerConfig, retryCount = 0) {
|
||||
const MAX_RETRIES = 3;
|
||||
const RETRY_DELAY = 3000; // 3秒
|
||||
|
||||
console.log('使用 Gemini CLI 调用...');
|
||||
|
||||
if (!providerConfig.accessToken) {
|
||||
throw new Error(`Gemini Access Token (${providerConfig.envKey}) 未设置`);
|
||||
}
|
||||
|
||||
if (!providerConfig.endpoint) {
|
||||
throw new Error('CODE_ASSIST_ENDPOINT 未设置');
|
||||
}
|
||||
|
||||
// 将 prompt 写入临时文件,避免命令行参数过长
|
||||
const tempFile = `/tmp/gemini_prompt_${Date.now()}.txt`;
|
||||
fs.writeFileSync(tempFile, prompt, 'utf8');
|
||||
|
||||
try {
|
||||
// 构建环境变量
|
||||
const env = {
|
||||
...process.env,
|
||||
CODE_ASSIST_ENDPOINT: providerConfig.endpoint,
|
||||
GOOGLE_CLOUD_ACCESS_TOKEN: providerConfig.accessToken,
|
||||
GOOGLE_GENAI_USE_GCA: providerConfig.useGCA
|
||||
};
|
||||
|
||||
// 使用本地 gemini CLI 执行,从文件读取 prompt
|
||||
const geminiPath = './node_modules/.bin/gemini';
|
||||
const command = `${geminiPath} -m ${providerConfig.model} < "${tempFile}"`;
|
||||
console.log(`执行命令: ${geminiPath} -m ${providerConfig.model} < [prompt file]`);
|
||||
|
||||
const result = execSync(command, {
|
||||
env,
|
||||
encoding: 'utf8',
|
||||
maxBuffer: 50 * 1024 * 1024, // 50MB buffer
|
||||
timeout: 600000, // 10 分钟超时
|
||||
shell: true
|
||||
});
|
||||
|
||||
// 过滤掉 Gemini CLI 的提示信息
|
||||
const cleanedResult = result
|
||||
.replace(/Data collection is disabled\./g, '')
|
||||
.trim();
|
||||
|
||||
return cleanedResult;
|
||||
} catch (error) {
|
||||
// 判断是否应该重试
|
||||
const shouldRetry =
|
||||
// 命令执行超时
|
||||
error.code === 'ETIMEDOUT' ||
|
||||
// 命令执行失败(非零退出码),可能是临时网络问题
|
||||
(error.status && error.status !== 0 && retryCount < MAX_RETRIES);
|
||||
|
||||
if (shouldRetry) {
|
||||
console.warn(`Gemini CLI 调用失败 (${error.message}),正在进行第 ${retryCount + 1}/${MAX_RETRIES} 次重试...`);
|
||||
|
||||
// 清理临时文件
|
||||
if (fs.existsSync(tempFile)) {
|
||||
fs.unlinkSync(tempFile);
|
||||
}
|
||||
|
||||
// 等待后重试
|
||||
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY));
|
||||
return getGeminiCLIReview(prompt, providerConfig, retryCount + 1);
|
||||
}
|
||||
|
||||
// 不应该重试或已达到最大重试次数,抛出错误
|
||||
throw error;
|
||||
} finally {
|
||||
// 清理临时文件
|
||||
if (fs.existsSync(tempFile)) {
|
||||
fs.unlinkSync(tempFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 调用 AI API 进行评审
|
||||
async function getAIReview(prompt, projectConfig) {
|
||||
try {
|
||||
console.log('调用 AI...');
|
||||
console.log(prompt);
|
||||
|
||||
const model = projectConfig.aiModel || "gemini-3-pro-preview";
|
||||
const provider = projectConfig.provider || 'ablai';
|
||||
|
||||
console.log('provider', provider);
|
||||
|
||||
// 获取服务商配置
|
||||
const providerConfig = AI_PROVIDERS[provider];
|
||||
if (!providerConfig) {
|
||||
throw new Error(`不支持的服务商: ${provider}`);
|
||||
}
|
||||
|
||||
// 根据服务商类型选择调用方式
|
||||
if (providerConfig.type === 'cli') {
|
||||
// 使用 Gemini CLI 调用
|
||||
return await getGeminiCLIReview(prompt, providerConfig);
|
||||
}
|
||||
|
||||
// 使用 HTTP API 调用
|
||||
if (!providerConfig.apiKey) {
|
||||
throw new Error(`${providerConfig.name} API Key (${providerConfig.envKey}) 未设置`);
|
||||
}
|
||||
|
||||
// 创建 axios 实例
|
||||
const axiosInstance = axios.create({
|
||||
proxy: false,
|
||||
timeout: 600000 // 设置超时时间为 10 分钟
|
||||
});
|
||||
|
||||
// 使用重试机制发送请求
|
||||
const response = await retryWithDelay(async () => {
|
||||
return await axiosInstance.post(providerConfig.apiUrl, {
|
||||
model: model,
|
||||
messages: [{ role: "user", content: prompt }],
|
||||
temperature: 0.7,
|
||||
max_tokens: projectConfig.maxTokens || 5000
|
||||
}, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${providerConfig.apiKey}`
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return response.data.choices[0].message.content;
|
||||
} catch (error) {
|
||||
console.error('Error calling AI:', error);
|
||||
if (error.code === 'ECONNABORTED') {
|
||||
console.error('API 请求超时,请检查网络连接或增加超时时间');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取代码变更内容
|
||||
async function getChanges(projectId, sourceType, sourceId) {
|
||||
try {
|
||||
let changes;
|
||||
if (sourceType === 'merge_request') {
|
||||
console.log(`获取合并请求 ${sourceId} 的代码变更...`);
|
||||
changes = await api.MergeRequests.allDiffs(projectId, sourceId, {
|
||||
accessRawDiffs: true
|
||||
});
|
||||
console.log(`成功获取合并请求 ${sourceId} 的代码变更,共 ${changes.length} 个文件`);
|
||||
} else if (sourceType === 'push') {
|
||||
console.log(`获取提交 ${sourceId} 的代码变更...`);
|
||||
// 获取单个 commit 的变更
|
||||
const diff = await api.Commits.showDiff(projectId, sourceId);
|
||||
changes = diff.map(change => ({
|
||||
new_path: change.new_path,
|
||||
old_path: change.old_path,
|
||||
diff: change.diff
|
||||
}));
|
||||
console.log(`成功获取提交 ${sourceId} 的代码变更,共 ${changes.length} 个文件`);
|
||||
} else {
|
||||
console.error(`不支持的类型: ${sourceType}`);
|
||||
throw new Error(`不支持的类型: ${sourceType}`);
|
||||
}
|
||||
|
||||
const projectConfig = loadProjectConfig();
|
||||
const ignorePatterns = projectConfig.ignoreFiles || [];
|
||||
|
||||
// 获取变更文件的完整内容
|
||||
const changesWithContent = await Promise.all(changes
|
||||
.filter(change => {
|
||||
// 检查文件是否在忽略列表中
|
||||
return !ignorePatterns.some(pattern => {
|
||||
// 使用 minimatch 进行 glob 模式匹配
|
||||
const shouldIgnore =
|
||||
(change.new_path && minimatch(change.new_path, pattern)) ||
|
||||
(change.old_path && minimatch(change.old_path, pattern));
|
||||
|
||||
if (shouldIgnore) {
|
||||
console.log(`忽略文件: ${change.new_path || change.old_path} (匹配模式: ${pattern})`);
|
||||
}
|
||||
|
||||
return shouldIgnore;
|
||||
});
|
||||
})
|
||||
.map(async change => {
|
||||
const filePath = change.new_path || change.old_path;
|
||||
try {
|
||||
console.log(`正在获取文件 ${filePath} 的完整内容...`);
|
||||
// 获取文件的完整内容
|
||||
const fileContent = await api.RepositoryFiles.show(projectId, filePath, sourceId);
|
||||
// 对 base64 编码的内容进行解码
|
||||
const decodedContent = Buffer.from(fileContent.content, 'base64').toString('utf-8');
|
||||
console.log(`成功获取文件 ${filePath} 的完整内容`);
|
||||
return {
|
||||
file: filePath,
|
||||
diff: change.diff,
|
||||
content: decodedContent
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`无法获取文件 ${filePath} 的完整内容:`, error);
|
||||
return {
|
||||
file: filePath,
|
||||
diff: change.diff
|
||||
};
|
||||
}
|
||||
}));
|
||||
|
||||
console.log(`成功处理所有文件变更,共 ${changesWithContent.length} 个文件`);
|
||||
return changesWithContent;
|
||||
} catch (error) {
|
||||
console.error('获取代码变更失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加评审评论
|
||||
async function addReviewComment(projectId, sourceType, sourceId, review, retryCount = 0) {
|
||||
const MAX_RETRIES = 3;
|
||||
const RETRY_DELAY = 1000; // 1秒
|
||||
|
||||
try {
|
||||
console.log(`添加评审评论 - 项目ID: ${projectId}, 来源类型: ${sourceType}, 来源ID: ${sourceId}`);
|
||||
|
||||
if (!projectId) {
|
||||
throw new Error('项目ID不能为空');
|
||||
}
|
||||
if (!sourceId) {
|
||||
throw new Error('来源ID不能为空');
|
||||
}
|
||||
if (!review) {
|
||||
throw new Error('评审内容不能为空');
|
||||
}
|
||||
|
||||
const note = `🤖 AI 代码评审结果:\n\n${review}`;
|
||||
if (sourceType === 'merge_request') {
|
||||
console.log('正在为合并请求添加评论...');
|
||||
await api.MergeRequestNotes.create(projectId, sourceId, note);
|
||||
console.log('合并请求评论添加成功');
|
||||
} else if (sourceType === 'push') {
|
||||
console.log('正在为提交添加评论...');
|
||||
await api.CommitDiscussions.create(projectId, sourceId, note);
|
||||
console.log('提交评论添加成功');
|
||||
} else {
|
||||
throw new Error(`不支持的来源类型: ${sourceType}`);
|
||||
}
|
||||
} catch (error) {
|
||||
// 检查是否是连接错误(Keep-Alive 超时导致的 socket 关闭)
|
||||
const isSocketError = error.cause?.code === 'UND_ERR_SOCKET' ||
|
||||
error.message === 'fetch failed' ||
|
||||
error.cause?.message?.includes('other side closed');
|
||||
|
||||
if (isSocketError && retryCount < MAX_RETRIES) {
|
||||
console.warn(`连接错误,正在进行第 ${retryCount + 1}/${MAX_RETRIES} 次重试...`);
|
||||
// 等待一段时间后重试
|
||||
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY));
|
||||
return addReviewComment(projectId, sourceType, sourceId, review, retryCount + 1);
|
||||
}
|
||||
|
||||
console.error('添加评审评论失败:', {
|
||||
error: error.message,
|
||||
projectId,
|
||||
sourceType,
|
||||
sourceId,
|
||||
reviewLength: review?.length,
|
||||
retryCount
|
||||
});
|
||||
if (error.cause?.description) {
|
||||
console.error('错误详情:', error.cause.description);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 主处理函数
|
||||
async function processReview(projectId, sourceType, sourceId) {
|
||||
try {
|
||||
const projectConfig = loadProjectConfig();
|
||||
if (!projectConfig) {
|
||||
console.error('Project configuration not found');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (sourceType === 'push') {
|
||||
console.log('上一次提交的 SHA:', process.env.CI_COMMIT_BEFORE_SHA);
|
||||
console.log('当前提交的 SHA:', process.env.CI_COMMIT_SHA);
|
||||
console.log('当前分支:', process.env.CI_COMMIT_BRANCH);
|
||||
|
||||
// 获取本次 push 的所有 commit
|
||||
let commits;
|
||||
// 检查 CI_COMMIT_BEFORE_SHA 是否为全 0 (新分支的首次提交)
|
||||
const isNewBranch = !process.env.CI_COMMIT_BEFORE_SHA ||
|
||||
process.env.CI_COMMIT_BEFORE_SHA === '0000000000000000000000000000000000000000' ||
|
||||
/^0+$/.test(process.env.CI_COMMIT_BEFORE_SHA);
|
||||
|
||||
if (process.env.CI_COMMIT_BEFORE_SHA && process.env.CI_COMMIT_SHA && !isNewBranch) {
|
||||
// 正常的 push,比较两个 commit 之间的差异
|
||||
console.log('正常 push,比较提交差异...');
|
||||
commits = await api.Repositories.compare(projectId, process.env.CI_COMMIT_BEFORE_SHA, process.env.CI_COMMIT_SHA);
|
||||
commits = commits.commits || [];
|
||||
console.log('获取本次提交的信息:', commits);
|
||||
} else {
|
||||
// 新分支的首次提交或没有 BEFORE_SHA,只获取当前提交
|
||||
console.log('新分支首次提交或无法比较,获取当前提交...');
|
||||
commits = await api.Commits.all(projectId, {
|
||||
ref_name: process.env.CI_COMMIT_BRANCH,
|
||||
per_page: 1
|
||||
});
|
||||
console.log('获取首次提交的信息:', commits);
|
||||
}
|
||||
|
||||
// 过滤掉合并分支的提交
|
||||
commits = commits.filter(commit => !commit.message.startsWith('Merge branch'));
|
||||
console.log(`获取到 ${commits.length} 个提交需要评审(已过滤合并分支的提交)`);
|
||||
|
||||
// 对每个 commit 进行评审
|
||||
for (const commit of commits) {
|
||||
console.log(`开始评审提交: ${commit.id}`);
|
||||
console.log(`提交信息: ${commit.message}`);
|
||||
|
||||
// 检查提交是否已经被评审过
|
||||
const isReviewed = await isCommitReviewed(projectId, commit.id);
|
||||
if (isReviewed) {
|
||||
console.log(`提交 ${commit.id} 已经评审过,跳过评审`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 获取该 commit 的变更
|
||||
const changes = await getChanges(projectId, sourceType, commit.id);
|
||||
|
||||
if (changes.length === 0) {
|
||||
console.log(`提交 ${commit.id} 没有代码变更,跳过评审`);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`提交 ${commit.id} 包含 ${changes.length} 个文件变更`);
|
||||
|
||||
// 生成评审提示词
|
||||
const prompt = generateReviewPrompt(projectConfig, changes, {
|
||||
author_name: commit.author_name,
|
||||
created_at: commit.created_at,
|
||||
message: commit.message,
|
||||
ref_name: process.env.CI_COMMIT_BRANCH
|
||||
});
|
||||
|
||||
// 获取 AI 评审结果
|
||||
const review = await getAIReview(prompt, projectConfig);
|
||||
console.log(`AI 评审结果: ${review}`);
|
||||
|
||||
// 添加评审评论到 commit
|
||||
await addReviewComment(projectId, sourceType, commit.id, review);
|
||||
|
||||
console.log(`提交 ${commit.id} 评审完成`);
|
||||
}
|
||||
} else if (sourceType === 'merge_request') {
|
||||
const changes = await getChanges(projectId, sourceType, sourceId);
|
||||
if (changes.length === 0) {
|
||||
console.log('No changes to review');
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取合并请求信息
|
||||
const mrInfo = await api.MergeRequests.show(projectId, sourceId);
|
||||
|
||||
const prompt = generateReviewPrompt(projectConfig, changes, {
|
||||
author_name: mrInfo.author.name,
|
||||
created_at: mrInfo.created_at,
|
||||
message: mrInfo.description,
|
||||
ref_name: mrInfo.source_branch
|
||||
});
|
||||
|
||||
const review = await getAIReview(prompt, projectConfig);
|
||||
|
||||
await addReviewComment(projectId, sourceType, sourceId, review);
|
||||
}
|
||||
|
||||
console.log('Review completed successfully');
|
||||
} catch (error) {
|
||||
console.error('Error processing review:', error);
|
||||
if (error.cause?.description?.includes('401 Unauthorized')) {
|
||||
console.error('GitLab API authentication failed. Please check your GITLAB_TOKEN.');
|
||||
}
|
||||
// 在测试环境中抛出错误,而不是退出进程
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
throw error;
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 导出需要测试的函数
|
||||
module.exports = {
|
||||
loadProjectConfig,
|
||||
generateReviewPrompt,
|
||||
getAIReview,
|
||||
getGeminiCLIReview,
|
||||
getChanges,
|
||||
addReviewComment,
|
||||
processReview
|
||||
};
|
||||
|
||||
// 只在直接运行 index.js 时执行
|
||||
if (require.main === module) {
|
||||
const projectId = process.env.CI_PROJECT_ID;
|
||||
const sourceType = process.env.CI_PIPELINE_SOURCE === 'merge_request_event' ? 'merge_request' : 'push';
|
||||
const sourceId = sourceType === 'merge_request' ? process.env.CI_MERGE_REQUEST_IID : process.env.CI_COMMIT_SHA;
|
||||
|
||||
if (!GITLAB_TOKEN) {
|
||||
console.error('GITLAB_TOKEN is not set');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!projectId) {
|
||||
console.error('CI_PROJECT_ID is not set');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!sourceId) {
|
||||
console.error('Source ID is not set');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
processReview(projectId, sourceType, sourceId);
|
||||
}
|
||||
11011
mpuniapp/code-review/package-lock.json
自动生成的
普通文件
11011
mpuniapp/code-review/package-lock.json
自动生成的
普通文件
文件差异内容过多而无法显示
加载差异
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "uni-ai-code-review",
|
||||
"version": "1.0.0",
|
||||
"description": "AI 代码评审系统",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "jest",
|
||||
"start": "node index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@gitbeaker/rest": "^42.5.0",
|
||||
"@google/gemini-cli": "^0.19.1",
|
||||
"axios": "^1.6.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"minimatch": "^5.1.6",
|
||||
"yaml": "^2.3.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^29.7.0"
|
||||
},
|
||||
"jest": {
|
||||
"testEnvironment": "node",
|
||||
"coverageDirectory": "./coverage",
|
||||
"collectCoverageFrom": [
|
||||
"**/*.js",
|
||||
"!**/*.test.js",
|
||||
"!__tests__/setup.js"
|
||||
],
|
||||
"setupFiles": [
|
||||
"./__tests__/setup.js"
|
||||
],
|
||||
"testMatch": [
|
||||
"**/__tests__/**/*.test.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户