@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.
- package/dist/bin.d.ts +7 -6
- package/dist/bin.js +43 -800
- package/dist/channel-server.js +115 -17
- package/dist/config-wizard.d.ts +2 -61
- package/dist/config-wizard.js +22 -915
- package/dist/http-server.d.ts +6 -0
- package/dist/http-server.js +56 -0
- package/dist/index.d.ts +5 -15
- package/dist/index.js +5 -21
- package/dist/tools/index.js +14 -4
- package/package.json +4 -7
package/dist/http-server.d.ts
CHANGED
|
@@ -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;
|
package/dist/http-server.js
CHANGED
|
@@ -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
|
-
*
|
|
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 {
|
|
12
|
-
export {
|
|
13
|
-
export {
|
|
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
|
-
*
|
|
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
|
-
|
|
12
|
-
export {
|
|
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';
|
package/dist/tools/index.js
CHANGED
|
@@ -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
|
-
|
|
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": "
|
|
4
|
-
"description": "企业微信智能机器人 MCP
|
|
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
|
-
"
|
|
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",
|