Claude Code | | 约 43 分钟 | 16,825 字

用 Claude Code 构建自动化代码审查机器人

基于 SDK 和 Headless 模式打造 PR 自动审查流水线

项目目标

我们要构建一个自动化代码审查机器人,它能:

  1. 监听 GitHub PR 事件
  2. 自动分析代码变更
  3. 生成结构化的审查意见
  4. 将审查结果发布为 PR 评论
  5. 按严重程度分类问题

最终效果:每次有人提交 PR,机器人会在几分钟内给出专业的代码审查意见。


架构设计

┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│   GitHub     │     │   Review     │     │  Claude Code │
│   Webhook    │────▶│   Service    │────▶│    SDK       │
│              │     │              │     │              │
└──────────────┘     └──────┬───────┘     └──────────────┘

                     ┌──────▼───────┐
                     │   GitHub     │
                     │   API        │
                     │  (发布评论)   │
                     └──────────────┘

技术栈

组件技术说明
运行时Node.js 22 + TypeScript主要开发语言
AI 引擎@anthropic-ai/claude-code代码分析
GitHub 集成@octokit/restGitHub API 客户端
HTTP 服务Hono接收 Webhook
部署GitHub Actions无需额外服务器

方案选择

我们有两种实现方案:

方案 A:GitHub Actions(推荐)

直接在 GitHub Actions 中运行,无需额外服务器:

  • 优点:零运维、免费(公开仓库)、配置简单
  • 缺点:冷启动慢、运行时间有限制

方案 B:Webhook 服务

部署独立的 Webhook 服务:

  • 优点:响应快、可定制性强
  • 缺点:需要服务器、需要运维

本文以方案 A 为主,同时展示方案 B 的核心代码。


方案 A:GitHub Actions 实现

项目结构

.github/
├── workflows/
│   └── ai-review.yml        # GitHub Actions 工作流
└── scripts/
    ├── review.ts             # 审查主逻辑
    ├── analyzer.ts           # 代码分析器
    ├── formatter.ts          # 结果格式化
    └── github.ts             # GitHub API 封装

第一步:工作流配置

# .github/workflows/ai-review.yml
name: AI Code Review

on:
  pull_request:
    types: [opened, synchronize]
    paths:
      - 'src/**'
      - 'lib/**'
      - 'tests/**'

permissions:
  contents: read
  pull-requests: write

jobs:
  review:
    runs-on: ubuntu-latest
    timeout-minutes: 10

    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 22

      - name: Install Dependencies
        run: |
          npm install -g @anthropic-ai/claude-code tsx
          npm install @octokit/rest

      - name: Run AI Review
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
          BASE_SHA: ${{ github.event.pull_request.base.sha }}
          HEAD_SHA: ${{ github.event.pull_request.head.sha }}
        run: npx tsx .github/scripts/review.ts

第二步:审查主逻辑

// .github/scripts/review.ts
import { analyzeChanges } from "./analyzer";
import { formatReview } from "./formatter";
import { postReviewComment } from "./github";

async function main() {
  const prNumber = parseInt(process.env.PR_NUMBER || "0");
  const baseSha = process.env.BASE_SHA || "";
  const headSha = process.env.HEAD_SHA || "";

  if (!prNumber || !baseSha || !headSha) {
    console.error("缺少必要的环境变量");
    process.exit(1);
  }

  console.log(`开始审查 PR #${prNumber}`);
  console.log(`Base: ${baseSha.substring(0, 7)}, Head: ${headSha.substring(0, 7)}`);

  // 1. 分析代码变更
  const analysis = await analyzeChanges(baseSha, headSha);

  // 2. 格式化审查结果
  const reviewBody = formatReview(analysis);

  // 3. 发布审查评论
  await postReviewComment(prNumber, reviewBody);

  console.log("审查完成");
}

main().catch((error) => {
  console.error("审查失败:", error);
  process.exit(1);
});

第三步:代码分析器

// .github/scripts/analyzer.ts
import { execSync } from "child_process";
import { claude } from "@anthropic-ai/claude-code";

export interface ReviewIssue {
  file: string;
  line: number;
  severity: "critical" | "warning" | "suggestion";
  category: string;
  message: string;
  suggestion: string;
}

export interface ReviewAnalysis {
  issues: ReviewIssue[];
  summary: string;
  score: number;
  stats: {
    filesReviewed: number;
    totalIssues: number;
    critical: number;
    warnings: number;
    suggestions: number;
  };
}

export async function analyzeChanges(
  baseSha: string,
  headSha: string
): Promise<ReviewAnalysis> {
  // 获取变更的文件列表
  const diffOutput = execSync(
    `git diff --name-only ${baseSha}...${headSha}`
  ).toString();
  const changedFiles = diffOutput
    .trim()
    .split("\n")
    .filter((f) => f.match(/\.(ts|tsx|js|jsx)$/));

  if (changedFiles.length === 0) {
    return {
      issues: [],
      summary: "没有需要审查的代码文件变更。",
      score: 10,
      stats: {
        filesReviewed: 0,
        totalIssues: 0,
        critical: 0,
        warnings: 0,
        suggestions: 0,
      },
    };
  }

  // 获取 diff 内容
  const diffContent = execSync(
    `git diff ${baseSha}...${headSha} -- ${changedFiles.join(" ")}`
  ).toString();

  // 使用 Claude Code 分析
  const response = await claude(
    `你是一个资深代码审查专家。请审查以下代码变更。

变更的文件:
${changedFiles.join("\n")}

Diff 内容:
\`\`\`diff
${diffContent.substring(0, 50000)}
\`\`\`

请以严格的 JSON 格式返回审查结果(不要包含 markdown 代码块标记):
{
  "issues": [
    {
      "file": "文件路径",
      "line": 行号,
      "severity": "critical|warning|suggestion",
      "category": "分类(security/performance/bug/style/maintainability)",
      "message": "问题描述",
      "suggestion": "改进建议"
    }
  ],
  "summary": "整体评价(2-3 句话)",
  "score": 1到10的评分
}

审查重点:
1. 安全漏洞(SQL 注入、XSS、敏感信息泄露)
2. 潜在 Bug(空指针、类型错误、边界条件)
3. 性能问题(不必要的计算、内存泄漏、N+1 查询)
4. 代码质量(可读性、重复代码、命名规范)
5. 最佳实践(错误处理、类型安全、测试覆盖)`,
    {
      cwd: process.cwd(),
      allowedTools: ["Read", "Glob", "Grep"],
      timeout: 120000,
    }
  );

  try {
    const result = JSON.parse(response.text);
    return {
      ...result,
      stats: {
        filesReviewed: changedFiles.length,
        totalIssues: result.issues.length,
        critical: result.issues.filter(
          (i: ReviewIssue) => i.severity === "critical"
        ).length,
        warnings: result.issues.filter(
          (i: ReviewIssue) => i.severity === "warning"
        ).length,
        suggestions: result.issues.filter(
          (i: ReviewIssue) => i.severity === "suggestion"
        ).length,
      },
    };
  } catch {
    console.error("解析审查结果失败,原始输出:", response.text);
    return {
      issues: [],
      summary: "审查结果解析失败,请手动检查。",
      score: 0,
      stats: {
        filesReviewed: changedFiles.length,
        totalIssues: 0,
        critical: 0,
        warnings: 0,
        suggestions: 0,
      },
    };
  }
}

第四步:结果格式化

// .github/scripts/formatter.ts
import type { ReviewAnalysis, ReviewIssue } from "./analyzer";

const SEVERITY_EMOJI = {
  critical: "🔴",
  warning: "🟡",
  suggestion: "🟢",
};

const SEVERITY_LABEL = {
  critical: "严重",
  warning: "警告",
  suggestion: "建议",
};

const CATEGORY_LABEL: Record<string, string> = {
  security: "安全",
  performance: "性能",
  bug: "Bug",
  style: "代码风格",
  maintainability: "可维护性",
};

export function formatReview(analysis: ReviewAnalysis): string {
  const lines: string[] = [];

  // 标题
  lines.push("## AI Code Review Report");
  lines.push("");

  // 评分
  const scoreBar = getScoreBar(analysis.score);
  lines.push(`### 评分: ${analysis.score}/10 ${scoreBar}`);
  lines.push("");

  // 统计
  lines.push("### 统计");
  lines.push("");
  lines.push(`| 指标 | 数量 |`);
  lines.push(`|------|------|`);
  lines.push(`| 审查文件数 | ${analysis.stats.filesReviewed} |`);
  lines.push(`| 🔴 严重问题 | ${analysis.stats.critical} |`);
  lines.push(`| 🟡 警告 | ${analysis.stats.warnings} |`);
  lines.push(`| 🟢 建议 | ${analysis.stats.suggestions} |`);
  lines.push("");

  // 总结
  lines.push("### 总结");
  lines.push("");
  lines.push(analysis.summary);
  lines.push("");

  // 详细问题
  if (analysis.issues.length > 0) {
    lines.push("### 详细问题");
    lines.push("");

    // 按严重程度分组
    const grouped = groupBySeverity(analysis.issues);

    for (const [severity, issues] of Object.entries(grouped)) {
      const emoji = SEVERITY_EMOJI[severity as keyof typeof SEVERITY_EMOJI];
      const label = SEVERITY_LABEL[severity as keyof typeof SEVERITY_LABEL];
      lines.push(`#### ${emoji} ${label} (${issues.length})`);
      lines.push("");

      for (const issue of issues) {
        const category =
          CATEGORY_LABEL[issue.category] || issue.category;
        lines.push(
          `- **${issue.file}:${issue.line}** [${category}]`
        );
        lines.push(`  ${issue.message}`);
        if (issue.suggestion) {
          lines.push(`  > 建议: ${issue.suggestion}`);
        }
        lines.push("");
      }
    }
  } else {
    lines.push("### 没有发现问题 ✅");
    lines.push("");
    lines.push("代码看起来很好,没有发现需要关注的问题。");
  }

  // 页脚
  lines.push("---");
  lines.push(
    "*由 AI Code Review Bot 自动生成 | Powered by Claude Code*"
  );

  return lines.join("\n");
}

function getScoreBar(score: number): string {
  const filled = "█".repeat(score);
  const empty = "░".repeat(10 - score);
  return `\`${filled}${empty}\``;
}

function groupBySeverity(
  issues: ReviewIssue[]
): Record<string, ReviewIssue[]> {
  const order = ["critical", "warning", "suggestion"];
  const grouped: Record<string, ReviewIssue[]> = {};

  for (const severity of order) {
    const filtered = issues.filter((i) => i.severity === severity);
    if (filtered.length > 0) {
      grouped[severity] = filtered;
    }
  }

  return grouped;
}

第五步:GitHub API 封装

// .github/scripts/github.ts
import { Octokit } from "@octokit/rest";

const octokit = new Octokit({
  auth: process.env.GITHUB_TOKEN,
});

const [owner, repo] = (process.env.GITHUB_REPOSITORY || "/").split("/");

export async function postReviewComment(
  prNumber: number,
  body: string
): Promise<void> {
  // 查找是否已有 bot 评论
  const { data: comments } = await octokit.issues.listComments({
    owner,
    repo,
    issue_number: prNumber,
  });

  const botComment = comments.find(
    (c) =>
      c.user?.login === "github-actions[bot]" &&
      c.body?.includes("AI Code Review Report")
  );

  if (botComment) {
    // 更新已有评论
    await octokit.issues.updateComment({
      owner,
      repo,
      comment_id: botComment.id,
      body,
    });
    console.log(`更新了评论 #${botComment.id}`);
  } else {
    // 创建新评论
    const { data } = await octokit.issues.createComment({
      owner,
      repo,
      issue_number: prNumber,
      body,
    });
    console.log(`创建了评论 #${data.id}`);
  }
}

方案 B:Webhook 服务

如果需要更快的响应和更多定制,可以部署独立的 Webhook 服务:

服务端代码

// src/server.ts
import { Hono } from "hono";
import { verify } from "@octokit/webhooks-methods";
import { handlePullRequest } from "./handler";

const app = new Hono();

const WEBHOOK_SECRET = process.env.GITHUB_WEBHOOK_SECRET || "";

app.post("/webhook", async (c) => {
  // 验证 Webhook 签名
  const signature = c.req.header("x-hub-signature-256") || "";
  const body = await c.req.text();

  const isValid = await verify(WEBHOOK_SECRET, body, signature);
  if (!isValid) {
    return c.json({ error: "Invalid signature" }, 401);
  }

  const event = c.req.header("x-github-event");
  const payload = JSON.parse(body);

  if (event === "pull_request") {
    const action = payload.action;
    if (action === "opened" || action === "synchronize") {
      // 异步处理,立即返回
      handlePullRequest(payload).catch(console.error);
      return c.json({ status: "processing" });
    }
  }

  return c.json({ status: "ignored" });
});

export default {
  port: 3000,
  fetch: app.fetch,
};

处理逻辑

// src/handler.ts
import { analyzeChanges } from "./analyzer";
import { formatReview } from "./formatter";
import { postReviewComment, cloneRepo } from "./github";

export async function handlePullRequest(payload: any) {
  const prNumber = payload.number;
  const repoFullName = payload.repository.full_name;
  const baseSha = payload.pull_request.base.sha;
  const headSha = payload.pull_request.head.sha;
  const cloneUrl = payload.repository.clone_url;

  console.log(`处理 PR #${prNumber} from ${repoFullName}`);

  // 克隆仓库到临时目录
  const repoDir = await cloneRepo(cloneUrl, headSha);

  try {
    // 分析变更
    const analysis = await analyzeChanges(baseSha, headSha);

    // 格式化结果
    const reviewBody = formatReview(analysis);

    // 发布评论
    const [owner, repo] = repoFullName.split("/");
    await postReviewComment(owner, repo, prNumber, reviewBody);

    console.log(`PR #${prNumber} 审查完成`);
  } finally {
    // 清理临时目录
    const { execSync } = await import("child_process");
    execSync(`rm -rf ${repoDir}`);
  }
}

审查质量优化

1. 分层审查

对不同类型的文件使用不同的审查策略:

async function layeredReview(files: string[]): Promise<ReviewIssue[]> {
  const layers = {
    security: files.filter((f) =>
      f.match(/auth|login|password|token|secret|crypto/i)
    ),
    api: files.filter((f) => f.match(/route|controller|handler|api/i)),
    ui: files.filter((f) => f.match(/\.(tsx|jsx)$/)),
    logic: files.filter((f) => f.match(/service|util|helper|lib/i)),
  };

  const results = await Promise.all([
    layers.security.length > 0
      ? claude(
          `作为安全专家审查:${layers.security.join(", ")}
          重点:认证、授权、注入、敏感信息`,
          { allowedTools: ["Read", "Grep"] }
        )
      : null,
    layers.api.length > 0
      ? claude(
          `作为 API 设计专家审查:${layers.api.join(", ")}
          重点:RESTful 规范、错误处理、参数校验、响应格式`,
          { allowedTools: ["Read", "Grep"] }
        )
      : null,
    layers.ui.length > 0
      ? claude(
          `作为前端专家审查:${layers.ui.join(", ")}
          重点:可访问性、性能、状态管理、组件设计`,
          { allowedTools: ["Read", "Grep"] }
        )
      : null,
    layers.logic.length > 0
      ? claude(
          `作为软件工程师审查:${layers.logic.join(", ")}
          重点:算法效率、错误处理、边界条件、可测试性`,
          { allowedTools: ["Read", "Grep"] }
        )
      : null,
  ]);

  // 合并所有结果
  return results
    .filter(Boolean)
    .flatMap((r) => {
      try {
        return JSON.parse(r!.text).issues;
      } catch {
        return [];
      }
    });
}

2. 上下文感知

让审查机器人了解项目的规范:

async function contextAwareReview(
  files: string[],
  projectPath: string
) {
  // 读取项目的 CLAUDE.md 获取上下文
  const fs = await import("fs/promises");
  let projectContext = "";
  try {
    projectContext = await fs.readFile(
      `${projectPath}/CLAUDE.md`,
      "utf-8"
    );
  } catch {
    // 没有 CLAUDE.md,使用默认上下文
  }

  // 读取 ESLint 配置了解代码规范
  let lintConfig = "";
  try {
    lintConfig = await fs.readFile(
      `${projectPath}/eslint.config.js`,
      "utf-8"
    );
  } catch {
    // 没有 ESLint 配置
  }

  return claude(
    `项目上下文:
    ${projectContext}

    代码规范:
    ${lintConfig}

    请基于以上项目上下文审查代码变更...`,
    {
      cwd: projectPath,
      allowedTools: ["Read", "Glob", "Grep"],
    }
  );
}

3. 严重程度分类标准

const SEVERITY_CRITERIA = {
  critical: [
    "安全漏洞(SQL 注入、XSS、CSRF)",
    "数据丢失风险",
    "认证/授权绕过",
    "敏感信息泄露",
    "会导致崩溃的 Bug",
  ],
  warning: [
    "潜在的运行时错误",
    "性能问题(O(n^2) 或更差)",
    "缺少错误处理",
    "类型安全问题",
    "不当的资源管理",
  ],
  suggestion: [
    "代码风格不一致",
    "可以简化的逻辑",
    "缺少注释或文档",
    "命名不够清晰",
    "可以提取的重复代码",
  ],
};

监控与维护

审查质量追踪

// 记录每次审查的统计数据
interface ReviewMetrics {
  prNumber: number;
  timestamp: string;
  filesReviewed: number;
  issuesFound: number;
  duration: number;
  tokensUsed: number;
  score: number;
}

async function trackMetrics(metrics: ReviewMetrics) {
  // 追加到日志文件
  const fs = await import("fs/promises");
  const logLine = JSON.stringify(metrics) + "\n";
  await fs.appendFile("review-metrics.jsonl", logLine);
}

成本监控

function estimateCost(inputTokens: number, outputTokens: number): number {
  const inputCost = (inputTokens / 1_000_000) * 3; // $3/1M input tokens
  const outputCost = (outputTokens / 1_000_000) * 15; // $15/1M output tokens
  return inputCost + outputCost;
}

// 在审查完成后记录成本
const cost = estimateCost(response.usage.inputTokens, response.usage.outputTokens);
console.log(`本次审查成本: $${cost.toFixed(4)}`);

错误告警

# 在 GitHub Actions 中添加失败通知
- name: Notify on Failure
  if: failure()
  uses: actions/github-script@v7
  with:
    script: |
      await github.rest.issues.createComment({
        owner: context.repo.owner,
        repo: context.repo.repo,
        issue_number: context.issue.number,
        body: '⚠️ AI Code Review 执行失败,请手动审查。'
      });

部署清单

GitHub Actions 方案

  • 创建 .github/workflows/ai-review.yml
  • 创建 .github/scripts/ 下的脚本文件
  • 在 GitHub Secrets 中添加 ANTHROPIC_API_KEY
  • 配置 PR 触发条件(路径过滤)
  • 测试:创建一个测试 PR 验证
  • 监控:检查 Actions 运行日志

Webhook 方案

  • 部署 Webhook 服务
  • 在 GitHub 仓库设置中配置 Webhook URL
  • 配置 Webhook Secret
  • 设置 ANTHROPIC_API_KEY 环境变量
  • 配置 GitHub App 或 Personal Access Token
  • 测试:创建一个测试 PR 验证
  • 监控:设置日志和告警

自动化代码审查不是要取代人工审查,而是为它提供一个强大的”第一道防线”。AI 可以在几分钟内发现人眼容易遗漏的问题——类型不安全、缺少错误处理、潜在的性能瓶颈。把机械的检查交给机器,把创造性的判断留给人类,这才是 AI 辅助开发的正确姿势。

评论

加载中...

相关文章

分享:

评论

加载中...