@vrs-soft/wecom-aibot-mcp 2.4.7 → 2.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -187,7 +187,8 @@ function connectSSE(ccId) {
187
187
  }
188
188
  sseConnected = true;
189
189
  sseCurrentCcId = ccId;
190
- const sseUrl = ccId ? `${MCP_URL}/sse/${ccId}` : `${MCP_URL}/sse`;
190
+ // SSE URL 添加 ccId 查询参数用于授权验证
191
+ const sseUrl = ccId ? `${MCP_URL}/sse/${ccId}?ccId=${ccId}` : `${MCP_URL}/sse`;
191
192
  logChannel('Connecting to SSE', { url: sseUrl, ccId, mcpServerReady: mcpServer ? 'yes' : 'no' });
192
193
  sseAbortController = new AbortController();
193
194
  // SSE fetch 配置:添加 keep-alive headers 确保连接稳定
package/dist/client.d.ts CHANGED
@@ -13,6 +13,7 @@ interface ApprovalRecord {
13
13
  keepaliveCount?: number;
14
14
  operationHash?: string;
15
15
  consumed?: boolean;
16
+ ccId?: string;
16
17
  }
17
18
  interface MessageRecord {
18
19
  seq: number;
@@ -114,5 +115,4 @@ declare class WecomClient extends EventEmitter {
114
115
  };
115
116
  }
116
117
  export declare function initClient(botId: string, secret: string, targetUserId: string, robotName: string): WecomClient;
117
- export declare function getClient(): WecomClient;
118
118
  export { WecomClient, ApprovalRecord, MessageRecord, PendingMessage, MAX_PENDING_MESSAGES };
package/dist/client.js CHANGED
@@ -12,7 +12,7 @@
12
12
  import AiBot from '@wecom/aibot-node-sdk';
13
13
  import { EventEmitter } from 'events';
14
14
  import { logConnected, logAuthenticated, logDisconnected, logReconnecting, logError, } from './connection-log.js';
15
- import { publishWecomMessage } from './message-bus.js';
15
+ import { publishWecomMessage, publishApprovalEvent } from './message-bus.js';
16
16
  import { hashOperation } from './utils/hash.js';
17
17
  import { logger } from './logger.js';
18
18
  // 最大待处理消息数量
@@ -199,9 +199,14 @@ class WecomClient extends EventEmitter {
199
199
  const cardEvent = event.template_card_event;
200
200
  const taskId = cardEvent?.task_id;
201
201
  const eventKey = cardEvent?.event_key; // 用户点击的按钮 key
202
- logger.log('wecom', `taskId=${taskId}, eventKey=${eventKey}, approvals keys: ${[...this.approvals.keys()].join(',')}`);
203
- if (!taskId)
202
+ // 打印当前 approvals Map 的所有 keys(诊断用)
203
+ const allTaskIds = [...this.approvals.keys()];
204
+ logger.log('wecom', `审批响应详情: taskId=${taskId}, eventKey=${eventKey}`);
205
+ logger.log('wecom', `当前 approvals keys (${allTaskIds.length}个): ${allTaskIds.join(',')}`);
206
+ if (!taskId) {
207
+ logger.log('wecom', `taskId 为空,cardEvent: ${JSON.stringify(cardEvent)}`);
204
208
  return;
209
+ }
205
210
  logger.log('wecom', `收到审批响应: taskId=${taskId}, key=${eventKey}`);
206
211
  const approval = this.approvals.get(taskId);
207
212
  if (approval && !approval.resolved) {
@@ -209,6 +214,15 @@ class WecomClient extends EventEmitter {
209
214
  approval.result = eventKey;
210
215
  approval.timestamp = Date.now();
211
216
  this.emit('approval_resolved', { taskId, result: approval.result });
217
+ logger.log('wecom', `审批已解决: taskId=${taskId}, result=${approval.result}`);
218
+ // 发布审批事件到消息总线(用于 SSE 推送)
219
+ publishApprovalEvent({
220
+ robotName: this.robotName,
221
+ taskId,
222
+ result: approval.result,
223
+ ccId: approval.ccId,
224
+ timestamp: Date.now(),
225
+ });
212
226
  // 发送确认消息给用户
213
227
  const resultText = eventKey === 'allow-once' ? '✅ 已允许(本次)'
214
228
  : eventKey === 'allow-always' ? '✅ 已允许(永久)'
@@ -224,7 +238,7 @@ class WecomClient extends EventEmitter {
224
238
  logger.log('wecom', `审批已解决,跳过点击: ${taskId}, resolved=${approval.resolved}, result=${approval.result}`);
225
239
  }
226
240
  else {
227
- logger.log('wecom', `审批记录不存在: ${taskId}`);
241
+ logger.log('wecom', `审批记录不存在: ${taskId}, 可能原因: 1) taskId 不匹配 2) 审批已过期清理 3) 审批由其他 client 创建`);
228
242
  }
229
243
  }
230
244
  // 使用 reply 方法回复审批结果(会有引用效果)
@@ -355,6 +369,7 @@ class WecomClient extends EventEmitter {
355
369
  toolInput,
356
370
  description, // 保存审批请求原文
357
371
  operationHash,
372
+ ccId, // 保存 ccId,用于 SSE 推送审批结果
358
373
  });
359
374
  // 断线时将审批请求加入队列,等待重连后发送
360
375
  if (!this.connected) {
@@ -368,13 +383,14 @@ class WecomClient extends EventEmitter {
368
383
  // 返回 taskId,审批记录已创建,等待重连后发送
369
384
  return taskId;
370
385
  }
371
- // 发送模板卡片
386
+ // 发送模板卡片(在 description 中显示 taskId 便于用户识别)
387
+ const displayDesc = description + `\n\n📋 TaskID: ${taskId}`;
372
388
  await this.wsClient.sendMessage(userId, {
373
389
  msgtype: 'template_card',
374
390
  template_card: {
375
391
  card_type: 'button_interaction',
376
392
  main_title: { title },
377
- sub_title_text: description,
393
+ sub_title_text: displayDesc,
378
394
  button_list: [
379
395
  { text: '允许', key: 'allow-once', style: 1 },
380
396
  { text: '默认', key: 'allow-always', style: 1 },
@@ -399,13 +415,14 @@ class WecomClient extends EventEmitter {
399
415
  return false;
400
416
  }
401
417
  const userId = targetUser || this.targetUserId;
402
- // 发送模板卡片
418
+ // 发送模板卡片(在 description 中显示 taskId 便于用户识别)
419
+ const displayDesc = description + `\n\n📋 TaskID: ${taskId}`;
403
420
  await this.wsClient.sendMessage(userId, {
404
421
  msgtype: 'template_card',
405
422
  template_card: {
406
423
  card_type: 'button_interaction',
407
424
  main_title: { title },
408
- sub_title_text: description,
425
+ sub_title_text: displayDesc,
409
426
  button_list: [
410
427
  { text: '允许', key: 'allow-once', style: 1 },
411
428
  { text: '默认', key: 'allow-always', style: 1 },
@@ -646,7 +663,7 @@ class WecomClient extends EventEmitter {
646
663
  };
647
664
  }
648
665
  }
649
- // 单例实例
666
+ // 单例实例(用于配置验证等场景)
650
667
  let instance = null;
651
668
  export function initClient(botId, secret, targetUserId, robotName) {
652
669
  if (instance) {
@@ -656,10 +673,6 @@ export function initClient(botId, secret, targetUserId, robotName) {
656
673
  instance.connect();
657
674
  return instance;
658
675
  }
659
- export function getClient() {
660
- if (!instance) {
661
- throw new Error('WecomClient 未初始化,请先调用 initClient');
662
- }
663
- return instance;
664
- }
676
+ // 注意:无参数版本的 getClient() 已删除
677
+ // 请使用 connection-manager.ts 的 getClient(robotName) 获取已连接的客户端
665
678
  export { WecomClient, MAX_PENDING_MESSAGES };
@@ -19,7 +19,7 @@ const SERVER_CONFIG_FILE = path.join(CONFIG_DIR, 'server.json'); // HTTP Server
19
19
  const CLAUDE_CONFIG_FILE = path.join(os.homedir(), '.claude.json');
20
20
  const CLAUDE_SETTINGS_FILE = path.join(os.homedir(), '.claude', 'settings.local.json');
21
21
  const HOOK_SCRIPT_PATH = path.join(CONFIG_DIR, 'permission-hook.sh');
22
- const TASK_COMPLETED_HOOK_SCRIPT_PATH = path.join(CONFIG_DIR, 'task-completed-hook.sh');
22
+ const STOP_HOOK_SCRIPT_PATH = path.join(CONFIG_DIR, 'stop-hook.sh');
23
23
  // Skill 模板路径(包内)- 使用 fileURLToPath 确保跨平台兼容
24
24
  const __filename = fileURLToPath(import.meta.url);
25
25
  const __dirname = path.dirname(__filename);
@@ -656,19 +656,19 @@ fi
656
656
  fs.writeFileSync(HOOK_SCRIPT_PATH, script, { mode: 0o755 });
657
657
  console.log(`[config] Hook 脚本已写入: ${HOOK_SCRIPT_PATH}`);
658
658
  }
659
- // 生成并写入 TaskCompleted hook 脚本
660
- // 用于任务完成后自动恢复微信消息轮询
661
- function writeTaskCompletedHookScript() {
659
+ // 生成并写入 Stop hook 脚本
660
+ // HTTP 模式使用:阻止 Claude 停止,提示调用 get_pending_messages 恢复轮询
661
+ function writeStopHookScript() {
662
662
  const script = `#!/bin/bash
663
- # wecom-aibot-mcp TaskCompleted hook
664
- # 任务完成后检查是否需要恢复微信消息轮询
663
+ # wecom-aibot-mcp Stop hook
664
+ # HTTP 模式使用:阻止 Claude 停止,提示调用 get_pending_messages 恢复轮询
665
665
  #
666
666
  # 固定端口: 18963
667
667
  # 检查 $(pwd)/.claude/wecom-aibot.json 的 wechatMode 和 autoApprove 字段
668
668
 
669
669
  MCP_PORT=18963
670
670
 
671
- # 先保存输入(TaskCompleted 事件数据)
671
+ # 先保存输入(Stop 事件数据)
672
672
  INPUT=$(cat)
673
673
 
674
674
  # 日志输出:--debug 模式下输出到 stderr,否则静默
@@ -679,7 +679,7 @@ log_debug() {
679
679
  fi
680
680
  }
681
681
 
682
- log_debug "[$(date)] TaskCompleted hook called. INPUT: \${INPUT:0:200}"
682
+ log_debug "[$(date)] Stop hook called. INPUT: \${INPUT:0:200}"
683
683
 
684
684
  # 检查项目目录的微信模式配置文件
685
685
  PROJECT_DIR=$(pwd)
@@ -687,9 +687,9 @@ CONFIG_FILE="$PROJECT_DIR/.claude/wecom-aibot.json"
687
687
 
688
688
  log_debug "[$(date)] Checking config: $CONFIG_FILE"
689
689
 
690
- # 配置文件不存在,不在微信模式
690
+ # 配置文件不存在,不在微信模式,允许停止
691
691
  if [[ ! -f "$CONFIG_FILE" ]]; then
692
- log_debug "[$(date)] No config file, exit 0 (allow complete)"
692
+ log_debug "[$(date)] No config file, exit 0 (allow stop)"
693
693
  exit 0
694
694
  fi
695
695
 
@@ -697,7 +697,7 @@ fi
697
697
  WECHAT_MODE=$(jq -r '.wechatMode // false' "$CONFIG_FILE" 2>/dev/null)
698
698
  log_debug "[$(date)] wechatMode: $WECHAT_MODE"
699
699
  if [[ "$WECHAT_MODE" != "true" ]]; then
700
- log_debug "[$(date)] wechatMode not true, exit 0 (allow complete)"
700
+ log_debug "[$(date)] wechatMode not true, exit 0 (allow stop)"
701
701
  exit 0
702
702
  fi
703
703
 
@@ -705,7 +705,7 @@ fi
705
705
  AUTO_APPROVE=$(jq -r '.autoApprove // false' "$CONFIG_FILE" 2>/dev/null)
706
706
  log_debug "[$(date)] autoApprove: $AUTO_APPROVE"
707
707
  if [[ "$AUTO_APPROVE" != "true" ]]; then
708
- log_debug "[$(date)] autoApprove not true, exit 0 (allow complete)"
708
+ log_debug "[$(date)] autoApprove not true, exit 0 (allow stop)"
709
709
  exit 0
710
710
  fi
711
711
 
@@ -727,15 +727,15 @@ if ! echo "$HEALTH" | jq -e '.status == "ok"' > /dev/null 2>&1; then
727
727
  [[ -n "$REMOTE_TOKEN" ]] && AUTH_ARGS=(-H "Authorization: Bearer $REMOTE_TOKEN")
728
728
  log_debug "[$(date)] Using remote server: $MCP_BASE_URL"
729
729
  else
730
- log_debug "[$(date)] MCP Server offline, exit 0 (allow complete)"
730
+ log_debug "[$(date)] MCP Server offline, exit 0 (allow stop)"
731
731
  exit 0
732
732
  fi
733
733
  else
734
- log_debug "[$(date)] MCP Server offline, exit 0 (allow complete)"
734
+ log_debug "[$(date)] MCP Server offline, exit 0 (allow stop)"
735
735
  exit 0
736
736
  fi
737
737
  else
738
- log_debug "[$(date)] MCP Server offline, exit 0 (allow complete)"
738
+ log_debug "[$(date)] MCP Server offline, exit 0 (allow stop)"
739
739
  exit 0
740
740
  fi
741
741
  fi
@@ -744,20 +744,20 @@ fi
744
744
  CC_ID=$(jq -r '.ccId // empty' "$CONFIG_FILE" 2>/dev/null)
745
745
  log_debug "[$(date)] ccId: $CC_ID"
746
746
  if [[ -z "$CC_ID" ]]; then
747
- log_debug "[$(date)] No ccId in config, exit 0 (allow complete)"
747
+ log_debug "[$(date)] No ccId in config, exit 0 (allow stop)"
748
748
  exit 0
749
749
  fi
750
750
 
751
751
  # 处于微信模式且 autoApprove 为 true,需要恢复轮询
752
- # 使用 exit code 2 阻止任务完成,并提示 Claude 调用 MCP 工具
753
- log_debug "[$(date)] ✅ WeChat mode active, blocking completion to resume polling"
752
+ # 使用 exit code 2 阻止停止,并提示 Claude 调用 MCP 工具
753
+ log_debug "[$(date)] ✅ WeChat mode active, blocking stop to resume polling"
754
754
  log_debug "[$(date)] ccId=$CC_ID, will prompt Claude to call get_pending_messages"
755
755
  echo "任务已完成,请调用 mcp__wecom-aibot__get_pending_messages(cc_id=\"$CC_ID\", timeout_ms=30000) 恢复微信消息轮询" >&2
756
756
  exit 2
757
757
  `;
758
758
  ensureConfigDir();
759
- fs.writeFileSync(TASK_COMPLETED_HOOK_SCRIPT_PATH, script, { mode: 0o755 });
760
- console.log(`[config] TaskCompleted Hook 脚本已写入: ${TASK_COMPLETED_HOOK_SCRIPT_PATH}`);
759
+ fs.writeFileSync(STOP_HOOK_SCRIPT_PATH, script, { mode: 0o755 });
760
+ console.log(`[config] Stop Hook 脚本已写入: ${STOP_HOOK_SCRIPT_PATH}`);
761
761
  }
762
762
  // 写入 MCP Server 配置到 ~/.claude.json
763
763
  function writeMcpServerConfig(config, instanceName) {
@@ -996,7 +996,7 @@ export function getDocMcpUrl(robotName) {
996
996
  error: `有多个机器人配置了文档 MCP URL,请通过 robot_name 参数指定使用哪个机器人。已配置文档能力的机器人: ${robotsWithDoc.map(r => r.name).join(', ')}`,
997
997
  };
998
998
  }
999
- // 写入 MCP 工具权限 + 注册 PermissionRequest hook 到 Claude settings
999
+ // 写入 MCP 工具权限到 Claude settings
1000
1000
  function writeMcpPermissions() {
1001
1001
  try {
1002
1002
  // 确保目录存在
@@ -1020,18 +1020,11 @@ function writeMcpPermissions() {
1020
1020
  if (!existingPerms.has(perm))
1021
1021
  settings.permissions.allow.push(perm);
1022
1022
  }
1023
- // 注册全局 PermissionRequest hook(支持 channel 模式,hook 内部有 wechatMode 检查)
1024
- if (!settings.hooks)
1025
- settings.hooks = {};
1026
- if (!settings.hooks['PermissionRequest'])
1027
- settings.hooks['PermissionRequest'] = [];
1028
- const hookCommand = HOOK_SCRIPT_PATH;
1029
- const alreadyRegistered = settings.hooks['PermissionRequest'].some((entry) => entry.hooks?.some?.((h) => h.command === hookCommand));
1030
- if (!alreadyRegistered) {
1031
- settings.hooks['PermissionRequest'].push({ hooks: [{ type: 'command', command: hookCommand }] });
1032
- }
1023
+ // 注意:PermissionRequest hook 通过项目级 settings.json 配置,不注册全局 hook
1024
+ // HTTP 模式:enter_headless_mode 在项目目录写入 hook
1025
+ // Channel 模式:由 Claude Code 自动审批,不需要 hook
1033
1026
  fs.writeFileSync(CLAUDE_SETTINGS_FILE, JSON.stringify(settings, null, 2));
1034
- // 确保 hook 脚本文件存在
1027
+ // 确保 hook 脚本文件存在(供项目级 hook 引用)
1035
1028
  writeHookScript();
1036
1029
  }
1037
1030
  catch (err) {
@@ -1042,7 +1035,7 @@ function writeMcpPermissions() {
1042
1035
  // 确保 hook 已安装(幂等,可多次调用)
1043
1036
  export function ensureHookInstalled() {
1044
1037
  writeMcpPermissions();
1045
- writeTaskCompletedHookScript();
1038
+ writeStopHookScript();
1046
1039
  }
1047
1040
  // 确保所有全局配置已写入(强制覆盖,不依赖智能体)
1048
1041
  export function ensureGlobalConfigs(mode = 'full', remoteOptions) {
@@ -50,7 +50,7 @@ export declare function isHeadlessMode(projectDir?: string): boolean;
50
50
  * 清理所有孤儿状态文件
51
51
  *
52
52
  * 扫描全局索引,检查每个项目的状态文件是否存在
53
- * 如果状态文件不存在,从索引中移除并清理 Hook 配置
53
+ * 如果状态文件不存在,从索引中移除
54
54
  */
55
55
  export declare function cleanupOrphanFiles(): void;
56
56
  /**
@@ -17,8 +17,8 @@ const CONFIG_DIR = path.join(os.homedir(), '.wecom-aibot-mcp');
17
17
  const HEADLESS_INDEX_FILE = path.join(CONFIG_DIR, 'headless-index.json');
18
18
  // 项目状态文件路径
19
19
  const PROJECT_STATE_FILE = 'headless.json';
20
- // Hook 脚本路径
21
- const HOOK_SCRIPT_PATH = path.join(CONFIG_DIR, 'permission-hook.sh');
20
+ // 注意:Hook 配置由 tools/index.ts 通过 project-config.ts 的函数统一管理
21
+ // PERMISSION_HOOK_SCRIPT_PATH 定义在 project-config.ts
22
22
  /**
23
23
  * 确保配置目录存在
24
24
  */
@@ -85,8 +85,7 @@ export function enterHeadlessMode(projectDir, agentName, robotName) {
85
85
  index.push(projectDir);
86
86
  writeHeadlessIndex(index);
87
87
  }
88
- // 3. 写入项目级 Hook 配置
89
- configureProjectHook(projectDir);
88
+ // 3. Hook 配置由 tools/index.ts 的 enter_headless_mode 工具调用 addPermissionHook 管理
90
89
  return state;
91
90
  }
92
91
  /**
@@ -114,8 +113,7 @@ export function exitHeadlessMode(projectDir) {
114
113
  const index = readHeadlessIndex();
115
114
  const newIndex = index.filter(p => p !== state.projectDir);
116
115
  writeHeadlessIndex(newIndex);
117
- // 3. 清除项目级 Hook 配置
118
- clearProjectHook(state.projectDir);
116
+ // 3. Hook 配置由 tools/index.ts 的 exit_headless_mode 工具调用 removePermissionHook 管理
119
117
  return state;
120
118
  }
121
119
  /**
@@ -159,77 +157,13 @@ export function isHeadlessMode(projectDir) {
159
157
  const dir = projectDir || process.cwd();
160
158
  return fs.existsSync(getProjectHeadlessFile(dir));
161
159
  }
162
- /**
163
- * 配置项目级 Hook
164
- */
165
- function configureProjectHook(projectDir) {
166
- const settingsDir = path.join(projectDir, '.claude');
167
- const settingsPath = path.join(settingsDir, 'settings.json');
168
- // 确保目录存在
169
- if (!fs.existsSync(settingsDir)) {
170
- fs.mkdirSync(settingsDir, { recursive: true });
171
- }
172
- // 读取现有配置
173
- let settings = {};
174
- if (fs.existsSync(settingsPath)) {
175
- try {
176
- const content = fs.readFileSync(settingsPath, 'utf-8');
177
- settings = JSON.parse(content);
178
- }
179
- catch (err) {
180
- logger.error(`[headless] 读取 settings.json 失败: ${settingsPath}`, err);
181
- }
182
- }
183
- // 设置 PermissionRequest hook
184
- if (!settings.hooks) {
185
- settings.hooks = {};
186
- }
187
- settings.hooks['PermissionRequest'] = [
188
- {
189
- matcher: '',
190
- hooks: [
191
- {
192
- type: 'command',
193
- command: HOOK_SCRIPT_PATH,
194
- timeout: 600,
195
- },
196
- ],
197
- },
198
- ];
199
- // 写入配置
200
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
201
- logger.log(`[headless] 已配置项目 Hook: ${settingsPath}`);
202
- }
203
- /**
204
- * 清除项目级 Hook 配置
205
- */
206
- function clearProjectHook(projectDir) {
207
- const settingsPath = path.join(projectDir, '.claude', 'settings.json');
208
- if (!fs.existsSync(settingsPath)) {
209
- return;
210
- }
211
- try {
212
- const content = fs.readFileSync(settingsPath, 'utf-8');
213
- const settings = JSON.parse(content);
214
- if (settings.hooks && settings.hooks['PermissionRequest']) {
215
- delete settings.hooks['PermissionRequest'];
216
- // 如果 hooks 对象为空,删除整个 hooks 字段
217
- if (Object.keys(settings.hooks).length === 0) {
218
- delete settings.hooks;
219
- }
220
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
221
- logger.log(`[headless] 已清除项目 Hook: ${settingsPath}`);
222
- }
223
- }
224
- catch (err) {
225
- logger.error(`[headless] 清除项目 Hook 失败: ${settingsPath}`, err);
226
- }
227
- }
160
+ // 注意:configureProjectHook 和 clearProjectHook 已移除
161
+ // Hook 配置由 tools/index.ts 通过 project-config.ts 的函数统一管理
228
162
  /**
229
163
  * 清理所有孤儿状态文件
230
164
  *
231
165
  * 扫描全局索引,检查每个项目的状态文件是否存在
232
- * 如果状态文件不存在,从索引中移除并清理 Hook 配置
166
+ * 如果状态文件不存在,从索引中移除
233
167
  */
234
168
  export function cleanupOrphanFiles() {
235
169
  ensureConfigDir();
@@ -241,8 +175,7 @@ export function cleanupOrphanFiles() {
241
175
  validProjects.push(projectDir);
242
176
  }
243
177
  else {
244
- // 状态文件不存在,清理 Hook 配置
245
- clearProjectHook(projectDir);
178
+ // 状态文件不存在,仅从索引移除,Hook 清理由用户手动处理
246
179
  logger.log(`[headless] 清理孤儿项目: ${projectDir}`);
247
180
  }
248
181
  }
@@ -23,7 +23,7 @@ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/
23
23
  import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
24
24
  import { registerTools } from './tools/index.js';
25
25
  import { getClient, getConnectionState, getAllConnectionStates, connectAllRobots } from './connection-manager.js';
26
- import { subscribeWecomMessage, getSubscriberCount } from './message-bus.js';
26
+ import { subscribeWecomMessage } from './message-bus.js';
27
27
  import { listAllRobots, VERSION, getAuthToken } from './config-wizard.js';
28
28
  import { logger } from './logger.js';
29
29
  // ESM 兼容的 __dirname
@@ -44,10 +44,20 @@ function sanitizeAgentName(agentName) {
44
44
  // 简化名称:移除特殊字符,限制长度
45
45
  return agentName.replace(/[^a-zA-Z0-9\u4e00-\u9fa5_-]/g, '').slice(0, 20);
46
46
  }
47
- // ccId 生成器(基于 agentName)- 不自动编号
47
+ // ccId 生成器(基于 agentName)- 自动避免冲突
48
48
  export function generateCcId(agentName) {
49
- const name = agentName ? sanitizeAgentName(agentName) : 'cc';
50
- return name; // 直接返回名称,不添加编号
49
+ const base = agentName ? sanitizeAgentName(agentName) : 'cc';
50
+ // 无冲突:直接使用
51
+ if (!ccIdRegistry.has(base))
52
+ return base;
53
+ // 有冲突:自动添加数字后缀(-2, -3, ...)
54
+ for (let i = 2; i <= 99; i++) {
55
+ const candidate = `${base}-${i}`;
56
+ if (!ccIdRegistry.has(candidate))
57
+ return candidate;
58
+ }
59
+ // 兜底:使用时间戳保证唯一性
60
+ return `${base}-${Date.now()}`;
51
61
  }
52
62
  // 推送微信消息到 MCP 客户端(通过 SSE notification)
53
63
  export async function pushMessageToSession(robotName, message) {
@@ -194,6 +204,20 @@ function initMcpServer() {
194
204
  subscribeWecomMessage((msg) => {
195
205
  handleWecomMessage(msg);
196
206
  });
207
+ // 定时清理过期审批条目(每 5 分钟清理超过 15 分钟的条目)
208
+ setInterval(() => {
209
+ const cutoff = Date.now() - 15 * 60 * 1000;
210
+ let cleaned = 0;
211
+ for (const [id, entry] of pendingApprovals) {
212
+ if (entry.createdAt < cutoff) {
213
+ pendingApprovals.delete(id);
214
+ cleaned++;
215
+ }
216
+ }
217
+ if (cleaned > 0) {
218
+ logger.log(`[http] 定时清理过期审批: ${cleaned} 条`);
219
+ }
220
+ }, 5 * 60 * 1000);
197
221
  }
198
222
  // 创建新的 MCP Server 实例
199
223
  function createMcpServerInstance() {
@@ -303,13 +327,14 @@ async function handleWecomMessage(msg) {
303
327
  logger.log('[http] 无活跃 MCP session,跳过消息处理');
304
328
  return;
305
329
  }
306
- const subscriberCount = getSubscriberCount(msg.robotName);
307
- logger.log(`[http] 机器人 ${msg.robotName} 订阅数: ${subscriberCount}`);
308
- if (subscriberCount === 0) {
309
- logger.log('[http] 无订阅者,跳过消息处理');
330
+ // HTTP 模式下检查是否有在线 CC(而非 subscriberCount,后者只对 Channel 模式有效)
331
+ const ccCount = getCCCount();
332
+ logger.log(`[http] 当前在线 CC 数: ${ccCount}`);
333
+ if (ccCount === 0) {
334
+ logger.log('[http] 无在线 CC,跳过消息处理');
310
335
  return;
311
336
  }
312
- if (subscriberCount === 1) {
337
+ if (ccCount === 1) {
313
338
  // 只有一个订阅者,直接广播(SSE 检查已在前面完成)
314
339
  logger.log('[http] 单订阅者 HTTP 模式,直接广播');
315
340
  for (const [sessionId, sessEntry] of transports) {
@@ -460,7 +485,7 @@ async function sendNoReferencePrompt(msg) {
460
485
  ${onlineList.map(id => `• 【${id}】`).join('\n')}
461
486
 
462
487
  示例:引用【${onlineList[0]}】的消息后回复`;
463
- await client.sendText(reply);
488
+ await client.sendText(reply, msg.chatid); // 发送到原始会话(群聊或单聊)
464
489
  }
465
490
  export async function startHttpServer(_server, port = HTTP_PORT, httpsConfig) {
466
491
  startTime = Date.now();
@@ -468,7 +493,9 @@ export async function startHttpServer(_server, port = HTTP_PORT, httpsConfig) {
468
493
  initMcpServer();
469
494
  return new Promise((resolve, reject) => {
470
495
  const requestHandler = async (req, res) => {
471
- res.setHeader('Access-Control-Allow-Origin', '*');
496
+ // CORS 设置:HTTPS 模式收紧为 'null',HTTP 本地模式宽松为 '*'
497
+ const isPublicMode = !!httpsConfig;
498
+ res.setHeader('Access-Control-Allow-Origin', isPublicMode ? 'null' : '*');
472
499
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
473
500
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Mcp-Session-Id');
474
501
  // 必须暴露 Mcp-Session-Id 头,让客户端能看到
@@ -630,6 +657,23 @@ export async function startHttpServer(_server, port = HTTP_PORT, httpsConfig) {
630
657
  res.end(JSON.stringify({ ok: true, ...result }));
631
658
  return;
632
659
  }
660
+ // ============================================
661
+ // 调试端点统一拦截
662
+ // ============================================
663
+ if (url.startsWith('/debug/')) {
664
+ // 生产环境禁用所有 debug 端点
665
+ if (process.env.NODE_ENV === 'production') {
666
+ res.writeHead(404);
667
+ res.end();
668
+ return;
669
+ }
670
+ // debug/sampling 额外要求配置了 Auth Token
671
+ if (url === '/debug/sampling' && !getAuthToken()) {
672
+ res.writeHead(403, { 'Content-Type': 'application/json' });
673
+ res.end(JSON.stringify({ error: 'debug/sampling 需要配置 Auth Token' }));
674
+ return;
675
+ }
676
+ }
633
677
  // 临时调试端点:手动进入 headless 模式
634
678
  if (req.method === 'POST' && url === '/debug/enter_headless') {
635
679
  const ccId = `debug-${Date.now()}`;
@@ -853,6 +897,7 @@ async function handleApprovalRequest(req, res) {
853
897
  taskId,
854
898
  status: 'pending',
855
899
  timestamp: Date.now(),
900
+ createdAt: Date.now(), // 写入时间,用于定时清理
856
901
  tool_name,
857
902
  tool_input,
858
903
  description,
@@ -878,6 +923,11 @@ function handleApprovalStatus(_req, res, url) {
878
923
  // 更新审批状态
879
924
  if (result !== 'pending') {
880
925
  entry.status = result;
926
+ // 延迟 5 分钟删除,给 Hook 最后一次轮询窗口
927
+ setTimeout(() => {
928
+ pendingApprovals.delete(taskId);
929
+ logger.log(`[http] 审批条目已清理: taskId=${taskId}`);
930
+ }, 5 * 60 * 1000);
881
931
  }
882
932
  res.writeHead(200, { 'Content-Type': 'application/json' });
883
933
  res.end(JSON.stringify({ status: result, result }));
@@ -892,9 +942,10 @@ function handleApprovalStatus(_req, res, url) {
892
942
  });
893
943
  return;
894
944
  }
895
- // 没找到对应的待处理审批,返回 pending
896
- res.writeHead(200, { 'Content-Type': 'application/json' });
897
- res.end(JSON.stringify({ status: 'pending', result: 'pending' }));
945
+ // 未找到 → 返回 404,让 Hook 识别"审批已丢失"并退出
946
+ logger.log(`[http] pendingApprovals 中未找到 taskId=${taskId}`);
947
+ res.writeHead(404, { 'Content-Type': 'application/json' });
948
+ res.end(JSON.stringify({ error: 'taskId not found', taskId }));
898
949
  }
899
950
  async function handleApprovalTimeout(req, res, url) {
900
951
  const taskId = url.replace('/approval_timeout/', '');
@@ -917,6 +968,7 @@ async function handleApprovalTimeout(req, res, url) {
917
968
  const success = client.setApprovalResult(taskId, result, reason);
918
969
  if (success) {
919
970
  entry.status = result;
971
+ pendingApprovals.delete(taskId); // 处理完立即删除
920
972
  res.writeHead(200, { 'Content-Type': 'application/json' });
921
973
  res.end(JSON.stringify({ success: true, taskId, result }));
922
974
  }
@@ -944,15 +996,23 @@ function handleHealthCheck(_req, res) {
944
996
  }, null, 2));
945
997
  }
946
998
  // SSE 连接处理(Channel 模式)
947
- function handleSSEConnect(req, res, url) {
948
- const ccId = decodeURIComponent(url.replace('/sse/', ''));
949
- const entry = getCCRegistryEntry(ccId);
999
+ function handleSSEConnect(req, res, _url) {
1000
+ const urlObj = new URL(req.url, 'http://localhost');
1001
+ const targetCcId = decodeURIComponent(urlObj.pathname.replace('/sse/', ''));
1002
+ const requestCcId = urlObj.searchParams.get('ccId'); // 请求方声明的身份
1003
+ const entry = getCCRegistryEntry(targetCcId);
950
1004
  if (!entry) {
951
1005
  res.writeHead(404, { 'Content-Type': 'text/plain' });
952
- res.end(`CC ${ccId} not found`);
1006
+ res.end(`CC ${targetCcId} not found`);
1007
+ return;
1008
+ }
1009
+ // 请求方 ccId 必须与目标 ccId 一致
1010
+ if (requestCcId && requestCcId !== targetCcId) {
1011
+ res.writeHead(403, { 'Content-Type': 'application/json' });
1012
+ res.end(JSON.stringify({ error: `无权订阅 ccId: ${targetCcId}` }));
953
1013
  return;
954
1014
  }
955
- const clientId = `${ccId}_${Date.now()}`;
1015
+ const clientId = `${targetCcId}_${Date.now()}`;
956
1016
  // 设置 SSE headers
957
1017
  res.writeHead(200, {
958
1018
  'Content-Type': 'text/event-stream',
@@ -963,12 +1023,12 @@ function handleSSEConnect(req, res, url) {
963
1023
  // 注册 SSE 客户端
964
1024
  sseClients.set(clientId, {
965
1025
  res,
966
- ccId,
1026
+ ccId: targetCcId,
967
1027
  robotName: entry.robotName,
968
1028
  });
969
- logger.log(`[http] SSE 客户端连接: clientId=${clientId}, ccId=${ccId}, robotName=${entry.robotName}`);
1029
+ logger.log(`[http] SSE 客户端连接: clientId=${clientId}, ccId=${targetCcId}, robotName=${entry.robotName}`);
970
1030
  // 发送连接确认
971
- res.write(`event: connected\ndata: {"clientId":"${clientId}","ccId":"${ccId}"}\n\n`);
1031
+ res.write(`event: connected\ndata: {"clientId":"${clientId}","ccId":"${targetCcId}"}\n\n`);
972
1032
  // 心跳机制:每 15 秒发送注释行保持连接活跃
973
1033
  const heartbeatInterval = setInterval(() => {
974
1034
  // SSE 注释行(以冒号开头)会被客户端忽略,但保持连接
@@ -1021,10 +1081,27 @@ async function handleNotify(req, res) {
1021
1081
  res.end(JSON.stringify({ error: err.message }));
1022
1082
  }
1023
1083
  }
1084
+ // push_notification 允许的 method 白名单
1085
+ const PUSH_NOTIFICATION_ALLOWED_METHODS = new Set([
1086
+ 'notifications/message',
1087
+ 'notifications/progress',
1088
+ 'notifications/resources/updated',
1089
+ 'notifications/tools/list_changed',
1090
+ ]);
1024
1091
  async function handlePushNotification(req, res) {
1025
1092
  try {
1026
1093
  const body = await readRequestBody(req);
1027
1094
  const { method, params } = JSON.parse(body);
1095
+ // method 白名单校验
1096
+ const effectiveMethod = method || 'notifications/message';
1097
+ if (!PUSH_NOTIFICATION_ALLOWED_METHODS.has(effectiveMethod)) {
1098
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1099
+ res.end(JSON.stringify({
1100
+ error: `不允许的 method: ${effectiveMethod}`,
1101
+ allowed: Array.from(PUSH_NOTIFICATION_ALLOWED_METHODS),
1102
+ }));
1103
+ return;
1104
+ }
1028
1105
  if (transports.size === 0) {
1029
1106
  res.writeHead(503, { 'Content-Type': 'application/json' });
1030
1107
  res.end(JSON.stringify({ error: '无活跃 MCP session' }));
@@ -1035,7 +1112,7 @@ async function handlePushNotification(req, res) {
1035
1112
  for (const [sessionId, entry] of transports) {
1036
1113
  try {
1037
1114
  await entry.server.server.notification({
1038
- method: method || 'notifications/message',
1115
+ method: effectiveMethod,
1039
1116
  params: params || {}
1040
1117
  });
1041
1118
  sent++;
@@ -1045,7 +1122,7 @@ async function handlePushNotification(req, res) {
1045
1122
  }
1046
1123
  }
1047
1124
  res.writeHead(200, { 'Content-Type': 'application/json' });
1048
- res.end(JSON.stringify({ success: true, method: method || 'notifications/message', sessions: sent }));
1125
+ res.end(JSON.stringify({ success: true, method: effectiveMethod, sessions: sent }));
1049
1126
  }
1050
1127
  catch (err) {
1051
1128
  logger.error('[http] 推送通知失败:', err);
package/dist/index.d.ts CHANGED
@@ -10,7 +10,9 @@
10
10
  */
11
11
  export { WecomClient, initClient } from './client.js';
12
12
  export { connectRobot, disconnectRobot, getClient, getConnectionState, getAllConnectionStates, } from './connection-manager.js';
13
- export { startHttpServer, stopHttpServer, HTTP_PORT, HOOK_SCRIPT_PATH, } from './http-server.js';
13
+ export { startHttpServer, stopHttpServer, HTTP_PORT, } from './http-server.js';
14
14
  export type { ApprovalRequest } from './http-server.js';
15
+ export { PERMISSION_HOOK_SCRIPT_PATH, STOP_HOOK_SCRIPT_PATH, } from './project-config.js';
16
+ export { PERMISSION_HOOK_SCRIPT_PATH as HOOK_SCRIPT_PATH } from './project-config.js';
15
17
  export { registerTools } from './tools/index.js';
16
18
  export { listAllRobots, runConfigWizard } from './config-wizard.js';
package/dist/index.js CHANGED
@@ -13,7 +13,11 @@ export { WecomClient, initClient } from './client.js';
13
13
  // 连接管理模块
14
14
  export { connectRobot, disconnectRobot, getClient, getConnectionState, getAllConnectionStates, } from './connection-manager.js';
15
15
  // HTTP 服务模块
16
- export { startHttpServer, stopHttpServer, HTTP_PORT, HOOK_SCRIPT_PATH, } from './http-server.js';
16
+ export { startHttpServer, stopHttpServer, HTTP_PORT, } from './http-server.js';
17
+ // Hook 脚本路径(统一从 project-config.ts 导出)
18
+ export { PERMISSION_HOOK_SCRIPT_PATH, STOP_HOOK_SCRIPT_PATH, } from './project-config.js';
19
+ // 向后兼容别名
20
+ export { PERMISSION_HOOK_SCRIPT_PATH as HOOK_SCRIPT_PATH } from './project-config.js';
17
21
  // 工具注册
18
22
  export { registerTools } from './tools/index.js';
19
23
  // 配置向导
@@ -23,16 +23,31 @@ export interface WecomMessage {
23
23
  timestamp: number;
24
24
  quoteContent?: string;
25
25
  }
26
+ export interface ApprovalEvent {
27
+ robotName: string;
28
+ taskId: string;
29
+ result: 'allow-once' | 'allow-always' | 'deny';
30
+ ccId?: string;
31
+ timestamp: number;
32
+ }
26
33
  export declare function getSubscriberCount(robotName: string): number;
27
34
  declare const wecomMessage$: Subject<WecomMessage>;
28
35
  /**
29
36
  * 发布微信消息(由 WecomClient 调用)
30
37
  */
31
38
  export declare function publishWecomMessage(msg: WecomMessage): void;
39
+ /**
40
+ * 发布审批事件(由 WecomClient 调用)
41
+ */
42
+ export declare function publishApprovalEvent(event: ApprovalEvent): void;
32
43
  /**
33
44
  * 订阅所有微信消息
34
45
  */
35
46
  export declare function subscribeWecomMessage(callback: (msg: WecomMessage) => void): import("rxjs").Subscription;
47
+ /**
48
+ * 订阅所有审批事件
49
+ */
50
+ export declare function subscribeApprovalEvent(callback: (event: ApprovalEvent) => void): import("rxjs").Subscription;
36
51
  /**
37
52
  * 订阅特定机器人的微信消息(带订阅计数)
38
53
  */
@@ -35,6 +35,7 @@ function decrementSubscriberCount(robotName) {
35
35
  // 消息总线(RxJS Subject)
36
36
  // ============================================
37
37
  const wecomMessage$ = new Subject();
38
+ const approvalEvent$ = new Subject();
38
39
  // ============================================
39
40
  // 发布/订阅接口
40
41
  // ============================================
@@ -44,12 +45,24 @@ const wecomMessage$ = new Subject();
44
45
  export function publishWecomMessage(msg) {
45
46
  wecomMessage$.next(msg);
46
47
  }
48
+ /**
49
+ * 发布审批事件(由 WecomClient 调用)
50
+ */
51
+ export function publishApprovalEvent(event) {
52
+ approvalEvent$.next(event);
53
+ }
47
54
  /**
48
55
  * 订阅所有微信消息
49
56
  */
50
57
  export function subscribeWecomMessage(callback) {
51
58
  return wecomMessage$.subscribe(callback);
52
59
  }
60
+ /**
61
+ * 订阅所有审批事件
62
+ */
63
+ export function subscribeApprovalEvent(callback) {
64
+ return approvalEvent$.subscribe(callback);
65
+ }
53
66
  /**
54
67
  * 订阅特定机器人的微信消息(带订阅计数)
55
68
  */
@@ -114,6 +114,8 @@ export declare function validateConfig(config: Partial<ProjectConfig>): boolean;
114
114
  * 获取项目 settings.json 路径
115
115
  */
116
116
  export declare function getProjectSettingsPath(projectDir: string): string;
117
+ export declare const PERMISSION_HOOK_SCRIPT_PATH: string;
118
+ export declare const STOP_HOOK_SCRIPT_PATH: string;
117
119
  /**
118
120
  * 添加 PermissionRequest hook 到项目 settings.json
119
121
  */
@@ -130,16 +132,17 @@ export declare function removePermissionHook(projectDir: string): {
130
132
  existed: boolean;
131
133
  };
132
134
  /**
133
- * 添加 TaskCompleted hook 到项目 settings.json
135
+ * 添加 Stop hook 到项目 settings.json
136
+ * HTTP 模式使用:阻止 Claude 停止,提示调用 get_pending_messages 恢复轮询
134
137
  */
135
- export declare function addTaskCompletedHook(projectDir: string): {
138
+ export declare function addStopHook(projectDir: string): {
136
139
  success: boolean;
137
140
  path: string;
138
141
  };
139
142
  /**
140
- * 删除 TaskCompleted hook 从项目 settings.json
143
+ * 删除 Stop hook 从项目 settings.json
141
144
  */
142
- export declare function removeTaskCompletedHook(projectDir: string): {
145
+ export declare function removeStopHook(projectDir: string): {
143
146
  success: boolean;
144
147
  path: string;
145
148
  existed: boolean;
@@ -249,23 +249,26 @@ export function validateConfig(config) {
249
249
  export function getProjectSettingsPath(projectDir) {
250
250
  return path.join(projectDir, '.claude', 'settings.json');
251
251
  }
252
+ // ============================================
253
+ // Hook 脚本路径(统一定义)
254
+ // ============================================
255
+ const CONFIG_DIR = path.join(os.homedir(), '.wecom-aibot-mcp');
256
+ export const PERMISSION_HOOK_SCRIPT_PATH = path.join(CONFIG_DIR, 'permission-hook.sh');
257
+ export const STOP_HOOK_SCRIPT_PATH = path.join(CONFIG_DIR, 'stop-hook.sh');
252
258
  /**
253
259
  * PermissionRequest hook 配置
254
260
  */
255
261
  const PERMISSION_HOOK = {
256
262
  matcher: '',
257
- hooks: [{ type: 'command', command: '~/.wecom-aibot-mcp/permission-hook.sh' }],
263
+ hooks: [{ type: 'command', command: PERMISSION_HOOK_SCRIPT_PATH }],
258
264
  };
259
265
  /**
260
- * TaskCompleted hook 脚本路径
266
+ * Stop hook 配置
267
+ * 用于 HTTP 模式:阻止 Claude 停止,提示调用 get_pending_messages 恢复轮询
261
268
  */
262
- const TASK_COMPLETED_HOOK_SCRIPT_PATH = path.join(os.homedir(), '.wecom-aibot-mcp', 'task-completed-hook.sh');
263
- /**
264
- * TaskCompleted hook 配置
265
- */
266
- const TASK_COMPLETED_HOOK = {
269
+ const STOP_HOOK = {
267
270
  matcher: '',
268
- hooks: [{ type: 'command', command: TASK_COMPLETED_HOOK_SCRIPT_PATH }],
271
+ hooks: [{ type: 'command', command: STOP_HOOK_SCRIPT_PATH }],
269
272
  };
270
273
  /**
271
274
  * 添加 PermissionRequest hook 到项目 settings.json
@@ -335,9 +338,10 @@ export function removePermissionHook(projectDir) {
335
338
  }
336
339
  }
337
340
  /**
338
- * 添加 TaskCompleted hook 到项目 settings.json
341
+ * 添加 Stop hook 到项目 settings.json
342
+ * HTTP 模式使用:阻止 Claude 停止,提示调用 get_pending_messages 恢复轮询
339
343
  */
340
- export function addTaskCompletedHook(projectDir) {
344
+ export function addStopHook(projectDir) {
341
345
  const settingsPath = getProjectSettingsPath(projectDir);
342
346
  const settingsDir = path.dirname(settingsPath);
343
347
  // 确保目录存在
@@ -355,26 +359,26 @@ export function addTaskCompletedHook(projectDir) {
355
359
  // ignore
356
360
  }
357
361
  }
358
- // 添加 hooks.TaskCompleted
362
+ // 添加 hooks.Stop
359
363
  if (!settings.hooks) {
360
364
  settings.hooks = {};
361
365
  }
362
- settings.hooks.TaskCompleted = [TASK_COMPLETED_HOOK];
366
+ settings.hooks.Stop = [STOP_HOOK];
363
367
  // 写入配置
364
368
  try {
365
369
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
366
- logger.log(`[project-config] 已添加 TaskCompleted hook: ${settingsPath}`);
370
+ logger.log(`[project-config] 已添加 Stop hook: ${settingsPath}`);
367
371
  return { success: true, path: settingsPath };
368
372
  }
369
373
  catch (err) {
370
- logger.error(`[project-config] 添加 TaskCompleted hook 失败: ${err}`);
374
+ logger.error(`[project-config] 添加 Stop hook 失败: ${err}`);
371
375
  return { success: false, path: settingsPath };
372
376
  }
373
377
  }
374
378
  /**
375
- * 删除 TaskCompleted hook 从项目 settings.json
379
+ * 删除 Stop hook 从项目 settings.json
376
380
  */
377
- export function removeTaskCompletedHook(projectDir) {
381
+ export function removeStopHook(projectDir) {
378
382
  const settingsPath = getProjectSettingsPath(projectDir);
379
383
  if (!fs.existsSync(settingsPath)) {
380
384
  return { success: true, path: settingsPath, existed: false };
@@ -382,22 +386,22 @@ export function removeTaskCompletedHook(projectDir) {
382
386
  try {
383
387
  const content = fs.readFileSync(settingsPath, 'utf-8');
384
388
  const settings = JSON.parse(content);
385
- // 删除 hooks.TaskCompleted
386
- if (settings.hooks && settings.hooks.TaskCompleted) {
387
- delete settings.hooks.TaskCompleted;
389
+ // 删除 hooks.Stop
390
+ if (settings.hooks && settings.hooks.Stop) {
391
+ delete settings.hooks.Stop;
388
392
  // 如果 hooks 为空,删除整个 hooks 字段
389
393
  if (Object.keys(settings.hooks).length === 0) {
390
394
  delete settings.hooks;
391
395
  }
392
396
  // 写入配置
393
397
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
394
- logger.log(`[project-config] 已删除 TaskCompleted hook: ${settingsPath}`);
398
+ logger.log(`[project-config] 已删除 Stop hook: ${settingsPath}`);
395
399
  return { success: true, path: settingsPath, existed: true };
396
400
  }
397
401
  return { success: true, path: settingsPath, existed: false };
398
402
  }
399
403
  catch (err) {
400
- logger.error(`[project-config] 删除 TaskCompleted hook 失败: ${err}`);
404
+ logger.error(`[project-config] 删除 Stop hook 失败: ${err}`);
401
405
  return { success: false, path: settingsPath, existed: false };
402
406
  }
403
407
  }
@@ -25,7 +25,7 @@ import { callDocTool } from '../doc-proxy.js';
25
25
  import { connectRobot, disconnectRobot, getClient, getConnectionState, } from '../connection-manager.js';
26
26
  import { registerCcId, unregisterCcId, getRobotByCcId, getProjectDirByCcId, generateCcId, } from '../http-server.js';
27
27
  import { subscribeWecomMessageByCcId } from '../message-bus.js';
28
- import { updateWechatModeConfig, loadWechatModeConfig, addPermissionHook, removePermissionHook, addTaskCompletedHook, removeTaskCompletedHook } from '../project-config.js';
28
+ import { updateWechatModeConfig, loadWechatModeConfig, addPermissionHook, removePermissionHook, addStopHook, removeStopHook } from '../project-config.js';
29
29
  import { logger } from '../logger.js';
30
30
  // 辅助函数:从 ccId 获取客户端
31
31
  async function getConnectedClient(ccId) {
@@ -436,8 +436,8 @@ npx @vrs-soft/wecom-aibot-mcp
436
436
  const skillResult = installSkill(projectDir);
437
437
  // 添加 PermissionRequest hook 到项目 settings.json
438
438
  const hookResult = addPermissionHook(projectDir);
439
- // HTTP 模式添加 TaskCompleted hook(Channel 模式不需要,消息自动推送)
440
- const taskCompletedHookResult = mode === 'http' ? addTaskCompletedHook(projectDir) : { added: false, message: 'Channel 模式不需要 TaskCompleted hook' };
439
+ // HTTP 模式添加 Stop hook(Channel 模式不需要,消息自动推送)
440
+ const stopHookResult = mode === 'http' ? addStopHook(projectDir) : { success: false, path: '' };
441
441
  // 发送确认消息(头部标注来源 ccId 和 mode)
442
442
  const modeDesc = mode === 'channel' ? 'Channel模式,消息自动推送' : 'HTTP模式,请定期轮询获取消息';
443
443
  await result.client.sendText(`【${finalCcId}】已进入微信模式(${modeDesc}),使用机器人「${selectedRobot.name}」。`);
@@ -452,11 +452,11 @@ npx @vrs-soft/wecom-aibot-mcp
452
452
  agentName: effectiveAgentName, // 返回使用的 agentName
453
453
  mode,
454
454
  hook: hookResult,
455
- taskCompletedHook: taskCompletedHookResult,
455
+ stopHook: stopHookResult,
456
456
  skill: skillResult, // skill 安装结果(如果 success=false,包含 skillUrl)
457
- sseEndpoint: mode === 'channel' ? `http://127.0.0.1:18963/sse/${finalCcId}` : undefined,
457
+ sseEndpoint: mode === 'channel' ? `http://127.0.0.1:18963/sse/${finalCcId}?ccId=${finalCcId}` : undefined,
458
458
  message: mode === 'channel'
459
- ? `连接 SSE endpoint: http://127.0.0.1:18963/sse/${finalCcId} 接收推送消息`
459
+ ? `连接 SSE endpoint: http://127.0.0.1:18963/sse/${finalCcId}?ccId=${finalCcId} 接收推送消息`
460
460
  : '已进入微信模式(HTTP)',
461
461
  }),
462
462
  }],
@@ -491,8 +491,8 @@ npx @vrs-soft/wecom-aibot-mcp
491
491
  updateWechatModeConfig(projectDir, { wechatMode: false });
492
492
  // 删除 PermissionRequest hook 从项目 settings.json
493
493
  const hookResult = removePermissionHook(projectDir);
494
- // 删除 TaskCompleted hook 从项目 settings.json
495
- const taskCompletedHookResult = removeTaskCompletedHook(projectDir);
494
+ // 删除 Stop hook 从项目 settings.json
495
+ const stopHookResult = removeStopHook(projectDir);
496
496
  return {
497
497
  content: [{
498
498
  type: 'text',
@@ -501,7 +501,7 @@ npx @vrs-soft/wecom-aibot-mcp
501
501
  headless: false,
502
502
  robotName,
503
503
  hook: hookResult,
504
- taskCompletedHook: taskCompletedHookResult,
504
+ stopHook: stopHookResult,
505
505
  message: '审批将使用默认 UI',
506
506
  }),
507
507
  }],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vrs-soft/wecom-aibot-mcp",
3
- "version": "2.4.7",
3
+ "version": "2.4.8",
4
4
  "description": "企业微信智能机器人 MCP 服务 - Claude Code 审批通道",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",