乐于分享
好东西不私藏

【81】代码审查助手实战:让 AI 自动 Review PR,提前拦截低级错误

【81】代码审查助手实战:让 AI 自动 Review PR,提前拦截低级错误

引言

代码审查(Code Review)是保证代码质量的重要手段,但现实很骨感:团队成员水平参差不齐,有人 Review 走马观花,有人 Review 过于严格拖慢进度,还有人干脆不 Review 直接 Merge。

我见过最离谱的是一个 20 人的开发团队,每天几十个 PR,真正认真 Review 的不超过 3 个人。代码质量全靠”信任”——这不翻车才怪。

今天这篇文章,我教大家用 OpenClaw 搭建一个AI 代码审查助手,接 GitHub PR,自动分析代码,给出 Review 意见,让团队把精力放在真正需要人工判断的地方。

正文

一、系统架构

【触发条件】
  GitHub PR 事件(opened / synchronize / review requested)
          ↓
【OpenClaw 审查引擎】
  ├── 代码解析(获取 diff / 文件列表)
  ├── 安全扫描(敏感信息 / SQL 注入 / XSS)
  ├── 代码风格检查(命名 / 注释 / 格式)
  ├── 性能分析(循环 / 递归 / 复杂度)
  └── AI 智能建议(逻辑错误 / 重构方向)
          ↓
【审查报告】
  ├── GitHub PR 评论(Review Comment)
  ├── 审查汇总(严重/建议/通过)
  └── 审查状态(Approve / Request Changes)

二、GitHub Webhook 对接

首先搭建 GitHub 事件的接收和处理:

// github-webhook.js
const crypto = require('crypto');

const WEBHOOK_SECRET = process.env.GITHUB_WEBHOOK_SECRET;

// Express 服务器接收 Webhook
const express = require('express');
const app = express();

app.use(express.json());

app.post('/webhook/github', (req, res) => {
  // 验证 Webhook 签名
  const signature = req.headers['x-hub-signature-256'];
  if (!verifySignature(req.body, signature)) {
    return res.status(401).send('Invalid signature');
  }

  const event = req.headers['x-github-event'];
  const action = req.body.action;
  const pr = req.body.pull_request;

  console.log(`📬 GitHub Webhook: ${event} - ${action}`);

  // 处理不同事件
  if (event === 'pull_request') {
    handlePullRequest(pr, action);
  } else if (event === 'issue_comment') {
    handleComment(req.body);
  }

  res.status(200).send('OK');
});

function verifySignature(body, signature) {
  const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET);
  const digest = 'sha256=' + hmac.update(JSON.stringify(body)).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(digest), Buffer.from(signature));
}

async function handlePullRequest(pr, action) {
  if (['opened', 'synchronize', 'reopened'].includes(action)) {
    // 触发代码审查
    await triggerCodeReview(pr);
  }
}

app.listen(3000, () => console.log('🚀 GitHub Webhook 服务已启动'));

三、代码审查核心模块

// code-reviewer.js
const { getPRDiff, getFileContent } = require('./github-api');
const { scanSecurity } = require('./security-scanner');
const { analyzeComplexity } = require('./code-metrics');
const { generateAIReview } = require('./openclaw-ai');

// 审查配置
const REVIEW_CONFIG = {
  // 严重级别定义
  severity: {
    blocker: '🔴 Blocker - 必须修复',
    major: '🟠 Major - 建议修复',
    minor: '🟡 Minor - 改进建议',
    info: '🔵 Info - 参考'
  },
  // 审查规则
  rules: {
    security: true,    // 安全扫描
    style: true,      // 代码风格
    performance: true, // 性能分析
    aiReview: true    // AI 智能审查
  }
};

// 主审查函数
async function performCodeReview(pr) {
  const { owner, repo, number } = pr;

  console.log(`🔍 开始审查 PR #${number} [${owner}/${repo}]`);

  // 1. 获取 PR 差异
  const diff = await getPRDiff(owner, repo, number);

  // 2. 分析每个变更文件
  const fileReviews = [];

  for (const file of diff.files) {
    const review = await analyzeFile(file);
    fileReviews.push(review);
  }

  // 3. AI 综合审查
  const aiReview = await generateAIComprehensiveReview(diff);

  // 4. 生成审查报告
  const report = {
    pr: { number, title: pr.title, url: pr.html_url },
    timestamp: new Date().toISOString(),
    summary: generateSummary(fileReviews, aiReview),
    files: fileReviews,
    aiInsights: aiReview,
    recommendation: generateRecommendation(fileReviews, aiReview)
  };

  // 5. 发布到 GitHub
  await postReviewToGitHub(report);

  return report;
}

四、安全扫描模块

代码安全是审查的重中之重:

// security-scanner.js
const { Severity } = require('./constants');

// 安全漏洞模式库
const SECURITY_PATTERNS = [
  {
    id: 'SEC001',
    pattern: /password\s*=\s*['"][^'"]+['"]/gi,
    message: '硬编码密码:敏感信息不应直接写在代码中',
    severity: Severity.BLOCKER,
    fix: '使用环境变量或密钥管理服务'
  },
  {
    id: 'SEC002',
    pattern: /api[_-]?key\s*=\s*['"][^'"]+['"]/gi,
    message: '硬编码 API Key:建议使用密钥管理服务',
    severity: Severity.BLOCKER,
    fix: 'process.env.API_KEY 或云 KMS'
  },
  {
    id: 'SEC003',
    pattern: /eval\s*\(/g,
    message: 'Eval 使用风险:可能导致代码注入',
    severity: Severity.MAJOR,
    fix: '使用 JSON.parse() 或其他安全方案'
  },
  {
    id: 'SEC004',
    pattern: /innerHTML\s*=/g,
    message: 'XSS 风险:直接赋值 innerHTML 可能导致 XSS 攻击',
    severity: Severity.MAJOR,
    fix: '使用 textContent 或 DOMPurify 消毒'
  },
  {
    id: 'SEC005',
    pattern: /SELECT\s+\*\s+FROM/gi,
    message: 'SQL 注入风险:使用参数化查询',
    severity: Severity.BLOCKER,
    fix: '使用 prepared statements'
  },
  {
    id: 'SEC006',
    pattern: /\.env$/gm,
    message: '提交 .env 文件:敏感信息可能泄露',
    severity: Severity.BLOCKER,
    fix: '确保 .env 在 .gitignore 中'
  },
  {
    id: 'SEC007',
    pattern: /console\.log\(.*password/gi,
    message: '日志泄露密码风险',
    severity: Severity.MAJOR,
    fix: '移除或使用安全的日志方案'
  },
  {
    id: 'SEC008',
    pattern: /Math\.random\(\)/g,
    message: '随机数不安全:用于安全目的应使用 crypto.randomBytes',
    severity: Severity.MAJOR,
    fix: '使用 crypto.randomBytes() 或 crypto.randomUUID()'
  }
];

function scanSecurity(fileContent, filename) {
  const issues = [];

  for (const rule of SECURITY_PATTERNS) {
    const matches = fileContent.match(rule.pattern);
    if (matches) {
      for (const match of matches) {
        const lines = findLineNumbers(fileContent, match);
        issues.push({
          ...rule,
          file: filename,
          match,
          lines
        });
      }
    }
  }

  return issues;
}

function findLineNumbers(content, match) {
  const lines = [];
  const contentLines = content.split('\n');
  for (let i = 0; i < contentLines.length; i++) {
    if (contentLines[i].includes(match)) {
      lines.push(i + 1);
    }
  }
  return lines;
}

module.exports = { scanSecurity, SECURITY_PATTERNS };

五、AI 智能审查

让 GPT 做更深层的代码逻辑审查:

// ai-reviewer.js
async function generateAIComprehensiveReview(diff) {
  const prompt = `
你是资深代码审查专家,负责审查 Pull Request 的代码质量。

【PR 信息】
标题: ${diff.prTitle}
描述: ${diff.prDescription || '无'}
作者: ${diff.author}

【代码变更统计】
修改文件: ${diff.files.length}
新增代码: ${diff.additions} 行
删除代码: ${diff.deletions} 行

【主要变更】
${diff.files.slice(0, 5).map(f => `
文件: ${f.filename}
变更: +${f.additions}/-${f.deletions}
diff:
${f.patch || '(binary or large file)'}`.join('\n')}

请从以下维度进行审查:
1. **逻辑正确性**:代码逻辑是否有错误

2. **边界情况**:是否考虑了空值、异常等情况

3. **可维护性**:代码是否清晰易读

4. **最佳实践**:是否符合该语言的开发规范

5. **潜在问题**:可能隐藏的 bug 或风险点

输出格式(JSON):
{
  "overallScore": 1-10,
  "summary": "总体评价(100字以内)",
  "strengths": ["优点1", "优点2"],
  "concerns": [
    {"severity": "high/medium/low", "file": "文件名", "issue": "问题描述", "suggestion": "改进建议"}
  ],
  "mustFix": ["必须修复的问题列表"],
  "suggestions": ["改进建议列表"]
}
`;

  const response = await openai.chat.completions.create({
    model: 'gpt-4',
    messages: [{ role: 'user', content: prompt }],
    temperature: 0.3
  });

  return JSON.parse(response.choices[0].message.content);
}

六、发布审查结果到 GitHub

// github-poster.js
const { githubClient } = require('./github-api');

async function postReviewToGitHub(report) {
  const { pr, files, aiInsights, recommendation } = report;

  // 1. 发布文件级别的 Review Comments
  for (const file of files) {
    if (file.issues.length > 0 || file.comments.length > 0) {
      for (const issue of file.issues) {
        await githubClient.rest.issues.createComment({
          owner: pr.owner,
          repo: pr.repo,
          issue_number: pr.number,
          body: formatIssueComment(issue)
        });
      }
    }
  }

  // 2. 发布整体 Review(带状态)
  const reviewBody = formatOverallReview(report);

  await githubClient.rest.pulls.createReview({
    owner: pr.owner,
    repo: pr.repo,
    pull_number: pr.number,
    body: reviewBody,
    event: recommendation.approved ? 'APPROVE' : 'REQUEST_CHANGES',
    comments: files.filter(f => f.issues.length > 0).map(f => ({
      path: f.filename,
      line: f.issues[0].lines[0],
      body: formatFileComment(f)
    }))
  });

  console.log(`✅ Review 已发布 - ${recommendation.approved ? '✅ 批准' : '❌ 需修改'}`);
}

function formatIssueComment(issue) {
  return `
## 🔍 ${issue.id}: ${issue.message}

**文件**: \`${issue.file}\`
**行号**: ${issue.lines.join(', ')}
**严重级别**: ${issue.severity}

**建议修复方案**:
${issue.fix}

---
*OpenClaw AI Code Review*
  `.trim();
}

function formatOverallReview(report) {
  const emoji = report.recommendation.approved ? '✅' : '⚠️';
  return `
${emoji} **OpenClaw 自动代码审查报告**

**审查时间**: ${new Date(report.timestamp).toLocaleString()}

---

### 📊 审查汇总

<table style="width:100%;border-collapse:collapse;margin:12px 0 16px 0;font-size:14px;"><thead><tr><th style="border:1px solid #d9e2f0;background:#f5f9ff;padding:8px 10px;text-align:left;color:#1f2d3d;">维度</th><th style="border:1px solid #d9e2f0;background:#f5f9ff;padding:8px 10px;text-align:left;color:#1f2d3d;">结果</th></tr></thead><tbody><tr><td style="border:1px solid #e6ecf5;padding:8px 10px;color:#333;vertical-align:top;">文件数</td><td style="border:1px solid #e6ecf5;padding:8px 10px;color:#333;vertical-align:top;">${report.files.length}</td></tr><tr><td style="border:1px solid #e6ecf5;padding:8px 10px;color:#333;vertical-align:top;">发现问题</td><td style="border:1px solid #e6ecf5;padding:8px 10px;color:#333;vertical-align:top;">${report.summary.totalIssues}</td></tr><tr><td style="border:1px solid #e6ecf5;padding:8px 10px;color:#333;vertical-align:top;">安全漏洞</td><td style="border:1px solid #e6ecf5;padding:8px 10px;color:#333;vertical-align:top;">🔴 ${report.summary.securityIssues}</td></tr><tr><td style="border:1px solid #e6ecf5;padding:8px 10px;color:#333;vertical-align:top;">代码风格</td><td style="border:1px solid #e6ecf5;padding:8px 10px;color:#333;vertical-align:top;">🟠 ${report.summary.styleIssues}</td></tr><tr><td style="border:1px solid #e6ecf5;padding:8px 10px;color:#333;vertical-align:top;">AI 综合评分</td><td style="border:1px solid #e6ecf5;padding:8px 10px;color:#333;vertical-align:top;">${report.aiInsights.overallScore}/10</td></tr></tbody></table>---

### 🏆 优点

${report.aiInsights.strengths.map(s => `- ✅ ${s}`).join('\n') || '暂无明显优点'}

---

### ⚠️ 需要关注

${report.aiInsights.concerns.map(c => `- ${c.severity === 'high' ? '🔴' : '🟠'} **${c.file}**: ${c.issue}`).join('\n') || '无'}

---

### 🔧 必须修复

${report.aiInsights.mustFix.map(m => `- ❌ ${m}`).join('\n') || '无'}

---

### 💡 建议

${report.aiInsights.suggestions.map(s => `- 💡 ${s}`).join('\n') || '无'}

---

### 🤖 审查结论

**${report.recommendation.message}**

---
*🤖 Powered by OpenClaw AI Review*
  `.trim();
}

七、避坑指南

坑1:误报太多

初期的安全扫描可能产生大量误报,需要配置白名单:

const SECURITY_WHITELIST = [
  // 示例:测试文件中的模拟密码
  { file: '**/*.test.js', pattern: /password\s*=\s*['"]test['"]/i },
  { file: '**/fixtures/**', pattern: /api[_-]?key/i }
];

function isWhitelisted(issue) {
  return SECURITY_WHITELIST.some(w => {
    const matchFile = new RegExp(w.file.replace('**/', '.*').replace('*', '.*'));
    const matchPattern = new RegExp(w.pattern);
    return matchFile.test(issue.file) && matchPattern.test(issue.match);
  });
}

坑2:审查太慢阻塞 PR

大 PR 审查需要时间,应该异步处理:

async function handleLargePR(pr, diff) {
  const fileCount = diff.files.length;

  if (fileCount > 20) {
    // 大 PR 先快速反馈,再异步深入分析
    await postQuickFeedback(pr, {
      message: '🔍 收到大 PR,AI 审查正在进行中,预计 5 分钟完成...',
      status: 'PENDING'
    });

    // 异步深度审查
    setTimeout(() => performFullReview(pr), 0);
  } else {
    await performCodeReview(pr);
  }
}

坑3:Review Comment 被 GitHub 限流

GitHub API 有速率限制,批量评论需要控制频率:

const RATE_LIMIT = 100; // 每小时评论数限制

async function batchComment(comments, delayMs = 1000) {
  for (let i = 0; i < comments.length; i++) {
    await postComment(comments[i]);
    if (i < comments.length - 1) {
      await sleep(delayMs); // 控制频率
    }
  }
}

总结

AI 代码审查助手核心价值:

功能 说明 效果
安全扫描 敏感信息、注入、XSS 检测 防止安全事故
风格检查 命名、注释、格式 代码更规范
复杂度分析 圈复杂度、嵌套深度 提前发现烂代码
AI 智能审查 逻辑错误、边界情况 提升审查深度
自动 Review 发布到 GitHub 零额外操作

部署成本:约 400 行代码 + GitHub App / Webhook
团队价值:每天节省 2-3 小时人工 Review 时间

互动话题

你们团队的代码 Review 现状是怎样的?是流于形式、无人 Review、还是太严格导致 PR 堆积?留言说说你的痛点,下期帮你设计合适的审查规则!