@vrs-soft/wecom-aibot-mcp 2.4.0 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,7 +14,7 @@
14
14
 
15
15
  ## 前置条件
16
16
 
17
- 企业微信管理后台创建智能机器人,连接方式选「使用长连接」,记录 **Bot ID** 和 **Secret**。
17
+ 企业微信管理后台创建智能机器人,连接方式选「使用长连接」,记录 **Bot ID** 和 **Secret** 以及 **DocURL**(文档url)。
18
18
 
19
19
  ---
20
20
 
package/dist/client.d.ts CHANGED
@@ -25,6 +25,12 @@ interface MessageRecord {
25
25
  chattype: 'single' | 'group';
26
26
  quoteContent?: string;
27
27
  }
28
+ interface PendingMessage {
29
+ type: 'text' | 'approval';
30
+ content: any;
31
+ targetUser?: string;
32
+ timestamp: number;
33
+ }
28
34
  declare class WecomClient extends EventEmitter {
29
35
  private wsClient;
30
36
  private approvals;
@@ -77,6 +83,10 @@ declare class WecomClient extends EventEmitter {
77
83
  * allow-once 消费后标记为已消费
78
84
  */
79
85
  consumeApproval(taskId: string): 'allow-once' | 'allow-always' | 'deny' | null;
86
+ /**
87
+ * 获取所有未解决的审批记录 Map(用于重连时迁移到新 client)
88
+ */
89
+ getUnresolvedApprovalMap(): Map<string, ApprovalRecord>;
80
90
  /**
81
91
  * 注入审批记录(MCP 重启恢复用)
82
92
  * 如果 taskId 已存在则跳过,避免覆盖用户已点击的结果
@@ -88,6 +98,15 @@ declare class WecomClient extends EventEmitter {
88
98
  cleanupMessages(maxAgeMs?: number): void;
89
99
  private flushPendingMessages;
90
100
  getPendingMessageCount(): number;
101
+ /**
102
+ * 取出所有待发送的审批消息并清空队列(用于 client 迁移)
103
+ * 仅迁移 approval 类型,text 通知类消息不需要迁移
104
+ */
105
+ takePendingApprovalMessages(): PendingMessage[];
106
+ /**
107
+ * 注入待发送消息(用于 client 迁移,新 client 连接后会自动重发)
108
+ */
109
+ injectPendingMessages(messages: PendingMessage[]): void;
91
110
  getReconnectStatus(): {
92
111
  wasReconnecting: boolean;
93
112
  attempt: number;
@@ -96,4 +115,4 @@ declare class WecomClient extends EventEmitter {
96
115
  }
97
116
  export declare function initClient(botId: string, secret: string, targetUserId: string, robotName: string): WecomClient;
98
117
  export declare function getClient(): WecomClient;
99
- export { WecomClient, ApprovalRecord, MessageRecord, MAX_PENDING_MESSAGES };
118
+ export { WecomClient, ApprovalRecord, MessageRecord, PendingMessage, MAX_PENDING_MESSAGES };
package/dist/client.js CHANGED
@@ -528,6 +528,18 @@ class WecomClient extends EventEmitter {
528
528
  }
529
529
  return approval.result;
530
530
  }
531
+ /**
532
+ * 获取所有未解决的审批记录 Map(用于重连时迁移到新 client)
533
+ */
534
+ getUnresolvedApprovalMap() {
535
+ const pending = new Map();
536
+ this.approvals.forEach((approval, taskId) => {
537
+ if (!approval.resolved) {
538
+ pending.set(taskId, approval);
539
+ }
540
+ });
541
+ return pending;
542
+ }
531
543
  /**
532
544
  * 注入审批记录(MCP 重启恢复用)
533
545
  * 如果 taskId 已存在则跳过,避免覆盖用户已点击的结果
@@ -607,6 +619,24 @@ class WecomClient extends EventEmitter {
607
619
  getPendingMessageCount() {
608
620
  return this.pendingMessages.length;
609
621
  }
622
+ /**
623
+ * 取出所有待发送的审批消息并清空队列(用于 client 迁移)
624
+ * 仅迁移 approval 类型,text 通知类消息不需要迁移
625
+ */
626
+ takePendingApprovalMessages() {
627
+ const approvalMessages = this.pendingMessages.filter(m => m.type === 'approval');
628
+ this.pendingMessages = this.pendingMessages.filter(m => m.type !== 'approval');
629
+ return approvalMessages;
630
+ }
631
+ /**
632
+ * 注入待发送消息(用于 client 迁移,新 client 连接后会自动重发)
633
+ */
634
+ injectPendingMessages(messages) {
635
+ if (messages.length === 0)
636
+ return;
637
+ this.pendingMessages.unshift(...messages);
638
+ logger.log(`[wecom] 注入待发送消息: ${messages.length} 条`);
639
+ }
610
640
  // 获取重连状态
611
641
  getReconnectStatus() {
612
642
  return {
@@ -444,13 +444,14 @@ if ! echo "$HEALTH" | jq -e '.status == "ok"' > /dev/null 2>&1; then
444
444
  exit 0
445
445
  fi
446
446
 
447
- # 读取当前项目使用的机器人名称
447
+ # 读取当前项目使用的机器人名称和 ccId
448
448
  ROBOT_NAME=$(jq -r '.robotName // empty' "$CONFIG_FILE" 2>/dev/null)
449
+ CC_ID=$(jq -r '.ccId // empty' "$CONFIG_FILE" 2>/dev/null)
449
450
 
450
451
  # 发送审批请求(使用 pwd 作为 projectDir)
451
452
  TOOL_INPUT=$(echo "$INPUT" | jq -c '.tool_input // {}')
452
- BODY=$(jq -n --arg tool_name "$TOOL_NAME" --argjson tool_input "$TOOL_INPUT" --arg project_dir "$PROJECT_DIR" --arg robot_name "$ROBOT_NAME" \\
453
- '{"tool_name":$tool_name,"tool_input":$tool_input,"projectDir":$project_dir,"robotName":$robot_name}')
453
+ BODY=$(jq -n --arg tool_name "$TOOL_NAME" --argjson tool_input "$TOOL_INPUT" --arg project_dir "$PROJECT_DIR" --arg robot_name "$ROBOT_NAME" --arg cc_id "$CC_ID" \\
454
+ '{"tool_name":$tool_name,"tool_input":$tool_input,"projectDir":$project_dir,"robotName":$robot_name,"ccId":$cc_id}')
454
455
 
455
456
  log_debug "[$(date)] Sending approval request..."
456
457
  RESPONSE=$(curl -s -m 10 -X POST "http://127.0.0.1:$MCP_PORT/approve" \\
@@ -146,7 +146,22 @@ export async function getClient(robotName) {
146
146
  const robot = await findRobotConfig(state.robotName);
147
147
  if (robot) {
148
148
  logger.log(`[connection] 重连机器人: ${robot.name}`);
149
+ const oldClient = state.client;
150
+ // 迁移旧 client 的未解决审批记录和待发送审批消息
151
+ const pendingApprovals = oldClient.getUnresolvedApprovalMap();
152
+ const pendingApprovalMessages = oldClient.takePendingApprovalMessages();
153
+ // 先断开旧 client,防止旧 client 重连后与新 client 并存导致事件分流
154
+ oldClient.disconnect();
149
155
  state.client = new WecomClient(robot.botId, robot.secret, robot.targetUserId, robot.name);
156
+ // 注入审批记录,确保 Hook 轮询和用户点击仍能正常处理
157
+ pendingApprovals.forEach((approval, taskId) => {
158
+ state.client.injectApprovalRecord(taskId, {
159
+ toolName: approval.toolName,
160
+ toolInput: approval.toolInput,
161
+ });
162
+ });
163
+ // 注入待发送的审批卡片消息,新 client 连接后会自动重发
164
+ state.client.injectPendingMessages(pendingApprovalMessages);
150
165
  state.client.connect();
151
166
  const connected = await waitForConnection(state.client, 5000);
152
167
  if (connected) {
@@ -833,7 +833,7 @@ async function handleApprovalRequest(req, res) {
833
833
  }
834
834
  const title = `【待审批】${tool_name}`;
835
835
  const requestId = `hook_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
836
- const taskId = await client.sendApprovalRequest(title, description, requestId);
836
+ const taskId = await client.sendApprovalRequest(title, description, requestId, undefined, tool_input, ccId);
837
837
  logger.log(`[http] 审批请求已发送: ${taskId} (机器人: ${robotName})`);
838
838
  // 存储审批并启动超时计时器
839
839
  const entry = {
@@ -873,6 +873,9 @@ function handleApprovalStatus(_req, res, url) {
873
873
  res.writeHead(200, { 'Content-Type': 'application/json' });
874
874
  res.end(JSON.stringify({ status: 'pending', result: 'pending' }));
875
875
  }
876
+ }).catch(() => {
877
+ res.writeHead(200, { 'Content-Type': 'application/json' });
878
+ res.end(JSON.stringify({ status: 'pending', result: 'pending' }));
876
879
  });
877
880
  return;
878
881
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vrs-soft/wecom-aibot-mcp",
3
- "version": "2.4.0",
3
+ "version": "2.4.1",
4
4
  "description": "企业微信智能机器人 MCP 服务 - Claude Code 审批通道",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",