hong-review-cli 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.md +74 -0
- package/bin/hong-review.js +89 -0
- package/index.js +166 -0
- package/package.json +30 -0
- package/src/commands/actions.js +122 -0
- package/src/commands/config.js +49 -0
- package/src/commands/list.js +65 -0
- package/src/commands/login.js +69 -0
- package/src/commands/mr.js +270 -0
- package/src/core/agent.js +141 -0
- package/src/core/ai.js +108 -0
- package/src/core/cache.js +221 -0
- package/src/core/gitlab.js +164 -0
- package/src/utils/hooks.js +60 -0
- package/src/utils/logger.js +52 -0
- package/src/utils/storage.js +67 -0
package/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# hong-review-cli
|
|
2
|
+
|
|
3
|
+
`hong-review-cli` 是一个基于命令行界面的智能代码审查工具。它通过结合大语言模型的能力与 GitLab 代码库的 MR(Merge Request)审查场景集成,旨在帮助开发者快速在终端实现交互式的高质量代码审核审查和自动化 MR 合并。
|
|
4
|
+
|
|
5
|
+
## 🌟 核心特性
|
|
6
|
+
|
|
7
|
+
- **终端直接审查**:无需离开编写代码的终端环境。
|
|
8
|
+
- **智能分析**:使用类似 ChatGPT 这样的强 AI 模型分析所有的文件增删改查。
|
|
9
|
+
- **全键盘操作**:直观的下钻式命令选项与带颜色的排版,不再枯燥。
|
|
10
|
+
- **集成 Hooks 挂钩**:与 CI/CD 轻易相连,审查前后触发定制脚本流程。
|
|
11
|
+
|
|
12
|
+
## 🚀 安装
|
|
13
|
+
|
|
14
|
+
要全局安装此命令行工具,您需要环境中已安装 Node.js > 14+。
|
|
15
|
+
|
|
16
|
+
在终端中执行以下命令进行全局安装:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install -g hong-review-cli
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## 🛠️ 开始使用
|
|
23
|
+
|
|
24
|
+
### 第一步:初始化登录配置
|
|
25
|
+
安装成功后,请在任何终端目录输入该命令绑定您的资源:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
hong-review login
|
|
29
|
+
```
|
|
30
|
+
*根据向导提示填写您的 GitLab 服务器域名,GitLab 令牌(Token)与模型(API Key)。配置会保存在您的本地安全目录中。*
|
|
31
|
+
|
|
32
|
+
### 第二步:一键智能审查
|
|
33
|
+
这是这个程序最强大的功能部分,只需要在任何地方运行:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
hong-review ls
|
|
37
|
+
```
|
|
38
|
+
系统将自动为您拉取目前分配到你身上的待处理 MR。通过方向键选择之后,工具将在后台呼唤 AI 进行高强度的深度审查与建议反馈。
|
|
39
|
+
|
|
40
|
+
或者,若您确切知道 GitLab 项目下的 MR 数字 ID,您也可以直接运行:
|
|
41
|
+
```bash
|
|
42
|
+
hong-review mr <mr_id>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
> **自动化特性支持**
|
|
46
|
+
> - 增加参数 `-R`(--report)可额外生成本地 Markdown 文件报告。
|
|
47
|
+
> - 增加参数 `-c`(--comment)可自动在 GitLab Merge Request 发送一条包含排版的 Markdown 代码评论。
|
|
48
|
+
> - 增加参数 `-m`(--merge)可以在 AI 判断通过/极低风险时执行静默合并代码!
|
|
49
|
+
|
|
50
|
+
### 其他指令
|
|
51
|
+
|
|
52
|
+
你可以随时执行 `hong-review` 或带上 `--help` 查看说明书:
|
|
53
|
+
- `hong-review comment <mr_id> "<评论文本>"` 快捷留言
|
|
54
|
+
- `hong-review merge <mr_id>` 快捷请求远端合并
|
|
55
|
+
- `hong-review view <mr_id>` 终端内分页展示代码 Diff 变动详情
|
|
56
|
+
- `hong-review config list` 列出所有系统配置的状态
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 🪝 流水线生命周期 (Hooks)
|
|
61
|
+
|
|
62
|
+
作为现代化的工具,它可以绑定到第三方系统的事件提醒中。通过命令写入系统全局 Hooks 来进行拦截事件:
|
|
63
|
+
```json
|
|
64
|
+
"hooks": {
|
|
65
|
+
"onReviewStart": "curl -X POST http://xxx",
|
|
66
|
+
"onReviewSuccess": "bash ./scripts/notify-slack.sh",
|
|
67
|
+
"onReviewFailed": "node ./scripts/error.js",
|
|
68
|
+
"onMerged": "..."
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## 📄 协议
|
|
73
|
+
|
|
74
|
+
本项目基于 MIT 协议开发开源。
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { program } = require('commander');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const figlet = require('figlet');
|
|
6
|
+
|
|
7
|
+
const path = require('path');
|
|
8
|
+
// 命令实现
|
|
9
|
+
const loginCmd = require(path.join(__dirname, '../src/commands/login'));
|
|
10
|
+
const configCmd = require(path.join(__dirname, '../src/commands/config'));
|
|
11
|
+
const mrCmd = require(path.join(__dirname, '../src/commands/mr'));
|
|
12
|
+
const actionsCmd = require(path.join(__dirname, '../src/commands/actions'));
|
|
13
|
+
const listCmd = require(path.join(__dirname, '../src/commands/list'));
|
|
14
|
+
|
|
15
|
+
console.log(
|
|
16
|
+
chalk.cyan(
|
|
17
|
+
figlet.textSync('AI Review', { horizontalLayout: 'full' })
|
|
18
|
+
)
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
program
|
|
22
|
+
.version('1.0.0')
|
|
23
|
+
.description('AI Code Reviewer - 基于 GitLab 智能代码审查终端系统');
|
|
24
|
+
|
|
25
|
+
// --- 基础配置 ---
|
|
26
|
+
program
|
|
27
|
+
.command('login')
|
|
28
|
+
.alias('init')
|
|
29
|
+
.description('初始化登录或引导式设置全局配置')
|
|
30
|
+
.action(loginCmd);
|
|
31
|
+
|
|
32
|
+
const configGroup = program
|
|
33
|
+
.command('config')
|
|
34
|
+
.description('管理全局配置');
|
|
35
|
+
|
|
36
|
+
configGroup
|
|
37
|
+
.command('set <key> <value>')
|
|
38
|
+
.description('设置单个配置项 (如 aiKey, gitlabToken 等)')
|
|
39
|
+
.action(configCmd.set);
|
|
40
|
+
|
|
41
|
+
configGroup
|
|
42
|
+
.command('list')
|
|
43
|
+
.description('列出当前所有的全局配置')
|
|
44
|
+
.action(configCmd.list);
|
|
45
|
+
|
|
46
|
+
// --- 审查核心功能 ---
|
|
47
|
+
program
|
|
48
|
+
.command('mr <mr_id>')
|
|
49
|
+
.description('智能审查指定的 GitLab Merge Request')
|
|
50
|
+
.option('-R, --report', '生成本地 Markdown 审查报告文件')
|
|
51
|
+
.option('-c, --comment', '审查完成后,自动将结果作为评论发至 MR 讨论区')
|
|
52
|
+
.option('-m, --merge', '审查通过且无明显问题时,自动合并该 MR')
|
|
53
|
+
.action((mr_id, options) => {
|
|
54
|
+
mrCmd(mr_id, options);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
program
|
|
58
|
+
.command('list')
|
|
59
|
+
.alias('ls')
|
|
60
|
+
.description('查看分配给我或待我处理的 MR 列表')
|
|
61
|
+
.action(listCmd);
|
|
62
|
+
|
|
63
|
+
// --- 单独动作 ---
|
|
64
|
+
program
|
|
65
|
+
.command('comment <mr_id> <text>')
|
|
66
|
+
.description('给指定的 MR 快捷留言/评论')
|
|
67
|
+
.action((mr_id, text) => {
|
|
68
|
+
actionsCmd.comment(mr_id, text);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
program
|
|
72
|
+
.command('merge <mr_id>')
|
|
73
|
+
.description('检查权限并快捷合并指定的 MR')
|
|
74
|
+
.action((mr_id) => {
|
|
75
|
+
actionsCmd.merge(mr_id);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
program
|
|
79
|
+
.command('view <mr_id>')
|
|
80
|
+
.description('在终端直接查看一个 MR 的文件差异 (Diff) 详情')
|
|
81
|
+
.action((mr_id) => {
|
|
82
|
+
actionsCmd.view(mr_id);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
program.parse(process.argv);
|
|
86
|
+
|
|
87
|
+
if (!process.argv.slice(2).length) {
|
|
88
|
+
program.outputHelp();
|
|
89
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { program } = require('commander');
|
|
4
|
+
const inquirer = require('inquirer');
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
const ora = require('ora');
|
|
7
|
+
const figlet = require('figlet');
|
|
8
|
+
|
|
9
|
+
// 延时函数用于模拟网络请求
|
|
10
|
+
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
11
|
+
|
|
12
|
+
// 伪造数据
|
|
13
|
+
const mockMRs = [
|
|
14
|
+
{ name: 'MR !102: [特性] 首页支持国际化切换', value: '102' },
|
|
15
|
+
{ name: 'MR !108: [修复] 解决页面滚动回弹失效问题', value: '108' },
|
|
16
|
+
{ name: 'MR !113: [重构] 抽离核心请求逻辑到 utils 模块', value: '113' }
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const mockReviewResults = [
|
|
20
|
+
{
|
|
21
|
+
file: 'src/pages/index/index.vue',
|
|
22
|
+
type: 'warning',
|
|
23
|
+
line: 45,
|
|
24
|
+
message: '国际化文案键值 `header.title` 未在 en.json 中定义,但在页面中被使用了,可能导致渲染为原神键。'
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
file: 'src/utils/request.ts',
|
|
28
|
+
type: 'error',
|
|
29
|
+
line: 112,
|
|
30
|
+
message: '直接在代码中硬编码了 Token,建议通过环境变量或统一配置传入,存在安全隐患。'
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
file: 'src/utils/request.ts',
|
|
34
|
+
type: 'success',
|
|
35
|
+
line: 80,
|
|
36
|
+
message: '请求异常拦截器处理得很完善,符合团队规约。'
|
|
37
|
+
}
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
// CLI 入口
|
|
41
|
+
async function main() {
|
|
42
|
+
console.log(
|
|
43
|
+
chalk.cyan(
|
|
44
|
+
figlet.textSync('AI Review', { horizontalLayout: 'full' })
|
|
45
|
+
)
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
program
|
|
49
|
+
.version('1.0.0')
|
|
50
|
+
.description('AI Code Review 命令行工具 (Demo)');
|
|
51
|
+
|
|
52
|
+
program
|
|
53
|
+
.command('init')
|
|
54
|
+
.description('初始化全局配置 (模拟)')
|
|
55
|
+
.action(async () => {
|
|
56
|
+
console.log(chalk.bold.blue('\n⚙️ 初始化 AI Code Reviewer 配置'));
|
|
57
|
+
const answers = await inquirer.prompt([
|
|
58
|
+
{
|
|
59
|
+
type: 'input',
|
|
60
|
+
name: 'gitlabUrl',
|
|
61
|
+
message: '请输入 GitLab 的地址:',
|
|
62
|
+
default: 'https://gitlab.example.com'
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
type: 'password',
|
|
66
|
+
name: 'gitlabToken',
|
|
67
|
+
message: '请输入 GitLab Private Token:',
|
|
68
|
+
mask: '*'
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
type: 'password',
|
|
72
|
+
name: 'aiKey',
|
|
73
|
+
message: '请输入 AI 大模型的 API Key:',
|
|
74
|
+
mask: '*'
|
|
75
|
+
}
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
const spinner = ora('正在保存并校验配置...').start();
|
|
79
|
+
await sleep(1500);
|
|
80
|
+
spinner.succeed('配置保存成功!您可以开始使用 review 命令进行代码审查了。');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
program
|
|
84
|
+
.command('review')
|
|
85
|
+
.description('审查 GitLab 上的合并请求 (Merge Request)')
|
|
86
|
+
.action(async () => {
|
|
87
|
+
console.log(chalk.bold.blue('\n🔍 进入代码审查模式\n'));
|
|
88
|
+
|
|
89
|
+
const { target } = await inquirer.prompt([
|
|
90
|
+
{
|
|
91
|
+
type: 'list',
|
|
92
|
+
name: 'target',
|
|
93
|
+
message: '请选择您要审查的 Merge Request (示例列表):',
|
|
94
|
+
choices: mockMRs,
|
|
95
|
+
}
|
|
96
|
+
]);
|
|
97
|
+
|
|
98
|
+
// 阶段 1:拉取代码
|
|
99
|
+
const fetchSpinner = ora('正在从 GitLab 拉取 MR 代码变更数据...').start();
|
|
100
|
+
await sleep(2000);
|
|
101
|
+
fetchSpinner.succeed(`成功拉取 MR !${target},共包含 12 个文件修改,合计新增 154 行,删除 32 行。`);
|
|
102
|
+
|
|
103
|
+
// 阶段 2:AI 审查
|
|
104
|
+
console.log();
|
|
105
|
+
const aiSpinner = ora({
|
|
106
|
+
text: 'AI 正在深度审查代码 (思考中...) \n',
|
|
107
|
+
spinner: 'dots12' // 一个比较好看的加载动画
|
|
108
|
+
}).start();
|
|
109
|
+
await sleep(3500);
|
|
110
|
+
aiSpinner.succeed('AI 审查完成!已生成诊断报告。\n');
|
|
111
|
+
|
|
112
|
+
// 结段 3:输出报告
|
|
113
|
+
console.log(chalk.bold.inverse(` ====== 📋 审查报告:MR !${target} ====== \n`));
|
|
114
|
+
|
|
115
|
+
let errorCount = 0;
|
|
116
|
+
let warningCount = 0;
|
|
117
|
+
|
|
118
|
+
mockReviewResults.forEach((issue) => {
|
|
119
|
+
let prefix = '';
|
|
120
|
+
if (issue.type === 'error') {
|
|
121
|
+
prefix = chalk.bgRed.black(' ERROR ') + chalk.red(' 严重');
|
|
122
|
+
errorCount++;
|
|
123
|
+
} else if (issue.type === 'warning') {
|
|
124
|
+
prefix = chalk.bgYellow.black(' WARN ') + chalk.yellow(' 建议');
|
|
125
|
+
warningCount++;
|
|
126
|
+
} else if (issue.type === 'success') {
|
|
127
|
+
prefix = chalk.bgGreen.black(' PASS ') + chalk.green(' 规范');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
console.log(`${prefix} ${chalk.underline(issue.file)}:${issue.line}`);
|
|
131
|
+
console.log(chalk.gray(` 💡 ${issue.message}\n`));
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
console.log('--------------------------------------------------');
|
|
135
|
+
const sumText = `审查总结:发现 ${chalk.red(errorCount + ' 个严重问题')},${chalk.yellow(warningCount + ' 个优化建议')}。`;
|
|
136
|
+
console.log(sumText);
|
|
137
|
+
console.log('--------------------------------------------------\n');
|
|
138
|
+
|
|
139
|
+
const { autoComment } = await inquirer.prompt([
|
|
140
|
+
{
|
|
141
|
+
type: 'confirm',
|
|
142
|
+
name: 'autoComment',
|
|
143
|
+
message: '是否将此报告结果自动推送到 GitLab 作为 MR 评论?',
|
|
144
|
+
default: true
|
|
145
|
+
}
|
|
146
|
+
]);
|
|
147
|
+
|
|
148
|
+
if (autoComment) {
|
|
149
|
+
const commentSpinner = ora('正在向 GitLab 发送评论...').start();
|
|
150
|
+
await sleep(1500);
|
|
151
|
+
commentSpinner.succeed('推送成功!已在此 MR 开启相应的讨论线程 (Thread)。\n');
|
|
152
|
+
} else {
|
|
153
|
+
console.log(chalk.green('操作已取消。感谢使用。\n'));
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
program.parse(process.argv);
|
|
158
|
+
|
|
159
|
+
if (!process.argv.slice(2).length) {
|
|
160
|
+
program.outputHelp();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
main().catch(err => {
|
|
165
|
+
console.error(err);
|
|
166
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hong-review-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "index.js",
|
|
5
|
+
"bin": {
|
|
6
|
+
"hong-review": "bin/hong-review.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"cli",
|
|
13
|
+
"code-review",
|
|
14
|
+
"gitlab",
|
|
15
|
+
"ai",
|
|
16
|
+
"chatgpt"
|
|
17
|
+
],
|
|
18
|
+
"author": "Your Name",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"description": "基于 AI 和 GitLab 的智能代码审查终端系统",
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"axios": "^1.13.5",
|
|
23
|
+
"chalk": "^4.1.2",
|
|
24
|
+
"cli-spinners": "^3.4.0",
|
|
25
|
+
"commander": "^14.0.3",
|
|
26
|
+
"figlet": "^1.10.0",
|
|
27
|
+
"inquirer": "^8.2.7",
|
|
28
|
+
"ora": "^5.4.1"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
const inquirer = require('inquirer');
|
|
2
|
+
const storage = require('../utils/storage');
|
|
3
|
+
const logger = require('../utils/logger');
|
|
4
|
+
const GitLabClient = require('../core/gitlab');
|
|
5
|
+
|
|
6
|
+
function getClient() {
|
|
7
|
+
const config = storage.getAll();
|
|
8
|
+
if (!config.gitlabToken) {
|
|
9
|
+
throw new Error('缺少 GitLab Token 配置,请运行 hong-review login');
|
|
10
|
+
}
|
|
11
|
+
return new GitLabClient({ host: config.gitlabUrl, token: config.gitlabToken });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function getProjectId() {
|
|
15
|
+
let projectId = storage.get('defaultProjectId');
|
|
16
|
+
if (!projectId) {
|
|
17
|
+
const answer = await inquirer.prompt([{
|
|
18
|
+
type: 'input',
|
|
19
|
+
name: 'pid',
|
|
20
|
+
message: '请输入当前 Git 仓库对应的 GitLab Project ID:'
|
|
21
|
+
}]);
|
|
22
|
+
projectId = answer.pid;
|
|
23
|
+
storage.set('defaultProjectId', projectId);
|
|
24
|
+
}
|
|
25
|
+
return projectId;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = {
|
|
29
|
+
async comment(mrId, text) {
|
|
30
|
+
if (!text) {
|
|
31
|
+
logger.error('请输入评论内容。用法: hong-review comment <mr_id> "您的评论"');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const projectId = await getProjectId();
|
|
36
|
+
const gitlab = getClient();
|
|
37
|
+
|
|
38
|
+
logger.startSpinner('comment', `正在向 MR !${mrId} 发送评论...`);
|
|
39
|
+
await gitlab.addMergeRequestNote(projectId, mrId, { body: text });
|
|
40
|
+
logger.stopSpinner('comment', true, '评论发送成功!');
|
|
41
|
+
|
|
42
|
+
} catch (e) {
|
|
43
|
+
logger.stopSpinner('comment', false, `评论发送失败: ${e.message}`);
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
async merge(mrId) {
|
|
48
|
+
try {
|
|
49
|
+
const projectId = await getProjectId();
|
|
50
|
+
const gitlab = getClient();
|
|
51
|
+
|
|
52
|
+
logger.startSpinner('merge-check', `正在检查 MR !${mrId} 合并权限...`);
|
|
53
|
+
const check = await gitlab.canMergeMR(projectId, mrId);
|
|
54
|
+
|
|
55
|
+
if (!check.canMerge) {
|
|
56
|
+
logger.stopSpinner('merge-check', false, `无法合并: ${check.reason}`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
logger.stopSpinner('merge-check', true, '权限检查通过。');
|
|
60
|
+
|
|
61
|
+
const answer = await inquirer.prompt([{
|
|
62
|
+
type: 'confirm',
|
|
63
|
+
name: 'confirmMerge',
|
|
64
|
+
message: `即将自动合并 MR !${mrId},是否确认?`,
|
|
65
|
+
default: false
|
|
66
|
+
}]);
|
|
67
|
+
|
|
68
|
+
if (answer.confirmMerge) {
|
|
69
|
+
logger.startSpinner('merge', '合并中...');
|
|
70
|
+
await gitlab.merge(projectId, mrId);
|
|
71
|
+
logger.stopSpinner('merge', true, '合并成功!');
|
|
72
|
+
} else {
|
|
73
|
+
logger.info('已取消合并。');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
} catch (e) {
|
|
77
|
+
if (spinners && spinners.has('merge')) logger.stopSpinner('merge', false);
|
|
78
|
+
logger.error(`合并失败: ${e.message}`);
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
async view(mrId) {
|
|
83
|
+
try {
|
|
84
|
+
const projectId = await getProjectId();
|
|
85
|
+
const gitlab = getClient();
|
|
86
|
+
|
|
87
|
+
logger.startSpinner('view', `拉取 MR !${mrId} 的文件变更...`);
|
|
88
|
+
const changes = await gitlab.getMergeRequestChanges(projectId, mrId);
|
|
89
|
+
logger.stopSpinner('view', true, `共拉取到 ${changes.length} 个文件的变更:\n`);
|
|
90
|
+
|
|
91
|
+
if (changes.length === 0) {
|
|
92
|
+
logger.info('该 MR 无文件变更。');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const choices = changes.map((c, i) => ({
|
|
97
|
+
name: `[${c.new_file ? '新增' : (c.deleted_file ? '删除' : '修改')}] ${c.new_path}`,
|
|
98
|
+
value: i
|
|
99
|
+
}));
|
|
100
|
+
|
|
101
|
+
const answer = await inquirer.prompt([{
|
|
102
|
+
type: 'list',
|
|
103
|
+
name: 'fileIndex',
|
|
104
|
+
message: '选择要查看 diff 的文件:',
|
|
105
|
+
choices: [...choices, new inquirer.Separator(), { name: '退出', value: -1 }]
|
|
106
|
+
}]);
|
|
107
|
+
|
|
108
|
+
if (answer.fileIndex !== -1) {
|
|
109
|
+
const selectedFile = changes[answer.fileIndex];
|
|
110
|
+
console.log('\n--- Diff 开始 ---');
|
|
111
|
+
console.log(selectedFile.diff);
|
|
112
|
+
console.log('--- Diff 结束 ---\n');
|
|
113
|
+
|
|
114
|
+
// 如果有需求可以加个无限循环,看完一个文件还能选另一个。
|
|
115
|
+
// 但为了 CLI 简洁,目前设计看完即退
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
} catch (e) {
|
|
119
|
+
logger.error(`拉取变更失败: ${e.message}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const storage = require('../utils/storage');
|
|
2
|
+
const logger = require('../utils/logger');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
list() {
|
|
7
|
+
const config = storage.getAll();
|
|
8
|
+
logger.divider();
|
|
9
|
+
logger.info('当前全局配置 (存储在 ~/.hong-review-config.json)');
|
|
10
|
+
logger.divider();
|
|
11
|
+
|
|
12
|
+
for (const [key, value] of Object.entries(config)) {
|
|
13
|
+
if (typeof value === 'object') {
|
|
14
|
+
console.log(`${chalk.bold(key)}:`);
|
|
15
|
+
for (const [subKey, subValue] of Object.entries(value)) {
|
|
16
|
+
console.log(` ${chalk.gray('-')} ${subKey}: ${subValue ? chalk.green('已配置') : chalk.gray('未配置')}`);
|
|
17
|
+
}
|
|
18
|
+
} else {
|
|
19
|
+
let displayValue = value;
|
|
20
|
+
if ((key.toLowerCase().includes('token') || key.toLowerCase().includes('key')) && value) {
|
|
21
|
+
displayValue = value.substring(0, 4) + '*'.repeat(value.length - 8) + value.substring(value.length - 4);
|
|
22
|
+
} else if (!value) {
|
|
23
|
+
displayValue = chalk.gray('(未提供)');
|
|
24
|
+
}
|
|
25
|
+
console.log(`${chalk.bold(key)}: ${displayValue}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
logger.divider();
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
set(key, value) {
|
|
32
|
+
if (!key || value === undefined) {
|
|
33
|
+
logger.error('参数错误。用法: hong-review config set <key> <value>');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 处理嵌套对象赋值,如 hooks.onReviewStart
|
|
38
|
+
if (key.includes('.')) {
|
|
39
|
+
const [parentKey, subKey] = key.split('.');
|
|
40
|
+
const currentParent = storage.get(parentKey) || {};
|
|
41
|
+
currentParent[subKey] = value;
|
|
42
|
+
storage.set(parentKey, currentParent);
|
|
43
|
+
} else {
|
|
44
|
+
storage.set(key, value);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
logger.success(`配置项 ${chalk.bold(key)} 已更新。`);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const storage = require('../utils/storage');
|
|
2
|
+
const logger = require('../utils/logger');
|
|
3
|
+
const GitLabClient = require('../core/gitlab');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
|
|
6
|
+
module.exports = async function() {
|
|
7
|
+
const config = storage.getAll();
|
|
8
|
+
if (!config.gitlabToken) {
|
|
9
|
+
logger.error('缺少 GitLab Token 配置,请运行 hong-review login');
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const gitlab = new GitLabClient({ host: config.gitlabUrl, token: config.gitlabToken });
|
|
14
|
+
|
|
15
|
+
logger.startSpinner('list', '正在向 GitLab 拉取您需要处理的 Merge Requests...');
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const mrs = await gitlab.getMyReviewMergeRequests();
|
|
19
|
+
logger.stopSpinner('list', true, `成功拉取!共分配给您或待您审核的 MR 有 ${mrs.length} 个。\n`);
|
|
20
|
+
|
|
21
|
+
if (mrs.length === 0) {
|
|
22
|
+
logger.info('暂时没有需要您处理的 Merge Request。喝杯咖啡休息一下吧 ☕️\n');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const inquirer = require('inquirer');
|
|
27
|
+
const mrCmd = require('./mr');
|
|
28
|
+
|
|
29
|
+
const choices = mrs.map((mr) => {
|
|
30
|
+
const titleStr = mr.title;
|
|
31
|
+
const refStr = chalk.dim(`(Project: ${mr.project_id} - !${mr.iid})`);
|
|
32
|
+
const branchStr = chalk.blue(`${mr.source_branch} -> ${mr.target_branch}`);
|
|
33
|
+
return {
|
|
34
|
+
name: `${titleStr} ${refStr} | ${branchStr}`,
|
|
35
|
+
value: mr
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
choices.push(new inquirer.Separator());
|
|
40
|
+
choices.push({ name: '退出 (Exit)', value: null });
|
|
41
|
+
|
|
42
|
+
const answer = await inquirer.prompt([
|
|
43
|
+
{
|
|
44
|
+
type: 'list',
|
|
45
|
+
name: 'selectedMr',
|
|
46
|
+
message: '请选择您要直接发起的代码审查 (按上下方向键选择):',
|
|
47
|
+
choices: choices,
|
|
48
|
+
pageSize: 10
|
|
49
|
+
}
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
if (answer.selectedMr) {
|
|
53
|
+
console.log('');
|
|
54
|
+
// 自动配置 projectId 避免下一次询问
|
|
55
|
+
storage.set('defaultProjectId', String(answer.selectedMr.project_id));
|
|
56
|
+
// 启动智能审查流程
|
|
57
|
+
await mrCmd(answer.selectedMr.iid, {});
|
|
58
|
+
} else {
|
|
59
|
+
logger.info('已退出。');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
} catch (e) {
|
|
63
|
+
logger.stopSpinner('list', false, `拉取 MR 列表失败: ${e.message}`);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const inquirer = require('inquirer');
|
|
2
|
+
const storage = require('../utils/storage');
|
|
3
|
+
const logger = require('../utils/logger');
|
|
4
|
+
const AIClient = require('../core/ai');
|
|
5
|
+
|
|
6
|
+
module.exports = async function() {
|
|
7
|
+
logger.info('欢迎使用 AI Code Reviewer 命令行工具 👋');
|
|
8
|
+
logger.logRaw('下面将引导您完成基础配置信息的初始化。\n');
|
|
9
|
+
|
|
10
|
+
const config = storage.getAll();
|
|
11
|
+
|
|
12
|
+
const answers = await inquirer.prompt([
|
|
13
|
+
{
|
|
14
|
+
type: 'input',
|
|
15
|
+
name: 'gitlabUrl',
|
|
16
|
+
message: '1. 请输入 GitLab 服务器地址:',
|
|
17
|
+
default: config.gitlabUrl || 'https://gitlab.example.com',
|
|
18
|
+
validate: (input) => input.startsWith('http') ? true : '请输入有效的 URL (包含 http/https)'
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
type: 'password',
|
|
22
|
+
name: 'gitlabToken',
|
|
23
|
+
message: '2. 请输入您的 GitLab 个人访问令牌 (Private Token):',
|
|
24
|
+
mask: '*',
|
|
25
|
+
validate: (input) => input.length > 0 ? true : 'Token 不能为空'
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
type: 'password',
|
|
29
|
+
name: 'aiKey',
|
|
30
|
+
message: '3. 请输入您的大模型 API Key (如 OpenAI Key):',
|
|
31
|
+
mask: '*',
|
|
32
|
+
validate: (input) => input.length > 0 ? true : 'API Key 不能为空'
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
type: 'input',
|
|
36
|
+
name: 'aiBaseUrl',
|
|
37
|
+
message: '4. (可选) 大模型的自定义 baseUrl:',
|
|
38
|
+
default: config.aiBaseUrl || 'https://api.openai.com/v1'
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
type: 'input',
|
|
42
|
+
name: 'aiModel',
|
|
43
|
+
message: '5. (可选) 指定审查使用的大模型名称:',
|
|
44
|
+
default: config.aiModel || 'gpt-4o'
|
|
45
|
+
}
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
// 测试 AI 连通性
|
|
49
|
+
logger.startSpinner('ai-test', '正在验证 API Key 和大模型连通性...');
|
|
50
|
+
const aiClient = new AIClient({
|
|
51
|
+
apiKey: answers.aiKey,
|
|
52
|
+
baseURL: answers.aiBaseUrl,
|
|
53
|
+
model: answers.aiModel
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const testResult = await aiClient.testConnection();
|
|
57
|
+
|
|
58
|
+
if (testResult.success) {
|
|
59
|
+
logger.stopSpinner('ai-test', true, `API Key 验证通过! (${testResult.responseTime}ms)`);
|
|
60
|
+
|
|
61
|
+
// 保存配置
|
|
62
|
+
storage.saveConfig(answers);
|
|
63
|
+
logger.success('✅ 所有配置已成功保存!');
|
|
64
|
+
logger.logRaw('\n💡 提示: 您随时可以运行 `hong-review config list` 查看当前配置,或者再次运行 `hong-review login` 重新配置。');
|
|
65
|
+
} else {
|
|
66
|
+
logger.stopSpinner('ai-test', false, `API 验证失败: ${testResult.message}`);
|
|
67
|
+
logger.error('配置并未保存。请检查您的网络环境、URL 或者 API Key。');
|
|
68
|
+
}
|
|
69
|
+
};
|