@vrs-soft/wecom-aibot-mcp 2.4.10 → 2.4.12

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/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
@@ -399,9 +399,8 @@ function registerChannelTools(server) {
399
399
  project_dir: z.string().optional().describe('项目目录路径(用于写入配置文件)'),
400
400
  mode: z.enum(['channel', 'http']).optional().default('http')
401
401
  .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 }) => {
402
+ auto_approve_timeout: z.number().optional().default(600).describe('超时自动决策等待时间(秒,默认 600 即 10 分钟)'),
403
+ }, async ({ agent_name, cc_id, robot_id, project_dir, mode, auto_approve_timeout }) => {
405
404
  // 转发请求
406
405
  const result = await forwardToHttpMcp('enter_headless_mode', {
407
406
  agent_name,
@@ -409,7 +408,6 @@ function registerChannelTools(server) {
409
408
  robot_id,
410
409
  project_dir: project_dir || process.cwd(),
411
410
  mode,
412
- auto_approve,
413
411
  auto_approve_timeout,
414
412
  });
415
413
  // 拦截响应,提取 ccId,建立 SSE 连接
@@ -425,6 +423,9 @@ function registerChannelTools(server) {
425
423
  const localProjectDir = project_dir || process.cwd();
426
424
  const hookResult = addPermissionHook(localProjectDir);
427
425
  logChannel('本地 PermissionRequest hook 已写入', { path: hookResult.path, success: hookResult.success });
426
+ // 注册本地 PID → projectDir(供本地 permission-hook.sh 通过进程树匹配项目)
427
+ registerActiveProject(process.ppid ?? process.pid, localProjectDir);
428
+ logChannel('本地 active-projects 已注册', { pid: process.ppid ?? process.pid, projectDir: localProjectDir });
428
429
  // Channel 模式:过滤 heartbeat 信息,简化消息
429
430
  if (mode === 'channel' || parsed.mode === 'channel') {
430
431
  delete parsed.heartbeat; // Channel 模式不需要 heartbeat loop
@@ -448,6 +449,7 @@ function registerChannelTools(server) {
448
449
  cc_id: z.string().describe('CC 唯一标识(enter_headless_mode 返回的 ccId)'),
449
450
  project_dir: z.string().optional().describe('项目目录路径(用于更新配置文件)'),
450
451
  }, async ({ cc_id, project_dir }) => {
452
+ const localProjectDir = project_dir || process.cwd();
451
453
  // 断开 SSE 连接(abort 后重连逻辑不会触发)
452
454
  if (sseAbortController) {
453
455
  sseAbortController.abort();
@@ -456,7 +458,10 @@ function registerChannelTools(server) {
456
458
  sseCurrentCcId = undefined;
457
459
  logChannel('SSE disconnected', { cc_id });
458
460
  }
459
- return forwardToHttpMcp('exit_headless_mode', { cc_id, project_dir: project_dir || process.cwd() });
461
+ // 注销本地 active-projects 记录
462
+ unregisterActiveProject(localProjectDir);
463
+ logChannel('本地 active-projects 已注销', { projectDir: localProjectDir });
464
+ return forwardToHttpMcp('exit_headless_mode', { cc_id, project_dir: localProjectDir });
460
465
  });
461
466
  // ============================================
462
467
  // 工具 11: 从消息识别用户
@@ -605,7 +610,7 @@ export async function startChannelServer() {
605
610
  tools: {},
606
611
  },
607
612
  // 告知 Claude 如何处理 channel 事件
608
- instructions: '企业微信消息通过 <channel> 标签推送。属性说明:from=发送者userid, chatid=会话ID(单聊=用户ID,群聊=群ID), chattype=single|group, cc_id=当前会话标识。收到消息后:1) 发送确认 send_message(cc_id, "收到...", target_user=chatid);2) 处理任务;3) 发送结果 send_message(cc_id, "【完成】...", target_user=chatid)。',
613
+ instructions: '企业微信消息通过 <channel> 标签推送。属性说明:from=发送者userid, chatid=会话ID(单聊=用户ID,群聊=群ID), chattype=single|group, cc_id=当前会话标识。【强制规则】收到任何用户消息后,必须先执行步骤1再执行步骤2,禁止跳过:1) 立即发送确认 send_message(cc_id, "收到,正在处理...", target_user=chatid);2) 处理任务;3) 发送结果 send_message(cc_id, "【完成】...", target_user=chatid)。',
609
614
  });
610
615
  // 注册工具
611
616
  registerChannelTools(mcpServer);
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();
@@ -344,7 +368,8 @@ class WecomClient extends EventEmitter {
344
368
  }
345
369
  // 发送审批请求(带按钮的模板卡片)
346
370
  async sendApprovalRequest(title, description, requestId, targetUser, toolInput, // v3.0: 用于去重
347
- ccId // v3.0: 参与哈希,防止跨 CC 复用审批
371
+ ccId, // v3.0: 参与哈希,防止跨 CC 复用审批
372
+ detailUrlBase // 详情页 h5 链接的 base(最终 URL = base/taskId)
348
373
  ) {
349
374
  const userId = targetUser || this.targetUserId;
350
375
  // 从 title 中提取工具名称(格式: 【待审批】Bash)
@@ -360,6 +385,7 @@ class WecomClient extends EventEmitter {
360
385
  }
361
386
  const taskId = `approval_${requestId}_${Date.now()}`;
362
387
  const operationHash = toolInput && toolName ? hashOperation(ccId ?? '', toolName, toolInput) : undefined;
388
+ const detailUrl = detailUrlBase ? `${detailUrlBase}/${taskId}` : undefined;
363
389
  // 始终存储审批记录(断线时也需要,让 Hook 能轮询到)
364
390
  this.approvals.set(taskId, {
365
391
  taskId,
@@ -370,6 +396,7 @@ class WecomClient extends EventEmitter {
370
396
  description, // 保存审批请求原文
371
397
  operationHash,
372
398
  ccId, // 保存 ccId,用于 SSE 推送审批结果
399
+ detailUrl, // 供排队重发时复用
373
400
  });
374
401
  // 断线时将审批请求加入队列,等待重连后发送
375
402
  if (!this.connected) {
@@ -383,22 +410,7 @@ class WecomClient extends EventEmitter {
383
410
  // 返回 taskId,审批记录已创建,等待重连后发送
384
411
  return taskId;
385
412
  }
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
- });
413
+ await this.wsClient.sendMessage(userId, buildApprovalCard(title, description, taskId, detailUrl));
402
414
  logger.log(`[wecom] 已发送审批请求到 ${userId}: ${taskId}`);
403
415
  return taskId;
404
416
  }
@@ -415,22 +427,7 @@ class WecomClient extends EventEmitter {
415
427
  return false;
416
428
  }
417
429
  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
- });
430
+ await this.wsClient.sendMessage(userId, buildApprovalCard(title, description, taskId, approval.detailUrl));
434
431
  logger.log(`[wecom] 已发送排队审批请求到 ${userId}: ${taskId}`);
435
432
  return true;
436
433
  }
@@ -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"
@@ -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>此页面随审批记录自动过期清理 · 请回到企业微信卡片点击审批按钮</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);
@@ -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.10",
3
+ "version": "2.4.12",
4
4
  "description": "企业微信智能机器人 MCP 服务 - Claude Code 审批通道",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",