@vrs-soft/wecom-aibot-mcp 2.6.0 → 3.1.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.
@@ -63,6 +63,12 @@ export declare function getCCRegistryEntry(ccId: string): CCRegistryEntry | null
63
63
  export declare function getCCCount(): number;
64
64
  export declare function getCCCountByRobot(robotName: string): number;
65
65
  export declare function getOnlineCcIds(): string[];
66
+ /** 当前是否存在某个 ccId 的活跃 SSE 客户端 */
67
+ export declare function hasActiveSseFor(ccId: string): boolean;
68
+ /** 把 cc 消息存进 pending queue(目标无 SSE 时使用) */
69
+ export declare function enqueueCcPending(msg: import('./message-bus.js').CcMessage): void;
70
+ /** 取出指定 ccId 的所有未过期 pending 消息,并清空队列 */
71
+ export declare function drainCcPending(ccId: string): import('./message-bus.js').CcMessage[];
66
72
  export declare function startHttpServer(_server: McpServer, port?: number, httpsConfig?: {
67
73
  certPath: string;
68
74
  keyPath: string;
@@ -210,6 +210,46 @@ export function getOnlineCcIds() {
210
210
  const pendingApprovals = new Map();
211
211
  const transports = new Map();
212
212
  const sseClients = new Map(); // clientId -> SSEClient
213
+ const ccPendingQueue = new Map();
214
+ const CC_PENDING_TTL_MS = 5 * 60 * 1000;
215
+ const CC_PENDING_MAX_PER_CC = 100;
216
+ /** 当前是否存在某个 ccId 的活跃 SSE 客户端 */
217
+ export function hasActiveSseFor(ccId) {
218
+ for (const [, client] of sseClients) {
219
+ if (client.ccId === ccId)
220
+ return true;
221
+ }
222
+ return false;
223
+ }
224
+ /** 把 cc 消息存进 pending queue(目标无 SSE 时使用) */
225
+ export function enqueueCcPending(msg) {
226
+ const list = ccPendingQueue.get(msg.toCc) || [];
227
+ list.push({ msg, enqueuedAt: Date.now() });
228
+ while (list.length > CC_PENDING_MAX_PER_CC)
229
+ list.shift();
230
+ ccPendingQueue.set(msg.toCc, list);
231
+ logger.info('cc_message queued', { to: msg.toCc, from: msg.fromCc, msgId: msg.msgId, depth: list.length });
232
+ }
233
+ /** 取出指定 ccId 的所有未过期 pending 消息,并清空队列 */
234
+ export function drainCcPending(ccId) {
235
+ const list = ccPendingQueue.get(ccId);
236
+ if (!list || list.length === 0)
237
+ return [];
238
+ ccPendingQueue.delete(ccId);
239
+ const now = Date.now();
240
+ return list.filter(item => now - item.enqueuedAt < CC_PENDING_TTL_MS).map(item => item.msg);
241
+ }
242
+ // 周期性清理过期项
243
+ setInterval(() => {
244
+ const now = Date.now();
245
+ for (const [ccId, list] of ccPendingQueue) {
246
+ const kept = list.filter(item => now - item.enqueuedAt < CC_PENDING_TTL_MS);
247
+ if (kept.length === 0)
248
+ ccPendingQueue.delete(ccId);
249
+ else if (kept.length !== list.length)
250
+ ccPendingQueue.set(ccId, kept);
251
+ }
252
+ }, 60 * 1000).unref?.();
213
253
  // 初始化 MCP Server(不再全局连接)
214
254
  function initMcpServer() {
215
255
  // 订阅消息总线,实现 SSE 推送
@@ -1176,6 +1216,8 @@ function handleSSEConnect(req, res, _url) {
1176
1216
  logger.log(`[http] SSE 心跳发送: clientId=${clientId}`);
1177
1217
  }, 15000);
1178
1218
  // 订阅发往当前 ccId 的 CC 间消息,转发为 cc_message SSE event
1219
+ // 注意 ordering:先 subscribe(捕获后续到达的消息)→ sseClients.set 已经在前面完成
1220
+ // (所以 send_to_cc 的 hasActiveSseFor 检测已开始返 true)→ 再 drain pending(不漏)
1179
1221
  const ccSub = subscribeCcMessageByTarget(targetCcId, (msg) => {
1180
1222
  try {
1181
1223
  const data = JSON.stringify(msg);
@@ -1186,6 +1228,20 @@ function handleSSEConnect(req, res, _url) {
1186
1228
  logger.error(`[http] cc_message 推送失败 clientId=${clientId}:`, err);
1187
1229
  }
1188
1230
  });
1231
+ // 把目标离线期间堆积的 cc_message 直接 flush 给当前连接(v2.6.1+)
1232
+ const pending = drainCcPending(targetCcId);
1233
+ if (pending.length > 0) {
1234
+ for (const msg of pending) {
1235
+ try {
1236
+ const data = JSON.stringify(msg);
1237
+ res.write(`event: cc_message\ndata: ${data}\n\n`);
1238
+ }
1239
+ catch (err) {
1240
+ logger.error(`[http] cc_message flush 失败 clientId=${clientId}:`, err);
1241
+ }
1242
+ }
1243
+ logger.info('cc_message flushed pending', { ccId: targetCcId, count: pending.length });
1244
+ }
1189
1245
  // 处理客户端断开
1190
1246
  req.on('close', () => {
1191
1247
  clearInterval(heartbeatInterval);
package/dist/index.d.ts CHANGED
@@ -1,18 +1,8 @@
1
1
  /**
2
- * MCP Server 模块入口
2
+ * wecom-aibot-mcp 公共 API
3
3
  *
4
- * 可作为库导入使用
5
- *
6
- * v2.0 架构变更:
7
- * - 使用 Session 管理
8
- * - 不再使用 projectDir
9
- * - robotName 作为连接索引
4
+ * 客户端模块入口(channel-server + config)
10
5
  */
11
- export { WecomClient, initClient } from './client.js';
12
- export { connectRobot, disconnectRobot, getClient, getConnectionState, getAllConnectionStates, } from './connection-manager.js';
13
- export { startHttpServer, stopHttpServer, HTTP_PORT, } from './http-server.js';
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';
17
- export { registerTools } from './tools/index.js';
18
- export { listAllRobots, runConfigWizard } from './config-wizard.js';
6
+ export { startChannelServer } from './channel-server.js';
7
+ export { VERSION, runRemoteInstallWizard, uninstall, getInstalledMode } from './config-wizard.js';
8
+ export { logger } from './logger.js';
package/dist/index.js CHANGED
@@ -1,24 +1,8 @@
1
1
  /**
2
- * MCP Server 模块入口
2
+ * wecom-aibot-mcp 公共 API
3
3
  *
4
- * 可作为库导入使用
5
- *
6
- * v2.0 架构变更:
7
- * - 使用 Session 管理
8
- * - 不再使用 projectDir
9
- * - robotName 作为连接索引
4
+ * 客户端模块入口(channel-server + config)
10
5
  */
11
- // Client 模块
12
- export { WecomClient, initClient } from './client.js';
13
- // 连接管理模块
14
- export { connectRobot, disconnectRobot, getClient, getConnectionState, getAllConnectionStates, } from './connection-manager.js';
15
- // HTTP 服务模块
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';
21
- // 工具注册
22
- export { registerTools } from './tools/index.js';
23
- // 配置向导
24
- export { listAllRobots, runConfigWizard } from './config-wizard.js';
6
+ export { startChannelServer } from './channel-server.js';
7
+ export { VERSION, runRemoteInstallWizard, uninstall, getInstalledMode } from './config-wizard.js';
8
+ export { logger } from './logger.js';
@@ -3,7 +3,7 @@ import { z } from 'zod';
3
3
  import { listAllRobots, getDocMcpUrl, installSkill, VERSION } from '../config-wizard.js';
4
4
  import { callDocTool } from '../doc-proxy.js';
5
5
  import { connectRobot, disconnectRobot, getClient, getConnectionState, getAllConnectionStates, } from '../connection-manager.js';
6
- import { registerCcId, unregisterCcId, getRobotByCcId, getProjectDirByCcId, generateCcId, getOnlineCcIds, getCCRegistryEntry, } from '../http-server.js';
6
+ import { registerCcId, unregisterCcId, getRobotByCcId, getProjectDirByCcId, generateCcId, getOnlineCcIds, getCCRegistryEntry, hasActiveSseFor, enqueueCcPending, } from '../http-server.js';
7
7
  import { subscribeWecomMessageByCcId, publishCcMessage } from '../message-bus.js';
8
8
  import { randomBytes } from 'crypto';
9
9
  import { updateWechatModeConfig, loadWechatModeConfig, addPermissionHook, removePermissionHook, addStopHook, removeStopHook, registerActiveProject, unregisterActiveProject } from '../project-config.js';
@@ -145,7 +145,7 @@ export function registerTools(server) {
145
145
  return { content: [{ type: 'text', text: JSON.stringify({ delivered: false, reason: 'target offline', to_cc }) }] };
146
146
  }
147
147
  const msgId = `cc_${Date.now()}_${randomBytes(4).toString('hex')}`;
148
- publishCcMessage({
148
+ const msg = {
149
149
  msgId,
150
150
  fromCc: cc_id,
151
151
  toCc: to_cc,
@@ -154,11 +154,21 @@ export function registerTools(server) {
154
154
  replyTo: reply_to,
155
155
  hopCount: 0,
156
156
  timestamp: Date.now(),
157
- });
157
+ };
158
+ // 目标当前是否有活跃 SSE 客户端?有 → 立即推送;无 → 入 pending queue(v2.6.1+)
159
+ // 解决:目标 CC 睡眠 / NAT 闭合 / channel-server 在 reconnect 窗口里时,
160
+ // publish 到 RxJS Subject 没有订阅者会蒸发的问题。
161
+ const live = hasActiveSseFor(to_cc);
162
+ if (live) {
163
+ publishCcMessage(msg);
164
+ }
165
+ else {
166
+ enqueueCcPending(msg);
167
+ }
158
168
  return {
159
169
  content: [{
160
170
  type: 'text',
161
- text: JSON.stringify({ delivered: true, msgId, to_cc, kind }),
171
+ text: JSON.stringify({ delivered: true, msgId, to_cc, kind, state: live ? 'live' : 'queued' }),
162
172
  }],
163
173
  };
164
174
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vrs-soft/wecom-aibot-mcp",
3
- "version": "2.6.0",
4
- "description": "企业微信智能机器人 MCP 服务 - Claude Code 审批通道",
3
+ "version": "3.1.0",
4
+ "description": "企业微信智能机器人 MCP 客户端 - 连接 wecom-aibot-server daemon",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
@@ -19,8 +19,7 @@
19
19
  "prepublishOnly": "npm run build",
20
20
  "test": "vitest run",
21
21
  "test:watch": "vitest",
22
- "test:coverage": "vitest run --coverage",
23
- "test:e2e": "vitest run tests/e2e"
22
+ "test:coverage": "vitest run --coverage"
24
23
  },
25
24
  "keywords": [
26
25
  "wecom",
@@ -38,9 +37,7 @@
38
37
  },
39
38
  "dependencies": {
40
39
  "@modelcontextprotocol/sdk": "^1.0.0",
41
- "@wecom/aibot-node-sdk": "^1.0.4",
42
- "dotenv": "^16.4.5",
43
- "rxjs": "^7.8.2"
40
+ "dotenv": "^16.4.5"
44
41
  },
45
42
  "devDependencies": {
46
43
  "@types/node": "^20.11.0",