@vrs-soft/wecom-aibot-mcp 1.2.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -44,7 +44,7 @@ PermissionRequest Hook 拦截
44
44
 
45
45
  ┌────────────────────────┐
46
46
  │ 检查 headless 模式状态 │
47
- │ (检查 .claude/wecom-aibot.json)
47
+ │ (检查 .claude/headless.json)
48
48
  └────────────────────────┘
49
49
 
50
50
  ┌───────┴───────┐
package/dist/bin.js CHANGED
@@ -38,6 +38,7 @@ function showHelp() {
38
38
  --version, -v 显示版本号
39
39
  --start 启动 MCP Server(后台服务模式)
40
40
  --stop 停止 MCP Server
41
+ --debug 前台启动 MCP Server(日志直接输出到终端,用于调试)
41
42
  --status 显示服务状态和机器人配置
42
43
  --config 重新配置默认机器人(修改 Bot ID / Secret / 目标用户)
43
44
  --add 添加新的机器人配置(多机器人场景)
@@ -86,11 +87,11 @@ function showStatus() {
86
87
  console.log('尚未配置机器人,请运行 npx @vrs-soft/wecom-aibot-mcp 启动配置向导');
87
88
  return;
88
89
  }
89
- // 构建机器人占用信息(v3.0: ccId 由 cc-registry 管理,连接状态不再含 ccId)
90
+ // 构建机器人占用信息
90
91
  const robotUsage = new Map();
91
92
  for (const conn of connections) {
92
- if (conn.connected) {
93
- robotUsage.set(conn.robotName, { ccId: '' });
93
+ if (conn.agentName) {
94
+ robotUsage.set(conn.robotName, { agentName: conn.agentName });
94
95
  }
95
96
  }
96
97
  console.log(`已配置 ${allRobots.length} 个机器人:\n`);
@@ -101,7 +102,7 @@ function showStatus() {
101
102
  console.log(` Bot ID: ${robot.botId}`);
102
103
  console.log(` 目标用户: ${robot.targetUserId}`);
103
104
  if (usage) {
104
- console.log(` 使用者: ${usage.ccId}`);
105
+ console.log(` 使用者: ${usage.agentName}`);
105
106
  }
106
107
  console.log('');
107
108
  }
@@ -118,8 +119,10 @@ function isServerRunning() {
118
119
  return true;
119
120
  }
120
121
  catch {
121
- // 进程不存在,清理 PID 文件
122
- fs.unlinkSync(PID_FILE);
122
+ // 进程不存在,清理 PID 文件(可能已被进程自身删除)
123
+ if (fs.existsSync(PID_FILE)) {
124
+ fs.unlinkSync(PID_FILE);
125
+ }
123
126
  return false;
124
127
  }
125
128
  }
@@ -146,13 +149,18 @@ function stopServer() {
146
149
  break;
147
150
  }
148
151
  }
149
- fs.unlinkSync(PID_FILE);
152
+ // 进程退出后删除 PID 文件(如果还存在)
153
+ if (fs.existsSync(PID_FILE)) {
154
+ fs.unlinkSync(PID_FILE);
155
+ }
150
156
  console.log('[mcp] 服务已停止');
151
157
  return true;
152
158
  }
153
159
  catch (err) {
154
160
  console.error('[mcp] 停止服务失败:', err);
155
- fs.unlinkSync(PID_FILE);
161
+ if (fs.existsSync(PID_FILE)) {
162
+ fs.unlinkSync(PID_FILE);
163
+ }
156
164
  return false;
157
165
  }
158
166
  }
@@ -174,13 +182,11 @@ async function waitForConnection(client, timeoutMs = 10000) {
174
182
  }
175
183
  // 启动 MCP Server(前台运行,供 --start 使用)
176
184
  async function startMcpServerForeground() {
177
- // 检查是否有任何机器人配置(支持 config.json 和 robot-*.json)
178
- const allRobots = listAllRobots();
179
- if (allRobots.length === 0) {
185
+ const savedConfig = loadConfig();
186
+ if (!savedConfig || !savedConfig.botId || !savedConfig.secret || !savedConfig.targetUserId) {
180
187
  console.error('[mcp] 未找到配置,请先运行: npx @vrs-soft/wecom-aibot-mcp');
181
188
  process.exit(1);
182
189
  }
183
- console.log(`[mcp] 发现 ${allRobots.length} 个机器人配置: ${allRobots.map(r => r.name).join(', ')}`);
184
190
  // 写入 PID 文件
185
191
  fs.writeFileSync(PID_FILE, String(process.pid));
186
192
  // 确保 hook 已安装
@@ -224,9 +230,9 @@ async function startMcpServerForeground() {
224
230
  }
225
231
  // 后台启动 MCP Server(使用 spawn)
226
232
  function startMcpServerBackground() {
227
- // 检查是否有任何机器人配置
228
- const allRobots = listAllRobots();
229
- if (allRobots.length === 0) {
233
+ // 检查配置是否存在
234
+ const savedConfig = loadConfig();
235
+ if (!savedConfig || !savedConfig.botId || !savedConfig.secret || !savedConfig.targetUserId) {
230
236
  console.error('[mcp] 未找到配置,请先运行: npx @vrs-soft/wecom-aibot-mcp');
231
237
  process.exit(1);
232
238
  }
@@ -246,6 +252,7 @@ function startMcpServerBackground() {
246
252
  console.log(`[mcp] HTTP endpoint: http://127.0.0.1:18963/mcp`);
247
253
  console.log('[mcp] 健康检查: curl http://127.0.0.1:18963/health');
248
254
  console.log('[mcp] 停止服务: npx @vrs-soft/wecom-aibot-mcp --stop');
255
+ console.log('[mcp] 调试模式: npx @vrs-soft/wecom-aibot-mcp --debug');
249
256
  }
250
257
  async function main() {
251
258
  const args = process.argv.slice(2);
@@ -287,11 +294,17 @@ async function main() {
287
294
  await deleteMcpConfigInteractive(instanceName);
288
295
  process.exit(0);
289
296
  }
290
- // --start --foreground:前台启动(内部调用)
297
+ // --start --foreground:前台启动(内部调用,输出到日志文件)
291
298
  if (args.includes('--start') && args.includes('--foreground')) {
292
299
  await startMcpServerForeground();
293
300
  return; // 保持运行,不 exit
294
301
  }
302
+ // --debug:前台启动,日志直接输出到终端
303
+ if (args.includes('--debug')) {
304
+ console.log('[mcp] Debug 模式:前台运行,Ctrl+C 退出');
305
+ await startMcpServerForeground();
306
+ return;
307
+ }
295
308
  // --start:后台启动
296
309
  if (args.includes('--start')) {
297
310
  startMcpServerBackground();
package/dist/client.d.ts CHANGED
@@ -8,6 +8,7 @@ interface ApprovalRecord {
8
8
  toolName?: string;
9
9
  toolInput?: Record<string, unknown>;
10
10
  projectDir?: string;
11
+ description?: string;
11
12
  lastKeepaliveMinute?: number;
12
13
  keepaliveCount?: number;
13
14
  operationHash?: string;
@@ -22,6 +23,7 @@ interface MessageRecord {
22
23
  from_userid: string;
23
24
  chatid: string;
24
25
  chattype: 'single' | 'group';
26
+ quoteContent?: string;
25
27
  }
26
28
  declare class WecomClient extends EventEmitter {
27
29
  private wsClient;
@@ -35,11 +37,13 @@ declare class WecomClient extends EventEmitter {
35
37
  private wasReconnecting;
36
38
  private reconnectAttempt;
37
39
  private lastDisconnectTime;
40
+ private disconnectNotifyCount;
38
41
  constructor(botId: string, secret: string, targetUserId: string, robotName: string);
39
42
  getAuthUrl(): string;
40
43
  private setupEventHandlers;
41
44
  private handleMessage;
42
45
  private handleApprovalResponse;
46
+ private replyApprovalResult;
43
47
  connect(): void;
44
48
  disconnect(): void;
45
49
  isConnected(): boolean;
@@ -53,6 +57,7 @@ declare class WecomClient extends EventEmitter {
53
57
  ccId?: string): Promise<string>;
54
58
  sendQueuedApproval(taskId: string, title: string, description: string, targetUser?: string): Promise<boolean>;
55
59
  getApprovalResult(taskId: string): 'pending' | 'allow-once' | 'allow-always' | 'deny';
60
+ setApprovalResult(taskId: string, result: 'allow-once' | 'deny', reason?: string): boolean;
56
61
  getPendingApprovals(): string[];
57
62
  getPendingApprovalsRecords(): ApprovalRecord[];
58
63
  getApprovalRecord(taskId: string): ApprovalRecord | undefined;
package/dist/client.js CHANGED
@@ -30,6 +30,7 @@ class WecomClient extends EventEmitter {
30
30
  wasReconnecting = false; // 跟踪是否处于重连状态
31
31
  reconnectAttempt = 0; // 重连尝试次数
32
32
  lastDisconnectTime = 0; // 最后断线时间
33
+ disconnectNotifyCount = 0; // 断线通知次数(最多1次)
33
34
  constructor(botId, secret, targetUserId, robotName) {
34
35
  super();
35
36
  this.botId = botId;
@@ -53,14 +54,15 @@ class WecomClient extends EventEmitter {
53
54
  }
54
55
  setupEventHandlers() {
55
56
  this.wsClient.on('connected', () => {
56
- logConnected(this.robotName);
57
+ logConnected();
57
58
  });
58
59
  this.wsClient.on('authenticated', () => {
59
60
  const wasReconnecting = this.wasReconnecting;
60
61
  this.connected = true;
61
62
  this.wasReconnecting = false;
62
63
  this.reconnectAttempt = 0;
63
- logAuthenticated(this.robotName);
64
+ this.disconnectNotifyCount = 0; // 重连成功后重置断线通知计数
65
+ logAuthenticated();
64
66
  // 重连成功后发送通知
65
67
  if (wasReconnecting) {
66
68
  this.sendText('【系统】连接已恢复').catch(err => {
@@ -74,18 +76,21 @@ class WecomClient extends EventEmitter {
74
76
  this.connected = false;
75
77
  this.wasReconnecting = true;
76
78
  this.lastDisconnectTime = Date.now();
77
- logDisconnected(this.robotName, reason);
78
- // 发送断线通知
79
- this.sendText('【系统】连接中断,正在重连...').catch(err => {
80
- console.error('[wecom] 发送断线通知失败:', err);
81
- });
79
+ logDisconnected(reason);
80
+ // 断线通知最多发1次
81
+ if (this.disconnectNotifyCount < 1) {
82
+ this.disconnectNotifyCount++;
83
+ this.sendText('【系统】连接中断,正在重连...').catch(err => {
84
+ console.error('[wecom] 发送断线通知失败:', err);
85
+ });
86
+ }
82
87
  });
83
88
  this.wsClient.on('reconnecting', (attempt) => {
84
89
  this.reconnectAttempt = attempt;
85
- logReconnecting(this.robotName, attempt);
90
+ logReconnecting(attempt);
86
91
  });
87
92
  this.wsClient.on('error', (err) => {
88
- logError(this.robotName, err.message);
93
+ logError(err.message);
89
94
  // 检测授权相关错误(40058: invalid Request Parameter)
90
95
  if (err.message.includes('40058') || err.message.includes('invalid Request Parameter')) {
91
96
  console.log('');
@@ -106,7 +111,8 @@ class WecomClient extends EventEmitter {
106
111
  });
107
112
  // 监听模板卡片事件(审批结果)
108
113
  this.wsClient.on('event.template_card_event', (frame) => {
109
- console.log('[wecom] 收到 template_card_event 事件');
114
+ console.log('[wecom] 收到 template_card_event 事件,完整 frame:');
115
+ console.log(JSON.stringify(frame, null, 2));
110
116
  this.handleApprovalResponse(frame);
111
117
  });
112
118
  // 监听进入会话事件
@@ -159,6 +165,7 @@ class WecomClient extends EventEmitter {
159
165
  from_userid,
160
166
  chatid,
161
167
  chattype,
168
+ quoteContent,
162
169
  };
163
170
  // v3.0: 检查队列上限
164
171
  if (this.messages.length >= MAX_PENDING_MESSAGES) {
@@ -183,14 +190,18 @@ class WecomClient extends EventEmitter {
183
190
  }
184
191
  handleApprovalResponse(frame) {
185
192
  const event = frame.body?.event;
186
- if (!event)
193
+ console.log('[wecom] handleApprovalResponse body.event:', JSON.stringify(event));
194
+ if (!event) {
195
+ console.log('[wecom] event 为空,frame.body:', JSON.stringify(frame.body));
187
196
  return;
188
- // template_card_event 结构在 event.template_card_event 中
197
+ }
198
+ // task_id 和 event_key 在 event.template_card_event 内部
189
199
  const cardEvent = event.template_card_event;
190
- if (!cardEvent)
200
+ const taskId = cardEvent?.task_id;
201
+ const eventKey = cardEvent?.event_key; // 用户点击的按钮 key
202
+ console.log(`[wecom] taskId=${taskId}, eventKey=${eventKey}, approvals keys:`, [...this.approvals.keys()]);
203
+ if (!taskId)
191
204
  return;
192
- const taskId = cardEvent.task_id;
193
- const eventKey = cardEvent.event_key; // 用户点击的按钮 key
194
205
  console.log(`[wecom] 收到审批响应: taskId=${taskId}, key=${eventKey}`);
195
206
  const approval = this.approvals.get(taskId);
196
207
  if (approval && !approval.resolved) {
@@ -203,10 +214,35 @@ class WecomClient extends EventEmitter {
203
214
  : eventKey === 'allow-always' ? '✅ 已允许(永久)'
204
215
  : '❌ 已拒绝';
205
216
  const toolInfo = approval.toolName ? `: ${approval.toolName}` : '';
206
- this.sendText(`**审批结果**${toolInfo}\n\n${resultText}`).catch(err => {
217
+ const descInfo = approval.description ? `\n\n> ${approval.description}` : '';
218
+ const content = `**审批结果**${toolInfo}\n\n${resultText}${descInfo}`;
219
+ this.sendText(content).catch(err => {
207
220
  console.error('[wecom] 发送审批确认失败:', err);
208
221
  });
209
222
  }
223
+ else if (approval && approval.resolved) {
224
+ console.log(`[wecom] 审批已解决,跳过点击: ${taskId}, resolved=${approval.resolved}, result=${approval.result}`);
225
+ }
226
+ else {
227
+ console.log(`[wecom] 审批记录不存在: ${taskId}`);
228
+ }
229
+ }
230
+ // 使用 reply 方法回复审批结果(会有引用效果)
231
+ async replyApprovalResult(frame, content) {
232
+ if (!this.connected) {
233
+ console.log('[wecom] 未连接,无法回复');
234
+ return;
235
+ }
236
+ try {
237
+ await this.wsClient.reply(frame, {
238
+ msgtype: 'markdown',
239
+ markdown: { content },
240
+ });
241
+ console.log(`[wecom] 已回复审批结果`);
242
+ }
243
+ catch (err) {
244
+ console.error(`[wecom] 回复失败: ${err}`);
245
+ }
210
246
  }
211
247
  // 连接
212
248
  connect() {
@@ -317,6 +353,7 @@ class WecomClient extends EventEmitter {
317
353
  timestamp: Date.now(),
318
354
  toolName,
319
355
  toolInput,
356
+ description, // 保存审批请求原文
320
357
  operationHash,
321
358
  });
322
359
  // 断线时将审批请求加入队列,等待重连后发送
@@ -391,6 +428,32 @@ class WecomClient extends EventEmitter {
391
428
  }
392
429
  return 'pending';
393
430
  }
431
+ // 手动设置审批结果(供 Hook 超时自动决策使用)
432
+ setApprovalResult(taskId, result, reason) {
433
+ const approval = this.approvals.get(taskId);
434
+ if (!approval) {
435
+ console.log(`[wecom] 设置审批结果失败:记录不存在 ${taskId}`);
436
+ return false;
437
+ }
438
+ if (approval.resolved) {
439
+ console.log(`[wecom] 设置审批结果失败:已解决 ${taskId}`);
440
+ return false;
441
+ }
442
+ approval.resolved = true;
443
+ approval.result = result;
444
+ approval.timestamp = Date.now();
445
+ this.emit('approval_resolved', { taskId, result });
446
+ // 发送确认消息给用户(包含超时原因和原文引用)
447
+ const resultText = result === 'deny' ? '❌ 已拒绝' : '✅ 已允许';
448
+ const reasonText = reason ? `\n\n原因:${reason}` : '';
449
+ const toolInfo = approval.toolName ? `: ${approval.toolName}` : '';
450
+ const descInfo = approval.description ? `\n\n> ${approval.description}` : '';
451
+ this.sendText(`**审批结果(超时自动决策)**${toolInfo}\n\n${resultText}${reasonText}${descInfo}`).catch(err => {
452
+ console.error('[wecom] 发送审批确认失败:', err);
453
+ });
454
+ console.log(`[wecom] 超时自动决策已设置: ${taskId} → ${result}`);
455
+ return true;
456
+ }
394
457
  // 获取所有待处理的审批任务 ID(供 hook 轮询使用)
395
458
  getPendingApprovals() {
396
459
  return Array.from(this.approvals.entries())
@@ -249,32 +249,16 @@ export function uninstall() {
249
249
  function writeHookScript() {
250
250
  const script = `#!/bin/bash
251
251
  # wecom-aibot-mcp PermissionRequest hook
252
- # HTTP Transport 版本 - v3.0
252
+ # HTTP Transport 版本
253
253
  #
254
- # v3.0 增强:
255
- # - 工具名验证(防止注入攻击)
256
- # - curl 错误处理(检查退出码)
257
- # - 重试机制(3 次重试)
254
+ # 固定端口: 18963
255
+ # 检查 $(pwd)/.claude/wecom-aibot.json 的 wechatMode 和 autoApprove 字段
258
256
 
259
257
  MCP_PORT=18963
260
- MAX_RETRIES=3
261
- RETRY_DELAY=2
262
258
 
263
259
  INPUT=$(cat)
264
260
  TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
265
261
 
266
- # ============================================
267
- # 工具名验证(防止注入攻击)
268
- # ============================================
269
- # 只允许字母、数字、下划线、连字符
270
- if [[ ! "$TOOL_NAME" =~ ^[a-zA-Z0-9_-]+$ ]]; then
271
- # MCP 工具名可能包含特殊字符(如 mcp__server__tool)
272
- if [[ ! "$TOOL_NAME" =~ ^[a-zA-Z0-9_-]+$ ]] && [[ ! "$TOOL_NAME" == mcp__* ]]; then
273
- echo "[hook] 无效的工具名: $TOOL_NAME" >&2
274
- exit 0
275
- fi
276
- fi
277
-
278
262
  # MCP 工具本身不需要拦截
279
263
  if [[ "$TOOL_NAME" == mcp__* ]]; then
280
264
  printf '%s\\n' '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow"}}}'
@@ -289,97 +273,51 @@ case "$TOOL_NAME" in
289
273
  ;;
290
274
  esac
291
275
 
292
- # 检查项目目录的 settings.json 是否有 hooks 配置
276
+ # 检查项目目录的微信模式配置文件
293
277
  PROJECT_DIR=$(pwd)
294
- SETTINGS_FILE="$PROJECT_DIR/.claude/settings.json"
278
+ CONFIG_FILE="$PROJECT_DIR/.claude/wecom-aibot.json"
295
279
 
296
- # 没有 hooks 配置,说明不在微信模式
297
- if [[ ! -f "$SETTINGS_FILE" ]]; then
280
+ # 配置文件不存在,不在微信模式
281
+ if [[ ! -f "$CONFIG_FILE" ]]; then
298
282
  exit 0
299
283
  fi
300
284
 
301
- # 检查是否有 PermissionRequest hook
302
- if ! jq -e '.hooks.PermissionRequest' "$SETTINGS_FILE" > /dev/null 2>&1; then
285
+ # 检查 wechatMode 是否为 true(微信模式开关)
286
+ WECHAT_MODE=$(jq -r '.wechatMode // false' "$CONFIG_FILE" 2>/dev/null)
287
+ if [[ "$WECHAT_MODE" != "true" ]]; then
303
288
  exit 0
304
289
  fi
305
290
 
306
- # ============================================
307
- # 带重试的 curl 函数
308
- # ============================================
309
- retry_curl() {
310
- local url="$1"
311
- local max_retries="$2"
312
- local attempt=1
313
- local response
314
-
315
- while [[ $attempt -le $max_retries ]]; do
316
- response=$(curl -s -m 10 "$url" 2>/dev/null)
317
- local exit_code=$?
318
-
319
- if [[ $exit_code -eq 0 ]]; then
320
- echo "$response"
321
- return 0
322
- fi
323
-
324
- echo "[hook] curl 失败 (尝试 $attempt/$max_retries, 退出码: $exit_code)" >&2
325
-
326
- if [[ $attempt -lt $max_retries ]]; then
327
- sleep $RETRY_DELAY
328
- fi
329
-
330
- ((attempt++))
331
- done
332
-
333
- return 1
334
- }
335
-
336
- # ============================================
337
- # 检查 MCP Server 健康状态
338
- # ============================================
339
- HEALTH=$(retry_curl "http://127.0.0.1:$MCP_PORT/health" "$MAX_RETRIES")
340
- if [[ $? -ne 0 ]] || ! echo "$HEALTH" | jq -e '.status == "ok"' > /dev/null 2>&1; then
341
- echo "[hook] MCP Server 不在线,跳过审批" >&2
291
+ # 检查 MCP Server 是否在线
292
+ HEALTH=$(curl -s -m 2 "http://127.0.0.1:$MCP_PORT/health" 2>/dev/null)
293
+ if ! echo "$HEALTH" | jq -e '.status == "ok"' > /dev/null 2>&1; then
342
294
  exit 0
343
295
  fi
344
296
 
345
- # ============================================
346
- # 发送审批请求
347
- # ============================================
297
+ # 发送审批请求(使用 pwd 作为 projectDir)
348
298
  TOOL_INPUT=$(echo "$INPUT" | jq -c '.tool_input // {}')
349
-
350
- # 验证 tool_input 是有效的 JSON
351
- if ! echo "$TOOL_INPUT" | jq -e . > /dev/null 2>&1; then
352
- echo "[hook] 无效的 tool_input JSON" >&2
353
- exit 0
354
- fi
355
-
356
299
  BODY=$(jq -n --arg tool_name "$TOOL_NAME" --argjson tool_input "$TOOL_INPUT" --arg project_dir "$PROJECT_DIR" \\
357
300
  '{"tool_name":$tool_name,"tool_input":$tool_input,"projectDir":$project_dir}')
358
301
 
359
302
  RESPONSE=$(curl -s -m 10 -X POST "http://127.0.0.1:$MCP_PORT/approve" \\
360
303
  -H "Content-Type: application/json" \\
361
- -d "$BODY" 2>/dev/null)
362
- CURL_EXIT=$?
363
-
364
- if [[ $CURL_EXIT -ne 0 ]]; then
365
- echo "[hook] 发送审批请求失败 (退出码: $CURL_EXIT)" >&2
366
- exit 0
367
- fi
304
+ -d "$BODY")
368
305
 
369
306
  TASK_ID=$(echo "$RESPONSE" | jq -r '.taskId // empty')
370
307
  if [[ -z "$TASK_ID" ]]; then
371
- echo "[hook] 未获取到 taskId" >&2
372
308
  exit 0
373
309
  fi
374
310
 
375
- # ============================================
376
- # 阻塞等待审批结果
377
- # ============================================
378
- WAIT_RESULT=$(curl -s -m 600 -X GET "http://127.0.0.1:$MCP_PORT/approval_wait/$TASK_ID" 2>/dev/null)
379
- CURL_EXIT=$?
311
+ # 轮询审批结果(带超时:10 分钟)
312
+ POLL_COUNT=0
313
+ MAX_POLL=300 # 300 * 2秒 = 600秒 = 10分钟
380
314
 
381
- if [[ $CURL_EXIT -eq 0 ]] && [[ -n "$WAIT_RESULT" ]]; then
382
- RESULT=$(echo "$WAIT_RESULT" | jq -r '.result // empty')
315
+ while [[ $POLL_COUNT -lt $MAX_POLL ]]; do
316
+ sleep 2
317
+ POLL_COUNT=$((POLL_COUNT + 1))
318
+
319
+ STATUS=$(curl -s -m 3 "http://127.0.0.1:$MCP_PORT/approval_status/$TASK_ID" 2>/dev/null)
320
+ RESULT=$(echo "$STATUS" | jq -r '.result // empty')
383
321
 
384
322
  if [[ "$RESULT" == "allow-once" || "$RESULT" == "allow-always" ]]; then
385
323
  printf '%s\\n' '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow"}}}'
@@ -388,33 +326,86 @@ if [[ $CURL_EXIT -eq 0 ]] && [[ -n "$WAIT_RESULT" ]]; then
388
326
  printf '%s\\n' '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"deny","message":"用户拒绝"}}}'
389
327
  exit 0
390
328
  fi
391
- fi
329
+ done
392
330
 
393
- # ============================================
394
- # 无限轮询审批结果
395
- # ============================================
396
- echo "[hook] 开始无限轮询审批结果" >&2
331
+ # 超时处理:根据 autoApprove 决定行为
332
+ # autoApprove: false → 继续等待(无限轮询)
333
+ # autoApprove: true → 智能代批
397
334
 
398
- while true; do
399
- sleep 2
335
+ AUTO_APPROVE=$(jq -r '.autoApprove // false' "$CONFIG_FILE" 2>/dev/null)
336
+ if [[ "$AUTO_APPROVE" != "true" ]]; then
337
+ # autoApprove 关闭,继续无限等待用户响应
338
+ while true; do
339
+ sleep 2
340
+ STATUS=$(curl -s -m 3 "http://127.0.0.1:$MCP_PORT/approval_status/$TASK_ID" 2>/dev/null)
341
+ RESULT=$(echo "$STATUS" | jq -r '.result // empty')
400
342
 
401
- STATUS=$(curl -s -m 3 "http://127.0.0.1:$MCP_PORT/approval_status/$TASK_ID" 2>/dev/null)
402
- CURL_EXIT=$?
343
+ if [[ "$RESULT" == "allow-once" || "$RESULT" == "allow-always" ]]; then
344
+ printf '%s\\n' '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow"}}}'
345
+ exit 0
346
+ elif [[ "$RESULT" == "deny" ]]; then
347
+ printf '%s\\n' '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"deny","message":"用户拒绝"}}}'
348
+ exit 0
349
+ fi
350
+ done
351
+ fi
403
352
 
404
- if [[ $CURL_EXIT -ne 0 ]]; then
405
- continue
353
+ # autoApprove: true,执行智能代批
354
+ # 规则:删除命令→拒绝,项目内操作→允许,项目外操作→拒绝
355
+
356
+ # 检查是否是删除命令
357
+ IS_DELETE=0
358
+ if [[ "$TOOL_NAME" == "Bash" ]]; then
359
+ CMD=$(echo "$TOOL_INPUT" | jq -r '.command // empty')
360
+ if [[ "$CMD" == rm* ]] || [[ "$CMD" == *" rm "* ]] || [[ "$CMD" == *"-rf"* ]]; then
361
+ IS_DELETE=1
406
362
  fi
363
+ fi
407
364
 
408
- RESULT=$(echo "$STATUS" | jq -r '.result // empty')
365
+ # 删除操作 永远拒绝
366
+ if [[ $IS_DELETE -eq 1 ]]; then
367
+ # 通知 MCP Server 发送微信消息
368
+ curl -s -m 5 -X POST "http://127.0.0.1:$MCP_PORT/approval_timeout/$TASK_ID" -H "Content-Type: application/json" -d '{"result":"deny","reason":"超时自动拒绝:删除操作需人工确认"}' > /dev/null 2>&1 &
369
+ printf '%s\\n' '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"deny","message":"超时自动拒绝:删除操作需人工确认"}}}'
370
+ exit 0
371
+ fi
409
372
 
410
- if [[ "$RESULT" == "allow-once" || "$RESULT" == "allow-always" ]]; then
411
- printf '%s\\n' '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow"}}}'
412
- exit 0
413
- elif [[ "$RESULT" == "deny" ]]; then
414
- printf '%s\\n' '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"deny","message":"用户拒绝"}}}'
415
- exit 0
416
- fi
417
- done
373
+ # 检查操作路径是否在项目内
374
+ IS_IN_PROJECT=0
375
+
376
+ case "$TOOL_NAME" in
377
+ Bash)
378
+ CMD=$(echo "$TOOL_INPUT" | jq -r '.command // empty')
379
+ if [[ "$CMD" == *"$PROJECT_DIR"* ]] || [[ "$CMD" == ./* ]] || [[ "$CMD" == npm* ]] || [[ "$CMD" == npx* ]] || [[ "$CMD" == git* ]] || [[ "$CMD" == node* ]]; then
380
+ IS_IN_PROJECT=1
381
+ fi
382
+ ;;
383
+ Write|Edit)
384
+ FILE_PATH=$(echo "$TOOL_INPUT" | jq -r '.file_path // empty')
385
+ if [[ "$FILE_PATH" == "$PROJECT_DIR"* ]] || [[ "$FILE_PATH" != /* ]]; then
386
+ IS_IN_PROJECT=1
387
+ fi
388
+ ;;
389
+ *)
390
+ FILE_PATH=$(echo "$TOOL_INPUT" | jq -r '.file_path // .path // .directory // empty')
391
+ if [[ -n "$FILE_PATH" ]]; then
392
+ if [[ "$FILE_PATH" == "$PROJECT_DIR"* ]] || [[ "$FILE_PATH" != /* ]]; then
393
+ IS_IN_PROJECT=1
394
+ fi
395
+ fi
396
+ ;;
397
+ esac
398
+
399
+ # 根据项目内/外决策
400
+ if [[ $IS_IN_PROJECT -eq 1 ]]; then
401
+ # 通知 MCP Server 发送微信消息
402
+ curl -s -m 5 -X POST "http://127.0.0.1:$MCP_PORT/approval_timeout/$TASK_ID" -H "Content-Type: application/json" -d '{"result":"allow-once","reason":"超时自动允许:项目内操作"}' > /dev/null 2>&1 &
403
+ printf '%s\\n' '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow","message":"超时自动允许:项目内操作"}}}'
404
+ else
405
+ # 通知 MCP Server 发送微信消息
406
+ curl -s -m 5 -X POST "http://127.0.0.1:$MCP_PORT/approval_timeout/$TASK_ID" -H "Content-Type: application/json" -d '{"result":"deny","reason":"超时自动拒绝:项目外操作需人工确认"}' > /dev/null 2>&1 &
407
+ printf '%s\\n' '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"deny","message":"超时自动拒绝:项目外操作需人工确认"}}}'
408
+ fi
418
409
  `;
419
410
  ensureConfigDir();
420
411
  fs.writeFileSync(HOOK_SCRIPT_PATH, script, { mode: 0o755 });
@@ -423,7 +414,7 @@ done
423
414
  // 写入 MCP Server 配置到 ~/.claude.json
424
415
  function writeMcpServerConfig(config, instanceName) {
425
416
  try {
426
- // 1. 写入机器人配置到 ~/.wecom-aibot-mcp/robot-${timestamp}.json
417
+ // 1. 写入机器人配置到 ~/.wecom-aibot-mcp/config.json
427
418
  ensureConfigDir();
428
419
  const botConfig = {
429
420
  botId: config.botId,
@@ -433,10 +424,8 @@ function writeMcpServerConfig(config, instanceName) {
433
424
  if (config.nameTag) {
434
425
  botConfig.nameTag = config.nameTag;
435
426
  }
436
- // 所有机器人都使用独立文件,不再有默认 config.json
437
- const robotConfigPath = path.join(CONFIG_DIR, `robot-${Date.now()}.json`);
438
- fs.writeFileSync(robotConfigPath, JSON.stringify(botConfig, null, 2));
439
- console.log(`[config] 机器人配置已写入: ${path.basename(robotConfigPath)}`);
427
+ fs.writeFileSync(BOT_CONFIG_FILE, JSON.stringify(botConfig, null, 2));
428
+ console.log('[config] 机器人配置已写入 ~/.wecom-aibot-mcp/config.json');
440
429
  // 2. 写入 MCP 配置到 ~/.claude.json(仅 URL)
441
430
  let claudeConfig = {};
442
431
  if (fs.existsSync(CLAUDE_CONFIG_FILE)) {
@@ -459,7 +448,7 @@ function writeMcpServerConfig(config, instanceName) {
459
448
  console.error('[config] 写入配置失败:', err);
460
449
  console.log('[config] ⚠️ 请手动配置:');
461
450
  console.log('');
462
- console.log('~/.wecom-aibot-mcp/robot-<timestamp>.json:');
451
+ console.log('~/.wecom-aibot-mcp/config.json:');
463
452
  console.log(JSON.stringify({
464
453
  botId: config.botId,
465
454
  secret: config.secret,
@@ -544,10 +533,19 @@ export async function addMcpConfig() {
544
533
  };
545
534
  // 确保配置目录存在
546
535
  ensureConfigDir();
547
- // 所有机器人都保存为独立文件(不再区分第一个)
536
+ // 如果是第一个机器人,保存为默认配置
537
+ const defaultConfigPath = BOT_CONFIG_FILE;
548
538
  const robotConfigPath = path.join(CONFIG_DIR, `robot-${Date.now()}.json`);
549
- fs.writeFileSync(robotConfigPath, JSON.stringify(robotConfig, null, 2));
550
- console.log(`\n[config] ✅ 已添加机器人: ${robotName}`);
539
+ if (!fs.existsSync(defaultConfigPath)) {
540
+ // 第一个机器人作为默认
541
+ fs.writeFileSync(defaultConfigPath, JSON.stringify(robotConfig, null, 2));
542
+ console.log(`\n[config] ✅ 已设为默认机器人: ${robotName}`);
543
+ }
544
+ else {
545
+ // 后续机器人保存为独立文件
546
+ fs.writeFileSync(robotConfigPath, JSON.stringify(robotConfig, null, 2));
547
+ console.log(`\n[config] ✅ 已添加新机器人: ${robotName}`);
548
+ }
551
549
  console.log(`[config] 用户 ID: ${targetUserId}`);
552
550
  // 列出所有机器人
553
551
  const robots = listAllRobots();