S
Codex SDK 教程 TypeScript
第 07 章

实战:代码审查机器人

实战:AI 代码审查机器人

动真格的了 — 用前面学的所有知识,构建一个能自动审查 Git 代码变更的命令行工具。

我们要做什么

一句话:输入一个 git diff,输出一份结构化的代码审查报告。

就像公司里的 Code Review,但这次审查员是 AI,7x24 小时在线,不请假不摸鱼。

功能清单

  • ✅ 读取 git diff 获取代码变更
  • ✅ 让 Codex 逐块分析代码质量
  • ✅ 用结构化输出生成标准化审查报告
  • ✅ 流式模式实时显示审查进度
  • ✅ 彩色终端输出,美观易读

技术架构

git diff → 解析变更 → 构造 Prompt → Codex SDK → 结构化报告 → 彩色输出

综合运用

  • 第 3 章学的流式处理
  • 第 5 章学的结构化输出(Zod)
  • 第 6 章学的线程配置

项目初始化

bashmkdir ai-code-reviewer && cd ai-code-reviewer
git init
npm init -y
npm install @openai/codex-sdk zod zod-to-json-schema
npm install -D typescript tsx @types/node

tsconfig.json

json{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "esModuleInterop": true,
    "strict": true,
    "outDir": "./dist"
  }
}

第一步:定义审查报告的 Schema

先想清楚我们希望 AI 返回什么格式的数据:

typescript// schema.ts
import { z } from "zod";

// 单个问题
export const IssueSchema = z.object({
  severity: z
    .enum(["info", "warning", "error", "critical"])
    .describe("问题严重程度: info=建议, warning=需关注, error=应修复, critical=必须修复"),
  category: z
    .enum(["bug", "security", "performance", "style", "maintainability", "logic"])
    .describe("问题分类"),
  file: z.string().describe("涉及的文件路径"),
  line: z.number().optional().describe("大致行号(如果能确定的话)"),
  title: z.string().describe("问题标题,10字以内"),
  description: z.string().describe("问题详细描述"),
  suggestion: z.string().describe("具体的修改建议"),
  code_snippet: z.string().optional().describe("有问题的代码片段"),
});

// 整体审查报告
export const ReviewSchema = z.object({
  summary: z.string().describe("整体评价,2-3 句话概括代码质量"),
  score: z.number().min(0).max(100).describe("代码质量评分,100分满分"),
  issues: z.array(IssueSchema).describe("发现的问题列表"),
  highlights: z.array(z.string()).describe("做得好的地方,每条一句话"),
  risk_level: z
    .enum(["low", "medium", "high"])
    .describe("总体风险级别"),
});

export type Review = z.infer<typeof ReviewSchema>;
export type Issue = z.infer<typeof IssueSchema>;

第二步:审查引擎

核心逻辑 — 把 diff 喂给 Codex,拿回结构化报告:

typescript// reviewer.ts
import { Codex } from "@openai/codex-sdk";
import type { ThreadEvent } from "@openai/codex-sdk";
import zodToJsonSchema from "zod-to-json-schema";
import { ReviewSchema, type Review } from "./schema.ts";

const REVIEW_PROMPT = `你是一位资深的代码审查专家,拥有 10 年以上的软件开发经验。

请仔细审查以下 git diff,从这几个维度分析:

1. **Bug 和逻辑错误** — 有没有明显的 bug、边界条件没处理、空指针等
2. **安全隐患** — SQL 注入、XSS、敏感信息泄露、不安全的依赖
3. **性能问题** — N+1 查询、不必要的循环、内存泄漏风险
4. **代码风格** — 命名规范、函数长度、注释质量
5. **可维护性** — 重复代码、过度耦合、缺少测试

审查标准:
- 不要吹毛求疵,只报告真正有意义的问题
- 每个问题都要给出具体的修改建议
- 如果代码写得好,也要在 highlights 里指出

以下是需要审查的 diff:`;

// 非流式版本 — 简单直接
export async function reviewDiff(diff: string): Promise<Review> {
  const codex = new Codex();
  const thread = codex.startThread({
    sandboxMode: "read-only",
    modelReasoningEffort: "high",
    skipGitRepoCheck: true,
  });

  const turn = await thread.run(`${REVIEW_PROMPT}\n\n\`\`\`diff\n${diff}\n\`\`\``, {
    outputSchema: zodToJsonSchema(ReviewSchema, { target: "openAi" }),
  });

  return ReviewSchema.parse(JSON.parse(turn.finalResponse));
}

// 流式版本 — 实时显示进度
export async function reviewDiffStreamed(
  diff: string,
  onProgress: (event: ThreadEvent) => void
): Promise<Review> {
  const codex = new Codex();
  const thread = codex.startThread({
    sandboxMode: "read-only",
    modelReasoningEffort: "high",
    skipGitRepoCheck: true,
  });

  const { events } = await thread.runStreamed(
    `${REVIEW_PROMPT}\n\n\`\`\`diff\n${diff}\n\`\`\``,
    { outputSchema: zodToJsonSchema(ReviewSchema, { target: "openAi" }) }
  );

  let finalResponse = "";

  for await (const event of events) {
    onProgress(event);

    if (event.type === "item.completed" && event.item.type === "agent_message") {
      finalResponse = event.item.text;
    }
  }

  return ReviewSchema.parse(JSON.parse(finalResponse));
}

第三步:漂亮的输出格式化

typescript// format.ts
import type { Review, Issue } from "./schema.ts";

const SEVERITY_CONFIG = {
  info:     { icon: "💬", color: "\x1b[36m", label: "建议" },
  warning:  { icon: "⚠️",  color: "\x1b[33m", label: "警告" },
  error:    { icon: "❌", color: "\x1b[31m", label: "错误" },
  critical: { icon: "🔥", color: "\x1b[91m", label: "严重" },
} as const;

const CATEGORY_LABELS: Record<string, string> = {
  bug: "Bug",
  security: "安全",
  performance: "性能",
  style: "风格",
  maintainability: "可维护性",
  logic: "逻辑",
};

const RESET = "\x1b[0m";
const BOLD = "\x1b[1m";
const DIM = "\x1b[2m";

export function formatReview(review: Review): string {
  const lines: string[] = [];

  // 标题栏
  lines.push("");
  lines.push(`${BOLD}═══════════════════════════════════════════${RESET}`);
  lines.push(`${BOLD}  📋 AI 代码审查报告${RESET}`);
  lines.push(`${BOLD}═══════════════════════════════════════════${RESET}`);

  // 评分和风险
  const scoreColor = review.score >= 80 ? "\x1b[32m" : review.score >= 60 ? "\x1b[33m" : "\x1b[31m";
  const riskIcon = { low: "🟢", medium: "🟡", high: "🔴" }[review.risk_level];

  lines.push("");
  lines.push(`  📊 评分: ${scoreColor}${BOLD}${review.score}/100${RESET}`);
  lines.push(`  ${riskIcon} 风险: ${review.risk_level.toUpperCase()}`);
  lines.push("");
  lines.push(`  ${review.summary}`);

  // 问题列表
  if (review.issues.length > 0) {
    lines.push("");
    lines.push(`${BOLD}───────────────────────────────────────────${RESET}`);
    lines.push(`${BOLD}  🔍 发现 ${review.issues.length} 个问题${RESET}`);
    lines.push(`${BOLD}───────────────────────────────────────────${RESET}`);

    for (const issue of review.issues) {
      const { icon, color, label } = SEVERITY_CONFIG[issue.severity];
      lines.push("");
      lines.push(`  ${icon} ${color}[${label}]${RESET} ${BOLD}${issue.title}${RESET}`);
      lines.push(`     ${DIM}${CATEGORY_LABELS[issue.category] || issue.category} · ${issue.file}${issue.line ? `:${issue.line}` : ""}${RESET}`);
      lines.push(`     ${issue.description}`);
      lines.push(`     💡 ${issue.suggestion}`);
      if (issue.code_snippet) {
        lines.push(`     ${DIM}>>> ${issue.code_snippet}${RESET}`);
      }
    }
  } else {
    lines.push("");
    lines.push("  ✅ 没有发现问题,代码质量很棒!");
  }

  // 亮点
  if (review.highlights.length > 0) {
    lines.push("");
    lines.push(`${BOLD}───────────────────────────────────────────${RESET}`);
    lines.push(`${BOLD}  ✨ 做得好的地方${RESET}`);
    lines.push(`${BOLD}───────────────────────────────────────────${RESET}`);
    for (const highlight of review.highlights) {
      lines.push(`  • ${highlight}`);
    }
  }

  // 统计
  const counts = {
    critical: review.issues.filter(i => i.severity === "critical").length,
    error: review.issues.filter(i => i.severity === "error").length,
    warning: review.issues.filter(i => i.severity === "warning").length,
    info: review.issues.filter(i => i.severity === "info").length,
  };

  lines.push("");
  lines.push(`${BOLD}═══════════════════════════════════════════${RESET}`);
  lines.push(`  🔥 ${counts.critical} 严重  ❌ ${counts.error} 错误  ⚠️ ${counts.warning} 警告  💬 ${counts.info} 建议`);
  lines.push(`${BOLD}═══════════════════════════════════════════${RESET}`);
  lines.push("");

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

第四步:CLI 入口

把所有东西串起来:

typescript// cli.ts
import { execSync } from "child_process";
import type { ThreadEvent } from "@openai/codex-sdk";
import { reviewDiffStreamed } from "./reviewer.ts";
import { formatReview } from "./format.ts";

async function main() {
  // 读取命令行参数
  const target = process.argv[2] || "HEAD~1";
  console.log(`\n📝 审查目标: git diff ${target}\n`);

  // 获取 diff
  let diff: string;
  try {
    diff = execSync(`git diff ${target}`, { encoding: "utf-8" });
  } catch (err) {
    console.error("❌ 获取 git diff 失败。确保你在 Git 仓库里,且目标有效。");
    process.exit(1);
  }

  if (!diff.trim()) {
    console.log("ℹ️ 没有代码变更,无需审查。");
    process.exit(0);
  }

  // 显示变更统计
  const fileCount = new Set(diff.match(/^diff --git .+/gm) || []).size;
  const additions = (diff.match(/^\+[^+]/gm) || []).length;
  const deletions = (diff.match(/^-[^-]/gm) || []).length;
  console.log(`📊 变更统计: ${fileCount} 个文件, +${additions} -${deletions} 行\n`);

  // 流式审查
  console.log("🤖 AI 正在审查...\n");

  const onProgress = (event: ThreadEvent): void => {
    switch (event.type) {
      case "turn.started":
        process.stdout.write("   ⏳ 分析中");
        break;
      case "item.started":
        process.stdout.write(".");
        break;
      case "item.completed":
        if (event.item.type === "command_execution") {
          process.stdout.write(`\n   🖥️ 执行: ${event.item.command}`);
        }
        break;
    }
  };

  try {
    const review = await reviewDiffStreamed(diff, onProgress);
    console.log(formatReview(review));

    // 退出码:有严重问题返回 1(方便 CI/CD 判断)
    const hasCritical = review.issues.some(i => i.severity === "critical" || i.severity === "error");
    process.exit(hasCritical ? 1 : 0);
  } catch (err) {
    console.error("\n❌ 审查失败:", err instanceof Error ? err.message : err);
    process.exit(2);
  }
}

main();

第五步:试一试

在任何 Git 仓库里,先做一些改动,然后运行审查:

bash# 审查最近一次提交的变更
npx tsx cli.ts HEAD~1

# 审查某个分支的所有变更
npx tsx cli.ts main...feature-branch

# 审查暂存区的变更
npx tsx cli.ts --staged

可以在 package.json 里加个快捷命令:

json{
  "scripts": {
    "review": "tsx cli.ts"
  }
}

然后直接:

bashnpm run review HEAD~3

输出效果展示

审查完成后,你会看到类似这样的报告:

═══════════════════════════════════════════
  📋 AI 代码审查报告
═══════════════════════════════════════════

  📊 评分: 72/100
  🟡 风险: medium

  整体来看代码功能实现正确,但有几处安全和性能问题需要处理。

───────────────────────────────────────────
  🔍 发现 3 个问题
───────────────────────────────────────────

  🔥 [严重] SQL 注入风险
     安全 · src/db/query.ts:42
     直接拼接用户输入到 SQL 查询中,存在注入风险
     💡 使用参数化查询代替字符串拼接

  ⚠️ [警告] 缺少错误处理
     Bug · src/api/handler.ts:18
     async 函数没有 try-catch,未处理的 Promise rejection 会导致崩溃
     💡 添加 try-catch 块,返回适当的错误响应

  💬 [建议] 变量命名可以更清晰
     风格 · src/utils/helpers.ts:7
     变量名 'x' 含义不清,影响可读性
     💡 改为更具描述性的名称,如 'retryCount'

───────────────────────────────────────────
  ✨ 做得好的地方
───────────────────────────────────────────
  • 函数职责单一,模块化做得好
  • TypeScript 类型定义完整

═══════════════════════════════════════════
  🔥 1 严重  ❌ 0 错误  ⚠️ 1 警告  💬 1 建议
═══════════════════════════════════════════

进一步扩展思路

这个工具的基础框架已经搭好了,你可以继续扩展:

  • 集成到 GitHub Actions — 在 PR 上自动审查,评论到 PR 里
  • Markdown 报告 — 输出 .md 文件,可以存档或发邮件
  • 多模型对比 — 用不同模型审查同一份代码,取交集
  • 自定义审查规则 — 通过 AGENTS.md 或 prompt 注入项目特定的编码规范
  • 增量审查 — 只审查本次新增的变更,跳过已审查过的

小结

这一章我们综合运用了:

技术 用在了哪里
Codex + Thread 创建审查引擎
runStreamed() 实时显示审查进度
outputSchema + Zod 结构化审查报告
sandboxMode: "read-only" 只读模式,安全审查
modelReasoningEffort: "high" 深度分析,不放过细节

一个真正实用的 AI 代码审查工具,200 多行代码搞定。

下一章实战:全能 AI 助手 — 终极 Boss 关,接入飞书、Skills 扩展、定时任务。