hong-review-cli 1.0.6 → 1.0.8

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.
@@ -1,104 +1,165 @@
1
- # OpenClaw 接入指南: `hong-review-cli` 自动审查流
1
+ # OpenClaw 接入指南: `hong-review-cli` 自动审查流与 Webhook 对接
2
2
 
3
- 本指南将指导您如何配置您的 OpenClaw,使其能够理解、使用 `hong-review` 命令行工具,并将审查结果路由推送到您的 Telegram 聊天中。
3
+ 本指南将详细介绍如何配置您的 OpenClaw 网关与 `hong-review-cli`,使其能够自动接收代码审查的实时进度与结果报告,并推送至您的通讯软件(如 Telegram/Slack/微信 等)。
4
4
 
5
5
  ---
6
6
 
7
- ## 步骤 1:为 CLI 配置 Webhook 推送地址
7
+ ## 核心工作原理
8
8
 
9
- 利用 `hong-review` 提供的交互式配置命令,可以一键绑定 OpenClaw `hooks` 接口。
9
+ `hong-review-cli` 在执行合并请求(Merge Request)分析时,会在生命周期的各个阶段触发**本地 Hook 事件**(如:开始拉取、AI分析中、审查成功、审查失败)。
10
+ 通过本指南的配置,CLI 将会把这些事件转化为标准的 HTTP POST 请求,附带 **JSON Payload** 与 **Bearer Token 鉴权头**,精准投递到 OpenClaw 的 `/hooks` 路由网关。
10
11
 
11
- 在您的终端环境中运行以下命令,并按提示输入您的 Webhook 地址和 Token:
12
+ 最后由 OpenClaw `Mappings` 系统,将收到的 JSON 变量插值到消息模板中,唤醒指定的 Agent 发送给您的聊天组。
13
+
14
+ ---
15
+
16
+ ## 阶段一:安装与配置 `hong-review-cli`
17
+
18
+ ### 1. 安装 CLI 工具
19
+ 在您的执行环境(开发机、服务器或 CI/CD Runner)中,拉取最新代码并全局安装:
20
+
21
+ ```bash
22
+ cd /path/to/cli
23
+ npm install -g .
24
+ ```
25
+
26
+ 检查是否安装成功:
27
+ ```bash
28
+ hong-review --version
29
+ ```
30
+
31
+ ### 2. 交互式初始化
32
+ 执行以下命令完成核心授权信息(GitLab & AI 模型)绑定:
33
+
34
+ ```bash
35
+ hong-review login
36
+ ```
37
+ 根据向导提示填入:GitLab 域名、Private Token、AI 大模型 API Key 等。
38
+
39
+ ### 3. (核心) 一键配置 OpenClaw 路由对接
40
+ 执行以下命令,告诉 CLI 应该把状态报告发给谁:
12
41
 
13
42
  ```bash
14
43
  hong-review setup-openclaw
15
44
  ```
16
45
 
17
- ## 步骤 2:配置 OpenClaw 的路由文件 (Hooks Mapping)
46
+ 您将会看到如下交互:
47
+ 1. **请输入 OpenClaw Webhook 地址**: 填写您的 OpenClaw 外网或内网地址(如 `http://127.0.0.1:18790/hooks/code-review`)。**注意:路径中的 `code-review` 必须与后续 OpenClaw Mappings 中的 `path` 一致。**
48
+ 2. **请输入 OpenClaw 授权 Token**: 填写您 OpenClaw 中配置的 `hooks.token` 的值,CLI 会自动在每次发送网络请求时附加 `Authorization: Bearer <Token>`。
49
+
50
+ > **提示:** 您也可以随时通过 `hong-review config list` 确认上述配置,或直接修改本地的配置 JSON 文件(如 `.hong-review-config.json` 或 `test-config.json`)。包含的字段应当有 `"hook": "http://..."` 与 `"hookToken": "YOUR_TOKEN"`。
51
+
52
+ ---
53
+
54
+ ## 阶段二:配置 OpenClaw 网关 (Hooks Mapping)
18
55
 
19
- 在您的 OpenClaw 配置文件 (`openclaw.yaml` 或 [.json](file:///Users/hong/Desktop/cli/package.json)) 中,找到 `hooks` 配置块。你需要新增一条映射规则,让 OpenClaw 知道收到 `code-review` 路径的请求时,发送什么消息给 Telegram。
56
+ 现在,CLI 已经有能力发送标准格式的 HTTP 请求了,我们需要教 OpenClaw 如何“听懂”这些请求。
20
57
 
21
- **针对您当前 JSON 配置的示例配置:**
58
+ 编辑您的 OpenClaw 主配置文件(通常为 `openclaw.yaml` 或 `openclaw.json`),找到或新建 `hooks` 块区块。
22
59
 
23
- 您可以直接将以下 `hooks` 代码片段复制替换掉您当前配置文件里的 `hooks` 部分。已经为您配置好了正确的 `tech-group` 发送代理目标和 Telegram 群组 ID:
60
+ ### 1. 开启 Hooks 网关与鉴权 Token
61
+ 确保 `hooks` 根节点中启用了验证 `token`,这个 Token 必须与刚刚 CLI 中填写的完全一致。
24
62
 
25
63
  ```json
26
64
  "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
- },
65
+ "token": "你的_OPENCLAW_WEBHOOK_安全_TOKEN"
66
+ }
67
+ ```
68
+
69
+ ### 2. 编写路由映射 (Mappings)
70
+ `hooks.mappings` 数组中添加一条专门针对 `hong-review-cli` 的解析规则:
71
+
72
+ ```json
73
+ "hooks": {
74
+ "token": "你的_OPENCLAW_WEBHOOK_安全_TOKEN",
39
75
  "mappings": [
40
76
  {
41
77
  "id": "hong-review-notification",
42
78
  "match": {
43
- "path": "code-review"
79
+ "path": "code-review"
44
80
  },
45
81
  "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*(日志及报告已由系统分析进程执行完毕)*"
82
+ "agentId": "tech-group",
83
+ "channel": "telegram",
84
+ "to": "-100123456789",
85
+ "messageTemplate": "🤖 **代码审查通知 ({{payload.status}})**\n- **阶段**: `{{payload.event}}`\n- **目标 MR**: !{{payload.mrId}}\n\n*您现在可以命令我:“去看看 {{payload.mrId}} 具体错在哪”,我会立刻为您执行底层查询。*"
51
86
  }
52
87
  ]
53
88
  }
54
89
  ```
55
- *(这段配置使用了 OpenClaw 原生的 [src/gateway/hooks-mapping.ts](file:///tmp/openclaw/src/gateway/hooks-mapping.ts) 解析能力,它允许插值获取 Payload 里的变量并组合成文本推出去)*
90
+
91
+ #### 模板变量说明 (`{{payload.xxx}}`)
92
+ 最新的 `hong-review-cli` 内核不仅会发送基本状态,还会**将 AI 的每一次内心思考过程和动作**作为上下文暴露出来。以下是您可以在 `messageTemplate` 中自由插值、组合排版的字段:
93
+
94
+ * `{{payload.event}}`:当前生命周期事件名。例如:
95
+ - `onReviewStart`, `onReviewSuccess`, `onReviewFailed`(基础阶段)
96
+ - `onReviewCLIContext`(CLI 向 AI 提供代码大纲上下文)
97
+ - `onReviewAgentAction`(AI 觉得信息不够,发出索要特定代码细节的请求决策)
98
+ - `onReviewCLIResponse`(CLI 代替用户向 AI 提交代码详细差异详情)
99
+ * `{{payload.status}}`:当前执行状态或动作的可读中文简述。如 `Started`, `AI 要求查阅代码细节`, `已向 AI 补充 3 个文件内容`。
100
+ * `{{payload.mrId}}`:当前正在审查的 GitLab Merge Request 的 ID 编号。
101
+ * `{{payload.reasoning}}`:*(仅 `onReviewAgentAction` 时存在)* AI 思考后给出的理由,比如“我想看 Utils 的代码看有没有泄漏 Token”。
102
+ * `{{payload.requestedFiles}}` / `{{payload.providedFiles}}`:动作中涉及的具体涉及代码文件路径数组。
103
+ * `{{payload.error}}`:*(仅 `onReviewFailed` 时存在)* 具体的报错和异常中止原因。
56
104
 
57
105
  ---
58
106
 
59
- ## 步骤 3:赋予 OpenClaw "调用能力" (Tool / System Prompt)
107
+ ## 阶段三:赋予 OpenClaw 操控 CLI 的权利 (System Prompt)
60
108
 
61
- 最后也是最关键的,你需要把下面这段文字,作为插件系统描述或者**核心系统提示词 (System Prompt)** 告诉管理你 Telegram 消息的 OpenClaw 机器人。
109
+ 前两个阶段解决了 **CLI 单向通知 OpenClaw** 的闭环,而第三阶段将赋予 **OpenClaw (机器人或大语言模型) 操控 CLI 并向您对话** 的能力。
62
110
 
63
- ### 👉 给 OpenClaw 机器人的 System Schema 设定:
111
+ 在您的群聊 Agent(示例配置中的 `tech-group`)系统提示词(System Prompt)中,加入以下指令规则:
64
112
 
65
113
  ```markdown
114
+ # 核心职责
66
115
  你是一个全能的代码审查助手。你现在被授权在宿主服务器上调用一个名为 `hong-review` 的终端核心程序来处理用户的 GitLab 审查请求。
67
116
 
68
- **重要规则**:
69
- 因为你处于非人机交互环境,你每次调用该程序时,**必须加上 `-y` 或 `--yes` 参数**,这代表启用静默安全模式。
70
- 程序在执行结束或出错后会自动处理,**你永远不要尝试去读取它的标准输出日志来提取长篇大论的报告**,程序的内建 Hooks 系统会自己将带有排版的最终结果推送到 Telegram,你要做的这只是触发它即可。
71
-
72
- **你可以使用它来回复用户的各种核心意图(请务必携带对应的命令和必要参数):**
117
+ **极其重要规则**:
118
+ 因为你处于非人机交互的纯命令环境,你每次调用该程序时,**必须加上 `-y` 或 `--yes` 参数**,这代表启用静默安全模式并自动跳过阻塞询问。
119
+ 如果程序报错或要求交互,说明你未加参数。你永远不要尝试去读取它的标准完整输出报告并发到聊天框刷屏,终端内建的 Webhook 系统会自己分阶段把精炼状态报告推送到 Telegram 群,你需要做的只是作为触发器替用户启动它。
73
120
 
74
- 1. 审查和分析具体合并请求:
75
- 如果用户说“去帮我分析一下 101 这个 MR” 或 “查下 101”
76
- 请在底层执行命令:`hong-review mr 101 -y`
121
+ ### 你的底层武器库:
122
+ 当用户产生以下意图时,直接在底层按格式执行 Shell 命令回复他们。请提取用户提到的话题编号 `<ID>`(通常是一串数字):
77
123
 
78
- 2. 查看待办列表:
79
- 如果用户问“我的待办列表里有什么 MR?”或“列出待审查代码”
80
- 请在底层执行命令:`hong-review ls -y`
124
+ 1. **全面审查某个 MR**
125
+ - 触发话术样例:“去帮我分析一下 101 这个 MR” 或 “查下 101”
126
+ - 执行底包名:`hong-review mr <ID> -y`
127
+
128
+ 2. **快速合并安全代码**
129
+ - 触发话术样例:“这代码没问题,确认合并 101 吧”
130
+ - 执行底包名:`hong-review merge <ID> -y`
81
131
 
82
- 3. 留言与评论:
83
- 如果用户想给 MR 直接留言,例如“给 101 说这个写的很棒”
84
- 请在底层执行命令:`hong-review comment 101 "这个写的很棒"`
132
+ 3. **代写评论与表扬**
133
+ - 触发话术样例:“给 101 留言点赞,就跟他说逻辑写得很棒”
134
+ - 执行底包名:`hong-review comment <ID> "逻辑写得很棒!" -y`
85
135
 
86
- 4. 自动代码合并:
87
- 如果用户在代码分析并确保安全后说“确认合并 101 吧”
88
- 请在底层执行命令:`hong-review merge 101 -y`
136
+ 4. **查看待办池**
137
+ - 触发话术样例:“目前积压了什么 MR ?”或“看下待审查代码”
138
+ - 执行底包名:`hong-review ls -y`
89
139
 
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`
140
+ 5. **查询执行日志 (排错/打点记录)**
141
+ - 触发话术样例:“查一下 CLI 的最新日志,刚才 webhook 没发出来”
142
+ - 工具行为准则:直接读取本地日志文件。日志文件夹路径获取方式:先执行 `node -e "console.log(require('/path/to/cli/src/utils/storage.js').get('logDir'))"` 获取配置目录(若为空则默认是在 `~/.hong-review-logs/`)。找到最新的 `hong-review-YYYY-MM-DD.log`,并 `tail -n 100` 或者 `cat` 读取并提炼关键错误信息回复给用户。
100
143
  ```
101
144
 
145
+ > **进阶设置:修改默认日志输出目录**
146
+ > 默认情况下,每日轮转的追踪日志(包含 Webhook 与生命周期全过程)存储在 `~/.hong-review-logs` 文件夹下。您可以在机器上使用 `hong-review config set logDir "/您的/自定义/绝对路径"` 来集中管理追踪内容。
147
+
102
148
  ---
103
149
 
104
- 完成以上三步,当您在 Telegram App 打开 OpenClaw 聊天窗口发送:"去查下 52 那个代码提交" 后。一切都会像齿轮一样无人值守转动,并在几秒或几十秒后把叮咚声推送到你的手机里!
150
+ ## 验证与测试联调
151
+
152
+ 完成所有部署重启 OpenClaw 后,您可以打开即时通讯软件跟您的 Bot 说一句:
153
+
154
+ > **您:** “老哥,帮我看下 74 号合并请求。”
155
+
156
+ 此时如果观察终端,应该是发生以下事情:
157
+ 1. OpenClaw 明白您的意图,触发本地命令:`hong-review mr 74 -y`。
158
+ 2. `hong-review` 进程启动,发来第一条 Webhook 进度推送。
159
+ 3. 您在通讯软件马上收到: `🤖 代码审查通知 (Started) - 阶段: onReviewStart`。
160
+ 4. CLI 抓取代码,并发来下一条推送。
161
+ 5. 通讯软件更新进度:`🤖 代码审查通知 (拉取合并请求代码中)`。
162
+ 6. CLI 经过一段时间的 AI 思考,最终审查结束。由于代码合规,发出结束信标。
163
+ 7. 通讯软件收到最后通报:`🤖 代码审查通知 (Success) - 阶段: onReviewSuccess`。
164
+
165
+ 祝您的 AI Code Review 工作流搭建顺利!
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hong-review-cli",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "main": "index.js",
5
5
  "bin": {
6
6
  "hong-review": "bin/hong-review.js"
@@ -25,6 +25,8 @@
25
25
  "commander": "^14.0.3",
26
26
  "figlet": "^1.10.0",
27
27
  "inquirer": "^8.2.7",
28
- "ora": "^5.4.1"
28
+ "ora": "^5.4.1",
29
+ "winston": "^3.19.0",
30
+ "winston-daily-rotate-file": "^5.0.0"
29
31
  }
30
32
  }
@@ -71,12 +71,22 @@ module.exports = async function (mrId, options) {
71
71
  let round = 1;
72
72
  const fileOverviews = GitLabClient.getFileOverview(changes);
73
73
  const initialMessage = formatFileList(fileOverviews);
74
+
75
+ // 初始上下文提供给 AI
76
+ await hooks.emit('onReviewCLIContext', {
77
+ projectId,
78
+ mrId: mrIid,
79
+ status: '已向 AI 递交初始的文件变更大纲',
80
+ contextFiles: fileOverviews.map(f => f.path)
81
+ });
82
+
74
83
  const conversationHistory = [{ role: 'user', content: initialMessage }];
75
84
 
76
85
  // 开始 Agent 轮询
77
86
  while (true) {
78
87
  if (!options.yes) logger.startSpinner('ai-thinking', `🤖 AI 思考中 (第 ${round} 轮)...`);
79
88
  await hooks.emit('onReviewProgress', { projectId, mrId: mrIid, status: `AI 思考中 (第 ${round} 轮)` });
89
+
80
90
  const aiResponse = await ai.chatWithJsonMode(AGENT_SYSTEM_PROMPT, conversationHistory);
81
91
  const action = parseAgentResponse(aiResponse);
82
92
 
@@ -87,17 +97,34 @@ module.exports = async function (mrId, options) {
87
97
  conversationHistory.push({ role: 'assistant', content: aiResponse });
88
98
 
89
99
  if (action.action === 'generate_report') {
90
- await hooks.emit('onReviewProgress', { projectId, mrId: mrIid, status: 'AI 审查完成,正在生成报告' });
100
+ await hooks.emit('onReviewProgress', { projectId, mrId: mrIid, status: 'AI 审查完成,正在生成最终报告' });
91
101
  reviewResult = action.result;
92
102
  break;
93
103
  } else if (action.action === 'request_files') {
94
104
  const reqFilesStr = action.files.join(', ');
95
105
  if (!options.yes) logger.stopSpinner('ai-thinking', true, `AI 请求查看额外文件: ${reqFilesStr}`);
96
- await hooks.emit('onReviewProgress', { projectId, mrId: mrIid, status: `AI 正在拉取依赖文件: ${reqFilesStr}` });
106
+
107
+ // 将 AI 想要看什么的决策和原因实时推送到 Webhook
108
+ await hooks.emit('onReviewAgentAction', {
109
+ projectId,
110
+ mrId: mrIid,
111
+ status: 'AI 要求查阅代码细节',
112
+ reasoning: action.reasoning,
113
+ requestedFiles: action.files
114
+ });
97
115
 
98
116
  const contents = await fetchFileContents(gitlab, projectId, detail.source_branch, changes, action.files, action.contentTypes);
99
117
  const fileContentMessage = formatFileContents(contents);
100
118
  conversationHistory.push({ role: 'user', content: fileContentMessage });
119
+
120
+ // 将 CLI 返回给 AI 的动作也推送到 Webhook
121
+ await hooks.emit('onReviewCLIResponse', {
122
+ projectId,
123
+ mrId: mrIid,
124
+ status: `已向 AI 补充 ${action.files.length} 个文件内容`,
125
+ providedFiles: action.files
126
+ });
127
+
101
128
  round++;
102
129
  }
103
130
  }
@@ -24,13 +24,8 @@ module.exports = async function setupOpenClaw() {
24
24
 
25
25
  const spinner = ora('正在写入 hooks 配置...').start();
26
26
 
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
-
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'));
27
+ configCmd.set('hook', answers.webhookUrl);
28
+ configCmd.set('hookToken', answers.token);
34
29
 
35
30
  spinner.succeed('配置完成!您已成功接入 OpenClaw 路由,审查过程的所有动态都将自动进行广播推送。');
36
31
  };
@@ -6,6 +6,7 @@ const logger = require('./logger');
6
6
  class Hooks {
7
7
  constructor() {
8
8
  this.hookConfig = storage.get('hook') || '';
9
+ this.hookToken = storage.get('hookToken') || '';
9
10
  }
10
11
 
11
12
  /**
@@ -13,29 +14,53 @@ class Hooks {
13
14
  */
14
15
  async emit(eventName, payload = {}) {
15
16
  // 每次执行时都实时拉取 config
16
- this.hookConfig = storage.get('hook');
17
+ this.hookConfig = storage.get('hook') || '';
18
+ this.hookToken = storage.get('hookToken') || '';
19
+
20
+ // 如果配置了具体的 HTTP Webhook (比如由 setup-openclaw 设置)
21
+ if (this.hookConfig && this.hookConfig.startsWith('http')) {
22
+ try {
23
+ // 将状态和特定字段从 payload 中拉平,用于 OpenClaw 的 payload 提取解析
24
+ const statusStr = payload.status || (eventName === 'onReviewStart' ? 'Started' : (eventName === 'onReviewSuccess' ? 'Success' : (eventName === 'onReviewFailed' ? 'Failed' : eventName)));
25
+
26
+ const hookPayload = {
27
+ event: eventName,
28
+ mrId: payload.mrId || '',
29
+ status: statusStr,
30
+ ...payload
31
+ };
17
32
 
18
- if (!this.hookConfig) return;
33
+ logger.info(`开始推送 Webhook [${eventName}] 到 ${this.hookConfig}`);
34
+ const response = await this.executeWebhook(this.hookConfig, hookPayload, this.hookToken);
35
+ logger.success(`Webhook 推送完成 [${eventName}] HTTP状态: ${response.status}`);
36
+ } catch (err) {
37
+ // 如果是用户配置的静态 URL,失败不中断主流程,仅打个 Warn
38
+ logger.warn(`Webhook 推送失败 [${eventName}]: ${err.message}`);
39
+ }
40
+ return;
41
+ }
19
42
 
20
- logger.info(`触发 Hook 事件: ${eventName}...`);
43
+ // 以往版本中,可能会配置一个指定的 shell command 或其它字段... 但目前主要是为了 openclaw 设计
44
+ const specificCommandHook = storage.get(`hooks.${eventName}`);
45
+ const commandToRun = specificCommandHook || this.hookConfig;
21
46
 
22
- try {
23
- if (this.hookConfig.startsWith('http')) {
24
- // 如果是 URL,执行 Webhook 发送,把事件名称混入 payload
25
- const hookPayload = { event: eventName, ...payload };
26
- await this.executeWebhook(this.hookConfig, hookPayload);
27
- } else {
28
- // 否则认为是本地的 shell command
29
- await this.executeShell(this.hookConfig, eventName, payload);
47
+ if (commandToRun && !commandToRun.startsWith('http')) {
48
+ logger.info(`触发本地 Hook: ${eventName}...`);
49
+ try {
50
+ await this.executeShell(commandToRun, eventName, payload);
51
+ logger.success(`本地 Hook [${eventName}] 执行成功`);
52
+ } catch (err) {
53
+ logger.error(`本地 Hook [${eventName}] 执行失败: ${err.message}`);
30
54
  }
31
- logger.success(`Hook [${eventName}] 执行成功`);
32
- } catch (err) {
33
- logger.error(`Hook [${eventName}] 执行失败: ${err.message}`);
34
55
  }
35
56
  }
36
57
 
37
- executeWebhook(url, payload) {
38
- return axios.post(url, payload, { timeout: 5000 });
58
+ executeWebhook(url, payload, token) {
59
+ const headers = { 'Content-Type': 'application/json' };
60
+ if (token) {
61
+ headers['Authorization'] = `Bearer ${token}`; // OpenClaw 要求的权限头
62
+ }
63
+ return axios.post(url, payload, { headers, timeout: 5000 });
39
64
  }
40
65
 
41
66
  executeShell(command, eventName, payload) {
@@ -46,8 +71,9 @@ class Hooks {
46
71
  env['HONG_REVIEW_EVENT'] = eventName; // 将被触发的事件名通过独立环境变量输出
47
72
  env['HONG_REVIEW_PAYLOAD'] = JSON.stringify(payload);
48
73
 
49
- if (payload.mrId) env['MR_ID'] = payload.mrId;
50
- if (payload.status) env['REVIEW_STATUS'] = payload.status;
74
+ if (payload.mrId) env['MR_ID'] = String(payload.mrId);
75
+ const statusStr = payload.status || (eventName === 'onReviewStart' ? 'Started' : (eventName === 'onReviewSuccess' ? 'Success' : (eventName === 'onReviewFailed' ? 'Failed' : eventName)));
76
+ env['REVIEW_STATUS'] = statusStr;
51
77
  if (payload.error) env['REVIEW_ERROR'] = payload.error;
52
78
 
53
79
  exec(command, { env }, (error, stdout, stderr) => {
@@ -1,51 +1,102 @@
1
1
  const chalk = require('chalk');
2
2
  const ora = require('ora');
3
+ const winston = require('winston');
4
+ const DailyRotateFile = require('winston-daily-rotate-file');
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const os = require('os');
8
+ const storage = require('./storage');
3
9
 
4
10
  const spinners = new Map();
5
11
 
6
12
  class Logger {
13
+ constructor() {
14
+ this.initWinston();
15
+ }
16
+
17
+ initWinston() {
18
+ // Find log directory setting or default to ~/.hong-review-logs
19
+ const configLogDir = storage.get('logDir');
20
+ let defaultLogDir = '';
21
+ if (configLogDir) {
22
+ defaultLogDir = configLogDir;
23
+ } else {
24
+ defaultLogDir = path.join(os.homedir(), '.hong-review-logs');
25
+ }
26
+
27
+ if (!fs.existsSync(defaultLogDir)) {
28
+ fs.mkdirSync(defaultLogDir, { recursive: true });
29
+ }
30
+
31
+ this.fileLogger = winston.createLogger({
32
+ level: 'info',
33
+ format: winston.format.combine(
34
+ winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
35
+ winston.format.printf(info => `[${info.timestamp}] ${info.level.toUpperCase()}: ${info.message}`)
36
+ ),
37
+ transports: [
38
+ new DailyRotateFile({
39
+ dirname: defaultLogDir,
40
+ filename: 'hong-review-%DATE%.log',
41
+ datePattern: 'YYYY-MM-DD',
42
+ maxFiles: '14d'
43
+ })
44
+ ]
45
+ });
46
+ }
47
+
7
48
  info(msg) {
8
49
  console.log(chalk.blue('ℹ ' + msg));
50
+ this.fileLogger.info(msg);
9
51
  }
10
-
52
+
11
53
  success(msg) {
12
54
  console.log(chalk.green('✔ ' + msg));
55
+ this.fileLogger.info(msg);
13
56
  }
14
-
57
+
15
58
  warn(msg) {
16
59
  console.log(chalk.yellow('⚠ ' + msg));
60
+ this.fileLogger.warn(msg);
17
61
  }
18
-
62
+
19
63
  error(msg) {
20
64
  console.log(chalk.red('✖ ' + msg));
65
+ this.fileLogger.error(msg);
21
66
  }
22
-
67
+
23
68
  startSpinner(id, text) {
24
69
  if (spinners.has(id)) {
25
70
  spinners.get(id).stop();
26
71
  }
27
72
  const spinner = ora(text).start();
28
73
  spinners.set(id, spinner);
74
+ this.fileLogger.info(`(Started) ${text}`);
29
75
  }
30
-
76
+
31
77
  stopSpinner(id, success = true, text = '') {
32
78
  const spinner = spinners.get(id);
33
79
  if (!spinner) return;
34
-
80
+
81
+ const finalMsg = text || spinner.text;
35
82
  if (success) {
36
- spinner.succeed(text || spinner.text);
83
+ spinner.succeed(finalMsg);
84
+ this.fileLogger.info(`(Completed) ${finalMsg}`);
37
85
  } else {
38
- spinner.fail(text || spinner.text);
86
+ spinner.fail(finalMsg);
87
+ this.fileLogger.error(`(Failed) ${finalMsg}`);
39
88
  }
40
89
  spinners.delete(id);
41
90
  }
42
91
 
43
92
  logRaw(msg) {
44
93
  console.log(msg);
94
+ this.fileLogger.info(msg);
45
95
  }
46
-
96
+
47
97
  divider() {
48
98
  console.log(chalk.gray('-'.repeat(50)));
99
+ this.fileLogger.info('-'.repeat(50));
49
100
  }
50
101
  }
51
102
 
@@ -1,107 +0,0 @@
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 DELETED
@@ -1,299 +0,0 @@
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
- }