@zhuxb-clouds/ai-code-review 1.0.2 → 1.1.0
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.
- package/README.md +52 -12
- package/bin/cli.mjs +19 -5
- package/bin/hook.mjs +212 -23
- package/package.json +8 -2
- package/test/test.mjs +534 -0
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# AI Code Reviewer & Commit Generator
|
|
2
2
|
|
|
3
|
-
基于 Node.js 和 OpenAI API 的 Git Hooks 集成方案。在执行 `git commit` 时自动进行代码审查,并根据 Diff 自动生成符合 [Conventional Commits](https://www.conventionalcommits.org/) 规范的提交信息。
|
|
3
|
+
基于 Node.js 和 OpenAI-compatible API 的 Git Hooks 集成方案。在执行 `git commit` 时自动进行代码审查,并根据 Diff 自动生成符合 [Conventional Commits](https://www.conventionalcommits.org/) 规范的提交信息。
|
|
4
|
+
|
|
5
|
+
**支持的 AI 提供商**: OpenAI, DeepSeek
|
|
4
6
|
|
|
5
7
|
## 🚀 核心特性
|
|
6
8
|
|
|
@@ -9,13 +11,15 @@
|
|
|
9
11
|
- **无感集成**:通过 Git Hooks 实现,无需改变原有开发习惯
|
|
10
12
|
- **成本可控**:支持 Diff 大小限制,避免 Token 浪费
|
|
11
13
|
- **一键安装**:作为 npm 包安装到任何项目
|
|
14
|
+
- **多提供商支持**:支持 OpenAI、DeepSeek 等兼容 API
|
|
15
|
+
- **代理支持**:支持 HTTP/HTTPS/SOCKS5 代理
|
|
12
16
|
|
|
13
17
|
---
|
|
14
18
|
|
|
15
19
|
## 🛠️ 技术架构
|
|
16
20
|
|
|
17
21
|
```
|
|
18
|
-
git commit → Husky (prepare-commit-msg) → ai-review-hook →
|
|
22
|
+
git commit → Husky (prepare-commit-msg) → ai-review-hook → AI API (OpenAI/DeepSeek)
|
|
19
23
|
↓
|
|
20
24
|
✅ 通过:自动填充 Commit Message
|
|
21
25
|
❌ 失败:拦截提交并输出建议
|
|
@@ -43,11 +47,26 @@ npx ai-review init
|
|
|
43
47
|
cp .env.example .env
|
|
44
48
|
```
|
|
45
49
|
|
|
46
|
-
|
|
50
|
+
#### 使用 OpenAI
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
AI_PROVIDER=openai
|
|
54
|
+
OPENAI_API_KEY=sk-your-openai-key-here
|
|
55
|
+
OPENAI_MODEL=gpt-4o-mini
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
#### 使用 DeepSeek
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
AI_PROVIDER=deepseek
|
|
62
|
+
DEEPSEEK_API_KEY=sk-your-deepseek-key-here
|
|
63
|
+
# OPENAI_MODEL=deepseek-chat # 可选,默认 deepseek-chat
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
#### 使用代理
|
|
47
67
|
|
|
48
68
|
```bash
|
|
49
|
-
|
|
50
|
-
OPENAI_BASE_URL=https://api.openai.com/v1 # 可选,用于自定义 API 地址
|
|
69
|
+
HTTPS_PROXY=http://127.0.0.1:7890
|
|
51
70
|
```
|
|
52
71
|
|
|
53
72
|
> ⚠️ **安全提示**:`.env` 已自动添加到 `.gitignore`,请勿手动提交!
|
|
@@ -112,13 +131,34 @@ git commit -m "your message"
|
|
|
112
131
|
|
|
113
132
|
通过环境变量配置:
|
|
114
133
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
|
118
|
-
|
|
|
119
|
-
| `
|
|
120
|
-
| `
|
|
121
|
-
| `
|
|
134
|
+
### 基础配置
|
|
135
|
+
|
|
136
|
+
| 环境变量 | 默认值 | 说明 |
|
|
137
|
+
| ------------------ | -------- | ---------------------------------------------------------------- |
|
|
138
|
+
| `AI_PROVIDER` | `openai` | AI 提供商:`openai` 或 `deepseek` |
|
|
139
|
+
| `OPENAI_API_KEY` | - | OpenAI API Key(使用 OpenAI 时必填) |
|
|
140
|
+
| `DEEPSEEK_API_KEY` | - | DeepSeek API Key(使用 DeepSeek 时必填) |
|
|
141
|
+
| `OPENAI_BASE_URL` | 自动设置 | 自定义 API 地址(可覆盖默认) |
|
|
142
|
+
| `OPENAI_MODEL` | 自动设置 | 模型名称(OpenAI 默认 gpt-4o-mini,DeepSeek 默认 deepseek-chat) |
|
|
143
|
+
|
|
144
|
+
### 网络配置
|
|
145
|
+
|
|
146
|
+
| 环境变量 | 默认值 | 说明 |
|
|
147
|
+
| ------------- | ------ | -------------------------- |
|
|
148
|
+
| `HTTPS_PROXY` | - | HTTP/HTTPS/SOCKS5 代理地址 |
|
|
149
|
+
| `HTTP_PROXY` | - | 同上,备选 |
|
|
150
|
+
|
|
151
|
+
### 行为配置
|
|
152
|
+
|
|
153
|
+
| 环境变量 | 默认值 | 说明 |
|
|
154
|
+
| ------------------------- | --------------- | -------------------------- |
|
|
155
|
+
| `AI_REVIEW_MAX_DIFF_SIZE` | `15000` | 最大 Diff 字符数,超出截断 |
|
|
156
|
+
| `AI_REVIEW_TIMEOUT` | `30000` | API 请求超时时间(毫秒) |
|
|
157
|
+
| `AI_REVIEW_MAX_RETRIES` | `3` | 失败时最大重试次数 |
|
|
158
|
+
| `AI_REVIEW_RETRY_DELAY` | `1000` | 重试间隔时间(毫秒) |
|
|
159
|
+
| `AI_REVIEW_VERBOSE` | `false` | 启用详细日志 |
|
|
160
|
+
| `AI_REVIEW_SKIP_BUILD` | `false` | 跳过构建检查 |
|
|
161
|
+
| `AI_REVIEW_BUILD_COMMAND` | `npm run build` | 构建命令 |
|
|
122
162
|
|
|
123
163
|
---
|
|
124
164
|
|
package/bin/cli.mjs
CHANGED
|
@@ -31,14 +31,28 @@ const HOOK_CONTENT = `#!/bin/sh
|
|
|
31
31
|
npx ai-review-hook "$1" "$2"
|
|
32
32
|
`;
|
|
33
33
|
|
|
34
|
-
const ENV_EXAMPLE = `#
|
|
35
|
-
|
|
36
|
-
OPENAI_BASE_URL=https://api.openai.com/v1
|
|
34
|
+
const ENV_EXAMPLE = `# AI 提供商选择 (openai / deepseek)
|
|
35
|
+
# AI_PROVIDER=openai
|
|
37
36
|
|
|
38
|
-
#
|
|
37
|
+
# OpenAI API 配置
|
|
38
|
+
OPENAI_API_KEY=sk-your-openai-api-key-here
|
|
39
|
+
# OPENAI_BASE_URL=https://api.openai.com/v1
|
|
39
40
|
# OPENAI_MODEL=gpt-4o-mini
|
|
40
|
-
|
|
41
|
+
|
|
42
|
+
# DeepSeek API 配置 (使用 AI_PROVIDER=deepseek)
|
|
43
|
+
# DEEPSEEK_API_KEY=sk-your-deepseek-api-key-here
|
|
44
|
+
|
|
45
|
+
# 代理配置 (可选)
|
|
46
|
+
# HTTPS_PROXY=http://127.0.0.1:7890
|
|
47
|
+
|
|
48
|
+
# 其他可选配置
|
|
41
49
|
# AI_REVIEW_TIMEOUT=30000
|
|
50
|
+
# AI_REVIEW_MAX_DIFF_SIZE=15000
|
|
51
|
+
# AI_REVIEW_MAX_RETRIES=3
|
|
52
|
+
# AI_REVIEW_RETRY_DELAY=1000
|
|
53
|
+
# AI_REVIEW_VERBOSE=false
|
|
54
|
+
# AI_REVIEW_SKIP_BUILD=false
|
|
55
|
+
# AI_REVIEW_BUILD_COMMAND=npm run build
|
|
42
56
|
`;
|
|
43
57
|
|
|
44
58
|
function showHelp() {
|
package/bin/hook.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import path from "path";
|
|
|
5
5
|
import { execSync } from "child_process";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
7
|
import OpenAI from "openai";
|
|
8
|
+
import { HttpsProxyAgent } from "https-proxy-agent";
|
|
8
9
|
|
|
9
10
|
// 查找项目根目录(包含 package.json 的目录)
|
|
10
11
|
function findProjectRoot(startDir) {
|
|
@@ -41,15 +42,110 @@ const projectRoot = findProjectRoot(process.cwd());
|
|
|
41
42
|
loadEnv(path.join(projectRoot, ".env"));
|
|
42
43
|
loadEnv(path.join(process.cwd(), ".env"));
|
|
43
44
|
|
|
45
|
+
// AI 提供商预设配置
|
|
46
|
+
const AI_PROVIDERS = {
|
|
47
|
+
openai: {
|
|
48
|
+
baseURL: "https://api.openai.com/v1",
|
|
49
|
+
defaultModel: "gpt-4o-mini",
|
|
50
|
+
envKey: "OPENAI_API_KEY",
|
|
51
|
+
},
|
|
52
|
+
deepseek: {
|
|
53
|
+
baseURL: "https://api.deepseek.com",
|
|
54
|
+
defaultModel: "deepseek-chat",
|
|
55
|
+
envKey: "DEEPSEEK_API_KEY",
|
|
56
|
+
},
|
|
57
|
+
// 可扩展更多提供商
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// 获取当前 AI 提供商配置
|
|
61
|
+
function getAIConfig() {
|
|
62
|
+
const provider = (process.env.AI_PROVIDER || "openai").toLowerCase();
|
|
63
|
+
const preset = AI_PROVIDERS[provider] || AI_PROVIDERS.openai;
|
|
64
|
+
|
|
65
|
+
// 优先使用专用 API Key,否则使用通用 OPENAI_API_KEY
|
|
66
|
+
const apiKey = process.env[preset.envKey] || process.env.OPENAI_API_KEY;
|
|
67
|
+
const baseURL = process.env.OPENAI_BASE_URL || preset.baseURL;
|
|
68
|
+
const model = process.env.OPENAI_MODEL || preset.defaultModel;
|
|
69
|
+
|
|
70
|
+
return { provider, apiKey, baseURL, model };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const aiConfig = getAIConfig();
|
|
74
|
+
|
|
44
75
|
// 配置
|
|
45
76
|
const CONFIG = {
|
|
46
|
-
model:
|
|
77
|
+
model: aiConfig.model,
|
|
47
78
|
maxDiffSize: parseInt(process.env.AI_REVIEW_MAX_DIFF_SIZE) || 15000,
|
|
48
79
|
timeout: parseInt(process.env.AI_REVIEW_TIMEOUT) || 30000,
|
|
49
80
|
skipBuild: process.env.AI_REVIEW_SKIP_BUILD === "true",
|
|
50
81
|
buildCommand: process.env.AI_REVIEW_BUILD_COMMAND || "npm run build",
|
|
82
|
+
verbose: process.env.AI_REVIEW_VERBOSE === "true",
|
|
83
|
+
maxRetries: parseInt(process.env.AI_REVIEW_MAX_RETRIES) || 3,
|
|
84
|
+
retryDelay: parseInt(process.env.AI_REVIEW_RETRY_DELAY) || 1000,
|
|
51
85
|
};
|
|
52
86
|
|
|
87
|
+
// 日志函数
|
|
88
|
+
function log(msg) {
|
|
89
|
+
console.log(msg);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function logVerbose(msg) {
|
|
93
|
+
if (CONFIG.verbose) {
|
|
94
|
+
console.log(`[DEBUG] ${msg}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function logTime(label) {
|
|
99
|
+
if (CONFIG.verbose) {
|
|
100
|
+
return { start: Date.now(), label };
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function logTimeEnd(timer) {
|
|
106
|
+
if (timer && CONFIG.verbose) {
|
|
107
|
+
console.log(`[DEBUG] ${timer.label}: ${Date.now() - timer.start}ms`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 延迟函数
|
|
112
|
+
function sleep(ms) {
|
|
113
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 带重试的 API 调用
|
|
117
|
+
async function callWithRetry(fn, retries = CONFIG.maxRetries) {
|
|
118
|
+
let lastError;
|
|
119
|
+
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
120
|
+
try {
|
|
121
|
+
return await fn();
|
|
122
|
+
} catch (error) {
|
|
123
|
+
lastError = error;
|
|
124
|
+
|
|
125
|
+
// 不可重试的错误
|
|
126
|
+
if (error.status === 401 || error.status === 403) {
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 速率限制,等待更长时间
|
|
131
|
+
if (error.status === 429) {
|
|
132
|
+
const waitTime = CONFIG.retryDelay * attempt * 2;
|
|
133
|
+
logVerbose(`速率限制,等待 ${waitTime}ms 后重试...`);
|
|
134
|
+
await sleep(waitTime);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 网络错误或服务器错误,重试
|
|
139
|
+
if (attempt < retries) {
|
|
140
|
+
const waitTime = CONFIG.retryDelay * attempt;
|
|
141
|
+
logVerbose(`请求失败 (${error.message}),${waitTime}ms 后重试 (${attempt}/${retries})...`);
|
|
142
|
+
await sleep(waitTime);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
throw lastError;
|
|
147
|
+
}
|
|
148
|
+
|
|
53
149
|
const commitMsgFile = process.argv[2];
|
|
54
150
|
const commitSource = process.argv[3]; // message, template, merge, squash, commit
|
|
55
151
|
|
|
@@ -59,44 +155,99 @@ if (["merge", "squash", "commit"].includes(commitSource)) {
|
|
|
59
155
|
process.exit(0);
|
|
60
156
|
}
|
|
61
157
|
|
|
62
|
-
if (!
|
|
63
|
-
console.error(
|
|
64
|
-
console.error(
|
|
158
|
+
if (!aiConfig.apiKey) {
|
|
159
|
+
console.error(`❌ 未找到 API Key,请在项目根目录创建 .env 文件`);
|
|
160
|
+
console.error(` 当前提供商: ${aiConfig.provider}`);
|
|
161
|
+
console.error(
|
|
162
|
+
` 需要设置: ${AI_PROVIDERS[aiConfig.provider]?.envKey || "OPENAI_API_KEY"}=sk-your-api-key-here`,
|
|
163
|
+
);
|
|
164
|
+
console.error(` 可选提供商: ${Object.keys(AI_PROVIDERS).join(", ")}`);
|
|
65
165
|
console.log("⚠️ 跳过 AI Review,允许提交");
|
|
66
166
|
process.exit(0);
|
|
67
167
|
}
|
|
68
168
|
|
|
169
|
+
// 获取代理配置
|
|
170
|
+
function getProxyAgent() {
|
|
171
|
+
const proxyUrl =
|
|
172
|
+
process.env.HTTPS_PROXY ||
|
|
173
|
+
process.env.HTTP_PROXY ||
|
|
174
|
+
process.env.https_proxy ||
|
|
175
|
+
process.env.http_proxy;
|
|
176
|
+
|
|
177
|
+
if (proxyUrl) {
|
|
178
|
+
logVerbose(`使用代理: ${proxyUrl}`);
|
|
179
|
+
return new HttpsProxyAgent(proxyUrl);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 创建 OpenAI 客户端(支持自定义配置和代理)
|
|
186
|
+
const httpAgent = getProxyAgent();
|
|
69
187
|
const openai = new OpenAI({
|
|
70
|
-
apiKey:
|
|
71
|
-
baseURL:
|
|
188
|
+
apiKey: aiConfig.apiKey,
|
|
189
|
+
baseURL: aiConfig.baseURL,
|
|
72
190
|
timeout: CONFIG.timeout,
|
|
191
|
+
maxRetries: 0, // 我们自己处理重试
|
|
192
|
+
httpAgent: httpAgent,
|
|
73
193
|
});
|
|
74
194
|
|
|
75
|
-
|
|
195
|
+
logVerbose(`AI 提供商: ${aiConfig.provider}`);
|
|
196
|
+
logVerbose(`API Base URL: ${aiConfig.baseURL}`);
|
|
197
|
+
logVerbose(`模型: ${aiConfig.model}`);
|
|
198
|
+
|
|
199
|
+
const SYSTEM_PROMPT = `
|
|
200
|
+
你是一个拥有 20 年经验的资深代码架构师。请分析提供的 Git Diff,执行以下任务:
|
|
76
201
|
|
|
77
|
-
1.
|
|
78
|
-
|
|
202
|
+
1. **代码审计 (Critique)**:
|
|
203
|
+
- 检查是否存在逻辑错误、安全漏洞(如敏感信息泄露)、或会导致 Crash 的严重隐患。
|
|
204
|
+
- 评估代码是否简洁,并提出改进建议(如变量命名、冗余代码)。
|
|
205
|
+
- 决策标准:
|
|
206
|
+
- 如果存在阻断性问题(Bug/安全),*is_passed* 返回 false。
|
|
207
|
+
- 如果只是优化建议或代码完美,*is_passed* 返回 true。
|
|
79
208
|
|
|
80
|
-
|
|
81
|
-
-
|
|
82
|
-
-
|
|
209
|
+
2. **生成提交信息 (Commit Message)**:
|
|
210
|
+
- 严格遵循 Conventional Commits 规范。
|
|
211
|
+
- 格式:<type>(<scope>): <description>
|
|
212
|
+
- 类型范围:feat, fix, docs, style, refactor, perf, test, chore, ci。
|
|
213
|
+
- 描述:使用中文,精准概括实质性变动。
|
|
83
214
|
|
|
84
|
-
|
|
85
|
-
|
|
215
|
+
3. **输出要求**:
|
|
216
|
+
- 必须严格返回 JSON 格式,不得包含任何 Markdown 格式说明或其他解释文字。
|
|
217
|
+
- 语言:*reason* 部分使用中文。
|
|
218
|
+
|
|
219
|
+
`;
|
|
86
220
|
|
|
87
221
|
async function runAIReview() {
|
|
222
|
+
const totalTimer = logTime("总耗时");
|
|
223
|
+
|
|
224
|
+
logVerbose(`项目根目录: ${projectRoot}`);
|
|
225
|
+
logVerbose(`模型: ${CONFIG.model}`);
|
|
226
|
+
logVerbose(`最大 Diff 大小: ${CONFIG.maxDiffSize}`);
|
|
227
|
+
logVerbose(`超时时间: ${CONFIG.timeout}ms`);
|
|
228
|
+
logVerbose(`跳过构建: ${CONFIG.skipBuild}`);
|
|
229
|
+
if (process.env.OPENAI_BASE_URL) {
|
|
230
|
+
logVerbose(`API Base URL: ${process.env.OPENAI_BASE_URL}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
88
233
|
try {
|
|
89
234
|
// 1. 获取暂存区 Diff
|
|
235
|
+
logVerbose("正在获取暂存区 Diff...");
|
|
236
|
+
const diffTimer = logTime("获取 Diff");
|
|
90
237
|
const diff = execSync("git diff --cached", { encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 });
|
|
238
|
+
logTimeEnd(diffTimer);
|
|
91
239
|
|
|
92
240
|
if (!diff.trim()) {
|
|
93
241
|
console.log("ℹ️ 没有暂存的更改");
|
|
94
242
|
process.exit(0);
|
|
95
243
|
}
|
|
96
244
|
|
|
245
|
+
logVerbose(`Diff 大小: ${(diff.length / 1000).toFixed(2)}KB`);
|
|
246
|
+
|
|
97
247
|
// 2. 运行构建检查
|
|
98
248
|
if (!CONFIG.skipBuild) {
|
|
99
249
|
console.log(`🔨 正在运行构建检查: ${CONFIG.buildCommand}`);
|
|
250
|
+
const buildTimer = logTime("构建检查");
|
|
100
251
|
try {
|
|
101
252
|
execSync(CONFIG.buildCommand, {
|
|
102
253
|
cwd: projectRoot,
|
|
@@ -104,11 +255,14 @@ async function runAIReview() {
|
|
|
104
255
|
encoding: "utf-8",
|
|
105
256
|
});
|
|
106
257
|
console.log("✅ 构建通过");
|
|
258
|
+
logTimeEnd(buildTimer);
|
|
107
259
|
} catch (buildError) {
|
|
108
260
|
console.error("❌ 构建失败,请修复后重新提交");
|
|
109
261
|
console.error("\n使用 git commit --no-verify 可跳过检查");
|
|
110
262
|
process.exit(1);
|
|
111
263
|
}
|
|
264
|
+
} else {
|
|
265
|
+
logVerbose("跳过构建检查 (AI_REVIEW_SKIP_BUILD=true)");
|
|
112
266
|
}
|
|
113
267
|
|
|
114
268
|
// 3. 检查 Diff 大小
|
|
@@ -120,19 +274,49 @@ async function runAIReview() {
|
|
|
120
274
|
|
|
121
275
|
// 4. 调用 OpenAI
|
|
122
276
|
console.log("🔍 正在进行 AI 代码审查...");
|
|
277
|
+
logVerbose(`发送 Diff 大小: ${(truncatedDiff.length / 1000).toFixed(2)}KB`);
|
|
278
|
+
logVerbose(`最大重试次数: ${CONFIG.maxRetries}`);
|
|
279
|
+
const apiTimer = logTime("API 调用");
|
|
123
280
|
|
|
124
|
-
const completion = await
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
281
|
+
const completion = await callWithRetry(() =>
|
|
282
|
+
openai.chat.completions.create({
|
|
283
|
+
model: CONFIG.model,
|
|
284
|
+
messages: [
|
|
285
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
286
|
+
{
|
|
287
|
+
role: "user",
|
|
288
|
+
content: `
|
|
289
|
+
任务:审计以下 Diff 并生成 commit message。
|
|
290
|
+
|
|
291
|
+
Diff 内容:
|
|
292
|
+
${truncatedDiff}
|
|
293
|
+
|
|
294
|
+
请按此 JSON 结构返回:
|
|
295
|
+
{
|
|
296
|
+
"is_passed": boolean,
|
|
297
|
+
"reason": "此处填写改进建议或未通过的具体原因,如无建议可为空字符串",
|
|
298
|
+
"message": "此处填写生成的 Conventional Commit 消息"
|
|
299
|
+
}
|
|
300
|
+
`,
|
|
301
|
+
},
|
|
302
|
+
],
|
|
303
|
+
response_format: { type: "json_object" },
|
|
304
|
+
temperature: 0.3,
|
|
305
|
+
}),
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
logTimeEnd(apiTimer);
|
|
133
309
|
|
|
134
310
|
const result = JSON.parse(completion.choices[0].message.content);
|
|
135
311
|
|
|
312
|
+
// 输出 token 使用情况
|
|
313
|
+
if (completion.usage) {
|
|
314
|
+
logVerbose(
|
|
315
|
+
`Token 使用: 总计 ${completion.usage.total_tokens} (prompt: ${completion.usage.prompt_tokens}, completion: ${completion.usage.completion_tokens})`,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
logVerbose(`使用模型: ${completion.model}`);
|
|
319
|
+
|
|
136
320
|
// 5. 处理结果
|
|
137
321
|
if (result.is_passed) {
|
|
138
322
|
fs.writeFileSync(commitMsgFile, result.message);
|
|
@@ -141,24 +325,29 @@ async function runAIReview() {
|
|
|
141
325
|
if (result.suggestions) {
|
|
142
326
|
console.log(`💡 建议: ${result.suggestions}`);
|
|
143
327
|
}
|
|
328
|
+
logTimeEnd(totalTimer);
|
|
144
329
|
} else {
|
|
145
330
|
console.error("❌ AI Review 未通过");
|
|
146
331
|
console.error(`📋 原因: ${result.reason}`);
|
|
147
332
|
console.error("\n使用 git commit --no-verify 可跳过检查");
|
|
333
|
+
logTimeEnd(totalTimer);
|
|
148
334
|
process.exit(1);
|
|
149
335
|
}
|
|
150
336
|
} catch (error) {
|
|
151
337
|
if (error.code === "ECONNREFUSED" || error.code === "ETIMEDOUT") {
|
|
152
338
|
console.error("❌ 无法连接到 OpenAI API,请检查网络");
|
|
339
|
+
logVerbose(`Base URL: ${process.env.OPENAI_BASE_URL || "https://api.openai.com/v1"}`);
|
|
153
340
|
} else if (error.status === 401) {
|
|
154
341
|
console.error("❌ API Key 无效,请检查 .env 配置");
|
|
155
342
|
} else if (error.status === 429) {
|
|
156
343
|
console.error("❌ API 请求频率超限,请稍后重试");
|
|
157
344
|
} else {
|
|
158
345
|
console.error("❌ AI Review 出错:", error.message);
|
|
346
|
+
logVerbose(`错误详情: ${JSON.stringify(error, null, 2)}`);
|
|
159
347
|
}
|
|
160
348
|
// 出错时允许提交,避免阻塞开发流程
|
|
161
349
|
console.log("⚠️ 跳过 AI Review,允许提交");
|
|
350
|
+
logTimeEnd(totalTimer);
|
|
162
351
|
process.exit(0);
|
|
163
352
|
}
|
|
164
353
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhuxb-clouds/ai-code-review",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "基于 OpenAI API 的 Git Hooks 集成方案,自动代码审查并生成 Conventional Commits 提交信息",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,12 @@
|
|
|
8
8
|
"ai-review-hook": "./bin/hook.mjs"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"
|
|
11
|
+
"build": "echo '无构建步骤'",
|
|
12
|
+
"test": "node test/test.mjs",
|
|
13
|
+
"test:cli": "node bin/cli.mjs",
|
|
14
|
+
"test:hook": "node bin/hook.mjs .git/COMMIT_EDITMSG",
|
|
15
|
+
"test:link": "npm link && echo '已全局链接,可在其他项目使用 npx ai-review'",
|
|
16
|
+
"test:unlink": "npm unlink -g @zhuxb-clouds/ai-code-review"
|
|
12
17
|
},
|
|
13
18
|
"keywords": [
|
|
14
19
|
"git",
|
|
@@ -21,6 +26,7 @@
|
|
|
21
26
|
"author": "",
|
|
22
27
|
"license": "MIT",
|
|
23
28
|
"dependencies": {
|
|
29
|
+
"https-proxy-agent": "^7.0.6",
|
|
24
30
|
"openai": "^4.0.0"
|
|
25
31
|
},
|
|
26
32
|
"peerDependencies": {
|
package/test/test.mjs
ADDED
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 本地测试脚本
|
|
5
|
+
* 用于测试 AI Code Review 的各项功能
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import { execSync, spawn } from "child_process";
|
|
11
|
+
import { fileURLToPath } from "url";
|
|
12
|
+
import { HttpsProxyAgent } from "https-proxy-agent";
|
|
13
|
+
|
|
14
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const projectRoot = path.resolve(__dirname, "..");
|
|
16
|
+
|
|
17
|
+
// 颜色输出
|
|
18
|
+
const colors = {
|
|
19
|
+
reset: "\x1b[0m",
|
|
20
|
+
green: "\x1b[32m",
|
|
21
|
+
red: "\x1b[31m",
|
|
22
|
+
yellow: "\x1b[33m",
|
|
23
|
+
blue: "\x1b[34m",
|
|
24
|
+
cyan: "\x1b[36m",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function log(msg, color = "reset") {
|
|
28
|
+
console.log(`${colors[color]}${msg}${colors.reset}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function logSection(title) {
|
|
32
|
+
console.log("\n" + "=".repeat(50));
|
|
33
|
+
log(`📋 ${title}`, "cyan");
|
|
34
|
+
console.log("=".repeat(50));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function logVerbose(msg) {
|
|
38
|
+
// 在测试中始终启用 verbose 日志
|
|
39
|
+
console.log(`[DEBUG] ${msg}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 从 OpenAI 错误提取诊断信息
|
|
43
|
+
function analyzeOpenAIError(error) {
|
|
44
|
+
const errorInfo = {
|
|
45
|
+
code: error.code,
|
|
46
|
+
status: error.status,
|
|
47
|
+
message: error.message,
|
|
48
|
+
type: error.type || error.constructor?.name,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// 尝试获取原始错误信息
|
|
52
|
+
if (error.response?.data?.error) {
|
|
53
|
+
const apiError = error.response.data.error;
|
|
54
|
+
errorInfo.apiError = {
|
|
55
|
+
type: apiError.type,
|
|
56
|
+
message: apiError.message,
|
|
57
|
+
param: apiError.param,
|
|
58
|
+
code: apiError.code,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 检查底层错误
|
|
63
|
+
if (error.cause) {
|
|
64
|
+
errorInfo.cause = {
|
|
65
|
+
code: error.cause.code,
|
|
66
|
+
errno: error.cause.errno,
|
|
67
|
+
syscall: error.cause.syscall,
|
|
68
|
+
hostname: error.cause.hostname,
|
|
69
|
+
port: error.cause.port,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return errorInfo;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 测试用例
|
|
77
|
+
const tests = {
|
|
78
|
+
// 测试环境变量加载
|
|
79
|
+
async testEnvLoading() {
|
|
80
|
+
logSection("测试环境变量加载");
|
|
81
|
+
|
|
82
|
+
const envPath = path.join(projectRoot, ".env");
|
|
83
|
+
if (fs.existsSync(envPath)) {
|
|
84
|
+
log("✅ .env 文件存在", "green");
|
|
85
|
+
|
|
86
|
+
const content = fs.readFileSync(envPath, "utf-8");
|
|
87
|
+
const hasApiKey = content.includes("OPENAI_API_KEY");
|
|
88
|
+
|
|
89
|
+
if (hasApiKey) {
|
|
90
|
+
log("✅ OPENAI_API_KEY 已配置", "green");
|
|
91
|
+
} else {
|
|
92
|
+
log("⚠️ OPENAI_API_KEY 未配置", "yellow");
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
log("⚠️ .env 文件不存在,请从 .env.example 复制", "yellow");
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
// 测试 CLI 命令
|
|
100
|
+
async testCLI() {
|
|
101
|
+
logSection("测试 CLI 命令");
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const helpOutput = execSync("node bin/cli.mjs help", {
|
|
105
|
+
cwd: projectRoot,
|
|
106
|
+
encoding: "utf-8",
|
|
107
|
+
});
|
|
108
|
+
log("✅ ai-review help 命令正常", "green");
|
|
109
|
+
console.log(helpOutput);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
log("❌ CLI 命令失败: " + error.message, "red");
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
// 测试 Git Diff 获取
|
|
116
|
+
async testGitDiff() {
|
|
117
|
+
logSection("测试 Git Diff 获取");
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const diff = execSync("git diff --cached", {
|
|
121
|
+
cwd: projectRoot,
|
|
122
|
+
encoding: "utf-8",
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
if (diff.trim()) {
|
|
126
|
+
log(`✅ 检测到暂存区变更 (${(diff.length / 1000).toFixed(1)}KB)`, "green");
|
|
127
|
+
console.log("前 500 字符预览:");
|
|
128
|
+
console.log(diff.slice(0, 500) + (diff.length > 500 ? "\n..." : ""));
|
|
129
|
+
} else {
|
|
130
|
+
log("ℹ️ 暂存区没有变更", "blue");
|
|
131
|
+
log(" 提示: 使用 git add <file> 添加文件后再测试", "yellow");
|
|
132
|
+
}
|
|
133
|
+
} catch (error) {
|
|
134
|
+
log("❌ Git Diff 获取失败: " + error.message, "red");
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
// 测试构建命令
|
|
139
|
+
async testBuild() {
|
|
140
|
+
logSection("测试构建命令");
|
|
141
|
+
|
|
142
|
+
const packageJsonPath = path.join(projectRoot, "package.json");
|
|
143
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
144
|
+
|
|
145
|
+
if (packageJson.scripts?.build) {
|
|
146
|
+
log("ℹ️ 发现 build 脚本,尝试运行...", "blue");
|
|
147
|
+
try {
|
|
148
|
+
execSync("npm run build", { cwd: projectRoot, stdio: "inherit" });
|
|
149
|
+
log("✅ 构建成功", "green");
|
|
150
|
+
} catch (error) {
|
|
151
|
+
log("❌ 构建失败", "red");
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
log("ℹ️ 没有 build 脚本(这是正常的,此项目不需要构建)", "blue");
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
// 模拟 Hook 调用(不实际调用 AI)
|
|
159
|
+
async testHookDryRun() {
|
|
160
|
+
logSection("测试 Hook 逻辑(模拟运行)");
|
|
161
|
+
|
|
162
|
+
// 检查必要文件
|
|
163
|
+
const hookPath = path.join(projectRoot, "bin/hook.mjs");
|
|
164
|
+
if (fs.existsSync(hookPath)) {
|
|
165
|
+
log("✅ hook.mjs 文件存在", "green");
|
|
166
|
+
} else {
|
|
167
|
+
log("❌ hook.mjs 文件不存在", "red");
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 检查依赖
|
|
172
|
+
try {
|
|
173
|
+
await import("openai");
|
|
174
|
+
log("✅ openai 依赖已安装", "green");
|
|
175
|
+
} catch {
|
|
176
|
+
log("❌ openai 依赖未安装,请运行 npm install", "red");
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
// AI 提供商预设配置
|
|
181
|
+
AI_PROVIDERS: {
|
|
182
|
+
openai: {
|
|
183
|
+
baseURL: "https://api.openai.com/v1",
|
|
184
|
+
defaultModel: "gpt-4o-mini",
|
|
185
|
+
envKey: "OPENAI_API_KEY",
|
|
186
|
+
},
|
|
187
|
+
deepseek: {
|
|
188
|
+
baseURL: "https://api.deepseek.com",
|
|
189
|
+
defaultModel: "deepseek-chat",
|
|
190
|
+
envKey: "DEEPSEEK_API_KEY",
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
// 获取当前 AI 配置
|
|
195
|
+
getAIConfig() {
|
|
196
|
+
const provider = (process.env.AI_PROVIDER || "openai").toLowerCase();
|
|
197
|
+
const preset = this.AI_PROVIDERS[provider] || this.AI_PROVIDERS.openai;
|
|
198
|
+
const apiKey = process.env[preset.envKey] || process.env.OPENAI_API_KEY;
|
|
199
|
+
const baseURL = process.env.OPENAI_BASE_URL || preset.baseURL;
|
|
200
|
+
const model = process.env.OPENAI_MODEL || preset.defaultModel;
|
|
201
|
+
return { provider, apiKey, baseURL, model };
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
// 测试 API 连接
|
|
205
|
+
async testAPI() {
|
|
206
|
+
logSection("测试 AI API 连接");
|
|
207
|
+
|
|
208
|
+
// 加载环境变量
|
|
209
|
+
const envPath = path.join(projectRoot, ".env");
|
|
210
|
+
if (!fs.existsSync(envPath)) {
|
|
211
|
+
log("⏭️ 跳过:未找到 .env 文件", "yellow");
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const envContent = fs.readFileSync(envPath, "utf-8");
|
|
216
|
+
envContent.split("\n").forEach((line) => {
|
|
217
|
+
const trimmed = line.trim();
|
|
218
|
+
if (trimmed && !trimmed.startsWith("#")) {
|
|
219
|
+
const [key, ...vals] = trimmed.split("=");
|
|
220
|
+
if (key && vals.length) {
|
|
221
|
+
process.env[key.trim()] = vals.join("=").trim();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const aiConfig = this.getAIConfig();
|
|
227
|
+
|
|
228
|
+
if (!aiConfig.apiKey) {
|
|
229
|
+
log(`⏭️ 跳过:未配置 API Key`, "yellow");
|
|
230
|
+
log(` 当前提供商: ${aiConfig.provider}`, "yellow");
|
|
231
|
+
log(
|
|
232
|
+
` 需要设置: ${this.AI_PROVIDERS[aiConfig.provider]?.envKey || "OPENAI_API_KEY"}`,
|
|
233
|
+
"yellow",
|
|
234
|
+
);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
log(`🔗 正在测试 ${aiConfig.provider.toUpperCase()} API 连接...`, "blue");
|
|
239
|
+
const timeout = parseInt(process.env.AI_REVIEW_TIMEOUT) || 30000;
|
|
240
|
+
const proxyUrl =
|
|
241
|
+
process.env.HTTPS_PROXY ||
|
|
242
|
+
process.env.HTTP_PROXY ||
|
|
243
|
+
process.env.https_proxy ||
|
|
244
|
+
process.env.http_proxy;
|
|
245
|
+
|
|
246
|
+
log(` 提供商: ${aiConfig.provider}`, "blue");
|
|
247
|
+
log(` Base URL: ${aiConfig.baseURL}`, "blue");
|
|
248
|
+
log(` 模型: ${aiConfig.model}`, "blue");
|
|
249
|
+
log(` 超时: ${timeout}ms`, "blue");
|
|
250
|
+
if (proxyUrl) {
|
|
251
|
+
log(` 代理: ${proxyUrl}`, "blue");
|
|
252
|
+
} else {
|
|
253
|
+
log(` 代理: 未配置 (设置 HTTPS_PROXY 环境变量启用)`, "yellow");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
const OpenAI = (await import("openai")).default;
|
|
258
|
+
|
|
259
|
+
// 创建代理 agent
|
|
260
|
+
let httpAgent = undefined;
|
|
261
|
+
if (proxyUrl) {
|
|
262
|
+
httpAgent = new HttpsProxyAgent(proxyUrl);
|
|
263
|
+
logVerbose(` 已创建代理 agent`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// 创建 OpenAI 客户端实例
|
|
267
|
+
log("\n 1️⃣ 初始化 AI 客户端...", "blue");
|
|
268
|
+
const openai = new OpenAI({
|
|
269
|
+
apiKey: aiConfig.apiKey,
|
|
270
|
+
baseURL: aiConfig.baseURL,
|
|
271
|
+
timeout: timeout,
|
|
272
|
+
maxRetries: 0, // 禁用自动重试,由我们控制
|
|
273
|
+
httpAgent: httpAgent,
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
logVerbose(` 客户端初始化成功`);
|
|
277
|
+
|
|
278
|
+
// 测试 API 连接 - 发送轻量级请求
|
|
279
|
+
log(" 2️⃣ 测试 API 连接...", "blue");
|
|
280
|
+
const startTime = Date.now();
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
const completion = await openai.chat.completions.create({
|
|
284
|
+
model: aiConfig.model,
|
|
285
|
+
messages: [
|
|
286
|
+
{
|
|
287
|
+
role: "user",
|
|
288
|
+
content: "Say 'OK' only.",
|
|
289
|
+
},
|
|
290
|
+
],
|
|
291
|
+
max_tokens: 10,
|
|
292
|
+
temperature: 0,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
const elapsed = Date.now() - startTime;
|
|
296
|
+
|
|
297
|
+
log(" 3️⃣ 解析响应...", "blue");
|
|
298
|
+
const response = completion.choices[0]?.message?.content || "";
|
|
299
|
+
|
|
300
|
+
log("\n✅ OpenAI API 连接成功!", "green");
|
|
301
|
+
log(` 实际模型: ${completion.model}`, "blue");
|
|
302
|
+
log(` 响应内容: ${response}`, "blue");
|
|
303
|
+
log(` 响应时间: ${elapsed}ms`, "blue");
|
|
304
|
+
|
|
305
|
+
if (completion.usage) {
|
|
306
|
+
log(` 📊 Token 使用:`, "blue");
|
|
307
|
+
log(` • Prompt tokens: ${completion.usage.prompt_tokens}`, "blue");
|
|
308
|
+
log(` • Completion tokens: ${completion.usage.completion_tokens}`, "blue");
|
|
309
|
+
log(` • Total tokens: ${completion.usage.total_tokens}`, "blue");
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
log(`\n✨ 所有检查通过,API 连接正常!`, "green");
|
|
313
|
+
} catch (apiError) {
|
|
314
|
+
// API 调用失败,但客户端创建成功说明基本连接没问题
|
|
315
|
+
throw apiError;
|
|
316
|
+
}
|
|
317
|
+
} catch (error) {
|
|
318
|
+
log("❌ API 测试失败", "red");
|
|
319
|
+
|
|
320
|
+
// 获取错误详情
|
|
321
|
+
const errorInfo = analyzeOpenAIError(error);
|
|
322
|
+
|
|
323
|
+
logVerbose(`\n错误对象分析:`);
|
|
324
|
+
logVerbose(` Code: ${errorInfo.code}`);
|
|
325
|
+
logVerbose(` Status: ${errorInfo.status}`);
|
|
326
|
+
logVerbose(` Type: ${errorInfo.type}`);
|
|
327
|
+
logVerbose(` Message: ${errorInfo.message}`);
|
|
328
|
+
if (errorInfo.apiError) {
|
|
329
|
+
logVerbose(` API Error: ${JSON.stringify(errorInfo.apiError)}`);
|
|
330
|
+
}
|
|
331
|
+
if (errorInfo.cause) {
|
|
332
|
+
logVerbose(` Cause: ${JSON.stringify(errorInfo.cause)}`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
log("\n🔍 诊断和修复建议:", "yellow");
|
|
336
|
+
|
|
337
|
+
// 详细的错误诊断
|
|
338
|
+
if (error.code === "ECONNREFUSED" || errorInfo.cause?.code === "ECONNREFUSED") {
|
|
339
|
+
log("\n 错误:无法连接到服务器", "red");
|
|
340
|
+
log(" 可能原因:", "blue");
|
|
341
|
+
log(" • Base URL 错误或服务器无法访问", "blue");
|
|
342
|
+
log(" • 网络连接问题", "blue");
|
|
343
|
+
log(" • 防火墙或代理阻止了请求", "blue");
|
|
344
|
+
log(
|
|
345
|
+
`\n 当前 Base URL: ${process.env.OPENAI_BASE_URL || "https://api.openai.com/v1"}`,
|
|
346
|
+
"yellow",
|
|
347
|
+
);
|
|
348
|
+
log(" 修复步骤:", "blue");
|
|
349
|
+
log(" 1. 测试网络: ping api.openai.com", "blue");
|
|
350
|
+
log(" 2. 检查代理: 如需代理,设置 HTTP_PROXY 或 HTTPS_PROXY", "blue");
|
|
351
|
+
log(" 3. 检查防火墙设置", "blue");
|
|
352
|
+
} else if (error.code === "ETIMEDOUT" || errorInfo.cause?.code === "ETIMEDOUT") {
|
|
353
|
+
log("\n 错误:请求超时", "red");
|
|
354
|
+
log(" 可能原因:", "blue");
|
|
355
|
+
log(" • 网络延迟过高", "blue");
|
|
356
|
+
log(" • API 响应缓慢", "blue");
|
|
357
|
+
log(" • 超时时间设置过短", "blue");
|
|
358
|
+
log(`\n 当前超时: ${process.env.AI_REVIEW_TIMEOUT || 30000}ms`, "yellow");
|
|
359
|
+
log(" 修复步骤:", "blue");
|
|
360
|
+
log(" 1. 增加超时: 设置 AI_REVIEW_TIMEOUT=60000", "blue");
|
|
361
|
+
log(" 2. 检查网络延迟: ping api.openai.com", "blue");
|
|
362
|
+
log(" 3. 稍后重试", "blue");
|
|
363
|
+
} else if (error.code === "ENOTFOUND" || errorInfo.cause?.code === "ENOTFOUND") {
|
|
364
|
+
log("\n 错误:DNS 解析失败", "red");
|
|
365
|
+
log(" 可能原因:", "blue");
|
|
366
|
+
log(" • DNS 配置错误", "blue");
|
|
367
|
+
log(" • 域名拼写错误", "blue");
|
|
368
|
+
log(" • 网络无法访问 DNS 服务", "blue");
|
|
369
|
+
log(
|
|
370
|
+
`\n 当前 Base URL: ${process.env.OPENAI_BASE_URL || "https://api.openai.com/v1"}`,
|
|
371
|
+
"yellow",
|
|
372
|
+
);
|
|
373
|
+
log(" 修复步骤:", "blue");
|
|
374
|
+
log(" 1. 检查 DNS 设置", "blue");
|
|
375
|
+
log(" 2. 尝试使用公共 DNS 8.8.8.8", "blue");
|
|
376
|
+
log(" 3. 检查 Base URL 拼写", "blue");
|
|
377
|
+
} else if (error.status === 401 || errorInfo.apiError?.code === "invalid_api_key") {
|
|
378
|
+
log("\n 错误:API Key 无效或已过期", "red");
|
|
379
|
+
log(" 可能原因:", "blue");
|
|
380
|
+
log(" • API Key 不正确或已删除", "blue");
|
|
381
|
+
log(" • API Key 已过期", "blue");
|
|
382
|
+
log(" • API Key 权限不足", "blue");
|
|
383
|
+
log("\n 修复步骤:", "blue");
|
|
384
|
+
log(" 1. 检查 .env 中的 OPENAI_API_KEY 是否正确", "blue");
|
|
385
|
+
log(" 2. 访问 https://platform.openai.com/api-keys 重新生成 Key", "blue");
|
|
386
|
+
log(" 3. 等待 60 秒后重试", "blue");
|
|
387
|
+
} else if (error.status === 429 || errorInfo.apiError?.code === "rate_limit_exceeded") {
|
|
388
|
+
log("\n 错误:请求频率超限或配额不足", "red");
|
|
389
|
+
log(" 可能原因:", "blue");
|
|
390
|
+
log(" • 请求过于频繁", "blue");
|
|
391
|
+
log(" • API 配额已用尽", "blue");
|
|
392
|
+
log(" • 账户余额不足", "blue");
|
|
393
|
+
log("\n 修复步骤:", "blue");
|
|
394
|
+
log(" 1. 等待几分钟后重试", "blue");
|
|
395
|
+
log(" 2. 检查账户余额: https://platform.openai.com/account/billing/overview", "blue");
|
|
396
|
+
log(" 3. 检查使用配额: https://platform.openai.com/account/rate-limits", "blue");
|
|
397
|
+
} else if (error.status === 404 || errorInfo.apiError?.code === "model_not_found") {
|
|
398
|
+
log("\n 错误:模型不存在或无权访问", "red");
|
|
399
|
+
log(" 可能原因:", "blue");
|
|
400
|
+
log(" • 模型名称错误或不存在", "blue");
|
|
401
|
+
log(" • 模型已下线", "blue");
|
|
402
|
+
log(" • 账户无权使用该模型", "blue");
|
|
403
|
+
log(`\n 当前模型: ${process.env.OPENAI_MODEL || "gpt-4o-mini"}`, "yellow");
|
|
404
|
+
log(" 修复步骤:", "blue");
|
|
405
|
+
log(" 1. 检查模型名称: https://platform.openai.com/docs/models", "blue");
|
|
406
|
+
log(" 2. 修改 OPENAI_MODEL 为有效的模型名", "blue");
|
|
407
|
+
log(" 3. 检查账户权限", "blue");
|
|
408
|
+
} else if (error.status === 500 || error.status === 502 || error.status === 503) {
|
|
409
|
+
log(`\n 错误:OpenAI 服务器错误 (${error.status})`, "red");
|
|
410
|
+
log(" 可能原因:", "blue");
|
|
411
|
+
log(" • OpenAI 服务暂时不可用", "blue");
|
|
412
|
+
log(" • 服务器故障", "blue");
|
|
413
|
+
log(" • 服务器维护中", "blue");
|
|
414
|
+
log("\n 修复步骤:", "blue");
|
|
415
|
+
log(" 1. 稍后重试", "blue");
|
|
416
|
+
log(" 2. 检查 OpenAI 状态: https://status.openai.com", "blue");
|
|
417
|
+
log(" 3. 查看官方通告", "blue");
|
|
418
|
+
} else {
|
|
419
|
+
log(`\n 错误: ${error.message}`, "yellow");
|
|
420
|
+
log(` 错误类型: ${errorInfo.type}`, "yellow");
|
|
421
|
+
log(` 错误代码: ${errorInfo.code || "未知"}`, "yellow");
|
|
422
|
+
if (error.status) {
|
|
423
|
+
log(` HTTP 状态: ${error.status}`, "yellow");
|
|
424
|
+
}
|
|
425
|
+
log("\n 通用排查步骤:", "blue");
|
|
426
|
+
log(" 1. 启用详细日志: AI_REVIEW_VERBOSE=true", "blue");
|
|
427
|
+
log(" 2. 检查 .env 配置文件中的所有参数", "blue");
|
|
428
|
+
log(" 3. 验证 API Key 和 Base URL", "blue");
|
|
429
|
+
log(" 4. 测试网络连接: curl https://api.openai.com/v1/models", "blue");
|
|
430
|
+
log(" 5. 查看完整错误信息(使用 AI_REVIEW_VERBOSE=true)", "blue");
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
},
|
|
434
|
+
|
|
435
|
+
// 测试完整流程(需要 API Key)
|
|
436
|
+
async testFullFlow() {
|
|
437
|
+
logSection("测试完整流程(需要暂存的更改和 API Key)");
|
|
438
|
+
|
|
439
|
+
const envPath = path.join(projectRoot, ".env");
|
|
440
|
+
if (!fs.existsSync(envPath)) {
|
|
441
|
+
log("⏭️ 跳过:未配置 .env", "yellow");
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
try {
|
|
446
|
+
const diff = execSync("git diff --cached", {
|
|
447
|
+
cwd: projectRoot,
|
|
448
|
+
encoding: "utf-8",
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
if (!diff.trim()) {
|
|
452
|
+
log("⏭️ 跳过:暂存区没有变更", "yellow");
|
|
453
|
+
log(" 要测试完整流程,请先 git add 一些文件", "blue");
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
log("🚀 运行完整 Hook 测试...", "blue");
|
|
458
|
+
log(" 这将调用 OpenAI API(会产生费用)", "yellow");
|
|
459
|
+
log(" 按 Ctrl+C 可取消\n", "yellow");
|
|
460
|
+
|
|
461
|
+
// 创建临时 commit msg 文件
|
|
462
|
+
const tmpMsgFile = path.join(projectRoot, ".git/COMMIT_EDITMSG_TEST");
|
|
463
|
+
fs.writeFileSync(tmpMsgFile, "");
|
|
464
|
+
|
|
465
|
+
try {
|
|
466
|
+
execSync(`node bin/hook.mjs "${tmpMsgFile}" message`, {
|
|
467
|
+
cwd: projectRoot,
|
|
468
|
+
stdio: "inherit",
|
|
469
|
+
env: { ...process.env },
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
const generatedMsg = fs.readFileSync(tmpMsgFile, "utf-8");
|
|
473
|
+
if (generatedMsg) {
|
|
474
|
+
log("\n✅ 完整流程测试成功!", "green");
|
|
475
|
+
log(`📝 生成的提交信息: ${generatedMsg}`, "cyan");
|
|
476
|
+
}
|
|
477
|
+
} finally {
|
|
478
|
+
// 清理临时文件
|
|
479
|
+
if (fs.existsSync(tmpMsgFile)) {
|
|
480
|
+
fs.unlinkSync(tmpMsgFile);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
} catch (error) {
|
|
484
|
+
log("❌ 完整流程测试失败: " + error.message, "red");
|
|
485
|
+
}
|
|
486
|
+
},
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
// 运行所有测试
|
|
490
|
+
async function runTests() {
|
|
491
|
+
console.log("\n🧪 AI Code Review 本地测试\n");
|
|
492
|
+
|
|
493
|
+
const testName = process.argv[2];
|
|
494
|
+
|
|
495
|
+
if (testName && tests[testName]) {
|
|
496
|
+
// 运行指定测试
|
|
497
|
+
await tests[testName]();
|
|
498
|
+
} else if (testName === "full") {
|
|
499
|
+
// 运行完整流程测试
|
|
500
|
+
await tests.testFullFlow();
|
|
501
|
+
} else {
|
|
502
|
+
// 运行所有基础测试
|
|
503
|
+
await tests.testEnvLoading();
|
|
504
|
+
await tests.testCLI();
|
|
505
|
+
await tests.testGitDiff();
|
|
506
|
+
await tests.testBuild();
|
|
507
|
+
await tests.testHookDryRun();
|
|
508
|
+
|
|
509
|
+
console.log("\n" + "=".repeat(50));
|
|
510
|
+
log("💡 提示", "cyan");
|
|
511
|
+
console.log("=".repeat(50));
|
|
512
|
+
console.log(`
|
|
513
|
+
测试 API 连接(会调用 API,消耗少量 token):
|
|
514
|
+
npm test testAPI
|
|
515
|
+
|
|
516
|
+
运行完整流程测试(会调用 API):
|
|
517
|
+
npm test full
|
|
518
|
+
|
|
519
|
+
单独运行某个测试:
|
|
520
|
+
npm test testEnvLoading
|
|
521
|
+
npm test testCLI
|
|
522
|
+
npm test testGitDiff
|
|
523
|
+
npm test testAPI
|
|
524
|
+
npm test testFullFlow
|
|
525
|
+
|
|
526
|
+
本地链接测试(在其他项目中使用):
|
|
527
|
+
npm run test:link
|
|
528
|
+
cd ../other-project
|
|
529
|
+
npx ai-review init
|
|
530
|
+
`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
runTests().catch(console.error);
|