@zhuxb-clouds/ai-code-review 1.0.1

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.
Files changed (4) hide show
  1. package/README.md +149 -0
  2. package/bin/cli.mjs +163 -0
  3. package/bin/hook.mjs +166 -0
  4. package/package.json +37 -0
package/README.md ADDED
@@ -0,0 +1,149 @@
1
+ # AI Code Reviewer & Commit Generator
2
+
3
+ 基于 Node.js 和 OpenAI API 的 Git Hooks 集成方案。在执行 `git commit` 时自动进行代码审查,并根据 Diff 自动生成符合 [Conventional Commits](https://www.conventionalcommits.org/) 规范的提交信息。
4
+
5
+ ## 🚀 核心特性
6
+
7
+ - **自动化审查**:在代码提交前拦截潜在 Bug 或不规范实践
8
+ - **语义化提交**:自动撰写符合 Conventional Commits 规范的提交信息
9
+ - **无感集成**:通过 Git Hooks 实现,无需改变原有开发习惯
10
+ - **成本可控**:支持 Diff 大小限制,避免 Token 浪费
11
+ - **一键安装**:作为 npm 包安装到任何项目
12
+
13
+ ---
14
+
15
+ ## 🛠️ 技术架构
16
+
17
+ ```
18
+ git commit → Husky (prepare-commit-msg) → ai-review-hook → OpenAI API
19
+
20
+ ✅ 通过:自动填充 Commit Message
21
+ ❌ 失败:拦截提交并输出建议
22
+ ```
23
+
24
+ ---
25
+
26
+ ## 📦 快速开始
27
+
28
+ ### 1. 安装到你的项目
29
+
30
+ ```bash
31
+ # 安装为开发依赖
32
+ npm install ai-code-review -D
33
+
34
+ # 初始化(自动安装 Husky 并配置 Git Hook)
35
+ npx ai-review init
36
+ ```
37
+
38
+ ### 2. 配置环境变量
39
+
40
+ 复制生成的 `.env.example` 为 `.env` 并填入你的 API Key:
41
+
42
+ ```bash
43
+ cp .env.example .env
44
+ ```
45
+
46
+ 编辑 `.env`:
47
+
48
+ ```bash
49
+ OPENAI_API_KEY=sk-your-api-key-here
50
+ OPENAI_BASE_URL=https://api.openai.com/v1 # 可选,用于自定义 API 地址
51
+ ```
52
+
53
+ > ⚠️ **安全提示**:`.env` 已自动添加到 `.gitignore`,请勿手动提交!
54
+
55
+ ### 3. 开始使用
56
+
57
+ ```bash
58
+ # 正常开发并暂存更改
59
+ git add .
60
+
61
+ # 发起提交(推荐不带 -m,让 AI 生成)
62
+ git commit
63
+
64
+ # AI 会自动审查并生成提交信息
65
+ # 🔍 正在进行 AI 代码审查...
66
+ # ✅ AI Review 通过
67
+ # 📝 生成的提交信息: feat(auth): add JWT token validation
68
+ ```
69
+
70
+ ---
71
+
72
+ ## 📂 安装后的目录结构
73
+
74
+ ```
75
+ your-project/
76
+ ├── .husky/
77
+ │ └── prepare-commit-msg # Git Hook(自动创建)
78
+ ├── .env # API Key(自己创建,不要提交!)
79
+ ├── .env.example # 配置示例(自动创建)
80
+ ├── .gitignore # 已包含 .env
81
+ └── package.json # 包含 ai-code-review 依赖
82
+ ```
83
+
84
+ ---
85
+
86
+ ## ⌨️ CLI 命令
87
+
88
+ ```bash
89
+ # 完整初始化(安装 Husky + 配置 Hook)
90
+ npx ai-review init
91
+
92
+ # 仅配置 Hook(如果 Husky 已安装)
93
+ npx ai-review setup
94
+
95
+ # 显示帮助
96
+ npx ai-review help
97
+ ```
98
+
99
+ ### 特殊情况
100
+
101
+ ```bash
102
+ # 跳过 AI 审查(紧急情况)
103
+ git commit --no-verify -m "hotfix: urgent fix"
104
+
105
+ # 带 -m 提交时仍会进行审查,但会使用你提供的消息
106
+ git commit -m "your message"
107
+ ```
108
+
109
+ ---
110
+
111
+ ## ⚙️ 配置选项
112
+
113
+ 通过环境变量配置:
114
+
115
+ | 环境变量 | 默认值 | 说明 |
116
+ | ------------------------- | --------------------------- | --------------------------------------- |
117
+ | `OPENAI_API_KEY` | - | **必填**,OpenAI API Key |
118
+ | `OPENAI_BASE_URL` | `https://api.openai.com/v1` | 自定义 API 地址 |
119
+ | `OPENAI_MODEL` | `gpt-4o-mini` | 模型,可选 `gpt-4o`、`gpt-3.5-turbo` 等 |
120
+ | `AI_REVIEW_MAX_DIFF_SIZE` | `15000` | 最大 Diff 字符数,超出将被截断 |
121
+ | `AI_REVIEW_TIMEOUT` | `30000` | API 请求超时时间(毫秒) |
122
+
123
+ ---
124
+
125
+ ## 🔄 替代方案对比
126
+
127
+ | 方案 | 优点 | 缺点 |
128
+ | ------------------------------------------------- | --------------------------------- | ---------------------- |
129
+ | **本方案 (ai-code-review)** | 一键安装、代码审查 + 提交信息生成 | 需要 OpenAI API Key |
130
+ | [aicommits](https://github.com/Nutlope/aicommits) | 开箱即用的 CLI | 不含代码审查功能 |
131
+ | [cz-git](https://cz-git.qbb.sh/) + AI | 交互式提交、规范完善 | 配置较复杂 |
132
+ | GitHub Copilot | IDE 集成、体验好 | 需要订阅、无 Hook 集成 |
133
+
134
+ ---
135
+
136
+ ## ⚠️ 注意事项
137
+
138
+ 1. **网络环境**:确保可访问 OpenAI API(或配置代理/自定义 Base URL)
139
+ 2. **成本控制**:
140
+ - 默认使用 `gpt-4o-mini`,成本约为 GPT-4 的 1/10
141
+ - 大改动建议分批 `git add` 提交
142
+ 3. **安全**:`.env` 文件绝对不要提交到仓库
143
+ 4. **容错**:脚本在 API 出错时会允许提交,不阻塞开发
144
+
145
+ ---
146
+
147
+ ## 📄 License
148
+
149
+ MIT
package/bin/cli.mjs ADDED
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import { execSync } from "child_process";
6
+ import { fileURLToPath } from "url";
7
+
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
+
10
+ // 查找项目根目录
11
+ function findProjectRoot(startDir) {
12
+ let dir = startDir;
13
+ while (dir !== path.dirname(dir)) {
14
+ if (fs.existsSync(path.join(dir, "package.json"))) {
15
+ return dir;
16
+ }
17
+ dir = path.dirname(dir);
18
+ }
19
+ return startDir;
20
+ }
21
+
22
+ const projectRoot = findProjectRoot(process.cwd());
23
+ const command = process.argv[2];
24
+
25
+ const HOOK_CONTENT = `#!/bin/sh
26
+
27
+ # AI Code Review Hook
28
+ # 将 Git 提供的参数传递给脚本
29
+ # $1: 提交消息文件路径
30
+ # $2: 提交来源 (message, template, merge, squash, commit)
31
+ npx ai-review-hook "$1" "$2"
32
+ `;
33
+
34
+ const ENV_EXAMPLE = `# OpenAI API 配置
35
+ OPENAI_API_KEY=sk-your-api-key-here
36
+ OPENAI_BASE_URL=https://api.openai.com/v1
37
+
38
+ # 可选配置
39
+ # OPENAI_MODEL=gpt-4o-mini
40
+ # AI_REVIEW_MAX_DIFF_SIZE=15000
41
+ # AI_REVIEW_TIMEOUT=30000
42
+ `;
43
+
44
+ function showHelp() {
45
+ console.log(`
46
+ AI Code Review CLI
47
+
48
+ 用法:
49
+ ai-review init 初始化 Husky 并配置 Git Hook
50
+ ai-review setup 仅配置 Git Hook(假设 Husky 已安装)
51
+ ai-review help 显示帮助信息
52
+
53
+ 初始化后:
54
+ 1. 在项目根目录创建 .env 文件并配置 OPENAI_API_KEY
55
+ 2. 正常使用 git add && git commit 即可
56
+
57
+ 跳过检查:
58
+ git commit --no-verify -m "your message"
59
+ `);
60
+ }
61
+
62
+ function initHusky() {
63
+ console.log("🚀 初始化 AI Code Review...\n");
64
+
65
+ // 检查是否在 Git 仓库中
66
+ try {
67
+ execSync("git rev-parse --git-dir", { cwd: projectRoot, stdio: "ignore" });
68
+ } catch {
69
+ console.error("❌ 当前目录不是 Git 仓库,请先执行 git init");
70
+ process.exit(1);
71
+ }
72
+
73
+ // 检查 husky 是否已安装
74
+ const packageJsonPath = path.join(projectRoot, "package.json");
75
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
76
+ const hasHusky = packageJson.devDependencies?.husky || packageJson.dependencies?.husky;
77
+
78
+ if (!hasHusky) {
79
+ console.log("📦 安装 Husky...");
80
+ try {
81
+ execSync("npm install husky -D", { cwd: projectRoot, stdio: "inherit" });
82
+ } catch {
83
+ console.error("❌ Husky 安装失败");
84
+ process.exit(1);
85
+ }
86
+ }
87
+
88
+ // 初始化 husky
89
+ console.log("\n🔧 初始化 Husky...");
90
+ try {
91
+ execSync("npx husky init", { cwd: projectRoot, stdio: "inherit" });
92
+ } catch {
93
+ // husky init 可能已经执行过,继续
94
+ }
95
+
96
+ setupHook();
97
+ }
98
+
99
+ function setupHook() {
100
+ const huskyDir = path.join(projectRoot, ".husky");
101
+
102
+ // 确保 .husky 目录存在
103
+ if (!fs.existsSync(huskyDir)) {
104
+ fs.mkdirSync(huskyDir, { recursive: true });
105
+ }
106
+
107
+ // 创建 prepare-commit-msg hook
108
+ const hookPath = path.join(huskyDir, "prepare-commit-msg");
109
+ fs.writeFileSync(hookPath, HOOK_CONTENT);
110
+ fs.chmodSync(hookPath, "755");
111
+ console.log("✅ 创建 Git Hook: .husky/prepare-commit-msg");
112
+
113
+ // 创建 .env.example
114
+ const envExamplePath = path.join(projectRoot, ".env.example");
115
+ if (!fs.existsSync(envExamplePath)) {
116
+ fs.writeFileSync(envExamplePath, ENV_EXAMPLE);
117
+ console.log("✅ 创建配置示例: .env.example");
118
+ }
119
+
120
+ // 更新 .gitignore
121
+ const gitignorePath = path.join(projectRoot, ".gitignore");
122
+ let gitignoreContent = "";
123
+ if (fs.existsSync(gitignorePath)) {
124
+ gitignoreContent = fs.readFileSync(gitignorePath, "utf-8");
125
+ }
126
+ if (!gitignoreContent.includes(".env")) {
127
+ fs.appendFileSync(gitignorePath, "\n# Environment variables\n.env\n.env.local\n");
128
+ console.log("✅ 更新 .gitignore: 添加 .env");
129
+ }
130
+
131
+ console.log(`
132
+ 🎉 配置完成!
133
+
134
+ 下一步:
135
+ 1. 复制 .env.example 为 .env 并填入你的 OpenAI API Key:
136
+ cp .env.example .env
137
+
138
+ 2. 正常提交代码即可:
139
+ git add .
140
+ git commit
141
+
142
+ 跳过检查:
143
+ git commit --no-verify -m "your message"
144
+ `);
145
+ }
146
+
147
+ // 主逻辑
148
+ switch (command) {
149
+ case "init":
150
+ initHusky();
151
+ break;
152
+ case "setup":
153
+ setupHook();
154
+ break;
155
+ case "help":
156
+ case "--help":
157
+ case "-h":
158
+ showHelp();
159
+ break;
160
+ default:
161
+ showHelp();
162
+ break;
163
+ }
package/bin/hook.mjs ADDED
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import { execSync } from "child_process";
6
+ import { fileURLToPath } from "url";
7
+ import OpenAI from "openai";
8
+
9
+ // 查找项目根目录(包含 package.json 的目录)
10
+ function findProjectRoot(startDir) {
11
+ let dir = startDir;
12
+ while (dir !== path.dirname(dir)) {
13
+ if (fs.existsSync(path.join(dir, "package.json"))) {
14
+ return dir;
15
+ }
16
+ dir = path.dirname(dir);
17
+ }
18
+ return startDir;
19
+ }
20
+
21
+ // 加载 .env 文件
22
+ function loadEnv(envPath) {
23
+ if (fs.existsSync(envPath)) {
24
+ const envContent = fs.readFileSync(envPath, "utf-8");
25
+ envContent.split("\n").forEach((line) => {
26
+ const trimmed = line.trim();
27
+ if (trimmed && !trimmed.startsWith("#")) {
28
+ const [key, ...vals] = trimmed.split("=");
29
+ if (key && vals.length) {
30
+ process.env[key.trim()] = vals.join("=").trim();
31
+ }
32
+ }
33
+ });
34
+ }
35
+ }
36
+
37
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
38
+ const projectRoot = findProjectRoot(process.cwd());
39
+
40
+ // 尝试从多个位置加载 .env
41
+ loadEnv(path.join(projectRoot, ".env"));
42
+ loadEnv(path.join(process.cwd(), ".env"));
43
+
44
+ // 配置
45
+ const CONFIG = {
46
+ model: process.env.OPENAI_MODEL || "gpt-4o-mini",
47
+ maxDiffSize: parseInt(process.env.AI_REVIEW_MAX_DIFF_SIZE) || 15000,
48
+ timeout: parseInt(process.env.AI_REVIEW_TIMEOUT) || 30000,
49
+ skipBuild: process.env.AI_REVIEW_SKIP_BUILD === "true",
50
+ buildCommand: process.env.AI_REVIEW_BUILD_COMMAND || "npm run build",
51
+ };
52
+
53
+ const commitMsgFile = process.argv[2];
54
+ const commitSource = process.argv[3]; // message, template, merge, squash, commit
55
+
56
+ // 如果是 merge/squash 或已有 message,跳过处理
57
+ if (["merge", "squash", "commit"].includes(commitSource)) {
58
+ console.log("ℹ️ 跳过 AI Review(merge/squash/amend 提交)");
59
+ process.exit(0);
60
+ }
61
+
62
+ if (!process.env.OPENAI_API_KEY) {
63
+ console.error("❌ 未找到 OPENAI_API_KEY,请在项目根目录创建 .env 文件");
64
+ console.error(" 示例: OPENAI_API_KEY=sk-your-api-key-here");
65
+ console.log("⚠️ 跳过 AI Review,允许提交");
66
+ process.exit(0);
67
+ }
68
+
69
+ const openai = new OpenAI({
70
+ apiKey: process.env.OPENAI_API_KEY,
71
+ baseURL: process.env.OPENAI_BASE_URL,
72
+ timeout: CONFIG.timeout,
73
+ });
74
+
75
+ const SYSTEM_PROMPT = `你是一个资深代码审查员。请分析以下 Git Diff,执行两个任务:
76
+
77
+ 1. **代码审查**:检查是否存在明显的 Bug、安全漏洞或严重的代码规范问题
78
+ 2. **生成提交信息**:按照 Conventional Commits 规范生成提交信息
79
+
80
+ 返回 JSON 格式:
81
+ - 如果代码通过审查:{"is_passed": true, "message": "type(scope): description"}
82
+ - 如果代码有问题:{"is_passed": false, "reason": "问题描述和修复建议"}
83
+
84
+ 提交类型:feat, fix, docs, style, refactor, perf, test, chore, ci
85
+ 注意:只有严重问题才返回 is_passed: false,代码风格建议可以在 reason 中提及但仍然通过`;
86
+
87
+ async function runAIReview() {
88
+ try {
89
+ // 1. 获取暂存区 Diff
90
+ const diff = execSync("git diff --cached", { encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 });
91
+
92
+ if (!diff.trim()) {
93
+ console.log("ℹ️ 没有暂存的更改");
94
+ process.exit(0);
95
+ }
96
+
97
+ // 2. 运行构建检查
98
+ if (!CONFIG.skipBuild) {
99
+ console.log(`🔨 正在运行构建检查: ${CONFIG.buildCommand}`);
100
+ try {
101
+ execSync(CONFIG.buildCommand, {
102
+ cwd: projectRoot,
103
+ stdio: "inherit",
104
+ encoding: "utf-8",
105
+ });
106
+ console.log("✅ 构建通过");
107
+ } catch (buildError) {
108
+ console.error("❌ 构建失败,请修复后重新提交");
109
+ console.error("\n使用 git commit --no-verify 可跳过检查");
110
+ process.exit(1);
111
+ }
112
+ }
113
+
114
+ // 3. 检查 Diff 大小
115
+ if (diff.length > CONFIG.maxDiffSize) {
116
+ console.warn(`⚠️ Diff 过大 (${(diff.length / 1000).toFixed(1)}KB),建议分批提交`);
117
+ console.warn(` 当前限制: ${CONFIG.maxDiffSize / 1000}KB,超出部分将被截断`);
118
+ }
119
+ const truncatedDiff = diff.slice(0, CONFIG.maxDiffSize);
120
+
121
+ // 4. 调用 OpenAI
122
+ console.log("🔍 正在进行 AI 代码审查...");
123
+
124
+ const completion = await openai.chat.completions.create({
125
+ model: CONFIG.model,
126
+ messages: [
127
+ { role: "system", content: SYSTEM_PROMPT },
128
+ { role: "user", content: truncatedDiff },
129
+ ],
130
+ response_format: { type: "json_object" },
131
+ temperature: 0.3,
132
+ });
133
+
134
+ const result = JSON.parse(completion.choices[0].message.content);
135
+
136
+ // 5. 处理结果
137
+ if (result.is_passed) {
138
+ fs.writeFileSync(commitMsgFile, result.message);
139
+ console.log("✅ AI Review 通过");
140
+ console.log(`📝 生成的提交信息: ${result.message}`);
141
+ if (result.suggestions) {
142
+ console.log(`💡 建议: ${result.suggestions}`);
143
+ }
144
+ } else {
145
+ console.error("❌ AI Review 未通过");
146
+ console.error(`📋 原因: ${result.reason}`);
147
+ console.error("\n使用 git commit --no-verify 可跳过检查");
148
+ process.exit(1);
149
+ }
150
+ } catch (error) {
151
+ if (error.code === "ECONNREFUSED" || error.code === "ETIMEDOUT") {
152
+ console.error("❌ 无法连接到 OpenAI API,请检查网络");
153
+ } else if (error.status === 401) {
154
+ console.error("❌ API Key 无效,请检查 .env 配置");
155
+ } else if (error.status === 429) {
156
+ console.error("❌ API 请求频率超限,请稍后重试");
157
+ } else {
158
+ console.error("❌ AI Review 出错:", error.message);
159
+ }
160
+ // 出错时允许提交,避免阻塞开发流程
161
+ console.log("⚠️ 跳过 AI Review,允许提交");
162
+ process.exit(0);
163
+ }
164
+ }
165
+
166
+ runAIReview();
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@zhuxb-clouds/ai-code-review",
3
+ "version": "1.0.1",
4
+ "description": "基于 OpenAI API 的 Git Hooks 集成方案,自动代码审查并生成 Conventional Commits 提交信息",
5
+ "type": "module",
6
+ "bin": {
7
+ "ai-review": "./bin/cli.mjs",
8
+ "ai-review-hook": "./bin/hook.mjs"
9
+ },
10
+ "scripts": {
11
+ "test": "echo \"Error: no test specified\" && exit 1"
12
+ },
13
+ "keywords": [
14
+ "git",
15
+ "hooks",
16
+ "code-review",
17
+ "openai",
18
+ "conventional-commits",
19
+ "husky"
20
+ ],
21
+ "author": "",
22
+ "license": "MIT",
23
+ "dependencies": {
24
+ "openai": "^4.0.0"
25
+ },
26
+ "peerDependencies": {
27
+ "husky": ">=9.0.0"
28
+ },
29
+ "peerDependenciesMeta": {
30
+ "husky": {
31
+ "optional": true
32
+ }
33
+ },
34
+ "engines": {
35
+ "node": ">=18.0.0"
36
+ }
37
+ }