@vrs-soft/wecom-aibot-mcp 1.3.0 → 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/dist/bin.js CHANGED
@@ -15,7 +15,9 @@ import * as path from 'path';
15
15
  import * as os from 'os';
16
16
  import { runConfigWizard, loadConfig, saveConfig, deleteConfig, deleteMcpConfigInteractive, uninstall, addMcpConfig, detectUserIdFromMessage, ensureHookInstalled, listAllRobots, } from './config-wizard.js';
17
17
  import { initClient } from './client.js';
18
+ import { registerTools } from './tools/index.js';
18
19
  import { startHttpServer, stopHttpServer, HTTP_PORT } from './http-server.js';
20
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
19
21
  import { getAllConnectionStates } from './connection-manager.js';
20
22
  import { loadStats, cleanupOldLogs } from './connection-log.js';
21
23
  import { startKeepaliveMonitor, stopKeepaliveMonitor } from './keepalive-monitor.js';
@@ -36,6 +38,7 @@ function showHelp() {
36
38
  --version, -v 显示版本号
37
39
  --start 启动 MCP Server(后台服务模式)
38
40
  --stop 停止 MCP Server
41
+ --debug 前台启动 MCP Server(日志直接输出到终端,用于调试)
39
42
  --status 显示服务状态和机器人配置
40
43
  --config 重新配置默认机器人(修改 Bot ID / Secret / 目标用户)
41
44
  --add 添加新的机器人配置(多机器人场景)
@@ -116,8 +119,10 @@ function isServerRunning() {
116
119
  return true;
117
120
  }
118
121
  catch {
119
- // 进程不存在,清理 PID 文件
120
- fs.unlinkSync(PID_FILE);
122
+ // 进程不存在,清理 PID 文件(可能已被进程自身删除)
123
+ if (fs.existsSync(PID_FILE)) {
124
+ fs.unlinkSync(PID_FILE);
125
+ }
121
126
  return false;
122
127
  }
123
128
  }
@@ -144,13 +149,18 @@ function stopServer() {
144
149
  break;
145
150
  }
146
151
  }
147
- fs.unlinkSync(PID_FILE);
152
+ // 进程退出后删除 PID 文件(如果还存在)
153
+ if (fs.existsSync(PID_FILE)) {
154
+ fs.unlinkSync(PID_FILE);
155
+ }
148
156
  console.log('[mcp] 服务已停止');
149
157
  return true;
150
158
  }
151
159
  catch (err) {
152
160
  console.error('[mcp] 停止服务失败:', err);
153
- fs.unlinkSync(PID_FILE);
161
+ if (fs.existsSync(PID_FILE)) {
162
+ fs.unlinkSync(PID_FILE);
163
+ }
154
164
  return false;
155
165
  }
156
166
  }
@@ -184,6 +194,12 @@ async function startMcpServerForeground() {
184
194
  // 加载统计并清理旧日志
185
195
  loadStats();
186
196
  cleanupOldLogs(1 / 24);
197
+ // 创建 MCP Server
198
+ const server = new McpServer({
199
+ name: 'wecom-aibot-mcp',
200
+ version: VERSION,
201
+ });
202
+ registerTools(server);
187
203
  // 启动 HTTP 服务
188
204
  console.log('');
189
205
  console.log(' ╔════════════════════════════════════════════════════════╗');
@@ -192,7 +208,7 @@ async function startMcpServerForeground() {
192
208
  console.log(' ╚════════════════════════════════════════════════════════╝');
193
209
  console.log('');
194
210
  console.log(`[mcp] 启动 MCP HTTP Server (端口: ${HTTP_PORT})...`);
195
- await startHttpServer();
211
+ await startHttpServer(server);
196
212
  startKeepaliveMonitor();
197
213
  console.log(`[mcp] MCP Server 已就绪`);
198
214
  console.log(`[mcp] HTTP endpoint: http://127.0.0.1:${HTTP_PORT}/mcp`);
@@ -236,6 +252,7 @@ function startMcpServerBackground() {
236
252
  console.log(`[mcp] HTTP endpoint: http://127.0.0.1:18963/mcp`);
237
253
  console.log('[mcp] 健康检查: curl http://127.0.0.1:18963/health');
238
254
  console.log('[mcp] 停止服务: npx @vrs-soft/wecom-aibot-mcp --stop');
255
+ console.log('[mcp] 调试模式: npx @vrs-soft/wecom-aibot-mcp --debug');
239
256
  }
240
257
  async function main() {
241
258
  const args = process.argv.slice(2);
@@ -277,11 +294,17 @@ async function main() {
277
294
  await deleteMcpConfigInteractive(instanceName);
278
295
  process.exit(0);
279
296
  }
280
- // --start --foreground:前台启动(内部调用)
297
+ // --start --foreground:前台启动(内部调用,输出到日志文件)
281
298
  if (args.includes('--start') && args.includes('--foreground')) {
282
299
  await startMcpServerForeground();
283
300
  return; // 保持运行,不 exit
284
301
  }
302
+ // --debug:前台启动,日志直接输出到终端
303
+ if (args.includes('--debug')) {
304
+ console.log('[mcp] Debug 模式:前台运行,Ctrl+C 退出');
305
+ await startMcpServerForeground();
306
+ return;
307
+ }
285
308
  // --start:后台启动
286
309
  if (args.includes('--start')) {
287
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;
@@ -36,11 +37,13 @@ declare class WecomClient extends EventEmitter {
36
37
  private wasReconnecting;
37
38
  private reconnectAttempt;
38
39
  private lastDisconnectTime;
40
+ private disconnectNotifyCount;
39
41
  constructor(botId: string, secret: string, targetUserId: string, robotName: string);
40
42
  getAuthUrl(): string;
41
43
  private setupEventHandlers;
42
44
  private handleMessage;
43
45
  private handleApprovalResponse;
46
+ private replyApprovalResult;
44
47
  connect(): void;
45
48
  disconnect(): void;
46
49
  isConnected(): boolean;
@@ -54,6 +57,7 @@ declare class WecomClient extends EventEmitter {
54
57
  ccId?: string): Promise<string>;
55
58
  sendQueuedApproval(taskId: string, title: string, description: string, targetUser?: string): Promise<boolean>;
56
59
  getApprovalResult(taskId: string): 'pending' | 'allow-once' | 'allow-always' | 'deny';
60
+ setApprovalResult(taskId: string, result: 'allow-once' | 'deny', reason?: string): boolean;
57
61
  getPendingApprovals(): string[];
58
62
  getPendingApprovalsRecords(): ApprovalRecord[];
59
63
  getApprovalRecord(taskId: string): ApprovalRecord | undefined;
@@ -91,7 +95,5 @@ declare class WecomClient extends EventEmitter {
91
95
  };
92
96
  }
93
97
  export declare function initClient(botId: string, secret: string, targetUserId: string, robotName: string): WecomClient;
94
- export declare function getClient(robotName?: string): WecomClient;
95
- export declare function getAllClients(): Map<string, WecomClient>;
96
- export declare function disconnectClient(robotName: string): boolean;
98
+ export declare function getClient(): WecomClient;
97
99
  export { WecomClient, ApprovalRecord, MessageRecord, MAX_PENDING_MESSAGES };
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;
@@ -60,6 +61,7 @@ class WecomClient extends EventEmitter {
60
61
  this.connected = true;
61
62
  this.wasReconnecting = false;
62
63
  this.reconnectAttempt = 0;
64
+ this.disconnectNotifyCount = 0; // 重连成功后重置断线通知计数
63
65
  logAuthenticated();
64
66
  // 重连成功后发送通知
65
67
  if (wasReconnecting) {
@@ -75,10 +77,13 @@ class WecomClient extends EventEmitter {
75
77
  this.wasReconnecting = true;
76
78
  this.lastDisconnectTime = Date.now();
77
79
  logDisconnected(reason);
78
- // 发送断线通知
79
- this.sendText('【系统】连接中断,正在重连...').catch(err => {
80
- console.error('[wecom] 发送断线通知失败:', err);
81
- });
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;
@@ -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
  // 监听进入会话事件
@@ -184,23 +190,18 @@ class WecomClient extends EventEmitter {
184
190
  }
185
191
  handleApprovalResponse(frame) {
186
192
  const event = frame.body?.event;
187
- if (!event)
188
- return;
189
- // 调试:打印完整事件结构
190
- console.log('[wecom] 审批事件原始结构:', JSON.stringify(event).substring(0, 500));
191
- // task_id 和 event_key 可能在 event 层级(SDK 扁平化)
192
- // 也可能嵌套在 template_card_event 对象内(WeChat 原始结构)
193
- let taskId = event.task_id;
194
- let eventKey = event.event_key;
195
- // 如果直接找不到,尝试嵌套结构
196
- if (!taskId && event.template_card_event) {
197
- taskId = event.template_card_event.task_id;
198
- eventKey = event.template_card_event.event_key;
199
- }
200
- if (!taskId) {
201
- console.log('[wecom] 审批事件未找到 task_id,事件 keys:', Object.keys(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));
202
196
  return;
203
197
  }
198
+ // task_id 和 event_key 在 event.template_card_event 内部
199
+ const cardEvent = event.template_card_event;
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)
204
+ return;
204
205
  console.log(`[wecom] 收到审批响应: taskId=${taskId}, key=${eventKey}`);
205
206
  const approval = this.approvals.get(taskId);
206
207
  if (approval && !approval.resolved) {
@@ -213,10 +214,35 @@ class WecomClient extends EventEmitter {
213
214
  : eventKey === 'allow-always' ? '✅ 已允许(永久)'
214
215
  : '❌ 已拒绝';
215
216
  const toolInfo = approval.toolName ? `: ${approval.toolName}` : '';
216
- 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 => {
217
220
  console.error('[wecom] 发送审批确认失败:', err);
218
221
  });
219
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
+ }
220
246
  }
221
247
  // 连接
222
248
  connect() {
@@ -313,7 +339,7 @@ class WecomClient extends EventEmitter {
313
339
  if (toolInput && toolName) {
314
340
  const operationHash = hashOperation(ccId ?? '', toolName, toolInput);
315
341
  const existing = this.findApprovalByHash(operationHash);
316
- if (existing && !existing.resolved) {
342
+ if (existing) {
317
343
  console.log(`[wecom] 复用已有审批: ${existing.taskId} (hash: ${operationHash.slice(0, 8)}...)`);
318
344
  return existing.taskId;
319
345
  }
@@ -327,6 +353,7 @@ class WecomClient extends EventEmitter {
327
353
  timestamp: Date.now(),
328
354
  toolName,
329
355
  toolInput,
356
+ description, // 保存审批请求原文
330
357
  operationHash,
331
358
  });
332
359
  // 断线时将审批请求加入队列,等待重连后发送
@@ -401,6 +428,32 @@ class WecomClient extends EventEmitter {
401
428
  }
402
429
  return 'pending';
403
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
+ }
404
457
  // 获取所有待处理的审批任务 ID(供 hook 轮询使用)
405
458
  getPendingApprovals() {
406
459
  return Array.from(this.approvals.entries())
@@ -560,46 +613,20 @@ class WecomClient extends EventEmitter {
560
613
  };
561
614
  }
562
615
  }
563
- // 多实例支持(按 robotName 索引)
564
- const instances = new Map();
616
+ // 单例实例
617
+ let instance = null;
565
618
  export function initClient(botId, secret, targetUserId, robotName) {
566
- // 如果该机器人已存在,先断开旧连接
567
- if (instances.has(robotName)) {
568
- instances.get(robotName).disconnect();
619
+ if (instance) {
620
+ instance.disconnect();
569
621
  }
570
- const client = new WecomClient(botId, secret, targetUserId, robotName);
571
- client.connect();
572
- instances.set(robotName, client);
573
- return client;
622
+ instance = new WecomClient(botId, secret, targetUserId, robotName);
623
+ instance.connect();
624
+ return instance;
574
625
  }
575
- export function getClient(robotName) {
576
- if (robotName) {
577
- const client = instances.get(robotName);
578
- if (!client) {
579
- throw new Error(`WecomClient 未初始化:机器人 "${robotName}" 不存在`);
580
- }
581
- return client;
582
- }
583
- // 无参数时返回第一个实例(向后兼容)
584
- if (instances.size === 0) {
626
+ export function getClient() {
627
+ if (!instance) {
585
628
  throw new Error('WecomClient 未初始化,请先调用 initClient');
586
629
  }
587
- const first = instances.values().next().value;
588
- if (!first) {
589
- throw new Error('WecomClient 未初始化');
590
- }
591
- return first;
592
- }
593
- export function getAllClients() {
594
- return instances;
595
- }
596
- export function disconnectClient(robotName) {
597
- const client = instances.get(robotName);
598
- if (client) {
599
- client.disconnect();
600
- instances.delete(robotName);
601
- return true;
602
- }
603
- return false;
630
+ return instance;
604
631
  }
605
632
  export { WecomClient, MAX_PENDING_MESSAGES };
@@ -252,7 +252,7 @@ function writeHookScript() {
252
252
  # HTTP Transport 版本
253
253
  #
254
254
  # 固定端口: 18963
255
- # 直接检查 $(pwd)/.claude/headless.json
255
+ # 检查 $(pwd)/.claude/wecom-aibot.json 的 wechatMode 和 autoApprove 字段
256
256
 
257
257
  MCP_PORT=18963
258
258
 
@@ -273,12 +273,18 @@ case "$TOOL_NAME" in
273
273
  ;;
274
274
  esac
275
275
 
276
- # 直接检查项目目录的 headless 状态文件
276
+ # 检查项目目录的微信模式配置文件
277
277
  PROJECT_DIR=$(pwd)
278
- HEADLESS_FILE="$PROJECT_DIR/.claude/headless.json"
278
+ CONFIG_FILE="$PROJECT_DIR/.claude/wecom-aibot.json"
279
279
 
280
- # 不在 headless 模式
281
- if [[ ! -f "$HEADLESS_FILE" ]]; then
280
+ # 配置文件不存在,不在微信模式
281
+ if [[ ! -f "$CONFIG_FILE" ]]; then
282
+ exit 0
283
+ fi
284
+
285
+ # 检查 wechatMode 是否为 true(微信模式开关)
286
+ WECHAT_MODE=$(jq -r '.wechatMode // false' "$CONFIG_FILE" 2>/dev/null)
287
+ if [[ "$WECHAT_MODE" != "true" ]]; then
282
288
  exit 0
283
289
  fi
284
290
 
@@ -288,17 +294,10 @@ if ! echo "$HEALTH" | jq -e '.status == "ok"' > /dev/null 2>&1; then
288
294
  exit 0
289
295
  fi
290
296
 
291
- # headless.json 提取 ccId(优先 agentName,回退 ccId
292
- CC_ID=$(jq -r '.agentName // .ccId // empty' "$HEADLESS_FILE" 2>/dev/null)
293
-
294
- # 发送审批请求(包含 ccId 用于路由)
297
+ # 发送审批请求(使用 pwd 作为 projectDir
295
298
  TOOL_INPUT=$(echo "$INPUT" | jq -c '.tool_input // {}')
296
- BODY=$(jq -n \\
297
- --arg tool_name "$TOOL_NAME" \\
298
- --argjson tool_input "$TOOL_INPUT" \\
299
- --arg project_dir "$PROJECT_DIR" \\
300
- --arg cc_id "$CC_ID" \\
301
- '{"tool_name":$tool_name,"tool_input":$tool_input,"projectDir":$project_dir,"ccId":$cc_id}')
299
+ BODY=$(jq -n --arg tool_name "$TOOL_NAME" --argjson tool_input "$TOOL_INPUT" --arg project_dir "$PROJECT_DIR" \\
300
+ '{"tool_name":$tool_name,"tool_input":$tool_input,"projectDir":$project_dir}')
302
301
 
303
302
  RESPONSE=$(curl -s -m 10 -X POST "http://127.0.0.1:$MCP_PORT/approve" \\
304
303
  -H "Content-Type: application/json" \\
@@ -329,22 +328,44 @@ while [[ $POLL_COUNT -lt $MAX_POLL ]]; do
329
328
  fi
330
329
  done
331
330
 
332
- # 超时处理:智能代批
331
+ # 超时处理:根据 autoApprove 决定行为
332
+ # autoApprove: false → 继续等待(无限轮询)
333
+ # autoApprove: true → 智能代批
334
+
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')
342
+
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
352
+
353
+ # autoApprove: true,执行智能代批
333
354
  # 规则:删除命令→拒绝,项目内操作→允许,项目外操作→拒绝
334
355
 
335
356
  # 检查是否是删除命令
336
357
  IS_DELETE=0
337
358
  if [[ "$TOOL_NAME" == "Bash" ]]; then
338
359
  CMD=$(echo "$TOOL_INPUT" | jq -r '.command // empty')
339
- if [[ "$CMD" =~ ^rm\\ ]] || [[ "$CMD" =~ \\ rm\\ ]] || \
340
- [[ "$CMD" =~ ^rmdir\\ ]] || [[ "$CMD" =~ \\ rmdir\\ ]] || \
341
- [[ "$CMD" =~ ^unlink\\ ]] || [[ "$CMD" =~ rm\\ -rf ]]; then
360
+ if [[ "$CMD" == rm* ]] || [[ "$CMD" == *" rm "* ]] || [[ "$CMD" == *"-rf"* ]]; then
342
361
  IS_DELETE=1
343
362
  fi
344
363
  fi
345
364
 
346
365
  # 删除操作 → 永远拒绝
347
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 &
348
369
  printf '%s\\n' '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"deny","message":"超时自动拒绝:删除操作需人工确认"}}}'
349
370
  exit 0
350
371
  fi
@@ -355,12 +376,7 @@ IS_IN_PROJECT=0
355
376
  case "$TOOL_NAME" in
356
377
  Bash)
357
378
  CMD=$(echo "$TOOL_INPUT" | jq -r '.command // empty')
358
- if [[ "$CMD" == *"$PROJECT_DIR"* ]] || \
359
- [[ "$CMD" =~ ^\\./ ]] || \
360
- [[ "$CMD" =~ ^npm\\ ]] || \
361
- [[ "$CMD" =~ ^npx\\ ]] || \
362
- [[ "$CMD" =~ ^git\\ ]] || \
363
- [[ "$CMD" =~ ^node\\ ]]; then
379
+ if [[ "$CMD" == *"$PROJECT_DIR"* ]] || [[ "$CMD" == ./* ]] || [[ "$CMD" == npm* ]] || [[ "$CMD" == npx* ]] || [[ "$CMD" == git* ]] || [[ "$CMD" == node* ]]; then
364
380
  IS_IN_PROJECT=1
365
381
  fi
366
382
  ;;
@@ -382,8 +398,12 @@ esac
382
398
 
383
399
  # 根据项目内/外决策
384
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 &
385
403
  printf '%s\\n' '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow","message":"超时自动允许:项目内操作"}}}'
386
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 &
387
407
  printf '%s\\n' '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"deny","message":"超时自动拒绝:项目外操作需人工确认"}}}'
388
408
  fi
389
409
  `;
@@ -4,7 +4,7 @@
4
4
  * 记录 WebSocket 连接状态变化,便于分析连接问题
5
5
  */
6
6
  interface ConnectionRecord {
7
- event: 'connected' | 'authenticated' | 'disconnected' | 'reconnecting' | 'error' | 'warn';
7
+ event: 'connected' | 'authenticated' | 'disconnected' | 'reconnecting' | 'error';
8
8
  timestamp: string;
9
9
  isoTime: string;
10
10
  reason?: string;
@@ -28,7 +28,6 @@ export declare function logAuthenticated(): void;
28
28
  export declare function logDisconnected(reason: string): void;
29
29
  export declare function logReconnecting(attempt: number): void;
30
30
  export declare function logError(errorMessage: string): void;
31
- export declare function logWarn(message: string): void;
32
31
  export declare function getStats(): ConnectionStats;
33
32
  export declare function getRecentLogs(count?: number): ConnectionRecord[];
34
33
  export declare function cleanupOldLogs(daysToKeep?: number): void;
@@ -160,16 +160,6 @@ export function logError(errorMessage) {
160
160
  writeLog(record);
161
161
  updateStats(record);
162
162
  }
163
- export function logWarn(message) {
164
- const now = new Date();
165
- const record = {
166
- event: 'warn',
167
- timestamp: now.toISOString().replace('T', ' ').slice(0, 19),
168
- isoTime: now.toISOString(),
169
- errorMessage: message,
170
- };
171
- writeLog(record);
172
- }
173
163
  // 获取当前统计
174
164
  export function getStats() {
175
165
  return { ...stats };
@@ -19,8 +19,6 @@ import { WecomClient } from './client.js';
19
19
  import { listAllRobots } from './config-wizard.js';
20
20
  // 连接池:robotName → ConnectionState
21
21
  const connectionPool = new Map();
22
- // 空闲超时:30 分钟无活跃自动断开
23
- const INACTIVE_TIMEOUT = 30 * 60 * 1000;
24
22
  const CONFIG_DIR = path.join(os.homedir(), '.wecom-aibot-mcp');
25
23
  /**
26
24
  * 根据机器人名称查找配置
@@ -124,7 +122,6 @@ export async function connectRobot(robotName, agentName) {
124
122
  robotName: robot.name,
125
123
  client,
126
124
  connectedAt: Date.now(),
127
- lastActive: Date.now(),
128
125
  agentName,
129
126
  };
130
127
  connectionPool.set(robot.name, state);
@@ -153,9 +150,8 @@ export async function getClient(robotName) {
153
150
  if (!state) {
154
151
  return null;
155
152
  }
156
- // 如果已连接,更新 lastActive 并返回
153
+ // 如果已连接,直接返回
157
154
  if (state.client.isConnected()) {
158
- state.lastActive = Date.now();
159
155
  return state.client;
160
156
  }
161
157
  // 断开了,尝试重连
@@ -245,19 +241,3 @@ export async function connectAllRobots() {
245
241
  }
246
242
  }
247
243
  }
248
- /**
249
- * 清理空闲连接(30 分钟无活跃自动断开)
250
- * 每 5 分钟调用一次
251
- */
252
- function cleanupIdleConnections() {
253
- const now = Date.now();
254
- for (const [robotName, state] of connectionPool) {
255
- if (now - state.lastActive > INACTIVE_TIMEOUT) {
256
- console.log(`[connection] 断开空闲机器人: ${robotName}(${Math.floor((now - state.lastActive) / 60000)} 分钟无活跃)`);
257
- state.client.disconnect();
258
- connectionPool.delete(robotName);
259
- }
260
- }
261
- }
262
- // 启动空闲连接清理定时器(5 分钟)
263
- setInterval(cleanupIdleConnections, 5 * 60 * 1000).unref();
@@ -2,26 +2,37 @@
2
2
  * HTTP 服务模块
3
3
  *
4
4
  * 提供以下端点:
5
- * - POST /mcp - MCP Streamable HTTP endpoint
5
+ * - POST /mcp - MCP Streamable HTTP endpoint (stateful session mode)
6
6
  * - POST /approve - 审批请求
7
7
  * - GET /approval_status/:taskId - 审批状态查询
8
8
  * - GET /health - 健康检查
9
9
  * - GET /state - 系统状态查询
10
10
  *
11
- * 架构:使用 ccId 直接管理,不使用 sessionId 业务层概念。
12
- * MCP 协议层的 sessionId SDK 内部管理,不暴露给业务逻辑。
11
+ * v2.0 架构变更:
12
+ * - 使用 Session 管理,不再使用 projectDir
13
+ * - Session → robotName → WebSocket Connection
13
14
  */
14
- export declare const HTTP_PORT: number;
15
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
16
+ export declare const HTTP_PORT = 18963;
15
17
  export declare const HOOK_SCRIPT_PATH: string;
16
- export declare function registerActiveCcId(ccId: string): void;
17
- export declare function unregisterActiveCcId(ccId: string): void;
18
- export declare function hasActiveHeadlessSession(): boolean;
19
- export declare function getFirstActiveCcId(): {
20
- ccId: string;
18
+ interface SessionData {
21
19
  robotName: string;
22
- } | null;
20
+ agentName?: string;
21
+ ccId: string;
22
+ createdAt: number;
23
+ }
23
24
  export declare function generateCcId(): string;
24
- export declare function pushMessageToAllClients(robotName: string, message: {
25
+ export declare function setSessionData(sessionId: string, data: SessionData): void;
26
+ export declare function getSessionData(sessionId: string): SessionData | null;
27
+ export declare function getSessionDataById(sessionId: string | undefined): SessionData | null;
28
+ export declare function deleteSession(sessionId: string): void;
29
+ export declare function hasActiveHeadlessSession(): boolean;
30
+ export declare function getFirstActiveSession(): {
31
+ sessionId: string;
32
+ data: SessionData;
33
+ } | null;
34
+ export declare function findSessionByRobotName(robotName: string): string | null;
35
+ export declare function pushMessageToSession(robotName: string, message: {
25
36
  msgid: string;
26
37
  content: string;
27
38
  from_userid: string;
@@ -32,7 +43,13 @@ export declare function pushMessageToAllClients(robotName: string, message: {
32
43
  export interface ApprovalRequest {
33
44
  tool_name: string;
34
45
  tool_input: Record<string, unknown>;
46
+ projectDir?: string;
47
+ ccId?: string;
35
48
  }
36
- export declare function startHttpServer(port?: number): Promise<void>;
49
+ export declare function registerCcId(ccId: string, robotName: string): void;
50
+ export declare function unregisterCcId(ccId: string): void;
51
+ export declare function getRobotByCcId(ccId: string): string | null;
52
+ export declare function startHttpServer(_server: McpServer, port?: number): Promise<void>;
37
53
  export declare function stopHttpServer(): void;
38
54
  export declare function cleanupPortFile(): void;
55
+ export {};