@vrs-soft/wecom-aibot-mcp 2.4.11 → 2.4.16

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 CHANGED
@@ -1,149 +1,201 @@
1
- # @vrs-soft/wecom-aibot-mcp
1
+ # wecom-aibot-mcp
2
2
 
3
- 中文 | [English](README_EN.md)
3
+ English | [中文文档](README_ZH.md)
4
4
 
5
- 企业微信智能机器人 MCP 服务 - Claude Code 通过微信远程审批和交互。
5
+ Enterprise WeChat AI Bot MCP Service - Remote Approval Channel for Claude Code
6
6
 
7
- **核心功能**:
8
- - 远程审批敏感操作(Bash/Write/Edit),微信卡片一键通过/拒绝
9
- - 离开电脑后通过微信下达任务,实时接收进度通知
10
- - 支持群聊 @机器人,多机器人、多用户并发
11
- - 代理企业微信文档 MCP,支持文档和智能表格操作
7
+ > Handle Claude Code approval requests via WeChat, even when away from your computer.
12
8
 
13
- ---
9
+ ## Features
14
10
 
15
- ## 效果预览
11
+ - 🔐 **Remote Approval**: Approve/deny sensitive operations (Bash/Write/Edit) via WeChat cards
12
+ - 🔍 **Full Command View**: Approval cards include a detail link — open in browser to view the complete command
13
+ - 💬 **Bidirectional Communication**: Real-time task progress notifications
14
+ - 📱 **Headless Mode**: Switch to WeChat interaction when leaving terminal
15
+ - 🤖 **Multi-bot Support**: Multiple bots for team and group chat scenarios
16
+ - 🌐 **Remote Deployment**: MCP server can be deployed on a remote host with Bearer Token auth
17
+ - 🔄 **Auto-reconnect**: Channel mode automatically reconnects after network interruption or server restart
16
18
 
17
- <img src="docs/approval-card.png" width="320" alt="微信审批卡片" />
19
+ ## Architecture
18
20
 
19
- 每次 Claude 执行敏感操作(Bash 命令、编辑文件等)时,企业微信会推送审批卡片,点击**允许**或**拒绝**即可实时控制执行权限。超时未响应时,根据配置自动代批(允许项目内操作,拒绝删除命令)。
21
+ ```
22
+ ┌─────────────────┐ MCP (stdio) ┌──────────────────────┐
23
+ │ Claude Code │ ──────────────────▶ │ Channel MCP Proxy │
24
+ │ (MCP Client) │ ◀────────────────── │ (local, SSE client) │
25
+ └─────────────────┘ └──────────────────────┘
26
+ │ SSE
27
+
28
+ ┌─────────────────────┐
29
+ │ wecom-aibot-mcp │
30
+ │ HTTP MCP Server │
31
+ │ (local or remote) │
32
+ └─────────────────────┘
33
+
34
+ WebSocket Connection
35
+
36
+ ┌─────────────────────┐
37
+ │ Enterprise WeChat │
38
+ └─────────────────────┘
39
+
40
+
41
+ ┌─────────────────────┐
42
+ │ User WeChat Client │
43
+ │ (Mobile/Desktop) │
44
+ └─────────────────────┘
45
+ ```
20
46
 
21
- ---
47
+ ## Installation
22
48
 
23
- ## 使用场景
49
+ ### Prerequisites
24
50
 
25
- ### 场景一:离开电脑,微信远程监督
51
+ - **Node.js >= 18**
52
+ - Enterprise WeChat account (with bot creation permission)
53
+ - Claude Code
26
54
 
27
- 出门开会或离开座位时,告诉 Claude「现在开始通过微信联系」,进入微信模式后:
55
+ ### Step 1: Create Enterprise WeChat Bot
28
56
 
29
- - Claude 执行每一步操作前发送审批请求到微信
30
- - 你在手机上点击**允许 / 拒绝**,Claude 实时响应
31
- - 设置超时自动审批(`autoApproveTimeout`),无人值守时自动处理项目内操作
57
+ 1. Login to WeChat Work admin portal: https://work.weixin.qq.com
58
+ 2. Go to "Management Tools" → "Smart Bot"
59
+ 3. Click "Create Bot" → "Manual Creation"
60
+ 4. Fill in bot name (e.g., "Claude Approval Assistant")
61
+ 5. In "API Configuration":
62
+ - Select "Use Long Connection"
63
+ - Click "Get Secret"
64
+ 6. Record **Bot ID** and **Secret**
32
65
 
33
- ### 场景二:微信直接下达任务
66
+ ### Step 2: Run Configuration Wizard
34
67
 
35
- 不在电脑前,直接在企业微信给机器人发消息:
68
+ ```bash
69
+ npx @vrs-soft/wecom-aibot-mcp --setup
70
+ ```
36
71
 
37
- - 「帮我跑一下单元测试,把结果发给我」
38
- - 「把 src/index.ts 里的 TODO 都处理掉」
39
- - 「最近有什么错误日志?」
72
+ Choose the appropriate role flag:
40
73
 
41
- Claude 执行完成后自动回复进度和结果到微信。
74
+ | Command | Role | Description |
75
+ |---------|------|-------------|
76
+ | `--setup` | Interactive | Guides through local or remote setup |
77
+ | `--setup --server` | Server-side | Configure bot + Token, no local MCP config |
78
+ | `--setup --channel` | Channel client | Connect to remote server, write Channel MCP config |
79
+ | `--setup --server --channel` | Full local | HTTP + Channel full install |
42
80
 
43
- ### 场景三:团队共享机器人,群聊协作
81
+ **Start server after setup**:
44
82
 
45
- 在企业微信群中 @机器人,多个成员可以同时:
83
+ ```bash
84
+ npx @vrs-soft/wecom-aibot-mcp --http-only --start
85
+ ```
46
86
 
47
- - 查询项目状态
48
- - 触发 CI 任务
49
- - 审批自己负责的操作(审批请求精确路由到对应 Claude 窗口)
87
+ ## Commands
50
88
 
51
- ---
89
+ | Command | Description |
90
+ |---------|-------------|
91
+ | `--start / --stop` | Start/stop background service |
92
+ | `--status` | View service status and bot list |
93
+ | `--config` | Modify default bot configuration |
94
+ | `--add / --delete` | Add/delete bot |
95
+ | `--set-token [token]` | Set Auth Token (for remote deployment) |
96
+ | `--set-token --clear` | Clear Auth Token |
97
+ | `--debug` | Start in foreground with debug output |
98
+ | `--http-only` | Start HTTP MCP Server only (server-side use) |
99
+ | `--channel-only` | Configure Channel MCP only (requires `MCP_URL`) |
100
+ | `--clean-cache` | Clear CC registry cache |
101
+ | `--upgrade` | Force upgrade global configs |
102
+ | `--uninstall` | Complete uninstall |
52
103
 
53
- ## 前置条件
104
+ ## Run Modes
54
105
 
55
- 企业微信管理后台创建智能机器人,连接方式选「使用长连接」,记录 **Bot ID** **Secret** 以及 **DocURL**(文档url)。
106
+ | | Channel Mode | HTTP Mode |
107
+ |-|-------------|-----------|
108
+ | Message delivery | SSE push (instant) | `/loop` heartbeat polling |
109
+ | Latency | Immediate | ≤1 minute |
110
+ | Claude account | claude.ai direct only | Any (including API relay) |
111
+ | Reconnect | Auto (including server restart) | Auto via heartbeat |
56
112
 
57
- ---
113
+ To enter WeChat mode, tell Claude: **"Now contact me via WeChat"** — this triggers the `headless-mode` skill automatically.
58
114
 
59
- ## 安装
115
+ **Claude startup command for Channel mode**:
60
116
 
61
117
  ```bash
62
- npx @vrs-soft/wecom-aibot-mcp --setup
118
+ claude --dangerously-load-development-channels server:wecom-aibot-channel
63
119
  ```
64
120
 
65
- 根据部署角色选择参数:
66
-
67
- | 命令 | 角色 | 说明 |
68
- |------|------|------|
69
- | `--setup` | 交互式 | 询问本地 / 远程,自动引导 |
70
- | `--setup --server` | 服务器端 | 配置机器人 + Token,不写本机 MCP 配置 |
71
- | `--setup --channel` | Channel 客户端 | 连接远程 Server,写入 Channel MCP |
72
- | `--setup --server --channel` | 本地完整 | HTTP + Channel 全安装 |
121
+ ## Usage Example
73
122
 
74
- **Server 端安装后启动**:
123
+ ### Headless Mode (Remote Approval)
75
124
 
76
- ```bash
77
- npx @vrs-soft/wecom-aibot-mcp --http-only --start
78
125
  ```
126
+ You: Now contact me via WeChat
79
127
 
80
- **后台启动 / 停止(本地或 Server 端)**:
81
-
82
- ```bash
83
- npx @vrs-soft/wecom-aibot-mcp --start # 后台启动
84
- npx @vrs-soft/wecom-aibot-mcp --stop # 停止
85
- ```
128
+ Claude: Entered WeChat mode. All interactions will go through Enterprise WeChat.
86
129
 
87
- ---
130
+ [You leave the computer. Claude needs to run a command.]
88
131
 
89
- ## 运行模式对比
132
+ WeChat receives approval card:
133
+ ┌──────────────────────────────┐
134
+ │ 【Pending Approval】Bash │
135
+ │ Command: npm run build... │
136
+ │ 📋 TaskID: approval_xxx │
137
+ │ [Allow Once] [Default] [Deny]│
138
+ │ Details: View full command │
139
+ └──────────────────────────────┘
90
140
 
91
- | | Channel 模式 | HTTP 模式 |
92
- |-|-------------|----------|
93
- | 消息接收 | SSE 自动推送唤醒 | `/loop` 心跳轮询 |
94
- | 响应延迟 | 即时 | ≤1 分钟 |
95
- | 账号要求 | claude.ai 直连 | 任意(含 API 中转)|
141
+ [Tap "Allow Once" on phone, or open "View full command" to see complete output]
96
142
 
97
- 使用微信模式时告诉 Claude「**现在开始通过微信联系**」,会自动触发 `headless-mode` skill。
143
+ Claude continues execution and sends the result to WeChat.
98
144
 
99
- **Channel 模式下 Claude 的启动命令**:
145
+ You: I'm back
100
146
 
101
- ```bash
102
- claude --dangerously-load-development-channels server:wecom-aibot-channel
147
+ Claude: Exited WeChat mode.
103
148
  ```
104
149
 
105
- ---
150
+ ### Timeout Auto-Approval
106
151
 
107
- ## 常用命令
152
+ Configure in the bot config file or via `wecom-aibot.json`:
108
153
 
109
- | 命令 | 说明 |
110
- |------|------|
111
- | `--start / --stop` | 启动/停止后台服务 |
112
- | `--status` | 查看服务状态和机器人列表 |
113
- | `--config` | 修改默认机器人配置 |
114
- | `--add / --delete` | 添加/删除机器人 |
115
- | `--set-token [token]` | 设置 Auth Token(远程部署用) |
116
- | `--set-token --clear` | 清除 Auth Token |
117
- | `--debug` | 前台启动,输出调试日志 |
118
- | `--http-only` | 仅启动 HTTP MCP Server(服务器端用) |
119
- | `--channel-only` | 仅配置 Channel MCP(需 `MCP_URL` 环境变量) |
120
- | `--clean-cache` | 清空 CC 注册表缓存 |
121
- | `--upgrade` | 强制升级全局配置 |
122
- | `--uninstall` | 完全卸载 |
123
-
124
- 超时自动审批(默认 10 分钟):在机器人配置中设置 `"autoApproveTimeout": 600`。
154
+ ```json
155
+ {
156
+ "autoApproveTimeout": 600
157
+ }
158
+ ```
125
159
 
126
- ---
160
+ - `autoApproveTimeout`: Timeout in seconds (default 600s = 10 minutes)
161
+ - After timeout: operations **within** the project directory are auto-allowed; operations outside or delete commands are auto-denied
127
162
 
128
- ## 故障排查
163
+ ## Troubleshooting
129
164
 
130
165
  ```bash
131
- # 检查服务是否运行
166
+ # Check if service is running
132
167
  curl http://127.0.0.1:18963/health
133
168
 
134
- # Channel 不可用("Channels are not currently available"
135
- # → 使用 API Key 或中转服务,改用 HTTP 模式
169
+ # Channel unavailable ("Channels are not currently available")
170
+ # → Using API key or relay service? Switch to HTTP mode instead.
171
+
172
+ # Channel fails to reconnect after server restart
173
+ # → Auto-reconnect triggers within 5 seconds; no manual action needed.
174
+ # Requires v2.4.13 or later.
136
175
 
137
- # 端口占用
176
+ # Approval detail page shows "Unauthorized"
177
+ # → Upgrade to v2.4.14 or later; the /approval/ path is now auth-exempt.
178
+
179
+ # Port conflict
138
180
  lsof -i :18963 | grep LISTEN
139
181
  kill <PID>
140
182
 
141
- # 清理断线残留的 ccId 注册
183
+ # Clean up stale ccId registrations after disconnect
142
184
  npx @vrs-soft/wecom-aibot-mcp --clean-cache
143
185
  ```
144
186
 
145
- ---
187
+ ## MCP Tools
188
+
189
+ | Tool | Description | Key Parameters |
190
+ |------|-------------|----------------|
191
+ | `send_message` | Send message to WeChat | `content`, `cc_id`, `target_user` |
192
+ | `get_pending_messages` | Get pending messages (long poll) | `cc_id`, `timeout_ms` |
193
+ | `enter_headless_mode` | Enter WeChat mode | `cc_id`, `robot_id`, `mode` |
194
+ | `exit_headless_mode` | Exit WeChat mode | `cc_id` |
195
+ | `check_connection` | Check WebSocket connection status | - |
196
+ | `list_robots` | List all configured bots | - |
197
+ | `get_connection_stats` | Get connection stats and logs | `recent_logs` |
146
198
 
147
199
  ## License
148
200
 
149
- MIT · [企业微信机器人文档](https://developer.work.weixin.qq.com/document/path/101039) · [Channels 文档](https://code.claude.com/docs/en/channels-reference)
201
+ MIT · [Enterprise WeChat Bot Docs](https://developer.work.weixin.qq.com/document/path/101039) · [Channels Reference](https://code.claude.com/docs/en/channels-reference)
package/dist/bin.js CHANGED
@@ -9,7 +9,7 @@
9
9
  * - robotName 作为连接索引
10
10
  * - 不再使用 projectDir
11
11
  */
12
- import { spawn } from 'child_process';
12
+ import { spawn, execSync } from 'child_process';
13
13
  import * as fs from 'fs';
14
14
  import * as path from 'path';
15
15
  import * as os from 'os';
@@ -164,43 +164,81 @@ function isServerRunning() {
164
164
  return false;
165
165
  }
166
166
  }
167
+ // 通过端口查找进程 PID(fallback,当 PID 文件不存在时)
168
+ function findPidByPort(port) {
169
+ try {
170
+ // Linux: ss -tlnp | grep :18963
171
+ const output = execSync(`ss -tlnp 2>/dev/null | grep ':${port}'`, { encoding: 'utf-8' });
172
+ const match = output.match(/pid=(\d+)/);
173
+ if (match)
174
+ return parseInt(match[1]);
175
+ }
176
+ catch { /* ignore */ }
177
+ try {
178
+ // macOS: lsof -ti :18963
179
+ const output = execSync(`lsof -ti :${port} 2>/dev/null`, { encoding: 'utf-8' }).trim();
180
+ if (output)
181
+ return parseInt(output.split('\n')[0]);
182
+ }
183
+ catch { /* ignore */ }
184
+ return null;
185
+ }
167
186
  // 停止服务
168
187
  function stopServer() {
169
- if (!fs.existsSync(PID_FILE)) {
170
- console.log('[mcp] 服务未运行');
171
- return false;
188
+ let pid = null;
189
+ // 优先从 PID 文件获取
190
+ if (fs.existsSync(PID_FILE)) {
191
+ pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8').trim());
192
+ // 检查进程是否存在
193
+ try {
194
+ process.kill(pid, 0);
195
+ }
196
+ catch {
197
+ // PID 文件残留但进程已死,清理 PID 文件
198
+ console.log('[mcp] PID 文件残留,进程已退出,清理中...');
199
+ fs.unlinkSync(PID_FILE);
200
+ pid = null;
201
+ }
202
+ }
203
+ // PID 文件不存在或残留:通过端口查找
204
+ if (pid === null) {
205
+ pid = findPidByPort(HTTP_PORT);
206
+ if (pid === null) {
207
+ console.log('[mcp] 服务未运行');
208
+ return false;
209
+ }
210
+ console.log(`[mcp] 通过端口 ${HTTP_PORT} 找到进程 PID: ${pid}`);
172
211
  }
212
+ // 发送 SIGTERM
173
213
  try {
174
- const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8').trim());
175
214
  process.kill(pid, 'SIGTERM');
176
- // 等待进程退出
177
- let attempts = 0;
178
- while (attempts < 10) {
179
- try {
180
- process.kill(pid, 0);
181
- // 进程还存在,等待
182
- setTimeout(() => { }, 500);
183
- attempts++;
184
- }
185
- catch {
186
- // 进程已退出
187
- break;
188
- }
189
- }
190
- // 进程退出后删除 PID 文件(如果还存在)
191
- if (fs.existsSync(PID_FILE)) {
215
+ }
216
+ catch {
217
+ // ESRCH: 进程不存在,清理即可
218
+ if (fs.existsSync(PID_FILE))
192
219
  fs.unlinkSync(PID_FILE);
193
- }
194
220
  console.log('[mcp] 服务已停止');
195
221
  return true;
196
222
  }
197
- catch (err) {
198
- logger.error('[mcp] 停止服务失败:', err);
199
- if (fs.existsSync(PID_FILE)) {
200
- fs.unlinkSync(PID_FILE);
223
+ // 等待进程退出(最多 5 秒)
224
+ const deadline = Date.now() + 5000;
225
+ while (Date.now() < deadline) {
226
+ try {
227
+ process.kill(pid, 0);
228
+ // 进程还在,同步等待 100ms
229
+ const waitUntil = Date.now() + 100;
230
+ while (Date.now() < waitUntil) { /* busy wait */ }
201
231
  }
202
- return false;
232
+ catch {
233
+ break;
234
+ }
235
+ }
236
+ // 清理 PID 文件
237
+ if (fs.existsSync(PID_FILE)) {
238
+ fs.unlinkSync(PID_FILE);
203
239
  }
240
+ console.log('[mcp] 服务已停止');
241
+ return true;
204
242
  }
205
243
  // 等待连接验证(用于配置向导验证凭证)
206
244
  async function waitForConnection(client, timeoutMs = 10000) {
@@ -318,10 +356,13 @@ async function main() {
318
356
  // --channel: 作为 Channel MCP 代理运行,不应改写全局配置
319
357
  // --reinstall / --http-only: 有自己的处理逻辑
320
358
  // --version / -v: 只查版本,不写配置
359
+ // --stop / --status / --list / --clean-cache / --set-token / --config: 管理命令,不应改写配置
321
360
  const skipEnsure = args.includes('--reinstall') || args.includes('--http-only') ||
322
361
  args.includes('--setup') || args.includes('--channel') ||
323
362
  args.includes('--version') || args.includes('-v') ||
324
- args.includes('--start') || args.includes('--debug');
363
+ args.includes('--start') || args.includes('--debug') ||
364
+ args.includes('--stop') || args.includes('--status') || args.includes('--list') ||
365
+ args.includes('--clean-cache') || args.includes('--set-token') || args.includes('--config');
325
366
  if (!skipEnsure) {
326
367
  // 强制覆盖所有全局配置(不依赖智能体)
327
368
  ensureGlobalConfigs(installMode);
@@ -16,7 +16,7 @@ import * as fs from 'fs';
16
16
  import * as path from 'path';
17
17
  import * as os from 'os';
18
18
  import { VERSION } from './config-wizard.js';
19
- import { addPermissionHook } from './project-config.js';
19
+ import { addPermissionHook, registerActiveProject, unregisterActiveProject } from './project-config.js';
20
20
  const MCP_URL = process.env.MCP_URL || 'http://127.0.0.1:18963';
21
21
  const MCP_AUTH_TOKEN = process.env.MCP_AUTH_TOKEN;
22
22
  // 构建带 auth 的 fetch headers
@@ -205,6 +205,23 @@ function connectSSE(ccId) {
205
205
  if (!res.ok) {
206
206
  logChannel('SSE connect failed', { status: res.status });
207
207
  sseConnected = false;
208
+ // server 重启后 ccId 注册丢失(404),需重新注册再重连
209
+ if (!sseAbortController?.signal.aborted) {
210
+ const delay = res.status === 404 ? 5000 : 3000;
211
+ logChannel(`SSE 连接失败(${res.status}),${delay / 1000} 秒后重连`, { ccId });
212
+ setTimeout(async () => {
213
+ httpSessionId = null; // 重置 session,防止使用 server 重启前的旧 session
214
+ if (ccId) {
215
+ // 重新调 enter_headless_mode 恢复 server 端 ccId 注册
216
+ await forwardToHttpMcp('enter_headless_mode', {
217
+ cc_id: ccId,
218
+ mode: 'channel',
219
+ project_dir: process.cwd(),
220
+ }).catch((e) => logChannel('重注册 ccId 失败', { error: String(e) }));
221
+ }
222
+ connectSSE(ccId);
223
+ }, delay);
224
+ }
208
225
  return;
209
226
  }
210
227
  logChannel('SSE connected, waiting for messages', { status: res.status });
@@ -230,7 +247,7 @@ function connectSSE(ccId) {
230
247
  // 非主动断开时自动重连
231
248
  if (!sseAbortController?.signal.aborted) {
232
249
  logChannel('SSE 断线,3 秒后重连', { ccId });
233
- setTimeout(() => connectSSE(ccId), 3000);
250
+ setTimeout(() => { httpSessionId = null; connectSSE(ccId); }, 3000);
234
251
  }
235
252
  break;
236
253
  }
@@ -305,7 +322,7 @@ function connectSSE(ccId) {
305
322
  // 非主动断开时自动重连
306
323
  if (!sseAbortController?.signal.aborted) {
307
324
  logChannel('SSE 出错,3 秒后重连', { ccId });
308
- setTimeout(() => connectSSE(ccId), 3000);
325
+ setTimeout(() => { httpSessionId = null; connectSSE(ccId); }, 3000);
309
326
  }
310
327
  });
311
328
  }
@@ -399,9 +416,8 @@ function registerChannelTools(server) {
399
416
  project_dir: z.string().optional().describe('项目目录路径(用于写入配置文件)'),
400
417
  mode: z.enum(['channel', 'http']).optional().default('http')
401
418
  .describe('运行模式:channel=SSE推送(推荐),http=轮询(兼容)'),
402
- auto_approve: z.boolean().optional().default(true).describe('超时自动审批(默认 true)'),
403
- auto_approve_timeout: z.number().optional().default(600).describe('自动审批超时时间(秒,默认 600 10 分钟)'),
404
- }, async ({ agent_name, cc_id, robot_id, project_dir, mode, auto_approve, auto_approve_timeout }) => {
419
+ auto_approve_timeout: z.number().optional().default(600).describe('超时自动决策等待时间(秒,默认 600 即 10 分钟)'),
420
+ }, async ({ agent_name, cc_id, robot_id, project_dir, mode, auto_approve_timeout }) => {
405
421
  // 转发请求
406
422
  const result = await forwardToHttpMcp('enter_headless_mode', {
407
423
  agent_name,
@@ -409,7 +425,6 @@ function registerChannelTools(server) {
409
425
  robot_id,
410
426
  project_dir: project_dir || process.cwd(),
411
427
  mode,
412
- auto_approve,
413
428
  auto_approve_timeout,
414
429
  });
415
430
  // 拦截响应,提取 ccId,建立 SSE 连接
@@ -425,6 +440,9 @@ function registerChannelTools(server) {
425
440
  const localProjectDir = project_dir || process.cwd();
426
441
  const hookResult = addPermissionHook(localProjectDir);
427
442
  logChannel('本地 PermissionRequest hook 已写入', { path: hookResult.path, success: hookResult.success });
443
+ // 注册本地 PID → projectDir(供本地 permission-hook.sh 通过进程树匹配项目)
444
+ registerActiveProject(process.ppid ?? process.pid, localProjectDir);
445
+ logChannel('本地 active-projects 已注册', { pid: process.ppid ?? process.pid, projectDir: localProjectDir });
428
446
  // Channel 模式:过滤 heartbeat 信息,简化消息
429
447
  if (mode === 'channel' || parsed.mode === 'channel') {
430
448
  delete parsed.heartbeat; // Channel 模式不需要 heartbeat loop
@@ -448,6 +466,7 @@ function registerChannelTools(server) {
448
466
  cc_id: z.string().describe('CC 唯一标识(enter_headless_mode 返回的 ccId)'),
449
467
  project_dir: z.string().optional().describe('项目目录路径(用于更新配置文件)'),
450
468
  }, async ({ cc_id, project_dir }) => {
469
+ const localProjectDir = project_dir || process.cwd();
451
470
  // 断开 SSE 连接(abort 后重连逻辑不会触发)
452
471
  if (sseAbortController) {
453
472
  sseAbortController.abort();
@@ -456,7 +475,10 @@ function registerChannelTools(server) {
456
475
  sseCurrentCcId = undefined;
457
476
  logChannel('SSE disconnected', { cc_id });
458
477
  }
459
- return forwardToHttpMcp('exit_headless_mode', { cc_id, project_dir: project_dir || process.cwd() });
478
+ // 注销本地 active-projects 记录
479
+ unregisterActiveProject(localProjectDir);
480
+ logChannel('本地 active-projects 已注销', { projectDir: localProjectDir });
481
+ return forwardToHttpMcp('exit_headless_mode', { cc_id, project_dir: localProjectDir });
460
482
  });
461
483
  // ============================================
462
484
  // 工具 11: 从消息识别用户
package/dist/client.d.ts CHANGED
@@ -14,6 +14,7 @@ interface ApprovalRecord {
14
14
  operationHash?: string;
15
15
  consumed?: boolean;
16
16
  ccId?: string;
17
+ detailUrl?: string;
17
18
  }
18
19
  interface MessageRecord {
19
20
  seq: number;
@@ -61,7 +62,8 @@ declare class WecomClient extends EventEmitter {
61
62
  }>;
62
63
  sendText(content: string, targetUser?: string): Promise<boolean>;
63
64
  sendApprovalRequest(title: string, description: string, requestId: string, targetUser?: string, toolInput?: Record<string, unknown>, // v3.0: 用于去重
64
- ccId?: string): Promise<string>;
65
+ ccId?: string, // v3.0: 参与哈希,防止跨 CC 复用审批
66
+ detailUrlBase?: string): Promise<string>;
65
67
  sendQueuedApproval(taskId: string, title: string, description: string, targetUser?: string): Promise<boolean>;
66
68
  getApprovalResult(taskId: string): 'pending' | 'allow-once' | 'allow-always' | 'deny';
67
69
  setApprovalResult(taskId: string, result: 'allow-once' | 'deny', reason?: string): boolean;
package/dist/client.js CHANGED
@@ -19,6 +19,30 @@ import { logger } from './logger.js';
19
19
  const MAX_PENDING_MESSAGES = 100;
20
20
  // 全局消息序号计数器
21
21
  let globalMessageSeq = 0;
22
+ // 审批卡片正文长度上限;超过则截断,余下由详情链接承接
23
+ const APPROVAL_DESC_MAX = 200;
24
+ // 构建审批卡片 payload(发首次审批 + 排队重发共用)
25
+ function buildApprovalCard(title, description, taskId, detailUrl) {
26
+ const truncated = description.length > APPROVAL_DESC_MAX
27
+ ? description.slice(0, APPROVAL_DESC_MAX) + '…(已截断,点击「详情」查看完整内容)'
28
+ : description;
29
+ const subTitle = truncated + `\n\n📋 TaskID: ${taskId}`;
30
+ const card = {
31
+ card_type: 'button_interaction',
32
+ main_title: { title },
33
+ sub_title_text: subTitle,
34
+ button_list: [
35
+ { text: '允许', key: 'allow-once', style: 1 },
36
+ { text: '默认', key: 'allow-always', style: 1 },
37
+ { text: '拒绝', key: 'deny', style: 2 },
38
+ ],
39
+ task_id: taskId,
40
+ ...(detailUrl
41
+ ? { horizontal_content_list: [{ keyname: '详情', value: '查看完整命令', type: 1, url: detailUrl }] }
42
+ : {}),
43
+ };
44
+ return { msgtype: 'template_card', template_card: card };
45
+ }
22
46
  class WecomClient extends EventEmitter {
23
47
  wsClient;
24
48
  approvals = new Map();
@@ -228,8 +252,9 @@ class WecomClient extends EventEmitter {
228
252
  : eventKey === 'allow-always' ? '✅ 已允许(永久)'
229
253
  : '❌ 已拒绝';
230
254
  const toolInfo = approval.toolName ? `: ${approval.toolName}` : '';
231
- const descInfo = approval.description ? `\n\n> ${approval.description}` : '';
232
- const content = `**审批结果**${toolInfo}\n\n${resultText}${descInfo}`;
255
+ const desc = approval.description || '';
256
+ const descSnippet = desc ? `\n执行命令: ${desc.slice(0, 100)}${desc.length > 100 ? '…' : ''}` : '';
257
+ const content = `**审批结果**${toolInfo}\n\n${resultText}${descSnippet}`;
233
258
  this.sendText(content).catch(err => {
234
259
  logger.error('wecom', `发送审批确认失败: ${err}`);
235
260
  });
@@ -344,7 +369,8 @@ class WecomClient extends EventEmitter {
344
369
  }
345
370
  // 发送审批请求(带按钮的模板卡片)
346
371
  async sendApprovalRequest(title, description, requestId, targetUser, toolInput, // v3.0: 用于去重
347
- ccId // v3.0: 参与哈希,防止跨 CC 复用审批
372
+ ccId, // v3.0: 参与哈希,防止跨 CC 复用审批
373
+ detailUrlBase // 详情页 h5 链接的 base(最终 URL = base/taskId)
348
374
  ) {
349
375
  const userId = targetUser || this.targetUserId;
350
376
  // 从 title 中提取工具名称(格式: 【待审批】Bash)
@@ -360,6 +386,7 @@ class WecomClient extends EventEmitter {
360
386
  }
361
387
  const taskId = `approval_${requestId}_${Date.now()}`;
362
388
  const operationHash = toolInput && toolName ? hashOperation(ccId ?? '', toolName, toolInput) : undefined;
389
+ const detailUrl = detailUrlBase ? `${detailUrlBase}/${taskId}` : undefined;
363
390
  // 始终存储审批记录(断线时也需要,让 Hook 能轮询到)
364
391
  this.approvals.set(taskId, {
365
392
  taskId,
@@ -370,6 +397,7 @@ class WecomClient extends EventEmitter {
370
397
  description, // 保存审批请求原文
371
398
  operationHash,
372
399
  ccId, // 保存 ccId,用于 SSE 推送审批结果
400
+ detailUrl, // 供排队重发时复用
373
401
  });
374
402
  // 断线时将审批请求加入队列,等待重连后发送
375
403
  if (!this.connected) {
@@ -383,22 +411,7 @@ class WecomClient extends EventEmitter {
383
411
  // 返回 taskId,审批记录已创建,等待重连后发送
384
412
  return taskId;
385
413
  }
386
- // 发送模板卡片(在 description 中显示 taskId 便于用户识别)
387
- const displayDesc = description + `\n\n📋 TaskID: ${taskId}`;
388
- await this.wsClient.sendMessage(userId, {
389
- msgtype: 'template_card',
390
- template_card: {
391
- card_type: 'button_interaction',
392
- main_title: { title },
393
- sub_title_text: displayDesc,
394
- button_list: [
395
- { text: '允许', key: 'allow-once', style: 1 },
396
- { text: '默认', key: 'allow-always', style: 1 },
397
- { text: '拒绝', key: 'deny', style: 2 },
398
- ],
399
- task_id: taskId,
400
- },
401
- });
414
+ await this.wsClient.sendMessage(userId, buildApprovalCard(title, description, taskId, detailUrl));
402
415
  logger.log(`[wecom] 已发送审批请求到 ${userId}: ${taskId}`);
403
416
  return taskId;
404
417
  }
@@ -415,22 +428,7 @@ class WecomClient extends EventEmitter {
415
428
  return false;
416
429
  }
417
430
  const userId = targetUser || this.targetUserId;
418
- // 发送模板卡片(在 description 中显示 taskId 便于用户识别)
419
- const displayDesc = description + `\n\n📋 TaskID: ${taskId}`;
420
- await this.wsClient.sendMessage(userId, {
421
- msgtype: 'template_card',
422
- template_card: {
423
- card_type: 'button_interaction',
424
- main_title: { title },
425
- sub_title_text: displayDesc,
426
- button_list: [
427
- { text: '允许', key: 'allow-once', style: 1 },
428
- { text: '默认', key: 'allow-always', style: 1 },
429
- { text: '拒绝', key: 'deny', style: 2 },
430
- ],
431
- task_id: taskId,
432
- },
433
- });
431
+ await this.wsClient.sendMessage(userId, buildApprovalCard(title, description, taskId, approval.detailUrl));
434
432
  logger.log(`[wecom] 已发送排队审批请求到 ${userId}: ${taskId}`);
435
433
  return true;
436
434
  }
@@ -464,8 +462,9 @@ class WecomClient extends EventEmitter {
464
462
  const resultText = result === 'deny' ? '❌ 已拒绝' : '✅ 已允许';
465
463
  const reasonText = reason ? `\n\n原因:${reason}` : '';
466
464
  const toolInfo = approval.toolName ? `: ${approval.toolName}` : '';
467
- const descInfo = approval.description ? `\n\n> ${approval.description}` : '';
468
- this.sendText(`**审批结果(超时自动决策)**${toolInfo}\n\n${resultText}${reasonText}${descInfo}`).catch(err => {
465
+ const desc = approval.description || '';
466
+ const descBlock = desc ? `\n\n**执行命令**\n\`\`\`\n${desc}\n\`\`\`` : '';
467
+ this.sendText(`**审批结果(超时自动决策)**${toolInfo}\n\n${resultText}${reasonText}${descBlock}`).catch(err => {
469
468
  logger.error('[wecom] 发送审批确认失败:', err);
470
469
  });
471
470
  logger.log(`[wecom] 超时自动决策已设置: ${taskId} → ${result}`);
@@ -413,7 +413,7 @@ function writeHookScript() {
413
413
  # HTTP Transport 版本
414
414
  #
415
415
  # 固定端口: 18963
416
- # 检查 $(pwd)/.claude/wecom-aibot.json wechatMode 和 autoApprove 字段
416
+ # 通过 PID 树查 ~/.wecom-aibot-mcp/active-projects.json 匹配项目,读 wechatMode 开关
417
417
 
418
418
  MCP_PORT=18963
419
419
 
@@ -594,37 +594,10 @@ while [[ $POLL_COUNT -lt $MAX_POLL ]]; do
594
594
  fi
595
595
  done
596
596
 
597
- log_debug "[$(date)] Timeout reached, checking autoApprove setting"
597
+ log_debug "[$(date)] Timeout reached, executing smart auto-approval"
598
598
 
599
- # 超时处理:根据 autoApprove 决定行为
600
- # autoApprove: false → 继续等待(无限轮询)
601
- # autoApprove: true → 智能代批
602
-
603
- AUTO_APPROVE=$(jq -r '.autoApprove // false' "$CONFIG_FILE" 2>/dev/null)
604
- log_debug "[$(date)] autoApprove: $AUTO_APPROVE"
605
- if [[ "$AUTO_APPROVE" != "true" ]]; then
606
- log_debug "[$(date)] autoApprove off, entering infinite wait"
607
- # autoApprove 关闭,继续无限等待用户响应
608
- while true; do
609
- sleep 2
610
- STATUS=$(curl -s -m 3 "\${AUTH_ARGS[@]}" "$MCP_BASE_URL/approval_status/$TASK_ID" 2>/dev/null)
611
- RESULT=$(echo "$STATUS" | jq -r '.result // empty')
612
-
613
- if [[ "$RESULT" == "allow-once" || "$RESULT" == "allow-always" ]]; then
614
- log_debug "[$(date)] Approved by user (infinite wait)"
615
- printf '%s\\n' '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow"}}}'
616
- exit 0
617
- elif [[ "$RESULT" == "deny" ]]; then
618
- log_debug "[$(date)] Denied by user (infinite wait)"
619
- printf '%s\\n' '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"deny","message":"用户拒绝"}}}'
620
- exit 0
621
- fi
622
- done
623
- fi
624
-
625
- # autoApprove: true,执行智能代批
599
+ # 超时处理:必须立即决策,Claude Code 的 hook timeout 会杀掉阻塞进程。
626
600
  # 规则:删除命令→拒绝,项目内操作→允许,项目外操作→拒绝
627
- log_debug "[$(date)] Executing smart auto-approval"
628
601
 
629
602
  # 检查是否是删除命令(仅匹配命令行本身,不匹配 heredoc 内容)
630
603
  IS_DELETE=0
@@ -726,7 +699,7 @@ function writeStopHookScript() {
726
699
  # HTTP 模式使用:阻止 Claude 停止,提示调用 get_pending_messages 恢复轮询
727
700
  #
728
701
  # 固定端口: 18963
729
- # 检查 $(pwd)/.claude/wecom-aibot.json 的 wechatMode 和 autoApprove 字段
702
+ # 只检查 $(pwd)/.claude/wecom-aibot.json 的 wechatMode 字段
730
703
 
731
704
  MCP_PORT=18963
732
705
 
@@ -763,14 +736,6 @@ if [[ "$WECHAT_MODE" != "true" ]]; then
763
736
  exit 0
764
737
  fi
765
738
 
766
- # 检查 autoApprove 是否为 true(需要恢复轮询的模式)
767
- AUTO_APPROVE=$(jq -r '.autoApprove // false' "$CONFIG_FILE" 2>/dev/null)
768
- log_debug "[$(date)] autoApprove: $AUTO_APPROVE"
769
- if [[ "$AUTO_APPROVE" != "true" ]]; then
770
- log_debug "[$(date)] autoApprove not true, exit 0 (allow stop)"
771
- exit 0
772
- fi
773
-
774
739
  # 确定 MCP Server 地址(本地优先,失败则尝试远程 channel 配置)
775
740
  MCP_BASE_URL="http://127.0.0.1:$MCP_PORT"
776
741
  AUTH_ARGS=()
@@ -810,7 +775,7 @@ if [[ -z "$CC_ID" ]]; then
810
775
  exit 0
811
776
  fi
812
777
 
813
- # 处于微信模式且 autoApprove 为 true,需要恢复轮询
778
+ # 处于微信模式,需要恢复轮询
814
779
  # 使用 exit code 2 阻止停止,并提示 Claude 调用 MCP 工具
815
780
  log_debug "[$(date)] ✅ WeChat mode active, blocking stop to resume polling"
816
781
  log_debug "[$(date)] ccId=$CC_ID, will prompt Claude to call get_pending_messages"
@@ -204,9 +204,9 @@ function initMcpServer() {
204
204
  subscribeWecomMessage((msg) => {
205
205
  handleWecomMessage(msg);
206
206
  });
207
- // 定时清理过期审批条目(每 5 分钟清理超过 15 分钟的条目)
207
+ // 定时清理过期审批条目(每 5 分钟清理超过 30 分钟的条目)
208
208
  setInterval(() => {
209
- const cutoff = Date.now() - 15 * 60 * 1000;
209
+ const cutoff = Date.now() - 30 * 60 * 1000;
210
210
  let cleaned = 0;
211
211
  for (const [id, entry] of pendingApprovals) {
212
212
  if (entry.createdAt < cutoff) {
@@ -506,9 +506,9 @@ export async function startHttpServer(_server, port = HTTP_PORT, httpsConfig) {
506
506
  return;
507
507
  }
508
508
  const url = req.url || '/';
509
- // Auth token 校验(排除 /health
509
+ // Auth token 校验(排除 /health 和 /approval/ 详情页,后者由浏览器直接访问)
510
510
  const authToken = getAuthToken();
511
- if (authToken && url !== '/health') {
511
+ if (authToken && url !== '/health' && !url.startsWith('/approval/')) {
512
512
  const authHeader = req.headers['authorization'];
513
513
  if (authHeader !== `Bearer ${authToken}`) {
514
514
  res.writeHead(401, { 'Content-Type': 'application/json' });
@@ -622,6 +622,10 @@ export async function startHttpServer(_server, port = HTTP_PORT, httpsConfig) {
622
622
  handleApprovalStatus(req, res, url);
623
623
  return;
624
624
  }
625
+ if (req.method === 'GET' && url.startsWith('/approval/')) {
626
+ handleApprovalDetail(req, res, url);
627
+ return;
628
+ }
625
629
  if (req.method === 'POST' && url.startsWith('/approval_timeout/')) {
626
630
  await handleApprovalTimeout(req, res, url);
627
631
  return;
@@ -877,7 +881,7 @@ async function handleApprovalRequest(req, res) {
877
881
  res.end(JSON.stringify({ error: '未连接机器人,请先进入微信模式' }));
878
882
  return;
879
883
  }
880
- const { tool_name, tool_input } = request;
884
+ const { tool_name, tool_input, projectDir } = request;
881
885
  let description = '';
882
886
  if (tool_name === 'Bash') {
883
887
  description = `执行命令: ${tool_input?.command || '(unknown)'}`;
@@ -890,8 +894,12 @@ async function handleApprovalRequest(req, res) {
890
894
  }
891
895
  const title = `【待审批】${tool_name}`;
892
896
  const requestId = `hook_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
893
- const taskId = await client.sendApprovalRequest(title, description, requestId, undefined, tool_input, ccId);
894
- logger.log(`[http] 审批请求已发送: ${taskId} (机器人: ${robotName})`);
897
+ // 构建卡片"详情"链接的 base(同源,从本请求的 Host/scheme 推断)
898
+ const scheme = req.socket.encrypted ? 'https' : 'http';
899
+ const host = req.headers.host || `127.0.0.1:${HTTP_PORT}`;
900
+ const detailUrlBase = `${scheme}://${host}/approval`;
901
+ const taskId = await client.sendApprovalRequest(title, description, requestId, undefined, tool_input, ccId, detailUrlBase);
902
+ logger.log(`[http] 审批请求已发送: ${taskId} (机器人: ${robotName}) 详情页: ${detailUrlBase}/${taskId}`);
895
903
  // 存储审批并启动超时计时器
896
904
  const entry = {
897
905
  taskId,
@@ -902,6 +910,8 @@ async function handleApprovalRequest(req, res) {
902
910
  tool_input,
903
911
  description,
904
912
  robotName,
913
+ ccId,
914
+ projectDir,
905
915
  };
906
916
  pendingApprovals.set(taskId, entry);
907
917
  res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -913,6 +923,84 @@ async function handleApprovalRequest(req, res) {
913
923
  res.end(JSON.stringify({ error: err.message }));
914
924
  }
915
925
  }
926
+ function escapeHtml(raw) {
927
+ return raw
928
+ .replace(/&/g, '&amp;')
929
+ .replace(/</g, '&lt;')
930
+ .replace(/>/g, '&gt;')
931
+ .replace(/"/g, '&quot;')
932
+ .replace(/'/g, '&#39;');
933
+ }
934
+ function handleApprovalDetail(_req, res, url) {
935
+ const taskId = url.replace('/approval/', '');
936
+ const entry = pendingApprovals.get(taskId);
937
+ const respondHtml = (status, body) => {
938
+ res.writeHead(status, { 'Content-Type': 'text/html; charset=utf-8' });
939
+ res.end(body);
940
+ };
941
+ if (!entry) {
942
+ respondHtml(404, `<!doctype html><meta charset="utf-8"><title>审批不存在</title>
943
+ <body style="font-family:-apple-system,system-ui,sans-serif;padding:24px;color:#333">
944
+ <h2>审批已过期或不存在</h2>
945
+ <p>TaskID: <code>${escapeHtml(taskId)}</code></p>
946
+ <p>此条记录可能已被清理(用户已决策或超时)。</p>
947
+ </body>`);
948
+ return;
949
+ }
950
+ const inputPretty = (() => {
951
+ try {
952
+ return JSON.stringify(entry.tool_input ?? {}, null, 2);
953
+ }
954
+ catch {
955
+ return String(entry.tool_input);
956
+ }
957
+ })();
958
+ const statusLabel = {
959
+ 'pending': '⏳ 待审批',
960
+ 'allow-once': '✅ 已允许',
961
+ 'deny': '❌ 已拒绝',
962
+ }[entry.status] ?? entry.status;
963
+ const html = `<!doctype html>
964
+ <html>
965
+ <head>
966
+ <meta charset="utf-8">
967
+ <meta name="viewport" content="width=device-width, initial-scale=1">
968
+ <title>审批详情 · ${escapeHtml(entry.tool_name)}</title>
969
+ <style>
970
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
971
+ max-width: 780px; margin: 0 auto; padding: 16px; color: #222; background: #f7f7f9; }
972
+ h1 { font-size: 20px; margin: 8px 0 16px; }
973
+ .meta { background: #fff; border-radius: 8px; padding: 12px 16px; margin-bottom: 12px;
974
+ box-shadow: 0 1px 2px rgba(0,0,0,.04); }
975
+ .meta .row { display: flex; padding: 4px 0; border-bottom: 1px dashed #eee; }
976
+ .meta .row:last-child { border-bottom: none; }
977
+ .meta .k { width: 96px; color: #888; flex-shrink: 0; }
978
+ .meta .v { flex: 1; word-break: break-all; }
979
+ pre { background: #fff; border-radius: 8px; padding: 12px 16px;
980
+ overflow-x: auto; font-size: 13px; line-height: 1.5;
981
+ box-shadow: 0 1px 2px rgba(0,0,0,.04); white-space: pre-wrap; word-break: break-all; }
982
+ .tag { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 12px;
983
+ background: #eef; color: #446; }
984
+ footer { color: #aaa; font-size: 12px; text-align: center; margin-top: 16px; }
985
+ </style>
986
+ </head>
987
+ <body>
988
+ <h1>审批详情</h1>
989
+ <div class="meta">
990
+ <div class="row"><div class="k">状态</div><div class="v">${statusLabel}</div></div>
991
+ <div class="row"><div class="k">工具</div><div class="v"><span class="tag">${escapeHtml(entry.tool_name)}</span></div></div>
992
+ <div class="row"><div class="k">概要</div><div class="v">${escapeHtml(entry.description)}</div></div>
993
+ ${entry.projectDir ? `<div class="row"><div class="k">项目目录</div><div class="v">${escapeHtml(entry.projectDir)}</div></div>` : ''}
994
+ ${entry.ccId ? `<div class="row"><div class="k">CC</div><div class="v">${escapeHtml(entry.ccId)}</div></div>` : ''}
995
+ <div class="row"><div class="k">TaskID</div><div class="v"><code>${escapeHtml(taskId)}</code></div></div>
996
+ </div>
997
+ <h3>完整参数</h3>
998
+ <pre>${escapeHtml(inputPretty)}</pre>
999
+ <footer>审批记录保留 30 分钟后自动清理 · 请回到企业微信卡片点击审批按钮</footer>
1000
+ </body>
1001
+ </html>`;
1002
+ respondHtml(200, html);
1003
+ }
916
1004
  function handleApprovalStatus(_req, res, url) {
917
1005
  const taskId = url.replace('/approval_status/', '');
918
1006
  const entry = pendingApprovals.get(taskId);
@@ -968,7 +1056,11 @@ async function handleApprovalTimeout(req, res, url) {
968
1056
  const success = client.setApprovalResult(taskId, result, reason);
969
1057
  if (success) {
970
1058
  entry.status = result;
971
- pendingApprovals.delete(taskId); // 处理完立即删除
1059
+ // 保留 30 分钟供事后查看详情,不立即删除
1060
+ setTimeout(() => {
1061
+ pendingApprovals.delete(taskId);
1062
+ logger.log(`[http] 超时审批条目已清理: taskId=${taskId}`);
1063
+ }, 30 * 60 * 1000);
972
1064
  res.writeHead(200, { 'Content-Type': 'application/json' });
973
1065
  res.end(JSON.stringify({ success: true, taskId, result }));
974
1066
  }
@@ -17,7 +17,6 @@ export interface WechatModeConfig {
17
17
  robotName?: string;
18
18
  wechatMode: boolean;
19
19
  ccId?: string;
20
- autoApprove?: boolean;
21
20
  autoApproveTimeout?: number;
22
21
  heartbeatJobId?: string;
23
22
  mode?: 'channel' | 'http';
@@ -270,6 +270,14 @@ const STOP_HOOK = {
270
270
  matcher: '',
271
271
  hooks: [{ type: 'command', command: STOP_HOOK_SCRIPT_PATH }],
272
272
  };
273
+ /**
274
+ * 进入微信模式时默认预批的 MCP 工具通配(避免每次都走 hook 增加延迟)
275
+ * hook 本身对 mcp__* 也会放行,加入 allow 只是让 Claude Code 跳过 hook。
276
+ */
277
+ const DEFAULT_MCP_ALLOW = [
278
+ 'mcp__wecom-aibot__*',
279
+ 'mcp__wecom-aibot-channel__*',
280
+ ];
273
281
  /**
274
282
  * 添加 PermissionRequest hook 到项目 settings.json
275
283
  */
@@ -296,6 +304,16 @@ export function addPermissionHook(projectDir) {
296
304
  settings.hooks = {};
297
305
  }
298
306
  settings.hooks.PermissionRequest = [PERMISSION_HOOK];
307
+ // 合并默认 MCP 通配到 permissions.allow(去重保序)
308
+ const perms = settings.permissions ?? {};
309
+ const existingAllow = Array.isArray(perms.allow) ? perms.allow : [];
310
+ const merged = [...existingAllow];
311
+ for (const entry of DEFAULT_MCP_ALLOW) {
312
+ if (!merged.includes(entry))
313
+ merged.push(entry);
314
+ }
315
+ perms.allow = merged;
316
+ settings.permissions = perms;
299
317
  // 写入配置
300
318
  try {
301
319
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
@@ -1,23 +1,2 @@
1
- /**
2
- * MCP 工具注册入口
3
- *
4
- * 注册以下工具:
5
- * - send_message: 发送消息
6
- * - send_approval_request: 发送审批请求
7
- * - get_approval_result: 获取审批结果
8
- * - check_connection: 检查连接状态
9
- * - get_pending_messages: 获取待处理消息
10
- * - get_setup_guide: 获取安装指南
11
- * - add_robot_config: 添加机器人配置
12
- * - list_robots: 列出所有机器人
13
- * - get_robot_status: 获取机器人状态
14
- * - enter_headless_mode: 进入微信模式
15
- * - exit_headless_mode: 退出微信模式
16
- * - detect_user_from_message: 从消息识别用户
17
- *
18
- * v2.0 架构变更:
19
- * - 不再使用 projectDir 参数
20
- * - 从 Session 自动获取 robotName
21
- */
22
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
23
2
  export declare function registerTools(server: McpServer): void;
@@ -1,24 +1,4 @@
1
- /**
2
- * MCP 工具注册入口
3
- *
4
- * 注册以下工具:
5
- * - send_message: 发送消息
6
- * - send_approval_request: 发送审批请求
7
- * - get_approval_result: 获取审批结果
8
- * - check_connection: 检查连接状态
9
- * - get_pending_messages: 获取待处理消息
10
- * - get_setup_guide: 获取安装指南
11
- * - add_robot_config: 添加机器人配置
12
- * - list_robots: 列出所有机器人
13
- * - get_robot_status: 获取机器人状态
14
- * - enter_headless_mode: 进入微信模式
15
- * - exit_headless_mode: 退出微信模式
16
- * - detect_user_from_message: 从消息识别用户
17
- *
18
- * v2.0 架构变更:
19
- * - 不再使用 projectDir 参数
20
- * - 从 Session 自动获取 robotName
21
- */
1
+ // MCP 工具注册入口。完整工具清单见 design/tools-api.md。
22
2
  import { z } from 'zod';
23
3
  import { listAllRobots, getDocMcpUrl, installSkill, VERSION } from '../config-wizard.js';
24
4
  import { callDocTool } from '../doc-proxy.js';
@@ -356,9 +336,8 @@ npx @vrs-soft/wecom-aibot-mcp
356
336
  project_dir: z.string().optional().describe('项目目录路径(用于写入配置文件)'),
357
337
  mode: z.enum(['channel', 'http']).optional().default('http')
358
338
  .describe('运行模式:channel=SSE推送(推荐),http=轮询(兼容)'),
359
- auto_approve: z.boolean().optional().default(true).describe('超时自动审批(默认 true)'),
360
- auto_approve_timeout: z.number().optional().default(300).describe('自动审批超时时间(秒,默认 300 5 分钟)'),
361
- }, async ({ agent_name, cc_id, robot_id, project_dir, mode, auto_approve, auto_approve_timeout }, extra) => {
339
+ auto_approve_timeout: z.number().optional().default(300).describe('超时自动决策等待时间(秒,默认 300 即 5 分钟)'),
340
+ }, async ({ agent_name, cc_id, robot_id, project_dir, mode, auto_approve_timeout }, extra) => {
362
341
  // 获取项目目录
363
342
  const projectDir = project_dir || process.cwd();
364
343
  // 智能体名称(用于生成 ccId)
@@ -429,7 +408,6 @@ npx @vrs-soft/wecom-aibot-mcp
429
408
  wechatMode: true,
430
409
  robotName: selectedRobot.name,
431
410
  ccId: finalCcId,
432
- autoApprove: auto_approve,
433
411
  autoApproveTimeout: auto_approve_timeout,
434
412
  mode,
435
413
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vrs-soft/wecom-aibot-mcp",
3
- "version": "2.4.11",
3
+ "version": "2.4.16",
4
4
  "description": "企业微信智能机器人 MCP 服务 - Claude Code 审批通道",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",