@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 +29 -6
- package/dist/client.d.ts +5 -3
- package/dist/client.js +85 -58
- package/dist/config-wizard.js +45 -25
- package/dist/connection-log.d.ts +1 -2
- package/dist/connection-log.js +0 -10
- package/dist/connection-manager.js +1 -21
- package/dist/http-server.d.ts +29 -12
- package/dist/http-server.js +255 -155
- package/dist/index.d.ts +1 -3
- package/dist/index.js +1 -3
- package/dist/keepalive-monitor.js +6 -3
- package/dist/tools/index.js +9 -13
- package/dist/utils/hash.d.ts +12 -3
- package/dist/utils/hash.js +18 -8
- package/package.json +2 -1
- package/skills/headless-mode/SKILL.md +32 -35
- package/dist/approval-manager.d.ts +0 -38
- package/dist/approval-manager.js +0 -129
- package/dist/cc-registry.d.ts +0 -62
- package/dist/cc-registry.js +0 -278
- package/dist/tools/headless.d.ts +0 -8
- package/dist/tools/headless.js +0 -248
- package/dist/tools/messaging.d.ts +0 -7
- package/dist/tools/messaging.js +0 -170
- package/dist/tools/utils-tools.d.ts +0 -11
- package/dist/tools/utils-tools.js +0 -249
- package/dist/utils/atomic-write.d.ts +0 -4
- package/dist/utils/atomic-write.js +0 -9
- package/dist/utils/sanitize.d.ts +0 -59
- package/dist/utils/sanitize.js +0 -246
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.
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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.
|
|
80
|
-
|
|
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
|
-
|
|
188
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
564
|
-
|
|
616
|
+
// 单例实例
|
|
617
|
+
let instance = null;
|
|
565
618
|
export function initClient(botId, secret, targetUserId, robotName) {
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
instances.get(robotName).disconnect();
|
|
619
|
+
if (instance) {
|
|
620
|
+
instance.disconnect();
|
|
569
621
|
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
return client;
|
|
622
|
+
instance = new WecomClient(botId, secret, targetUserId, robotName);
|
|
623
|
+
instance.connect();
|
|
624
|
+
return instance;
|
|
574
625
|
}
|
|
575
|
-
export function getClient(
|
|
576
|
-
if (
|
|
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
|
-
|
|
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 };
|
package/dist/config-wizard.js
CHANGED
|
@@ -252,7 +252,7 @@ function writeHookScript() {
|
|
|
252
252
|
# HTTP Transport 版本
|
|
253
253
|
#
|
|
254
254
|
# 固定端口: 18963
|
|
255
|
-
#
|
|
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
|
-
#
|
|
276
|
+
# 检查项目目录的微信模式配置文件
|
|
277
277
|
PROJECT_DIR=$(pwd)
|
|
278
|
-
|
|
278
|
+
CONFIG_FILE="$PROJECT_DIR/.claude/wecom-aibot.json"
|
|
279
279
|
|
|
280
|
-
#
|
|
281
|
-
if [[ ! -f "$
|
|
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
|
-
#
|
|
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
|
-
|
|
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"
|
|
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
|
`;
|
package/dist/connection-log.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* 记录 WebSocket 连接状态变化,便于分析连接问题
|
|
5
5
|
*/
|
|
6
6
|
interface ConnectionRecord {
|
|
7
|
-
event: 'connected' | 'authenticated' | 'disconnected' | 'reconnecting' | 'error'
|
|
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;
|
package/dist/connection-log.js
CHANGED
|
@@ -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
|
-
//
|
|
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();
|
package/dist/http-server.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* v2.0 架构变更:
|
|
12
|
+
* - 使用 Session 管理,不再使用 projectDir
|
|
13
|
+
* - Session → robotName → WebSocket Connection
|
|
13
14
|
*/
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20
|
+
agentName?: string;
|
|
21
|
+
ccId: string;
|
|
22
|
+
createdAt: number;
|
|
23
|
+
}
|
|
23
24
|
export declare function generateCcId(): string;
|
|
24
|
-
export declare function
|
|
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
|
|
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 {};
|