git-ai-shen 1.0.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.en.md +36 -0
- package/README.md +23 -0
- package/bin/git-ai.js +97 -0
- package/git-ai-working.js +393 -0
- package/lib/commands/commit.js +256 -0
- package/lib/core/ai-reviewer.js +321 -0
- package/lib/core/config.js +120 -0
- package/lib/core/git-operations.js +285 -0
- package/lib/core/simple-config.js +171 -0
- package/lib/index.js +19 -0
- package/lib/utils/file-utils.js +98 -0
- package/lib/utils/logger.js +71 -0
- package/node +0 -0
- package/package.json +25 -0
- package/simple-init.js +76 -0
- package/test-ai.js +42 -0
- package/test-init.js +28 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
const { GitOperations } = require('../core/git-operations');
|
|
2
|
+
const { AIReviewer } = require('../core/ai-reviewer');
|
|
3
|
+
const { SimpleConfigManager } = require('../core/simple-config');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const inquirer = require('inquirer');
|
|
6
|
+
|
|
7
|
+
class CommitCommand {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.config = new SimpleConfigManager();
|
|
10
|
+
this.gitOps = new GitOperations();
|
|
11
|
+
this.aiReviewer = new AIReviewer(this.config);
|
|
12
|
+
this.sshPassword = null; // 存储 SSH 密码
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async run(message, options) {
|
|
16
|
+
console.log(chalk.cyan('\n🔍 Git AI 智能提交工具\n'));
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
// 检查是否是git仓库
|
|
20
|
+
const isRepo = await this.gitOps.isGitRepo();
|
|
21
|
+
if (!isRepo) {
|
|
22
|
+
throw new Error('当前目录不是Git仓库');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 获取变更文件
|
|
26
|
+
console.log(chalk.cyan('📝 检测文件变更...'));
|
|
27
|
+
const changedFiles = await this.gitOps.getChangedFiles();
|
|
28
|
+
|
|
29
|
+
if (changedFiles.length === 0) {
|
|
30
|
+
console.log(chalk.yellow('⚠️ 没有检测到文件变更'));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
console.log(chalk.green(`✅ 检测到 ${changedFiles.length} 个变更文件:`));
|
|
35
|
+
changedFiles.forEach(file => {
|
|
36
|
+
console.log(` ${chalk.gray('-')} ${file}`);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// AI审查 - 使用基于 diff 的精确审查
|
|
40
|
+
if (!options.skipReview && this.config.get('review.enabled')) {
|
|
41
|
+
console.log(chalk.cyan('\n🔍 AI审查中(基于行级变更)...'));
|
|
42
|
+
|
|
43
|
+
// 获取详细的 diff 信息
|
|
44
|
+
console.log(chalk.gray(' 正在分析文件变更...'));
|
|
45
|
+
const diffs = await this.gitOps.getDiffs(changedFiles, false); // false 表示未暂存的变更
|
|
46
|
+
|
|
47
|
+
// 使用基于 diff 的审查
|
|
48
|
+
const review = await this.aiReviewer.reviewWithDiff(changedFiles, diffs);
|
|
49
|
+
|
|
50
|
+
if (review.risks.length > 0) {
|
|
51
|
+
console.log(chalk.yellow('\n⚠️ 检测到风险项:'));
|
|
52
|
+
review.risks.forEach(risk => {
|
|
53
|
+
console.log(` ${chalk.red('[风险]')} ${risk}`);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (review.suggestions.length > 0) {
|
|
58
|
+
console.log(chalk.cyan('\n💡 优化建议:'));
|
|
59
|
+
review.suggestions.forEach(suggestion => {
|
|
60
|
+
console.log(` ${chalk.yellow('[建议]')} ${suggestion}`);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 如果检测到风险,询问是否继续
|
|
65
|
+
if (review.risks.length > 0) {
|
|
66
|
+
const { confirm } = await inquirer.prompt([
|
|
67
|
+
{
|
|
68
|
+
type: 'confirm',
|
|
69
|
+
name: 'confirm',
|
|
70
|
+
message: '存在风险项,是否继续提交?',
|
|
71
|
+
default: false
|
|
72
|
+
}
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
if (!confirm) {
|
|
76
|
+
console.log(chalk.yellow('⚠️ 提交已取消'));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 获取提交信息 - 优先使用AI生成
|
|
83
|
+
// 获取提交信息 - 优先使用AI生成
|
|
84
|
+
let commitMessage = message;
|
|
85
|
+
if (!commitMessage) {
|
|
86
|
+
// 检查是否有API密钥,有则尝试AI生成
|
|
87
|
+
const apiKey = this.config.get('ai.apiKey');
|
|
88
|
+
if (apiKey && this.aiReviewer.isAvailable()) {
|
|
89
|
+
console.log(chalk.cyan('\n🤖 AI正在基于代码变更生成提交信息...'));
|
|
90
|
+
try {
|
|
91
|
+
// 获取 diffs 用于生成提交信息
|
|
92
|
+
const diffs = await this.gitOps.getDiffs(changedFiles, false);
|
|
93
|
+
commitMessage = await this.aiReviewer.generateCommitMessageWithDiff(changedFiles, diffs, this.gitOps);
|
|
94
|
+
console.log(chalk.green(`✅ AI生成提交信息: ${commitMessage}`));
|
|
95
|
+
|
|
96
|
+
// 让用户确认或修改
|
|
97
|
+
const { action } = await inquirer.prompt([
|
|
98
|
+
{
|
|
99
|
+
type: 'list',
|
|
100
|
+
name: 'action',
|
|
101
|
+
message: '请选择操作:',
|
|
102
|
+
choices: [
|
|
103
|
+
{ name: '使用此信息', value: 'use' },
|
|
104
|
+
{ name: '重新生成', value: 'regenerate' },
|
|
105
|
+
{ name: '手动输入', value: 'manual' }
|
|
106
|
+
]
|
|
107
|
+
}
|
|
108
|
+
]);
|
|
109
|
+
|
|
110
|
+
if (action === 'regenerate') {
|
|
111
|
+
// 递归调用重新生成
|
|
112
|
+
return this.run(message, options);
|
|
113
|
+
} else if (action === 'manual') {
|
|
114
|
+
const { customMessage } = await inquirer.prompt([
|
|
115
|
+
{
|
|
116
|
+
type: 'input',
|
|
117
|
+
name: 'customMessage',
|
|
118
|
+
message: '请输入提交信息:',
|
|
119
|
+
validate: input => input ? true : '提交信息不能为空'
|
|
120
|
+
}
|
|
121
|
+
]);
|
|
122
|
+
commitMessage = customMessage;
|
|
123
|
+
}
|
|
124
|
+
// 如果选择 'use',保持生成的 message
|
|
125
|
+
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.log(chalk.yellow(`⚠️ AI生成失败: ${error.message}`));
|
|
128
|
+
// AI生成失败时回退到手动输入
|
|
129
|
+
const { customMessage } = await inquirer.prompt([
|
|
130
|
+
{
|
|
131
|
+
type: 'input',
|
|
132
|
+
name: 'customMessage',
|
|
133
|
+
message: '请输入提交信息:',
|
|
134
|
+
validate: input => input ? true : '提交信息不能为空'
|
|
135
|
+
}
|
|
136
|
+
]);
|
|
137
|
+
commitMessage = customMessage;
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
// 没有API密钥,手动输入
|
|
141
|
+
console.log(chalk.yellow('\n⚠️ 未配置AI API密钥,使用手动输入'));
|
|
142
|
+
const { customMessage } = await inquirer.prompt([
|
|
143
|
+
{
|
|
144
|
+
type: 'input',
|
|
145
|
+
name: 'customMessage',
|
|
146
|
+
message: '请输入提交信息:',
|
|
147
|
+
validate: input => input ? true : '提交信息不能为空'
|
|
148
|
+
}
|
|
149
|
+
]);
|
|
150
|
+
commitMessage = customMessage;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 添加文件
|
|
155
|
+
if (options.autoAdd !== false && this.config.get('git.autoAdd')) {
|
|
156
|
+
console.log(chalk.cyan('\n📦 添加文件到暂存区...'));
|
|
157
|
+
await this.gitOps.addFiles();
|
|
158
|
+
console.log(chalk.green('✅ 添加成功'));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 提交
|
|
162
|
+
console.log(chalk.cyan('\n💾 提交变更...'));
|
|
163
|
+
await this.gitOps.commit(commitMessage);
|
|
164
|
+
console.log(chalk.green('✅ 提交成功'));
|
|
165
|
+
|
|
166
|
+
// 修改拉取部分
|
|
167
|
+
if (this.config.get('git.autoPull')) {
|
|
168
|
+
console.log(chalk.cyan('\n⬇️ 拉取远程更新...'));
|
|
169
|
+
|
|
170
|
+
let pullSuccess = false;
|
|
171
|
+
let needPassword = false;
|
|
172
|
+
|
|
173
|
+
do {
|
|
174
|
+
const pullResult = await this.gitOps.pull('origin', null, this.sshPassword);
|
|
175
|
+
|
|
176
|
+
if (pullResult.success) {
|
|
177
|
+
console.log(chalk.green('✅ 拉取成功'));
|
|
178
|
+
pullSuccess = true;
|
|
179
|
+
} else {
|
|
180
|
+
if (pullResult.needPassword) {
|
|
181
|
+
console.log(chalk.yellow('\n⚠️ 需要 SSH 密钥密码'));
|
|
182
|
+
|
|
183
|
+
// 询问密码
|
|
184
|
+
const { password } = await inquirer.prompt([
|
|
185
|
+
{
|
|
186
|
+
type: 'password',
|
|
187
|
+
name: 'password',
|
|
188
|
+
message: '请输入 SSH 密钥密码:',
|
|
189
|
+
mask: '*',
|
|
190
|
+
validate: input => input ? true : '密码不能为空'
|
|
191
|
+
}
|
|
192
|
+
]);
|
|
193
|
+
|
|
194
|
+
this.sshPassword = password;
|
|
195
|
+
needPassword = true;
|
|
196
|
+
} else {
|
|
197
|
+
console.log(chalk.yellow(`⚠️ 拉取失败: ${pullResult.error}`));
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
} while (needPassword && !pullSuccess);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 修改推送部分
|
|
205
|
+
if (!options.noPush && this.config.get('git.autoPush')) {
|
|
206
|
+
console.log(chalk.cyan('\n⬆️ 推送到远程...'));
|
|
207
|
+
|
|
208
|
+
let pushSuccess = false;
|
|
209
|
+
let needPassword = false;
|
|
210
|
+
|
|
211
|
+
do {
|
|
212
|
+
const pushResult = await this.gitOps.push('origin', null, this.sshPassword);
|
|
213
|
+
|
|
214
|
+
if (pushResult.success) {
|
|
215
|
+
console.log(chalk.green('✅ 推送成功'));
|
|
216
|
+
pushSuccess = true;
|
|
217
|
+
} else {
|
|
218
|
+
if (pushResult.needPassword) {
|
|
219
|
+
if (!this.sshPassword) {
|
|
220
|
+
console.log(chalk.yellow('\n⚠️ 需要 SSH 密钥密码'));
|
|
221
|
+
|
|
222
|
+
const { password } = await inquirer.prompt([
|
|
223
|
+
{
|
|
224
|
+
type: 'password',
|
|
225
|
+
name: 'password',
|
|
226
|
+
message: '请输入 SSH 密钥密码:',
|
|
227
|
+
mask: '*',
|
|
228
|
+
validate: input => input ? true : '密码不能为空'
|
|
229
|
+
}
|
|
230
|
+
]);
|
|
231
|
+
|
|
232
|
+
this.sshPassword = password;
|
|
233
|
+
needPassword = true;
|
|
234
|
+
}
|
|
235
|
+
} else {
|
|
236
|
+
console.log(chalk.red(`❌ 推送失败: ${pushResult.error}`));
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
} while (needPassword && !pushSuccess);
|
|
241
|
+
|
|
242
|
+
if (!pushSuccess) {
|
|
243
|
+
console.log(chalk.red('❌ 推送失败'));
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
console.log(chalk.green(`\n✨ 完成!提交信息: ${commitMessage}`));
|
|
249
|
+
|
|
250
|
+
} catch (error) {
|
|
251
|
+
console.log(chalk.red(`\n❌ 错误: ${error.message}`));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
module.exports = { CommitCommand };
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const ignore = require('ignore');
|
|
3
|
+
const chalk = require('chalk'); // 添加这一行
|
|
4
|
+
const { OpenAI } = require('openai');
|
|
5
|
+
class AIReviewer {
|
|
6
|
+
constructor(config) {
|
|
7
|
+
this.config = config;
|
|
8
|
+
this.openai = null;
|
|
9
|
+
this.initOpenAI();
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* 基于 diff 进行精确的代码审查
|
|
13
|
+
* @param {Array<string>} files 文件列表
|
|
14
|
+
* @param {Object} diffs diff 信息
|
|
15
|
+
* @returns {Object} 审查结果
|
|
16
|
+
*/
|
|
17
|
+
async reviewWithDiff(files, diffs) {
|
|
18
|
+
const risks = [];
|
|
19
|
+
const suggestions = [];
|
|
20
|
+
|
|
21
|
+
for (const file of files) {
|
|
22
|
+
const diff = diffs[file];
|
|
23
|
+
if (!diff || !diff.changes || diff.changes.length === 0) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 只审查新增和修改的行
|
|
28
|
+
for (const hunk of diff.changes) {
|
|
29
|
+
for (const change of hunk.changes) {
|
|
30
|
+
if (change.type === 'add' || change.type === 'delete') {
|
|
31
|
+
const lineContent = change.content;
|
|
32
|
+
const lineNumber = change.lineNumber || '?';
|
|
33
|
+
|
|
34
|
+
// 检查硬编码密钥(只检查新增的行)
|
|
35
|
+
if (change.type === 'add') {
|
|
36
|
+
if (/password\s*[:=]\s*['"][^'"]+['"]/i.test(lineContent)) {
|
|
37
|
+
risks.push(`${file}:${lineNumber} 行可能包含密码硬编码`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (/api[_-]?key\s*[:=]\s*['"][^'"]+['"]/i.test(lineContent)) {
|
|
41
|
+
risks.push(`${file}:${lineNumber} 行可能包含API密钥硬编码`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (/token\s*[:=]\s*['"][^'"]+['"]/i.test(lineContent)) {
|
|
45
|
+
risks.push(`${file}:${lineNumber} 行可能包含令牌硬编码`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 检查调试代码
|
|
50
|
+
if (change.type === 'add') {
|
|
51
|
+
if (/console\.log/.test(lineContent)) {
|
|
52
|
+
suggestions.push(`${file}:${lineNumber} 行添加了console.log,考虑移除`);
|
|
53
|
+
}
|
|
54
|
+
if (/debugger;/.test(lineContent)) {
|
|
55
|
+
risks.push(`${file}:${lineNumber} 行添加了debugger语句`);
|
|
56
|
+
}
|
|
57
|
+
if (/TODO/.test(lineContent)) {
|
|
58
|
+
suggestions.push(`${file}:${lineNumber} 行添加了TODO注释`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 生成变更摘要
|
|
66
|
+
suggestions.push(`${file}: ${diff.summary}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { risks, suggestions };
|
|
70
|
+
}
|
|
71
|
+
initOpenAI() {
|
|
72
|
+
const apiKey = this.config.get('ai.apiKey');
|
|
73
|
+
const apiUrl = this.config.get('ai.apiUrl');
|
|
74
|
+
|
|
75
|
+
if (apiKey) {
|
|
76
|
+
try {
|
|
77
|
+
this.openai = new OpenAI({
|
|
78
|
+
apiKey: apiKey,
|
|
79
|
+
baseURL: apiUrl // 使用你配置的国产API地址
|
|
80
|
+
});
|
|
81
|
+
console.log(chalk.green('✅ AI客户端初始化成功'));
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.log(chalk.yellow(`⚠️ AI客户端初始化失败: ${error.message}`));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async basicReview(files, gitOps) {
|
|
88
|
+
const risks = [];
|
|
89
|
+
const suggestions = [];
|
|
90
|
+
|
|
91
|
+
// 确保 files 是数组
|
|
92
|
+
if (!Array.isArray(files)) {
|
|
93
|
+
console.log('警告: files 参数不是数组');
|
|
94
|
+
return { risks, suggestions };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (const file of files) {
|
|
98
|
+
// 确保 file 是字符串
|
|
99
|
+
if (typeof file !== 'string') {
|
|
100
|
+
console.log(`跳过非字符串文件: ${file}`);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const content = await gitOps.getFileContent(file);
|
|
106
|
+
if (!content) continue;
|
|
107
|
+
|
|
108
|
+
// 获取文件扩展名
|
|
109
|
+
const ext = path.extname(file).toLowerCase();
|
|
110
|
+
|
|
111
|
+
// 检查硬编码密钥
|
|
112
|
+
if (this.config.get('review.checkSecrets')) {
|
|
113
|
+
if (/password\s*[:=]\s*['"][^'"]+['"]/i.test(content)) {
|
|
114
|
+
risks.push(`${file}: 可能包含密码硬编码`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (/api[_-]?key\s*[:=]\s*['"][^'"]+['"]/i.test(content)) {
|
|
118
|
+
risks.push(`${file}: 可能包含API密钥硬编码`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (/token\s*[:=]\s*['"][^'"]+['"]/i.test(content)) {
|
|
122
|
+
risks.push(`${file}: 可能包含令牌硬编码`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (/secret\s*[:=]\s*['"][^'"]+['"]/i.test(content)) {
|
|
126
|
+
risks.push(`${file}: 可能包含密钥硬编码`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 检查TODO和FIXME
|
|
131
|
+
if (this.config.get('review.checkTodos')) {
|
|
132
|
+
if (/TODO/i.test(content)) {
|
|
133
|
+
suggestions.push(`${file}: 包含待办事项(TODO)`);
|
|
134
|
+
}
|
|
135
|
+
if (/FIXME/i.test(content)) {
|
|
136
|
+
suggestions.push(`${file}: 包含待修复项(FIXME)`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 检查调试代码
|
|
141
|
+
if (this.config.get('review.checkDebug')) {
|
|
142
|
+
// JavaScript/TypeScript 文件
|
|
143
|
+
if (['.js', '.jsx', '.ts', '.tsx'].includes(ext)) {
|
|
144
|
+
if (/console\.log/.test(content)) {
|
|
145
|
+
suggestions.push(`${file}: 包含console.log,考虑移除`);
|
|
146
|
+
}
|
|
147
|
+
if (/debugger;/.test(content)) {
|
|
148
|
+
risks.push(`${file}: 包含debugger语句`);
|
|
149
|
+
}
|
|
150
|
+
if (/alert\(/.test(content)) {
|
|
151
|
+
suggestions.push(`${file}: 包含alert语句,考虑移除`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Python 文件
|
|
156
|
+
if (['.py'].includes(ext)) {
|
|
157
|
+
if (/print\(/.test(content)) {
|
|
158
|
+
suggestions.push(`${file}: 包含print语句,考虑使用日志记录`);
|
|
159
|
+
}
|
|
160
|
+
if (/import pdb;/.test(content)) {
|
|
161
|
+
risks.push(`${file}: 包含pdb调试代码`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// TODO: 调用AI模型进行深度审查
|
|
167
|
+
if (this.config.get('ai.apiKey') && content.length < 10000) {
|
|
168
|
+
// AI审查逻辑...
|
|
169
|
+
}
|
|
170
|
+
} catch (error) {
|
|
171
|
+
console.log(`审查文件 ${file} 时出错: ${error.message}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return { risks, suggestions };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 确保 isAvailable 方法存在
|
|
179
|
+
isAvailable() {
|
|
180
|
+
return !!this.config.get('ai.apiKey');
|
|
181
|
+
}
|
|
182
|
+
async generateCommitMessage(files, gitOps) {
|
|
183
|
+
if (!this.isAvailable()) {
|
|
184
|
+
return '自动提交';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
// 获取文件变更的简要信息
|
|
189
|
+
const fileList = files.slice(0, 5).map(f => `- ${f}`).join('\n');
|
|
190
|
+
const fileCount = files.length > 5 ? `\n- ... 等${files.length}个文件` : '';
|
|
191
|
+
|
|
192
|
+
// 获取部分文件内容作为上下文
|
|
193
|
+
let fileContents = '';
|
|
194
|
+
for (const file of files.slice(0, 3)) {
|
|
195
|
+
const content = await gitOps.getFileContent(file);
|
|
196
|
+
if (content) {
|
|
197
|
+
fileContents += `\n文件 ${file} 的部分内容:\n${content.substring(0, 200)}...\n`;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const response = await this.openai.chat.completions.create({
|
|
202
|
+
model: this.config.get('ai.model') || 'gpt-3.5-turbo',
|
|
203
|
+
messages: [
|
|
204
|
+
{
|
|
205
|
+
role: 'system',
|
|
206
|
+
content: '你是一个Git提交信息生成助手。根据变更文件生成符合常规提交格式的提交信息。'
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
role: 'user',
|
|
210
|
+
content: `根据以下变更文件生成一个简洁的Git提交信息:
|
|
211
|
+
变更文件:
|
|
212
|
+
${fileList}${fileCount}
|
|
213
|
+
${fileContents}
|
|
214
|
+
|
|
215
|
+
请返回一个符合常规提交格式的提交信息,例如:
|
|
216
|
+
- feat: 添加用户登录功能
|
|
217
|
+
- fix: 修复空指针异常
|
|
218
|
+
- docs: 更新README文档
|
|
219
|
+
- refactor: 重构用户验证逻辑
|
|
220
|
+
- test: 添加单元测试
|
|
221
|
+
|
|
222
|
+
只需要返回提交信息本身,不要有其他内容。`
|
|
223
|
+
}
|
|
224
|
+
],
|
|
225
|
+
temperature: 0.3,
|
|
226
|
+
max_tokens: 50
|
|
227
|
+
});
|
|
228
|
+
return response.choices[0].message.content.trim();
|
|
229
|
+
} catch (error) {
|
|
230
|
+
console.log(chalk.yellow(`⚠️ 生成提交信息失败: ${error.message}`));
|
|
231
|
+
return '自动提交';
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* 基于 diff 生成提交信息
|
|
236
|
+
* @param {Array<string>} files 文件列表
|
|
237
|
+
* @param {Object} diffs diff 信息
|
|
238
|
+
* @param {Object} gitOps Git 操作实例
|
|
239
|
+
* @returns {Promise<string>} 提交信息
|
|
240
|
+
*/
|
|
241
|
+
async generateCommitMessageWithDiff(files, diffs, gitOps) {
|
|
242
|
+
if (!this.isAvailable()) {
|
|
243
|
+
return '自动提交';
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
// 构建变更摘要
|
|
248
|
+
let diffSummary = '';
|
|
249
|
+
let totalAdd = 0;
|
|
250
|
+
let totalDelete = 0;
|
|
251
|
+
|
|
252
|
+
for (const file of files) {
|
|
253
|
+
const diff = diffs[file];
|
|
254
|
+
if (diff) {
|
|
255
|
+
diffSummary += `\n文件: ${file}\n`;
|
|
256
|
+
diffSummary += `变更: ${diff.summary}\n`;
|
|
257
|
+
totalAdd += diff.addCount || 0;
|
|
258
|
+
totalDelete += diff.deleteCount || 0;
|
|
259
|
+
|
|
260
|
+
// 添加具体的变更内容(最多5个关键变更)
|
|
261
|
+
let changeCount = 0;
|
|
262
|
+
for (const hunk of diff.changes) {
|
|
263
|
+
for (const change of hunk.changes) {
|
|
264
|
+
if (change.type === 'add' || change.type === 'delete') {
|
|
265
|
+
if (changeCount < 5) {
|
|
266
|
+
const prefix = change.type === 'add' ? '+' : '-';
|
|
267
|
+
// 只显示非空行和有意义的变更
|
|
268
|
+
if (change.content.trim()) {
|
|
269
|
+
diffSummary += `${prefix} ${change.content.trim().substring(0, 50)}\n`;
|
|
270
|
+
changeCount++;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 如果有变更,添加到提示中
|
|
280
|
+
const changeSummary = totalAdd > 0 || totalDelete > 0
|
|
281
|
+
? `\n总体变更: 新增 ${totalAdd} 行,删除 ${totalDelete} 行`
|
|
282
|
+
: '';
|
|
283
|
+
|
|
284
|
+
const response = await this.openai.chat.completions.create({
|
|
285
|
+
model: this.config.get('ai.model') || 'gpt-3.5-turbo',
|
|
286
|
+
messages: [
|
|
287
|
+
{
|
|
288
|
+
role: 'system',
|
|
289
|
+
content: '你是一个Git提交信息生成助手。根据代码变更生成符合常规提交格式的提交信息。'
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
role: 'user',
|
|
293
|
+
content: `根据以下代码变更生成一个简洁的Git提交信息:
|
|
294
|
+
${changeSummary}
|
|
295
|
+
${diffSummary}
|
|
296
|
+
|
|
297
|
+
请返回一个符合常规提交格式的提交信息,例如:
|
|
298
|
+
- feat: 添加用户登录功能
|
|
299
|
+
- fix: 修复空指针异常
|
|
300
|
+
- docs: 更新README文档
|
|
301
|
+
- refactor: 重构用户验证逻辑
|
|
302
|
+
- test: 添加单元测试
|
|
303
|
+
- style: 格式化代码
|
|
304
|
+
- chore: 更新依赖
|
|
305
|
+
|
|
306
|
+
只需要返回提交信息本身,不要有其他内容。`
|
|
307
|
+
}
|
|
308
|
+
],
|
|
309
|
+
temperature: this.config.get('ai.temperature') || 0.3,
|
|
310
|
+
max_tokens: this.config.get('ai.maxTokens') || 50
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
return response.choices[0].message.content.trim();
|
|
314
|
+
} catch (error) {
|
|
315
|
+
console.log(chalk.yellow(`⚠️ 生成提交信息失败: ${error.message}`));
|
|
316
|
+
return '自动提交';
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
module.exports = { AIReviewer };
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
const Conf = require('conf');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const { exec } = require('child_process');
|
|
4
|
+
|
|
5
|
+
class ConfigManager {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.config = new Conf({
|
|
8
|
+
projectName: 'git-ai',
|
|
9
|
+
defaults: this.getDefaultConfig()
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
getDefaultConfig() {
|
|
14
|
+
return {
|
|
15
|
+
ai: {
|
|
16
|
+
provider: 'openai',
|
|
17
|
+
apiKey: 'ms-114c4bf4-ab3e-442b-a30c-6b355db26263',
|
|
18
|
+
apiUrl: 'https://api-inference.modelscope.cn/v1',
|
|
19
|
+
model: 'Qwen/Qwen3-Coder-30B-A3B-Instruct',
|
|
20
|
+
maxTokens: 500,
|
|
21
|
+
temperature: 0.3
|
|
22
|
+
},
|
|
23
|
+
git: {
|
|
24
|
+
autoAdd: true,
|
|
25
|
+
autoPull: true,
|
|
26
|
+
autoPush: true,
|
|
27
|
+
remote: 'origin'
|
|
28
|
+
},
|
|
29
|
+
review: {
|
|
30
|
+
enabled: true,
|
|
31
|
+
skipPatterns: [
|
|
32
|
+
'*.lock',
|
|
33
|
+
'*.log',
|
|
34
|
+
'*.min.js',
|
|
35
|
+
'*.min.css',
|
|
36
|
+
'*.pyc',
|
|
37
|
+
'__pycache__',
|
|
38
|
+
'node_modules',
|
|
39
|
+
'dist',
|
|
40
|
+
'build'
|
|
41
|
+
],
|
|
42
|
+
maxFileSize: 1024 * 100,
|
|
43
|
+
checkSecrets: true,
|
|
44
|
+
checkTodos: true,
|
|
45
|
+
checkDebug: true
|
|
46
|
+
},
|
|
47
|
+
ui: {
|
|
48
|
+
showProgress: true,
|
|
49
|
+
colorOutput: true
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get(key) {
|
|
55
|
+
return this.config.get(key);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
set(key, value) {
|
|
59
|
+
const keys = key.split('.');
|
|
60
|
+
if (keys.length > 1) {
|
|
61
|
+
const obj = {};
|
|
62
|
+
let current = obj;
|
|
63
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
64
|
+
current[keys[i]] = {};
|
|
65
|
+
current = current[keys[i]];
|
|
66
|
+
}
|
|
67
|
+
current[keys[keys.length - 1]] = value;
|
|
68
|
+
this.config.set(obj);
|
|
69
|
+
} else {
|
|
70
|
+
this.config.set(key, value);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
getAll() {
|
|
75
|
+
return this.config.store;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
init() {
|
|
79
|
+
this.config.clear();
|
|
80
|
+
this.config.set(this.getDefaultConfig());
|
|
81
|
+
|
|
82
|
+
console.log(chalk.cyan('请配置你的AI服务提供商:'));
|
|
83
|
+
|
|
84
|
+
const readline = require('readline').createInterface({
|
|
85
|
+
input: process.stdin,
|
|
86
|
+
output: process.stdout
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
readline.question('OpenAI API密钥 (可选): ', (apiKey) => {
|
|
90
|
+
if (apiKey) {
|
|
91
|
+
this.set('ai.apiKey', apiKey);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
readline.question('使用自定义API地址? (y/N): ', (customUrl) => {
|
|
95
|
+
if (customUrl.toLowerCase() === 'y') {
|
|
96
|
+
readline.question('API地址: ', (url) => {
|
|
97
|
+
this.set('ai.apiUrl', url);
|
|
98
|
+
readline.close();
|
|
99
|
+
});
|
|
100
|
+
} else {
|
|
101
|
+
readline.close();
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
openEditor() {
|
|
108
|
+
const configPath = this.config.path;
|
|
109
|
+
console.log(chalk.cyan(`配置文件路径: ${configPath}`));
|
|
110
|
+
|
|
111
|
+
const editor = process.env.EDITOR || 'notepad';
|
|
112
|
+
exec(`${editor} "${configPath}"`, (error) => {
|
|
113
|
+
if (error) {
|
|
114
|
+
console.error(chalk.red(`无法打开编辑器: ${error.message}`));
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = { ConfigManager };
|