hong-review-cli 1.0.0 → 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.
- package/bin/hong-review.js +8 -3
- package/openclaw_integration_setup.md +76 -0
- package/package.json +1 -1
- package/src/commands/actions.js +19 -14
- package/src/commands/list.js +14 -3
- package/src/commands/mr.js +14 -18
package/bin/hong-review.js
CHANGED
|
@@ -50,6 +50,7 @@ program
|
|
|
50
50
|
.option('-R, --report', '生成本地 Markdown 审查报告文件')
|
|
51
51
|
.option('-c, --comment', '审查完成后,自动将结果作为评论发至 MR 讨论区')
|
|
52
52
|
.option('-m, --merge', '审查通过且无明显问题时,自动合并该 MR')
|
|
53
|
+
.option('-y, --yes', '非交互式静默模式 (自动跳过所有确认)')
|
|
53
54
|
.action((mr_id, options) => {
|
|
54
55
|
mrCmd(mr_id, options);
|
|
55
56
|
});
|
|
@@ -58,7 +59,10 @@ program
|
|
|
58
59
|
.command('list')
|
|
59
60
|
.alias('ls')
|
|
60
61
|
.description('查看分配给我或待我处理的 MR 列表')
|
|
61
|
-
.
|
|
62
|
+
.option('-y, --yes', '非交互式静默模式 (不提示直接输出文本)')
|
|
63
|
+
.action((options) => {
|
|
64
|
+
listCmd(options);
|
|
65
|
+
});
|
|
62
66
|
|
|
63
67
|
// --- 单独动作 ---
|
|
64
68
|
program
|
|
@@ -71,8 +75,9 @@ program
|
|
|
71
75
|
program
|
|
72
76
|
.command('merge <mr_id>')
|
|
73
77
|
.description('检查权限并快捷合并指定的 MR')
|
|
74
|
-
.
|
|
75
|
-
|
|
78
|
+
.option('-y, --yes', '非交互式静默执行')
|
|
79
|
+
.action((mr_id, options) => {
|
|
80
|
+
actionsCmd.merge(mr_id, options);
|
|
76
81
|
});
|
|
77
82
|
|
|
78
83
|
program
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# OpenClaw 接入指南: `hong-review-cli` 自动审查流
|
|
2
|
+
|
|
3
|
+
本指南将指导您如何配置您的 OpenClaw,使其能够理解、使用 `hong-review` 命令行工具,并将审查结果路由推送到您的 Telegram 聊天中。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 步骤 1:为 CLI 配置 Webhook 推送地址
|
|
8
|
+
|
|
9
|
+
利用 `hong-review` 自带的生命周期挂钩(Hooks)功能,告诉它在审查完成后把数据扔给谁(即 OpenClaw Server 的 `hooks` 接口)。
|
|
10
|
+
|
|
11
|
+
在您的服务器环境(安装了 OpenClaw 和该 CLI 的机器)上运行以下命令:
|
|
12
|
+
*(注意:请将 `http://127.0.0.1:3000` 替换为 OpenClaw Gateway 真实的内网通信地址,并将 `<YOUR_SECRET_TOKEN>` 替换为 OpenClaw 配置文件中 `hooks.token` 相同的值)*
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
hong-review config set hooks.onReviewSuccess "curl -X POST http://127.0.0.1:3000/hooks/code-review -H 'Authorization: Bearer <YOUR_SECRET_TOKEN>' -H 'Content-Type: application/json' -d '{\"mrId\": \"$MR_ID\", \"status\": \"Success\"}'"
|
|
16
|
+
|
|
17
|
+
hong-review config set hooks.onReviewFailed "curl -X POST http://127.0.0.1:3000/hooks/code-review -H 'Authorization: Bearer <YOUR_SECRET_TOKEN>' -H 'Content-Type: application/json' -d '{\"mrId\": \"$MR_ID\", \"status\": \"Failed\"}'"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## 步骤 2:配置 OpenClaw 的路由文件 (Hooks Mapping)
|
|
21
|
+
|
|
22
|
+
在您的 OpenClaw 配置文件 (`openclaw.yaml` 或 [.json](file:///Users/hong/Desktop/cli/package.json)) 中,找到 `hooks` 配置块。你需要新增一条映射规则,让 OpenClaw 知道收到 `code-review` 路径的请求时,发送什么消息给 Telegram。
|
|
23
|
+
|
|
24
|
+
**示例配置(YAML 风格):**
|
|
25
|
+
|
|
26
|
+
```yaml
|
|
27
|
+
hooks:
|
|
28
|
+
token: "<YOUR_SECRET_TOKEN>" # 这必须与第一步 CURL 里的 Token 一致
|
|
29
|
+
basePath: "/hooks" # 这也对应第一步的路径
|
|
30
|
+
mappings:
|
|
31
|
+
- id: "hong-review-notification"
|
|
32
|
+
match:
|
|
33
|
+
path: "code-review" # 匹配 /hooks/code-review
|
|
34
|
+
action: "agent" # 以 Agent 代理的口吻发送
|
|
35
|
+
agentId: "telegram-bot" # 你的 Telegram Bot 的 Agent 标识
|
|
36
|
+
name: "AI Code Reviewer"
|
|
37
|
+
channel: "telegram" # 明确发往 Telegram 通道
|
|
38
|
+
to: "@your_telegram_username" # 你的真实触达账号
|
|
39
|
+
messageTemplate: |
|
|
40
|
+
代码审查完成 🔔
|
|
41
|
+
- **MR ID**: {{payload.mrId}}
|
|
42
|
+
- **状态**: {{payload.status}}
|
|
43
|
+
*(日志及报告已由系统分析进程执行完毕)*
|
|
44
|
+
```
|
|
45
|
+
*(这段配置使用了 OpenClaw 原生的 [src/gateway/hooks-mapping.ts](file:///tmp/openclaw/src/gateway/hooks-mapping.ts) 解析能力,它允许插值获取 Payload 里的变量并组合成文本推出去)*
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 步骤 3:赋予 OpenClaw "调用能力" (Tool / System Prompt)
|
|
50
|
+
|
|
51
|
+
最后也是最关键的,你需要把下面这段文字,作为插件系统描述或者**核心系统提示词 (System Prompt)** 告诉管理你 Telegram 消息的 OpenClaw 机器人。
|
|
52
|
+
|
|
53
|
+
### 👉 给 OpenClaw 机器人的 System Schema 设定:
|
|
54
|
+
|
|
55
|
+
```markdown
|
|
56
|
+
你是一个全能的代码审查助手。你现在被授权在宿主服务器上调用一个名为 `hong-review` 的终端核心程序来处理用户的 GitLab 审查请求。
|
|
57
|
+
|
|
58
|
+
**重要规则**:
|
|
59
|
+
因为你处于非人机交互环境,你每次调用该程序时,**必须加上 `-y` 或 `--yes` 参数**,这代表启用静默安全模式。
|
|
60
|
+
程序在执行结束或出错后会自动处理,**你永远不要尝试去读取它的标准输出日志来提取长篇大论的报告**,程序的内建 Hooks 系统会自己将带有排版的最终结果推送到 Telegram,你要做的这只是触发它即可。
|
|
61
|
+
|
|
62
|
+
**你可以使用它来回复用户的三种核心意图:**
|
|
63
|
+
|
|
64
|
+
1. 如果用户问“我的待办列表里有什么 MR?”或“列出待审查代码”:
|
|
65
|
+
请在底层执行命令:`hong-review ls -y`
|
|
66
|
+
|
|
67
|
+
2. 如果用户说“去帮我分析一下 101 这个 MR” 或 “查下 101”:
|
|
68
|
+
请在底层执行命令:`hong-review mr 101 -y`
|
|
69
|
+
|
|
70
|
+
3. 如果用户经过阅读报告后说“确认合并 101 吧”:
|
|
71
|
+
请在底层执行命令:`hong-review merge 101 -y`
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
完成以上三步,当您在 Telegram App 打开 OpenClaw 聊天窗口发送:"去查下 52 那个代码提交" 后。一切都会像齿轮一样无人值守转动,并在几秒或几十秒后把叮咚声推送到你的手机里!
|
package/package.json
CHANGED
package/src/commands/actions.js
CHANGED
|
@@ -44,37 +44,42 @@ module.exports = {
|
|
|
44
44
|
}
|
|
45
45
|
},
|
|
46
46
|
|
|
47
|
-
async merge(mrId) {
|
|
47
|
+
async merge(mrId, options = {}) {
|
|
48
48
|
try {
|
|
49
49
|
const projectId = await getProjectId();
|
|
50
50
|
const gitlab = getClient();
|
|
51
51
|
|
|
52
|
-
logger.startSpinner('merge-check', `正在检查 MR !${mrId} 合并权限...`);
|
|
52
|
+
if (!options.yes) logger.startSpinner('merge-check', `正在检查 MR !${mrId} 合并权限...`);
|
|
53
53
|
const check = await gitlab.canMergeMR(projectId, mrId);
|
|
54
54
|
|
|
55
55
|
if (!check.canMerge) {
|
|
56
|
-
logger.stopSpinner('merge-check', false, `无法合并: ${check.reason}`);
|
|
56
|
+
if (!options.yes) logger.stopSpinner('merge-check', false, `无法合并: ${check.reason}`);
|
|
57
|
+
else console.error(`Failed to merge: ${check.reason}`);
|
|
57
58
|
return;
|
|
58
59
|
}
|
|
59
|
-
logger.stopSpinner('merge-check', true, '权限检查通过。');
|
|
60
|
+
if (!options.yes) logger.stopSpinner('merge-check', true, '权限检查通过。');
|
|
60
61
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
let confirm = options.yes;
|
|
63
|
+
if (!confirm) {
|
|
64
|
+
const answer = await inquirer.prompt([{
|
|
65
|
+
type: 'confirm',
|
|
66
|
+
name: 'confirmMerge',
|
|
67
|
+
message: `即将自动合并 MR !${mrId},是否确认?`,
|
|
68
|
+
default: false
|
|
69
|
+
}]);
|
|
70
|
+
confirm = answer.confirmMerge;
|
|
71
|
+
}
|
|
67
72
|
|
|
68
|
-
if (
|
|
69
|
-
logger.startSpinner('merge', '合并中...');
|
|
73
|
+
if (confirm) {
|
|
74
|
+
if (!options.yes) logger.startSpinner('merge', '合并中...');
|
|
70
75
|
await gitlab.merge(projectId, mrId);
|
|
71
|
-
logger.stopSpinner('merge', true, '合并成功!');
|
|
76
|
+
if (!options.yes) logger.stopSpinner('merge', true, '合并成功!');
|
|
77
|
+
else console.log(`✅ MR !${mrId} 合并成功!`);
|
|
72
78
|
} else {
|
|
73
79
|
logger.info('已取消合并。');
|
|
74
80
|
}
|
|
75
81
|
|
|
76
82
|
} catch (e) {
|
|
77
|
-
if (spinners && spinners.has('merge')) logger.stopSpinner('merge', false);
|
|
78
83
|
logger.error(`合并失败: ${e.message}`);
|
|
79
84
|
}
|
|
80
85
|
},
|
package/src/commands/list.js
CHANGED
|
@@ -3,7 +3,7 @@ const logger = require('../utils/logger');
|
|
|
3
3
|
const GitLabClient = require('../core/gitlab');
|
|
4
4
|
const chalk = require('chalk');
|
|
5
5
|
|
|
6
|
-
module.exports = async function() {
|
|
6
|
+
module.exports = async function(options = {}) {
|
|
7
7
|
const config = storage.getAll();
|
|
8
8
|
if (!config.gitlabToken) {
|
|
9
9
|
logger.error('缺少 GitLab Token 配置,请运行 hong-review login');
|
|
@@ -12,10 +12,20 @@ module.exports = async function() {
|
|
|
12
12
|
|
|
13
13
|
const gitlab = new GitLabClient({ host: config.gitlabUrl, token: config.gitlabToken });
|
|
14
14
|
|
|
15
|
-
logger.startSpinner('list', '正在向 GitLab 拉取您需要处理的 Merge Requests...');
|
|
15
|
+
if (!options.yes) logger.startSpinner('list', '正在向 GitLab 拉取您需要处理的 Merge Requests...');
|
|
16
16
|
|
|
17
17
|
try {
|
|
18
18
|
const mrs = await gitlab.getMyReviewMergeRequests();
|
|
19
|
+
|
|
20
|
+
if (options.yes) {
|
|
21
|
+
// 静默模式下,直接输出纯文本列表供其他程序抓取/处理
|
|
22
|
+
console.log(`找到 ${mrs.length} 个待处理的 MR:`);
|
|
23
|
+
mrs.forEach(mr => {
|
|
24
|
+
console.log(`- ProjectID: ${mr.project_id} | MR !${mr.iid} | Title: ${mr.title} | Branch: ${mr.source_branch} -> ${mr.target_branch}`);
|
|
25
|
+
});
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
19
29
|
logger.stopSpinner('list', true, `成功拉取!共分配给您或待您审核的 MR 有 ${mrs.length} 个。\n`);
|
|
20
30
|
|
|
21
31
|
if (mrs.length === 0) {
|
|
@@ -60,6 +70,7 @@ module.exports = async function() {
|
|
|
60
70
|
}
|
|
61
71
|
|
|
62
72
|
} catch (e) {
|
|
63
|
-
logger.stopSpinner('list', false, `拉取 MR 列表失败: ${e.message}`);
|
|
73
|
+
if (!options.yes) logger.stopSpinner('list', false, `拉取 MR 列表失败: ${e.message}`);
|
|
74
|
+
else console.error(`Error: 拉取 MR 列表失败 - ${e.message}`);
|
|
64
75
|
}
|
|
65
76
|
};
|
package/src/commands/mr.js
CHANGED
|
@@ -28,6 +28,10 @@ module.exports = async function(mrId, options) {
|
|
|
28
28
|
// 为了 demo 的通用性,我们直接要求用户确认或配置 projectId。
|
|
29
29
|
let projectId = config.defaultProjectId;
|
|
30
30
|
if (!projectId) {
|
|
31
|
+
if (options.yes) {
|
|
32
|
+
logger.error('静默模式下未找到 defaultProjectId 配置,请先配置或在有历史审查记录后再尝试。');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
31
35
|
const answer = await inquirer.prompt([{
|
|
32
36
|
type: 'input',
|
|
33
37
|
name: 'pid',
|
|
@@ -58,11 +62,12 @@ module.exports = async function(mrId, options) {
|
|
|
58
62
|
logger.stopSpinner('fetching', true, `成功拉取 MR [${detail.title}] 分析所需的 ${changes.length} 个文件变更。`);
|
|
59
63
|
|
|
60
64
|
if (changes.length === 0) {
|
|
61
|
-
logger.warn('该 MR 没有包含任何文件变更。');
|
|
65
|
+
if (!options.yes) logger.warn('该 MR 没有包含任何文件变更。');
|
|
66
|
+
else console.log('MR 审查结束: 没有包含任何文件变更。');
|
|
62
67
|
return;
|
|
63
68
|
}
|
|
64
69
|
|
|
65
|
-
logger.startSpinner('ai-thinking', `🤖 AI (${config.aiModel}) 正在深度分析中,请稍候...`);
|
|
70
|
+
if (!options.yes) logger.startSpinner('ai-thinking', `🤖 AI (${config.aiModel}) 正在深度分析中,请稍候...`);
|
|
66
71
|
|
|
67
72
|
let reviewResult = null;
|
|
68
73
|
let round = 1;
|
|
@@ -72,7 +77,7 @@ module.exports = async function(mrId, options) {
|
|
|
72
77
|
|
|
73
78
|
// 开始 Agent 轮询
|
|
74
79
|
while (true) {
|
|
75
|
-
logger.startSpinner('ai-thinking', `🤖 AI 思考中 (第 ${round} 轮)...`);
|
|
80
|
+
if (!options.yes) logger.startSpinner('ai-thinking', `🤖 AI 思考中 (第 ${round} 轮)...`);
|
|
76
81
|
const aiResponse = await ai.chatWithJsonMode(AGENT_SYSTEM_PROMPT, conversationHistory);
|
|
77
82
|
const action = parseAgentResponse(aiResponse);
|
|
78
83
|
|
|
@@ -86,7 +91,7 @@ module.exports = async function(mrId, options) {
|
|
|
86
91
|
reviewResult = action.result;
|
|
87
92
|
break;
|
|
88
93
|
} else if (action.action === 'request_files') {
|
|
89
|
-
logger.stopSpinner('ai-thinking', true, `AI 请求查看额外文件: ${action.files.join(', ')}`);
|
|
94
|
+
if (!options.yes) logger.stopSpinner('ai-thinking', true, `AI 请求查看额外文件: ${action.files.join(', ')}`);
|
|
90
95
|
|
|
91
96
|
const contents = await fetchFileContents(gitlab, projectId, detail.source_branch, changes, action.files, action.contentTypes);
|
|
92
97
|
const fileContentMessage = formatFileContents(contents);
|
|
@@ -95,7 +100,7 @@ module.exports = async function(mrId, options) {
|
|
|
95
100
|
}
|
|
96
101
|
}
|
|
97
102
|
|
|
98
|
-
logger.stopSpinner('ai-thinking', true, 'AI 审查完成!');
|
|
103
|
+
if (!options.yes) logger.stopSpinner('ai-thinking', true, 'AI 审查完成!');
|
|
99
104
|
|
|
100
105
|
// 打印报告
|
|
101
106
|
printReport(mrIid, detail, reviewResult);
|
|
@@ -124,19 +129,7 @@ module.exports = async function(mrId, options) {
|
|
|
124
129
|
logger.stopSpinner('comment', true, '评论提交成功。');
|
|
125
130
|
}
|
|
126
131
|
|
|
127
|
-
|
|
128
|
-
if (options.merge && isSuccess && reviewResult.issues.filter(i => i.severity === 'error').length === 0) {
|
|
129
|
-
logger.startSpinner('merge', '达到自动合并标准,尝试执行自动合并...');
|
|
130
|
-
try {
|
|
131
|
-
await gitlab.merge(projectId, mrIid);
|
|
132
|
-
logger.stopSpinner('merge', true, '合并执行成功!');
|
|
133
|
-
await hooks.emit('onMerged', { projectId, mrId: mrIid });
|
|
134
|
-
} catch (err) {
|
|
135
|
-
logger.stopSpinner('merge', false, `合并失败: ${err.message}`);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (!options.merge) {
|
|
132
|
+
if (!options.merge && !options.yes) {
|
|
140
133
|
console.log('\n');
|
|
141
134
|
const postReviewAnswer = await inquirer.prompt([{
|
|
142
135
|
type: 'list',
|
|
@@ -164,6 +157,9 @@ module.exports = async function(mrId, options) {
|
|
|
164
157
|
} else {
|
|
165
158
|
logger.info('操作已完成,退出审查。');
|
|
166
159
|
}
|
|
160
|
+
} else if (options.yes && !options.merge) {
|
|
161
|
+
// 静默模式下,不需要选择,直接结束
|
|
162
|
+
console.log(`✅ [Headless] MR !${mrIid} 代码审查执行结束。`);
|
|
167
163
|
}
|
|
168
164
|
|
|
169
165
|
} catch (e) {
|