hong-review-cli 1.0.3 → 1.0.6
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 -4
- package/mock-test-hooks.js +107 -0
- package/openclaw.json +299 -0
- package/openclaw_integration_setup.md +56 -25
- package/package.json +2 -2
- package/src/commands/actions.js +86 -87
- package/src/commands/list.js +7 -9
- package/src/commands/mr.js +55 -53
- package/src/commands/setup-openclaw.js +7 -5
- package/src/utils/hooks.js +19 -15
- package/src/utils/storage.js +3 -7
package/bin/hong-review.js
CHANGED
|
@@ -55,6 +55,7 @@ configGroup
|
|
|
55
55
|
program
|
|
56
56
|
.command('mr <mr_id>')
|
|
57
57
|
.description('智能审查指定的 GitLab Merge Request')
|
|
58
|
+
.option('-p, --project <id>', '指定 GitLab Project ID')
|
|
58
59
|
.option('-R, --report', '生成本地 Markdown 审查报告文件')
|
|
59
60
|
.option('-c, --comment', '审查完成后,自动将结果作为评论发至 MR 讨论区')
|
|
60
61
|
.option('-m, --merge', '审查通过且无明显问题时,自动合并该 MR')
|
|
@@ -76,14 +77,16 @@ program
|
|
|
76
77
|
program
|
|
77
78
|
.command('comment <mr_id> <text>')
|
|
78
79
|
.description('给指定的 MR 快捷留言/评论')
|
|
79
|
-
.
|
|
80
|
-
|
|
80
|
+
.option('-p, --project <id>', '指定 GitLab Project ID')
|
|
81
|
+
.action((mr_id, text, options) => {
|
|
82
|
+
actionsCmd.comment(mr_id, text, options);
|
|
81
83
|
});
|
|
82
84
|
|
|
83
85
|
program
|
|
84
86
|
.command('merge <mr_id>')
|
|
85
87
|
.description('检查权限并快捷合并指定的 MR')
|
|
86
88
|
.option('-y, --yes', '非交互式静默执行')
|
|
89
|
+
.option('-p, --project <id>', '指定 GitLab Project ID')
|
|
87
90
|
.action((mr_id, options) => {
|
|
88
91
|
actionsCmd.merge(mr_id, options);
|
|
89
92
|
});
|
|
@@ -91,8 +94,9 @@ program
|
|
|
91
94
|
program
|
|
92
95
|
.command('view <mr_id>')
|
|
93
96
|
.description('在终端直接查看一个 MR 的文件差异 (Diff) 详情')
|
|
94
|
-
.
|
|
95
|
-
|
|
97
|
+
.option('-p, --project <id>', '指定 GitLab Project ID')
|
|
98
|
+
.action((mr_id, options) => {
|
|
99
|
+
actionsCmd.view(mr_id, options);
|
|
96
100
|
});
|
|
97
101
|
|
|
98
102
|
program.parse(process.argv);
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
const http = require('http');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
// 强制使用环境变量加载测试配置
|
|
5
|
+
process.env.HONG_REVIEW_CONFIG = path.join(__dirname, 'test-config.json');
|
|
6
|
+
|
|
7
|
+
const storage = require('./src/utils/storage');
|
|
8
|
+
const hooks = require('./src/utils/hooks');
|
|
9
|
+
const GitLabClient = require('./src/core/gitlab');
|
|
10
|
+
const AIClient = require('./src/core/ai');
|
|
11
|
+
|
|
12
|
+
// 1. 启动一个本地 Mock Webhook Server 来接收并打印 hook 被触发的情况
|
|
13
|
+
const server = http.createServer((req, res) => {
|
|
14
|
+
let body = '';
|
|
15
|
+
req.on('data', chunk => body += chunk);
|
|
16
|
+
req.on('end', () => {
|
|
17
|
+
try {
|
|
18
|
+
const data = JSON.parse(body);
|
|
19
|
+
console.log(`\n📦 [Webhook 收信端] 捕获到事件: \x1b[36m${data.event}\x1b[0m`);
|
|
20
|
+
if (data.status) {
|
|
21
|
+
console.log(` └─ Status (进度): ${data.status}`);
|
|
22
|
+
}
|
|
23
|
+
if (data.result) {
|
|
24
|
+
console.log(` └─ Result (结果风险等级): ${data.result.riskLevel}`);
|
|
25
|
+
}
|
|
26
|
+
} catch(e) {}
|
|
27
|
+
res.writeHead(200);
|
|
28
|
+
res.end('OK');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
server.listen(33011, async () => {
|
|
33
|
+
console.log('🚀 本地 Mock Webhook Server 已启动在 33011 端口,准备拦截 Hook');
|
|
34
|
+
|
|
35
|
+
// 备份原有配置
|
|
36
|
+
const originalHook = storage.get('hook');
|
|
37
|
+
|
|
38
|
+
// 把 hook 地址强制设为咱们自己这个监听的服务
|
|
39
|
+
storage.set('hook', 'http://127.0.0.1:33011/test-hook');
|
|
40
|
+
// 假装有 Token 和 Key 绕过前提校验
|
|
41
|
+
storage.set('gitlabToken', 'mock-token');
|
|
42
|
+
storage.set('aiKey', 'mock-ai-key');
|
|
43
|
+
|
|
44
|
+
// 2. 模拟霸占 GitLabClient 的网络请求方法,不让它真去请求外网
|
|
45
|
+
GitLabClient.prototype.getMergeRequestDetail = async () => ({ title: 'Mock 功能:测试 Hooks 触发和多轮交互', source_branch: 'feature-hook-test' });
|
|
46
|
+
GitLabClient.prototype.getMergeRequestChanges = async () => ([{ new_path: 'src/app.js', diff: '+ console.log("hello");' }]);
|
|
47
|
+
GitLabClient.prototype.getFileContent = async () => ('const a = 1;');
|
|
48
|
+
GitLabClient.prototype.addMergeRequestNote = async () => ({});
|
|
49
|
+
|
|
50
|
+
// 3. 模拟霸占 AIClient 的核心方法,我们让它硬编码返回几个动作:
|
|
51
|
+
// 第一圈:请求文件 (request_files)
|
|
52
|
+
// 第二圈:再次请求文件 (request_files)
|
|
53
|
+
// 第三圈:出报告 (generate_report)
|
|
54
|
+
let aiCallCount = 0;
|
|
55
|
+
AIClient.prototype.chatWithJsonMode = async () => {
|
|
56
|
+
aiCallCount++;
|
|
57
|
+
if (aiCallCount === 1) {
|
|
58
|
+
return JSON.stringify({
|
|
59
|
+
action: 'request_files',
|
|
60
|
+
files: ['src/utils.js'],
|
|
61
|
+
contentTypes: ['full'],
|
|
62
|
+
reasoning: '我需要看看 utils.js 里的工具函数实现逻辑'
|
|
63
|
+
});
|
|
64
|
+
} else if (aiCallCount === 2) {
|
|
65
|
+
return JSON.stringify({
|
|
66
|
+
action: 'request_files',
|
|
67
|
+
files: ['package.json'],
|
|
68
|
+
contentTypes: ['diff'],
|
|
69
|
+
reasoning: '我还想看看是不是引入了不安全的新依赖'
|
|
70
|
+
});
|
|
71
|
+
} else {
|
|
72
|
+
return JSON.stringify({
|
|
73
|
+
action: 'generate_report',
|
|
74
|
+
result: {
|
|
75
|
+
riskLevel: 'medium', // 测试一下不是 high 时,走成功钩子
|
|
76
|
+
summary: '模拟 AI 分析:经过 3 轮查询,文件整体没什么大问题,只需小调整。',
|
|
77
|
+
issues: [
|
|
78
|
+
{ severity: 'warning', file: 'src/app.js', line: 10, title: '控制台输出', description: '代码里留了一个 console.log' }
|
|
79
|
+
],
|
|
80
|
+
suggestions: ['建议把测试用的 console 删掉']
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// 4. 调用核心审查流程 mrCmd
|
|
87
|
+
const mrCmd = require('./src/commands/mr');
|
|
88
|
+
console.log('\n======================================================');
|
|
89
|
+
console.log('>>> 开始执行 MR 审查流程 (使用 -y 自动跳过交互)');
|
|
90
|
+
console.log('======================================================\n');
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
// -y 代表 yes: true 参数,纯命令行静默执行,否则会有 inquirer 会卡在这
|
|
94
|
+
await mrCmd('999', { project: '10001', yes: true });
|
|
95
|
+
} catch(e) {
|
|
96
|
+
console.error(e);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
console.log('\n======================================================');
|
|
100
|
+
console.log('>>> 审查流程结束,正在清理测试环境配置...');
|
|
101
|
+
|
|
102
|
+
// 还原原始配置
|
|
103
|
+
storage.set('hook', originalHook);
|
|
104
|
+
|
|
105
|
+
server.close();
|
|
106
|
+
process.exit(0);
|
|
107
|
+
});
|
package/openclaw.json
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
{
|
|
2
|
+
"meta": {
|
|
3
|
+
"lastTouchedVersion": "2026.2.21-2",
|
|
4
|
+
"lastTouchedAt": "2026-02-22T18:56:21.832Z"
|
|
5
|
+
},
|
|
6
|
+
"wizard": {
|
|
7
|
+
"lastRunAt": "2026-02-20T18:04:07.103Z",
|
|
8
|
+
"lastRunVersion": "2026.2.15",
|
|
9
|
+
"lastRunCommand": "doctor",
|
|
10
|
+
"lastRunMode": "local"
|
|
11
|
+
},
|
|
12
|
+
"update": {
|
|
13
|
+
"checkOnStart": true
|
|
14
|
+
},
|
|
15
|
+
"browser": {
|
|
16
|
+
"defaultProfile": "chrome"
|
|
17
|
+
},
|
|
18
|
+
"auth": {
|
|
19
|
+
"profiles": {
|
|
20
|
+
"zai:default": {
|
|
21
|
+
"provider": "zai",
|
|
22
|
+
"mode": "api_key"
|
|
23
|
+
},
|
|
24
|
+
"xai:default": {
|
|
25
|
+
"provider": "xai",
|
|
26
|
+
"mode": "api_key"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"models": {
|
|
31
|
+
"mode": "merge",
|
|
32
|
+
"providers": {
|
|
33
|
+
"custom-ai-askhong-com": {
|
|
34
|
+
"baseUrl": "https://ai.askhong.com/v1",
|
|
35
|
+
"apiKey": "a3VwNTViNHQ3cjZqOWY4cDJrMW4yZDBuM3AxaTRsNXU2dzc4eHk",
|
|
36
|
+
"api": "openai-completions",
|
|
37
|
+
"models": [
|
|
38
|
+
{
|
|
39
|
+
"id": "gemini-3-pro",
|
|
40
|
+
"name": "gemini-3-pro (Custom Provider)",
|
|
41
|
+
"reasoning": false,
|
|
42
|
+
"input": [
|
|
43
|
+
"text"
|
|
44
|
+
],
|
|
45
|
+
"cost": {
|
|
46
|
+
"input": 0,
|
|
47
|
+
"output": 0,
|
|
48
|
+
"cacheRead": 0,
|
|
49
|
+
"cacheWrite": 0
|
|
50
|
+
},
|
|
51
|
+
"contextWindow": 128000,
|
|
52
|
+
"maxTokens": 8192
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
"zai": {
|
|
57
|
+
"baseUrl": "https://open.bigmodel.cn/api/coding/paas/v4",
|
|
58
|
+
"api": "openai-completions",
|
|
59
|
+
"models": [
|
|
60
|
+
{
|
|
61
|
+
"id": "glm-5",
|
|
62
|
+
"name": "GLM-5",
|
|
63
|
+
"reasoning": true,
|
|
64
|
+
"input": [
|
|
65
|
+
"text"
|
|
66
|
+
],
|
|
67
|
+
"cost": {
|
|
68
|
+
"input": 0,
|
|
69
|
+
"output": 0,
|
|
70
|
+
"cacheRead": 0,
|
|
71
|
+
"cacheWrite": 0
|
|
72
|
+
},
|
|
73
|
+
"contextWindow": 204800,
|
|
74
|
+
"maxTokens": 131072
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"id": "glm-4.7",
|
|
78
|
+
"name": "GLM-4.7",
|
|
79
|
+
"reasoning": true,
|
|
80
|
+
"input": [
|
|
81
|
+
"text"
|
|
82
|
+
],
|
|
83
|
+
"cost": {
|
|
84
|
+
"input": 0,
|
|
85
|
+
"output": 0,
|
|
86
|
+
"cacheRead": 0,
|
|
87
|
+
"cacheWrite": 0
|
|
88
|
+
},
|
|
89
|
+
"contextWindow": 204800,
|
|
90
|
+
"maxTokens": 131072
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"id": "glm-4.7-flash",
|
|
94
|
+
"name": "GLM-4.7 Flash",
|
|
95
|
+
"reasoning": true,
|
|
96
|
+
"input": [
|
|
97
|
+
"text"
|
|
98
|
+
],
|
|
99
|
+
"cost": {
|
|
100
|
+
"input": 0,
|
|
101
|
+
"output": 0,
|
|
102
|
+
"cacheRead": 0,
|
|
103
|
+
"cacheWrite": 0
|
|
104
|
+
},
|
|
105
|
+
"contextWindow": 204800,
|
|
106
|
+
"maxTokens": 131072
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"id": "glm-4.7-flashx",
|
|
110
|
+
"name": "GLM-4.7 FlashX",
|
|
111
|
+
"reasoning": true,
|
|
112
|
+
"input": [
|
|
113
|
+
"text"
|
|
114
|
+
],
|
|
115
|
+
"cost": {
|
|
116
|
+
"input": 0,
|
|
117
|
+
"output": 0,
|
|
118
|
+
"cacheRead": 0,
|
|
119
|
+
"cacheWrite": 0
|
|
120
|
+
},
|
|
121
|
+
"contextWindow": 204800,
|
|
122
|
+
"maxTokens": 131072
|
|
123
|
+
}
|
|
124
|
+
]
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
"agents": {
|
|
129
|
+
"defaults": {
|
|
130
|
+
"model": {
|
|
131
|
+
"primary": "zai/glm-4.7"
|
|
132
|
+
},
|
|
133
|
+
"models": {
|
|
134
|
+
"zai/glm-5": {
|
|
135
|
+
"alias": "GLM"
|
|
136
|
+
},
|
|
137
|
+
"zai/glm-4.7": {}
|
|
138
|
+
},
|
|
139
|
+
"workspace": "/Users/hong/.openclaw/workspace",
|
|
140
|
+
"memorySearch": {
|
|
141
|
+
"enabled": true,
|
|
142
|
+
"sources": [
|
|
143
|
+
"memory"
|
|
144
|
+
],
|
|
145
|
+
"extraPaths": [
|
|
146
|
+
"MEMORY.md",
|
|
147
|
+
"memory/*.md"
|
|
148
|
+
],
|
|
149
|
+
"provider": "local",
|
|
150
|
+
"sync": {
|
|
151
|
+
"onSessionStart": true,
|
|
152
|
+
"onSearch": true,
|
|
153
|
+
"watch": true
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
"compaction": {
|
|
157
|
+
"mode": "safeguard"
|
|
158
|
+
},
|
|
159
|
+
"blockStreamingDefault": "on",
|
|
160
|
+
"blockStreamingBreak": "text_end",
|
|
161
|
+
"maxConcurrent": 4,
|
|
162
|
+
"subagents": {
|
|
163
|
+
"maxConcurrent": 8
|
|
164
|
+
},
|
|
165
|
+
"timeoutSeconds": 180
|
|
166
|
+
},
|
|
167
|
+
"list": [
|
|
168
|
+
{
|
|
169
|
+
"id": "main",
|
|
170
|
+
"subagents": {
|
|
171
|
+
"allowAgents": [
|
|
172
|
+
"tech-group",
|
|
173
|
+
"invest-group"
|
|
174
|
+
]
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
"id": "tech-group",
|
|
179
|
+
"name": "tech-group",
|
|
180
|
+
"workspace": "/Users/hong/.openclaw/workspace-tech",
|
|
181
|
+
"agentDir": "/Users/hong/.openclaw/agents/tech-group/agent"
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
"id": "invest-group",
|
|
185
|
+
"name": "invest-group",
|
|
186
|
+
"workspace": "/Users/hong/.openclaw/workspace-invest",
|
|
187
|
+
"agentDir": "/Users/hong/.openclaw/agents/invest-group/agent"
|
|
188
|
+
}
|
|
189
|
+
]
|
|
190
|
+
},
|
|
191
|
+
"bindings": [
|
|
192
|
+
{
|
|
193
|
+
"agentId": "tech-group",
|
|
194
|
+
"match": {
|
|
195
|
+
"channel": "telegram",
|
|
196
|
+
"peer": {
|
|
197
|
+
"kind": "group",
|
|
198
|
+
"id": "-5063603196"
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
"agentId": "invest-group",
|
|
204
|
+
"match": {
|
|
205
|
+
"channel": "telegram",
|
|
206
|
+
"peer": {
|
|
207
|
+
"kind": "group",
|
|
208
|
+
"id": "-5196600027"
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
],
|
|
213
|
+
"messages": {
|
|
214
|
+
"ackReactionScope": "all"
|
|
215
|
+
},
|
|
216
|
+
"commands": {
|
|
217
|
+
"native": "auto",
|
|
218
|
+
"nativeSkills": "auto",
|
|
219
|
+
"restart": true,
|
|
220
|
+
"ownerDisplay": "raw"
|
|
221
|
+
},
|
|
222
|
+
"hooks": {
|
|
223
|
+
"token": "e4e2136cb731cfb14f1ad190833738c5bdadfbf79cf7c0f5",
|
|
224
|
+
"internal": {
|
|
225
|
+
"enabled": true,
|
|
226
|
+
"entries": {
|
|
227
|
+
"command-logger": {
|
|
228
|
+
"enabled": true
|
|
229
|
+
},
|
|
230
|
+
"session-memory": {
|
|
231
|
+
"enabled": false
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
"mappings": [
|
|
236
|
+
{
|
|
237
|
+
"id": "hong-review-notification",
|
|
238
|
+
"match": {
|
|
239
|
+
"path": "code-review"
|
|
240
|
+
},
|
|
241
|
+
"action": "agent",
|
|
242
|
+
"agentId": "tech-group",
|
|
243
|
+
"name": "AI Code Reviewer",
|
|
244
|
+
"channel": "telegram",
|
|
245
|
+
"to": "-5063603196",
|
|
246
|
+
"messageTemplate": "代码审查完成 🔔\n- **MR ID**: {{payload.mrId}}\n- **状态**: {{payload.status}}\n*(日志及报告已由系统分析进程执行完毕)*"
|
|
247
|
+
}
|
|
248
|
+
]
|
|
249
|
+
},
|
|
250
|
+
"channels": {
|
|
251
|
+
"telegram": {
|
|
252
|
+
"enabled": true,
|
|
253
|
+
"dmPolicy": "pairing",
|
|
254
|
+
"botToken": "8489148682:AAFrvw72YgSP6SO6Sj082twbMOaLLodDrhI",
|
|
255
|
+
"groups": {
|
|
256
|
+
"*": {
|
|
257
|
+
"requireMention": false
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
"groupPolicy": "open",
|
|
261
|
+
"streaming": true
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
"gateway": {
|
|
265
|
+
"port": 18790,
|
|
266
|
+
"mode": "local",
|
|
267
|
+
"bind": "loopback",
|
|
268
|
+
"controlUi": {
|
|
269
|
+
"allowedOrigins": [
|
|
270
|
+
"https://mini.askhong.com:18789",
|
|
271
|
+
"http://mini.askhong.com:18789"
|
|
272
|
+
]
|
|
273
|
+
},
|
|
274
|
+
"auth": {
|
|
275
|
+
"mode": "token",
|
|
276
|
+
"token": "e4e2136cb731cfb14f1ad190833738c5bdadfbf79cf7c0f5"
|
|
277
|
+
},
|
|
278
|
+
"tailscale": {
|
|
279
|
+
"mode": "off",
|
|
280
|
+
"resetOnExit": false
|
|
281
|
+
},
|
|
282
|
+
"nodes": {
|
|
283
|
+
"denyCommands": [
|
|
284
|
+
]
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
"skills": {
|
|
288
|
+
"install": {
|
|
289
|
+
"nodeManager": "npm"
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
"plugins": {
|
|
293
|
+
"entries": {
|
|
294
|
+
"telegram": {
|
|
295
|
+
"enabled": true
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
@@ -18,26 +18,39 @@ hong-review setup-openclaw
|
|
|
18
18
|
|
|
19
19
|
在您的 OpenClaw 配置文件 (`openclaw.yaml` 或 [.json](file:///Users/hong/Desktop/cli/package.json)) 中,找到 `hooks` 配置块。你需要新增一条映射规则,让 OpenClaw 知道收到 `code-review` 路径的请求时,发送什么消息给 Telegram。
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
21
|
+
**针对您当前 JSON 配置的示例配置:**
|
|
22
|
+
|
|
23
|
+
您可以直接将以下 `hooks` 代码片段复制替换掉您当前配置文件里的 `hooks` 部分。已经为您配置好了正确的 `tech-group` 发送代理目标和 Telegram 群组 ID:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
"hooks": {
|
|
27
|
+
"token": "e4e2136cb731cfb14f1ad190833738c5bdadfbf79cf7c0f5",
|
|
28
|
+
"internal": {
|
|
29
|
+
"enabled": true,
|
|
30
|
+
"entries": {
|
|
31
|
+
"command-logger": {
|
|
32
|
+
"enabled": true
|
|
33
|
+
},
|
|
34
|
+
"session-memory": {
|
|
35
|
+
"enabled": false
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"mappings": [
|
|
40
|
+
{
|
|
41
|
+
"id": "hong-review-notification",
|
|
42
|
+
"match": {
|
|
43
|
+
"path": "code-review"
|
|
44
|
+
},
|
|
45
|
+
"action": "agent",
|
|
46
|
+
"agentId": "tech-group",
|
|
47
|
+
"name": "AI Code Reviewer",
|
|
48
|
+
"channel": "telegram",
|
|
49
|
+
"to": "-5063603196",
|
|
50
|
+
"messageTemplate": "代码审查完成 🔔\n- **MR ID**: {{payload.mrId}}\n- **状态**: {{payload.status}}\n*(日志及报告已由系统分析进程执行完毕)*"
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
}
|
|
41
54
|
```
|
|
42
55
|
*(这段配置使用了 OpenClaw 原生的 [src/gateway/hooks-mapping.ts](file:///tmp/openclaw/src/gateway/hooks-mapping.ts) 解析能力,它允许插值获取 Payload 里的变量并组合成文本推出去)*
|
|
43
56
|
|
|
@@ -56,16 +69,34 @@ hooks:
|
|
|
56
69
|
因为你处于非人机交互环境,你每次调用该程序时,**必须加上 `-y` 或 `--yes` 参数**,这代表启用静默安全模式。
|
|
57
70
|
程序在执行结束或出错后会自动处理,**你永远不要尝试去读取它的标准输出日志来提取长篇大论的报告**,程序的内建 Hooks 系统会自己将带有排版的最终结果推送到 Telegram,你要做的这只是触发它即可。
|
|
58
71
|
|
|
59
|
-
|
|
72
|
+
**你可以使用它来回复用户的各种核心意图(请务必携带对应的命令和必要参数):**
|
|
60
73
|
|
|
61
|
-
1.
|
|
74
|
+
1. 审查和分析具体合并请求:
|
|
75
|
+
如果用户说“去帮我分析一下 101 这个 MR” 或 “查下 101”
|
|
76
|
+
请在底层执行命令:`hong-review mr 101 -y`
|
|
77
|
+
|
|
78
|
+
2. 查看待办列表:
|
|
79
|
+
如果用户问“我的待办列表里有什么 MR?”或“列出待审查代码”
|
|
62
80
|
请在底层执行命令:`hong-review ls -y`
|
|
63
81
|
|
|
64
|
-
|
|
65
|
-
|
|
82
|
+
3. 留言与评论:
|
|
83
|
+
如果用户想给 MR 直接留言,例如“给 101 说这个写的很棒”
|
|
84
|
+
请在底层执行命令:`hong-review comment 101 "这个写的很棒"`
|
|
66
85
|
|
|
67
|
-
|
|
86
|
+
4. 自动代码合并:
|
|
87
|
+
如果用户在代码分析并确保安全后说“确认合并 101 吧”
|
|
68
88
|
请在底层执行命令:`hong-review merge 101 -y`
|
|
89
|
+
|
|
90
|
+
5. 快速查看代码的原始变更 (Diff):
|
|
91
|
+
如果用户问“我想直接看看 101 改了啥代码”
|
|
92
|
+
请在底层执行命令:`hong-review view 101`
|
|
93
|
+
|
|
94
|
+
**附录: 额外的底层管理员命令集**
|
|
95
|
+
*(在绝大数情况下不需要触发,仅供全能 AI 掌握系统全貌,切勿对普通用户暴露)*
|
|
96
|
+
- 程序功能登录与初始化配置:`hong-review login` (或 `init`)
|
|
97
|
+
- 查看配置信息:`hong-review config list`
|
|
98
|
+
- 修改配置:`hong-review config set <key> <value>`
|
|
99
|
+
- 路由挂载初始化:`hong-review setup-openclaw`
|
|
69
100
|
```
|
|
70
101
|
|
|
71
102
|
---
|
package/package.json
CHANGED
package/src/commands/actions.js
CHANGED
|
@@ -11,117 +11,116 @@ function getClient() {
|
|
|
11
11
|
return new GitLabClient({ host: config.gitlabUrl, token: config.gitlabToken });
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
async function getProjectId() {
|
|
15
|
-
let projectId =
|
|
14
|
+
async function getProjectId(options = {}) {
|
|
15
|
+
let projectId = options.project;
|
|
16
16
|
if (!projectId) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
storage.set('defaultProjectId', projectId);
|
|
17
|
+
const answer = await inquirer.prompt([{
|
|
18
|
+
type: 'input',
|
|
19
|
+
name: 'pid',
|
|
20
|
+
message: '请输入当前 Git 仓库对应的 GitLab Project ID (或使用 -p 参数):'
|
|
21
|
+
}]);
|
|
22
|
+
projectId = answer.pid;
|
|
24
23
|
}
|
|
25
24
|
return projectId;
|
|
26
25
|
}
|
|
27
26
|
|
|
28
27
|
module.exports = {
|
|
29
|
-
async comment(mrId, text) {
|
|
28
|
+
async comment(mrId, text, options = {}) {
|
|
30
29
|
if (!text) {
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
logger.error('请输入评论内容。用法: hong-review comment <mr_id> "您的评论"');
|
|
31
|
+
return;
|
|
33
32
|
}
|
|
34
33
|
try {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
const projectId = await getProjectId(options);
|
|
35
|
+
const gitlab = getClient();
|
|
36
|
+
|
|
37
|
+
logger.startSpinner('comment', `正在向 MR !${mrId} 发送评论...`);
|
|
38
|
+
await gitlab.addMergeRequestNote(projectId, mrId, { body: text });
|
|
39
|
+
logger.stopSpinner('comment', true, '评论发送成功!');
|
|
41
40
|
|
|
42
41
|
} catch (e) {
|
|
43
|
-
|
|
42
|
+
logger.stopSpinner('comment', false, `评论发送失败: ${e.message}`);
|
|
44
43
|
}
|
|
45
44
|
},
|
|
46
45
|
|
|
47
46
|
async merge(mrId, options = {}) {
|
|
48
47
|
try {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
48
|
+
const projectId = await getProjectId(options);
|
|
49
|
+
const gitlab = getClient();
|
|
50
|
+
|
|
51
|
+
if (!options.yes) logger.startSpinner('merge-check', `正在检查 MR !${mrId} 合并权限...`);
|
|
52
|
+
const check = await gitlab.canMergeMR(projectId, mrId);
|
|
53
|
+
|
|
54
|
+
if (!check.canMerge) {
|
|
55
|
+
if (!options.yes) logger.stopSpinner('merge-check', false, `无法合并: ${check.reason}`);
|
|
56
|
+
else console.error(`Failed to merge: ${check.reason}`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (!options.yes) logger.stopSpinner('merge-check', true, '权限检查通过。');
|
|
60
|
+
|
|
61
|
+
let confirm = options.yes;
|
|
62
|
+
if (!confirm) {
|
|
63
|
+
const answer = await inquirer.prompt([{
|
|
64
|
+
type: 'confirm',
|
|
65
|
+
name: 'confirmMerge',
|
|
66
|
+
message: `即将自动合并 MR !${mrId},是否确认?`,
|
|
67
|
+
default: false
|
|
68
|
+
}]);
|
|
69
|
+
confirm = answer.confirmMerge;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (confirm) {
|
|
73
|
+
if (!options.yes) logger.startSpinner('merge', '合并中...');
|
|
74
|
+
await gitlab.merge(projectId, mrId);
|
|
75
|
+
if (!options.yes) logger.stopSpinner('merge', true, '合并成功!');
|
|
76
|
+
else console.log(`✅ MR !${mrId} 合并成功!`);
|
|
77
|
+
} else {
|
|
78
|
+
logger.info('已取消合并。');
|
|
79
|
+
}
|
|
81
80
|
|
|
82
81
|
} catch (e) {
|
|
83
|
-
|
|
82
|
+
logger.error(`合并失败: ${e.message}`);
|
|
84
83
|
}
|
|
85
84
|
},
|
|
86
85
|
|
|
87
|
-
async view(mrId) {
|
|
86
|
+
async view(mrId, options = {}) {
|
|
88
87
|
try {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
88
|
+
const projectId = await getProjectId(options);
|
|
89
|
+
const gitlab = getClient();
|
|
90
|
+
|
|
91
|
+
logger.startSpinner('view', `拉取 MR !${mrId} 的文件变更...`);
|
|
92
|
+
const changes = await gitlab.getMergeRequestChanges(projectId, mrId);
|
|
93
|
+
logger.stopSpinner('view', true, `共拉取到 ${changes.length} 个文件的变更:\n`);
|
|
94
|
+
|
|
95
|
+
if (changes.length === 0) {
|
|
96
|
+
logger.info('该 MR 无文件变更。');
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const choices = changes.map((c, i) => ({
|
|
101
|
+
name: `[${c.new_file ? '新增' : (c.deleted_file ? '删除' : '修改')}] ${c.new_path}`,
|
|
102
|
+
value: i
|
|
103
|
+
}));
|
|
104
|
+
|
|
105
|
+
const answer = await inquirer.prompt([{
|
|
106
|
+
type: 'list',
|
|
107
|
+
name: 'fileIndex',
|
|
108
|
+
message: '选择要查看 diff 的文件:',
|
|
109
|
+
choices: [...choices, new inquirer.Separator(), { name: '退出', value: -1 }]
|
|
110
|
+
}]);
|
|
111
|
+
|
|
112
|
+
if (answer.fileIndex !== -1) {
|
|
113
|
+
const selectedFile = changes[answer.fileIndex];
|
|
114
|
+
console.log('\n--- Diff 开始 ---');
|
|
115
|
+
console.log(selectedFile.diff);
|
|
116
|
+
console.log('--- Diff 结束 ---\n');
|
|
117
|
+
|
|
118
|
+
// 如果有需求可以加个无限循环,看完一个文件还能选另一个。
|
|
119
|
+
// 但为了 CLI 简洁,目前设计看完即退
|
|
120
|
+
}
|
|
122
121
|
|
|
123
122
|
} catch (e) {
|
|
124
|
-
|
|
123
|
+
logger.error(`拉取变更失败: ${e.message}`);
|
|
125
124
|
}
|
|
126
125
|
}
|
|
127
126
|
};
|
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(options = {}) {
|
|
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');
|
|
@@ -11,12 +11,12 @@ module.exports = async function(options = {}) {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
const gitlab = new GitLabClient({ host: config.gitlabUrl, token: config.gitlabToken });
|
|
14
|
-
|
|
14
|
+
|
|
15
15
|
if (!options.yes) logger.startSpinner('list', '正在向 GitLab 拉取您需要处理的 Merge Requests...');
|
|
16
|
-
|
|
16
|
+
|
|
17
17
|
try {
|
|
18
18
|
const mrs = await gitlab.getMyReviewMergeRequests();
|
|
19
|
-
|
|
19
|
+
|
|
20
20
|
if (options.yes) {
|
|
21
21
|
// 静默模式下,直接输出纯文本列表供其他程序抓取/处理
|
|
22
22
|
console.log(`找到 ${mrs.length} 个待处理的 MR:`);
|
|
@@ -27,7 +27,7 @@ module.exports = async function(options = {}) {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
logger.stopSpinner('list', true, `成功拉取!共分配给您或待您审核的 MR 有 ${mrs.length} 个。\n`);
|
|
30
|
-
|
|
30
|
+
|
|
31
31
|
if (mrs.length === 0) {
|
|
32
32
|
logger.info('暂时没有需要您处理的 Merge Request。喝杯咖啡休息一下吧 ☕️\n');
|
|
33
33
|
return;
|
|
@@ -61,14 +61,12 @@ module.exports = async function(options = {}) {
|
|
|
61
61
|
|
|
62
62
|
if (answer.selectedMr) {
|
|
63
63
|
console.log('');
|
|
64
|
-
// 自动配置 projectId 避免下一次询问
|
|
65
|
-
storage.set('defaultProjectId', String(answer.selectedMr.project_id));
|
|
66
64
|
// 启动智能审查流程
|
|
67
|
-
await mrCmd(answer.selectedMr.iid, {});
|
|
65
|
+
await mrCmd(answer.selectedMr.iid, { project: answer.selectedMr.project_id });
|
|
68
66
|
} else {
|
|
69
67
|
logger.info('已退出。');
|
|
70
68
|
}
|
|
71
|
-
|
|
69
|
+
|
|
72
70
|
} catch (e) {
|
|
73
71
|
if (!options.yes) logger.stopSpinner('list', false, `拉取 MR 列表失败: ${e.message}`);
|
|
74
72
|
else console.error(`Error: 拉取 MR 列表失败 - ${e.message}`);
|
package/src/commands/mr.js
CHANGED
|
@@ -7,38 +7,34 @@ const logger = require('../utils/logger');
|
|
|
7
7
|
const hooks = require('../utils/hooks');
|
|
8
8
|
const GitLabClient = require('../core/gitlab');
|
|
9
9
|
const AIClient = require('../core/ai');
|
|
10
|
-
const {
|
|
11
|
-
AGENT_SYSTEM_PROMPT,
|
|
12
|
-
parseAgentResponse,
|
|
13
|
-
formatFileList,
|
|
10
|
+
const {
|
|
11
|
+
AGENT_SYSTEM_PROMPT,
|
|
12
|
+
parseAgentResponse,
|
|
13
|
+
formatFileList,
|
|
14
14
|
formatFileContents,
|
|
15
15
|
createUserMessage,
|
|
16
16
|
createAssistantMessage
|
|
17
17
|
} = require('../core/agent');
|
|
18
18
|
|
|
19
|
-
module.exports = async function(mrId, options) {
|
|
19
|
+
module.exports = async function (mrId, options) {
|
|
20
20
|
const config = storage.getAll();
|
|
21
21
|
if (!config.gitlabToken || !config.aiKey) {
|
|
22
22
|
logger.error('缺少必要的配置,请先运行 `hong-review login` 初始化配置。');
|
|
23
23
|
return;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
// 在真实场景中,项目 ID 通常从本地 git remote 推断或配置中读取。
|
|
28
|
-
// 为了 demo 的通用性,我们直接要求用户确认或配置 projectId。
|
|
29
|
-
let projectId = config.defaultProjectId;
|
|
26
|
+
let projectId = options.project;
|
|
30
27
|
if (!projectId) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
storage.set('defaultProjectId', projectId);
|
|
28
|
+
if (options.yes) {
|
|
29
|
+
logger.error('静默模式下未通过 -p 指定 Project ID,操作中止。');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const answer = await inquirer.prompt([{
|
|
33
|
+
type: 'input',
|
|
34
|
+
name: 'pid',
|
|
35
|
+
message: '请输入当前 Git 仓库对应的 GitLab Project ID:'
|
|
36
|
+
}]);
|
|
37
|
+
projectId = answer.pid;
|
|
42
38
|
}
|
|
43
39
|
|
|
44
40
|
const mrIid = parseInt(mrId);
|
|
@@ -48,7 +44,7 @@ module.exports = async function(mrId, options) {
|
|
|
48
44
|
}
|
|
49
45
|
|
|
50
46
|
logger.info(`开始准备审查 Project [${projectId}] 的 MR !${mrIid}`);
|
|
51
|
-
|
|
47
|
+
|
|
52
48
|
// 触发 Hook
|
|
53
49
|
await hooks.emit('onReviewStart', { projectId, mrId: mrIid });
|
|
54
50
|
|
|
@@ -57,6 +53,7 @@ module.exports = async function(mrId, options) {
|
|
|
57
53
|
|
|
58
54
|
try {
|
|
59
55
|
logger.startSpinner('fetching', '正在从 GitLab 拉取合并请求详情与代码变更...');
|
|
56
|
+
await hooks.emit('onReviewProgress', { projectId, mrId: mrIid, status: '拉取合并请求代码中' });
|
|
60
57
|
const detail = await gitlab.getMergeRequestDetail(projectId, mrIid);
|
|
61
58
|
const changes = await gitlab.getMergeRequestChanges(projectId, mrIid);
|
|
62
59
|
logger.stopSpinner('fetching', true, `成功拉取 MR [${detail.title}] 分析所需的 ${changes.length} 个文件变更。`);
|
|
@@ -68,7 +65,8 @@ module.exports = async function(mrId, options) {
|
|
|
68
65
|
}
|
|
69
66
|
|
|
70
67
|
if (!options.yes) logger.startSpinner('ai-thinking', `🤖 AI (${config.aiModel}) 正在深度分析中,请稍候...`);
|
|
71
|
-
|
|
68
|
+
await hooks.emit('onReviewProgress', { projectId, mrId: mrIid, status: 'AI 深度分析代码中' });
|
|
69
|
+
|
|
72
70
|
let reviewResult = null;
|
|
73
71
|
let round = 1;
|
|
74
72
|
const fileOverviews = GitLabClient.getFileOverview(changes);
|
|
@@ -78,6 +76,7 @@ module.exports = async function(mrId, options) {
|
|
|
78
76
|
// 开始 Agent 轮询
|
|
79
77
|
while (true) {
|
|
80
78
|
if (!options.yes) logger.startSpinner('ai-thinking', `🤖 AI 思考中 (第 ${round} 轮)...`);
|
|
79
|
+
await hooks.emit('onReviewProgress', { projectId, mrId: mrIid, status: `AI 思考中 (第 ${round} 轮)` });
|
|
81
80
|
const aiResponse = await ai.chatWithJsonMode(AGENT_SYSTEM_PROMPT, conversationHistory);
|
|
82
81
|
const action = parseAgentResponse(aiResponse);
|
|
83
82
|
|
|
@@ -88,11 +87,14 @@ module.exports = async function(mrId, options) {
|
|
|
88
87
|
conversationHistory.push({ role: 'assistant', content: aiResponse });
|
|
89
88
|
|
|
90
89
|
if (action.action === 'generate_report') {
|
|
90
|
+
await hooks.emit('onReviewProgress', { projectId, mrId: mrIid, status: 'AI 审查完成,正在生成报告' });
|
|
91
91
|
reviewResult = action.result;
|
|
92
92
|
break;
|
|
93
93
|
} else if (action.action === 'request_files') {
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
const reqFilesStr = action.files.join(', ');
|
|
95
|
+
if (!options.yes) logger.stopSpinner('ai-thinking', true, `AI 请求查看额外文件: ${reqFilesStr}`);
|
|
96
|
+
await hooks.emit('onReviewProgress', { projectId, mrId: mrIid, status: `AI 正在拉取依赖文件: ${reqFilesStr}` });
|
|
97
|
+
|
|
96
98
|
const contents = await fetchFileContents(gitlab, projectId, detail.source_branch, changes, action.files, action.contentTypes);
|
|
97
99
|
const fileContentMessage = formatFileContents(contents);
|
|
98
100
|
conversationHistory.push({ role: 'user', content: fileContentMessage });
|
|
@@ -101,7 +103,7 @@ module.exports = async function(mrId, options) {
|
|
|
101
103
|
}
|
|
102
104
|
|
|
103
105
|
if (!options.yes) logger.stopSpinner('ai-thinking', true, 'AI 审查完成!');
|
|
104
|
-
|
|
106
|
+
|
|
105
107
|
// 打印报告
|
|
106
108
|
printReport(mrIid, detail, reviewResult);
|
|
107
109
|
|
|
@@ -181,10 +183,10 @@ async function fetchFileContents(gitlabClient, projectId, ref, changes, requeste
|
|
|
181
183
|
contents.push({ path: filePath, contentType: 'full', content });
|
|
182
184
|
} else {
|
|
183
185
|
const fileChange = changes.find(f => f.new_path === filePath);
|
|
184
|
-
contents.push({
|
|
185
|
-
path: filePath,
|
|
186
|
-
contentType: 'diff',
|
|
187
|
-
content: fileChange ? fileChange.diff : '(文件未找到)'
|
|
186
|
+
contents.push({
|
|
187
|
+
path: filePath,
|
|
188
|
+
contentType: 'diff',
|
|
189
|
+
content: fileChange ? fileChange.diff : '(文件未找到)'
|
|
188
190
|
});
|
|
189
191
|
}
|
|
190
192
|
} catch (err) {
|
|
@@ -201,30 +203,30 @@ function printReport(mrIid, detail, result) {
|
|
|
201
203
|
|
|
202
204
|
let errorCount = 0;
|
|
203
205
|
let warningCount = 0;
|
|
204
|
-
|
|
206
|
+
|
|
205
207
|
console.log(chalk.bold('⚠️ 发现的问题:'));
|
|
206
208
|
if (!result.issues || result.issues.length === 0) {
|
|
207
|
-
|
|
209
|
+
console.log(chalk.green(' 没有任何问题,代码很棒!\n'));
|
|
208
210
|
} else {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
211
|
+
result.issues.forEach(issue => {
|
|
212
|
+
let prefix = '';
|
|
213
|
+
let colorMsg = issue.description;
|
|
214
|
+
if (issue.severity === 'error') {
|
|
215
|
+
prefix = chalk.bgRed.white(' ERROR ');
|
|
216
|
+
colorMsg = chalk.red(issue.description);
|
|
217
|
+
errorCount++;
|
|
218
|
+
} else if (issue.severity === 'warning') {
|
|
219
|
+
prefix = chalk.bgYellow.black(' WARN ');
|
|
220
|
+
colorMsg = chalk.yellow(issue.description);
|
|
221
|
+
warningCount++;
|
|
222
|
+
} else {
|
|
223
|
+
prefix = chalk.bgBlue.white(' INFO ');
|
|
224
|
+
colorMsg = chalk.blue(issue.description);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
console.log(`${prefix} ${chalk.underline(issue.file)}${issue.line ? ':' + issue.line : ''} - ${chalk.bold(issue.title)}`);
|
|
228
|
+
console.log(` 💡 ${colorMsg}\n`);
|
|
229
|
+
});
|
|
228
230
|
}
|
|
229
231
|
|
|
230
232
|
if (result.suggestions && result.suggestions.length > 0) {
|
|
@@ -244,7 +246,7 @@ function buildMarkdownReport(mrIid, detail, result) {
|
|
|
244
246
|
md += `**标题**: ${detail.title}\n`;
|
|
245
247
|
md += `**风险评级**: ${result.riskLevel.toUpperCase()}\n\n`;
|
|
246
248
|
md += `## 整体概述\n${result.summary}\n\n`;
|
|
247
|
-
|
|
249
|
+
|
|
248
250
|
if (result.issues && result.issues.length > 0) {
|
|
249
251
|
md += `## 发现的问题\n\n`;
|
|
250
252
|
result.issues.forEach(issue => {
|
|
@@ -257,8 +259,8 @@ function buildMarkdownReport(mrIid, detail, result) {
|
|
|
257
259
|
}
|
|
258
260
|
|
|
259
261
|
if (result.suggestions && result.suggestions.length > 0) {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
+
md += `## 改进建议\n`;
|
|
263
|
+
result.suggestions.forEach(s => md += `- ${s}\n`);
|
|
262
264
|
}
|
|
263
265
|
|
|
264
266
|
md += `\n> *本报告由 \`hong-review\` CLI 工具自动生成。*`;
|
|
@@ -24,11 +24,13 @@ module.exports = async function setupOpenClaw() {
|
|
|
24
24
|
|
|
25
25
|
const spinner = ora('正在写入 hooks 配置...').start();
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
const
|
|
27
|
+
// 使用 \" 保证 JSON 结构合法,同时允许 bash 解析其中的 $MR_ID 和 $REVIEW_STATUS
|
|
28
|
+
const generateCurl = (statusValue) => `curl -s -X POST ${answers.webhookUrl} -H "Authorization: Bearer ${answers.token}" -H "Content-Type: application/json" -d "{\\"mrId\\": \\"$MR_ID\\", \\"status\\": \\"${statusValue}\\"}" > /dev/null`;
|
|
29
29
|
|
|
30
|
-
configCmd.set('hooks.
|
|
31
|
-
configCmd.set('hooks.
|
|
30
|
+
configCmd.set('hooks.onReviewStart', generateCurl('Started'));
|
|
31
|
+
configCmd.set('hooks.onReviewProgress', generateCurl('$REVIEW_STATUS'));
|
|
32
|
+
configCmd.set('hooks.onReviewSuccess', generateCurl('Success'));
|
|
33
|
+
configCmd.set('hooks.onReviewFailed', generateCurl('Failed'));
|
|
32
34
|
|
|
33
|
-
spinner.succeed('配置完成!您已成功接入 OpenClaw
|
|
35
|
+
spinner.succeed('配置完成!您已成功接入 OpenClaw 路由,审查过程的所有动态都将自动进行广播推送。');
|
|
34
36
|
};
|
package/src/utils/hooks.js
CHANGED
|
@@ -5,25 +5,28 @@ const logger = require('./logger');
|
|
|
5
5
|
|
|
6
6
|
class Hooks {
|
|
7
7
|
constructor() {
|
|
8
|
-
this.
|
|
8
|
+
this.hookConfig = storage.get('hook') || '';
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
12
|
+
* 执行统一的生命周期 Hook,自动注入事件名
|
|
13
13
|
*/
|
|
14
14
|
async emit(eventName, payload = {}) {
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
// 每次执行时都实时拉取 config
|
|
16
|
+
this.hookConfig = storage.get('hook');
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
if (!this.hookConfig) return;
|
|
19
|
+
|
|
20
|
+
logger.info(`触发 Hook 事件: ${eventName}...`);
|
|
19
21
|
|
|
20
22
|
try {
|
|
21
|
-
if (
|
|
22
|
-
// 如果是 URL,执行 Webhook
|
|
23
|
-
|
|
23
|
+
if (this.hookConfig.startsWith('http')) {
|
|
24
|
+
// 如果是 URL,执行 Webhook 发送,把事件名称混入 payload
|
|
25
|
+
const hookPayload = { event: eventName, ...payload };
|
|
26
|
+
await this.executeWebhook(this.hookConfig, hookPayload);
|
|
24
27
|
} else {
|
|
25
28
|
// 否则认为是本地的 shell command
|
|
26
|
-
await this.executeShell(
|
|
29
|
+
await this.executeShell(this.hookConfig, eventName, payload);
|
|
27
30
|
}
|
|
28
31
|
logger.success(`Hook [${eventName}] 执行成功`);
|
|
29
32
|
} catch (err) {
|
|
@@ -35,17 +38,18 @@ class Hooks {
|
|
|
35
38
|
return axios.post(url, payload, { timeout: 5000 });
|
|
36
39
|
}
|
|
37
40
|
|
|
38
|
-
executeShell(command, payload) {
|
|
41
|
+
executeShell(command, eventName, payload) {
|
|
39
42
|
return new Promise((resolve, reject) => {
|
|
40
|
-
// 将 payload 转换为环境变量传入 shell
|
|
41
43
|
const env = { ...process.env };
|
|
42
|
-
|
|
43
|
-
//
|
|
44
|
+
|
|
45
|
+
// 注入通用环境变量
|
|
46
|
+
env['HONG_REVIEW_EVENT'] = eventName; // 将被触发的事件名通过独立环境变量输出
|
|
44
47
|
env['HONG_REVIEW_PAYLOAD'] = JSON.stringify(payload);
|
|
45
|
-
|
|
48
|
+
|
|
46
49
|
if (payload.mrId) env['MR_ID'] = payload.mrId;
|
|
47
50
|
if (payload.status) env['REVIEW_STATUS'] = payload.status;
|
|
48
|
-
|
|
51
|
+
if (payload.error) env['REVIEW_ERROR'] = payload.error;
|
|
52
|
+
|
|
49
53
|
exec(command, { env }, (error, stdout, stderr) => {
|
|
50
54
|
if (error) {
|
|
51
55
|
reject(error);
|
package/src/utils/storage.js
CHANGED
|
@@ -2,7 +2,8 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const os = require('os');
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
// 支持通过环境变量指定自定义配置文件,非常适合测试和调试
|
|
6
|
+
const CONFIG_FILE = process.env.HONG_REVIEW_CONFIG || path.join(os.homedir(), '.hong-review-config.json');
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* 缺省配置
|
|
@@ -13,12 +14,7 @@ const DEFAULT_CONFIG = {
|
|
|
13
14
|
aiKey: '',
|
|
14
15
|
aiBaseUrl: 'https://api.openai.com/v1',
|
|
15
16
|
aiModel: 'gpt-4o',
|
|
16
|
-
|
|
17
|
-
onReviewStart: '',
|
|
18
|
-
onReviewSuccess: '',
|
|
19
|
-
onReviewFailed: '',
|
|
20
|
-
onMerged: ''
|
|
21
|
-
}
|
|
17
|
+
hook: ''
|
|
22
18
|
};
|
|
23
19
|
|
|
24
20
|
class Storage {
|