@vrs-soft/wecom-aibot-mcp 1.2.1 → 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/README.md +1 -1
- package/dist/bin.js +29 -16
- package/dist/client.d.ts +5 -0
- package/dist/client.js +79 -16
- package/dist/config-wizard.js +113 -115
- package/dist/connection-log.d.ts +9 -25
- package/dist/connection-log.js +37 -130
- package/dist/connection-manager.d.ts +27 -18
- package/dist/connection-manager.js +114 -62
- package/dist/headless-state.d.ts +62 -19
- package/dist/headless-state.js +260 -66
- package/dist/http-server.d.ts +40 -16
- package/dist/http-server.js +679 -263
- package/dist/index.d.ts +9 -5
- package/dist/index.js +9 -7
- package/dist/keepalive-monitor.js +8 -4
- package/dist/tools/index.d.ts +18 -5
- package/dist/tools/index.js +441 -11
- 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 +72 -37
- 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 -272
- package/dist/tools/headless.d.ts +0 -8
- package/dist/tools/headless.js +0 -246
- package/dist/tools/messaging.d.ts +0 -7
- package/dist/tools/messaging.js +0 -159
- 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/README.md
CHANGED
package/dist/bin.js
CHANGED
|
@@ -38,6 +38,7 @@ function showHelp() {
|
|
|
38
38
|
--version, -v 显示版本号
|
|
39
39
|
--start 启动 MCP Server(后台服务模式)
|
|
40
40
|
--stop 停止 MCP Server
|
|
41
|
+
--debug 前台启动 MCP Server(日志直接输出到终端,用于调试)
|
|
41
42
|
--status 显示服务状态和机器人配置
|
|
42
43
|
--config 重新配置默认机器人(修改 Bot ID / Secret / 目标用户)
|
|
43
44
|
--add 添加新的机器人配置(多机器人场景)
|
|
@@ -86,11 +87,11 @@ function showStatus() {
|
|
|
86
87
|
console.log('尚未配置机器人,请运行 npx @vrs-soft/wecom-aibot-mcp 启动配置向导');
|
|
87
88
|
return;
|
|
88
89
|
}
|
|
89
|
-
//
|
|
90
|
+
// 构建机器人占用信息
|
|
90
91
|
const robotUsage = new Map();
|
|
91
92
|
for (const conn of connections) {
|
|
92
|
-
if (conn.
|
|
93
|
-
robotUsage.set(conn.robotName, {
|
|
93
|
+
if (conn.agentName) {
|
|
94
|
+
robotUsage.set(conn.robotName, { agentName: conn.agentName });
|
|
94
95
|
}
|
|
95
96
|
}
|
|
96
97
|
console.log(`已配置 ${allRobots.length} 个机器人:\n`);
|
|
@@ -101,7 +102,7 @@ function showStatus() {
|
|
|
101
102
|
console.log(` Bot ID: ${robot.botId}`);
|
|
102
103
|
console.log(` 目标用户: ${robot.targetUserId}`);
|
|
103
104
|
if (usage) {
|
|
104
|
-
console.log(` 使用者: ${usage.
|
|
105
|
+
console.log(` 使用者: ${usage.agentName}`);
|
|
105
106
|
}
|
|
106
107
|
console.log('');
|
|
107
108
|
}
|
|
@@ -118,8 +119,10 @@ function isServerRunning() {
|
|
|
118
119
|
return true;
|
|
119
120
|
}
|
|
120
121
|
catch {
|
|
121
|
-
// 进程不存在,清理 PID
|
|
122
|
-
fs.
|
|
122
|
+
// 进程不存在,清理 PID 文件(可能已被进程自身删除)
|
|
123
|
+
if (fs.existsSync(PID_FILE)) {
|
|
124
|
+
fs.unlinkSync(PID_FILE);
|
|
125
|
+
}
|
|
123
126
|
return false;
|
|
124
127
|
}
|
|
125
128
|
}
|
|
@@ -146,13 +149,18 @@ function stopServer() {
|
|
|
146
149
|
break;
|
|
147
150
|
}
|
|
148
151
|
}
|
|
149
|
-
|
|
152
|
+
// 进程退出后删除 PID 文件(如果还存在)
|
|
153
|
+
if (fs.existsSync(PID_FILE)) {
|
|
154
|
+
fs.unlinkSync(PID_FILE);
|
|
155
|
+
}
|
|
150
156
|
console.log('[mcp] 服务已停止');
|
|
151
157
|
return true;
|
|
152
158
|
}
|
|
153
159
|
catch (err) {
|
|
154
160
|
console.error('[mcp] 停止服务失败:', err);
|
|
155
|
-
fs.
|
|
161
|
+
if (fs.existsSync(PID_FILE)) {
|
|
162
|
+
fs.unlinkSync(PID_FILE);
|
|
163
|
+
}
|
|
156
164
|
return false;
|
|
157
165
|
}
|
|
158
166
|
}
|
|
@@ -174,13 +182,11 @@ async function waitForConnection(client, timeoutMs = 10000) {
|
|
|
174
182
|
}
|
|
175
183
|
// 启动 MCP Server(前台运行,供 --start 使用)
|
|
176
184
|
async function startMcpServerForeground() {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if (allRobots.length === 0) {
|
|
185
|
+
const savedConfig = loadConfig();
|
|
186
|
+
if (!savedConfig || !savedConfig.botId || !savedConfig.secret || !savedConfig.targetUserId) {
|
|
180
187
|
console.error('[mcp] 未找到配置,请先运行: npx @vrs-soft/wecom-aibot-mcp');
|
|
181
188
|
process.exit(1);
|
|
182
189
|
}
|
|
183
|
-
console.log(`[mcp] 发现 ${allRobots.length} 个机器人配置: ${allRobots.map(r => r.name).join(', ')}`);
|
|
184
190
|
// 写入 PID 文件
|
|
185
191
|
fs.writeFileSync(PID_FILE, String(process.pid));
|
|
186
192
|
// 确保 hook 已安装
|
|
@@ -224,9 +230,9 @@ async function startMcpServerForeground() {
|
|
|
224
230
|
}
|
|
225
231
|
// 后台启动 MCP Server(使用 spawn)
|
|
226
232
|
function startMcpServerBackground() {
|
|
227
|
-
//
|
|
228
|
-
const
|
|
229
|
-
if (
|
|
233
|
+
// 检查配置是否存在
|
|
234
|
+
const savedConfig = loadConfig();
|
|
235
|
+
if (!savedConfig || !savedConfig.botId || !savedConfig.secret || !savedConfig.targetUserId) {
|
|
230
236
|
console.error('[mcp] 未找到配置,请先运行: npx @vrs-soft/wecom-aibot-mcp');
|
|
231
237
|
process.exit(1);
|
|
232
238
|
}
|
|
@@ -246,6 +252,7 @@ function startMcpServerBackground() {
|
|
|
246
252
|
console.log(`[mcp] HTTP endpoint: http://127.0.0.1:18963/mcp`);
|
|
247
253
|
console.log('[mcp] 健康检查: curl http://127.0.0.1:18963/health');
|
|
248
254
|
console.log('[mcp] 停止服务: npx @vrs-soft/wecom-aibot-mcp --stop');
|
|
255
|
+
console.log('[mcp] 调试模式: npx @vrs-soft/wecom-aibot-mcp --debug');
|
|
249
256
|
}
|
|
250
257
|
async function main() {
|
|
251
258
|
const args = process.argv.slice(2);
|
|
@@ -287,11 +294,17 @@ async function main() {
|
|
|
287
294
|
await deleteMcpConfigInteractive(instanceName);
|
|
288
295
|
process.exit(0);
|
|
289
296
|
}
|
|
290
|
-
// --start --foreground
|
|
297
|
+
// --start --foreground:前台启动(内部调用,输出到日志文件)
|
|
291
298
|
if (args.includes('--start') && args.includes('--foreground')) {
|
|
292
299
|
await startMcpServerForeground();
|
|
293
300
|
return; // 保持运行,不 exit
|
|
294
301
|
}
|
|
302
|
+
// --debug:前台启动,日志直接输出到终端
|
|
303
|
+
if (args.includes('--debug')) {
|
|
304
|
+
console.log('[mcp] Debug 模式:前台运行,Ctrl+C 退出');
|
|
305
|
+
await startMcpServerForeground();
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
295
308
|
// --start:后台启动
|
|
296
309
|
if (args.includes('--start')) {
|
|
297
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;
|
|
@@ -22,6 +23,7 @@ interface MessageRecord {
|
|
|
22
23
|
from_userid: string;
|
|
23
24
|
chatid: string;
|
|
24
25
|
chattype: 'single' | 'group';
|
|
26
|
+
quoteContent?: string;
|
|
25
27
|
}
|
|
26
28
|
declare class WecomClient extends EventEmitter {
|
|
27
29
|
private wsClient;
|
|
@@ -35,11 +37,13 @@ declare class WecomClient extends EventEmitter {
|
|
|
35
37
|
private wasReconnecting;
|
|
36
38
|
private reconnectAttempt;
|
|
37
39
|
private lastDisconnectTime;
|
|
40
|
+
private disconnectNotifyCount;
|
|
38
41
|
constructor(botId: string, secret: string, targetUserId: string, robotName: string);
|
|
39
42
|
getAuthUrl(): string;
|
|
40
43
|
private setupEventHandlers;
|
|
41
44
|
private handleMessage;
|
|
42
45
|
private handleApprovalResponse;
|
|
46
|
+
private replyApprovalResult;
|
|
43
47
|
connect(): void;
|
|
44
48
|
disconnect(): void;
|
|
45
49
|
isConnected(): boolean;
|
|
@@ -53,6 +57,7 @@ declare class WecomClient extends EventEmitter {
|
|
|
53
57
|
ccId?: string): Promise<string>;
|
|
54
58
|
sendQueuedApproval(taskId: string, title: string, description: string, targetUser?: string): Promise<boolean>;
|
|
55
59
|
getApprovalResult(taskId: string): 'pending' | 'allow-once' | 'allow-always' | 'deny';
|
|
60
|
+
setApprovalResult(taskId: string, result: 'allow-once' | 'deny', reason?: string): boolean;
|
|
56
61
|
getPendingApprovals(): string[];
|
|
57
62
|
getPendingApprovalsRecords(): ApprovalRecord[];
|
|
58
63
|
getApprovalRecord(taskId: string): ApprovalRecord | undefined;
|
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;
|
|
@@ -53,14 +54,15 @@ class WecomClient extends EventEmitter {
|
|
|
53
54
|
}
|
|
54
55
|
setupEventHandlers() {
|
|
55
56
|
this.wsClient.on('connected', () => {
|
|
56
|
-
logConnected(
|
|
57
|
+
logConnected();
|
|
57
58
|
});
|
|
58
59
|
this.wsClient.on('authenticated', () => {
|
|
59
60
|
const wasReconnecting = this.wasReconnecting;
|
|
60
61
|
this.connected = true;
|
|
61
62
|
this.wasReconnecting = false;
|
|
62
63
|
this.reconnectAttempt = 0;
|
|
63
|
-
|
|
64
|
+
this.disconnectNotifyCount = 0; // 重连成功后重置断线通知计数
|
|
65
|
+
logAuthenticated();
|
|
64
66
|
// 重连成功后发送通知
|
|
65
67
|
if (wasReconnecting) {
|
|
66
68
|
this.sendText('【系统】连接已恢复').catch(err => {
|
|
@@ -74,18 +76,21 @@ class WecomClient extends EventEmitter {
|
|
|
74
76
|
this.connected = false;
|
|
75
77
|
this.wasReconnecting = true;
|
|
76
78
|
this.lastDisconnectTime = Date.now();
|
|
77
|
-
logDisconnected(
|
|
78
|
-
//
|
|
79
|
-
this.
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
logDisconnected(reason);
|
|
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;
|
|
85
|
-
logReconnecting(
|
|
90
|
+
logReconnecting(attempt);
|
|
86
91
|
});
|
|
87
92
|
this.wsClient.on('error', (err) => {
|
|
88
|
-
logError(
|
|
93
|
+
logError(err.message);
|
|
89
94
|
// 检测授权相关错误(40058: invalid Request Parameter)
|
|
90
95
|
if (err.message.includes('40058') || err.message.includes('invalid Request Parameter')) {
|
|
91
96
|
console.log('');
|
|
@@ -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
|
// 监听进入会话事件
|
|
@@ -159,6 +165,7 @@ class WecomClient extends EventEmitter {
|
|
|
159
165
|
from_userid,
|
|
160
166
|
chatid,
|
|
161
167
|
chattype,
|
|
168
|
+
quoteContent,
|
|
162
169
|
};
|
|
163
170
|
// v3.0: 检查队列上限
|
|
164
171
|
if (this.messages.length >= MAX_PENDING_MESSAGES) {
|
|
@@ -183,14 +190,18 @@ class WecomClient extends EventEmitter {
|
|
|
183
190
|
}
|
|
184
191
|
handleApprovalResponse(frame) {
|
|
185
192
|
const event = frame.body?.event;
|
|
186
|
-
|
|
193
|
+
console.log('[wecom] handleApprovalResponse body.event:', JSON.stringify(event));
|
|
194
|
+
if (!event) {
|
|
195
|
+
console.log('[wecom] event 为空,frame.body:', JSON.stringify(frame.body));
|
|
187
196
|
return;
|
|
188
|
-
|
|
197
|
+
}
|
|
198
|
+
// task_id 和 event_key 在 event.template_card_event 内部
|
|
189
199
|
const cardEvent = event.template_card_event;
|
|
190
|
-
|
|
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)
|
|
191
204
|
return;
|
|
192
|
-
const taskId = cardEvent.task_id;
|
|
193
|
-
const eventKey = cardEvent.event_key; // 用户点击的按钮 key
|
|
194
205
|
console.log(`[wecom] 收到审批响应: taskId=${taskId}, key=${eventKey}`);
|
|
195
206
|
const approval = this.approvals.get(taskId);
|
|
196
207
|
if (approval && !approval.resolved) {
|
|
@@ -203,10 +214,35 @@ class WecomClient extends EventEmitter {
|
|
|
203
214
|
: eventKey === 'allow-always' ? '✅ 已允许(永久)'
|
|
204
215
|
: '❌ 已拒绝';
|
|
205
216
|
const toolInfo = approval.toolName ? `: ${approval.toolName}` : '';
|
|
206
|
-
|
|
217
|
+
const descInfo = approval.description ? `\n\n> ${approval.description}` : '';
|
|
218
|
+
const content = `**审批结果**${toolInfo}\n\n${resultText}${descInfo}`;
|
|
219
|
+
this.sendText(content).catch(err => {
|
|
207
220
|
console.error('[wecom] 发送审批确认失败:', err);
|
|
208
221
|
});
|
|
209
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
|
+
}
|
|
210
246
|
}
|
|
211
247
|
// 连接
|
|
212
248
|
connect() {
|
|
@@ -317,6 +353,7 @@ class WecomClient extends EventEmitter {
|
|
|
317
353
|
timestamp: Date.now(),
|
|
318
354
|
toolName,
|
|
319
355
|
toolInput,
|
|
356
|
+
description, // 保存审批请求原文
|
|
320
357
|
operationHash,
|
|
321
358
|
});
|
|
322
359
|
// 断线时将审批请求加入队列,等待重连后发送
|
|
@@ -391,6 +428,32 @@ class WecomClient extends EventEmitter {
|
|
|
391
428
|
}
|
|
392
429
|
return 'pending';
|
|
393
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
|
+
}
|
|
394
457
|
// 获取所有待处理的审批任务 ID(供 hook 轮询使用)
|
|
395
458
|
getPendingApprovals() {
|
|
396
459
|
return Array.from(this.approvals.entries())
|
package/dist/config-wizard.js
CHANGED
|
@@ -249,32 +249,16 @@ export function uninstall() {
|
|
|
249
249
|
function writeHookScript() {
|
|
250
250
|
const script = `#!/bin/bash
|
|
251
251
|
# wecom-aibot-mcp PermissionRequest hook
|
|
252
|
-
# HTTP Transport 版本
|
|
252
|
+
# HTTP Transport 版本
|
|
253
253
|
#
|
|
254
|
-
#
|
|
255
|
-
# -
|
|
256
|
-
# - curl 错误处理(检查退出码)
|
|
257
|
-
# - 重试机制(3 次重试)
|
|
254
|
+
# 固定端口: 18963
|
|
255
|
+
# 检查 $(pwd)/.claude/wecom-aibot.json 的 wechatMode 和 autoApprove 字段
|
|
258
256
|
|
|
259
257
|
MCP_PORT=18963
|
|
260
|
-
MAX_RETRIES=3
|
|
261
|
-
RETRY_DELAY=2
|
|
262
258
|
|
|
263
259
|
INPUT=$(cat)
|
|
264
260
|
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
|
|
265
261
|
|
|
266
|
-
# ============================================
|
|
267
|
-
# 工具名验证(防止注入攻击)
|
|
268
|
-
# ============================================
|
|
269
|
-
# 只允许字母、数字、下划线、连字符
|
|
270
|
-
if [[ ! "$TOOL_NAME" =~ ^[a-zA-Z0-9_-]+$ ]]; then
|
|
271
|
-
# MCP 工具名可能包含特殊字符(如 mcp__server__tool)
|
|
272
|
-
if [[ ! "$TOOL_NAME" =~ ^[a-zA-Z0-9_-]+$ ]] && [[ ! "$TOOL_NAME" == mcp__* ]]; then
|
|
273
|
-
echo "[hook] 无效的工具名: $TOOL_NAME" >&2
|
|
274
|
-
exit 0
|
|
275
|
-
fi
|
|
276
|
-
fi
|
|
277
|
-
|
|
278
262
|
# MCP 工具本身不需要拦截
|
|
279
263
|
if [[ "$TOOL_NAME" == mcp__* ]]; then
|
|
280
264
|
printf '%s\\n' '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow"}}}'
|
|
@@ -289,97 +273,51 @@ case "$TOOL_NAME" in
|
|
|
289
273
|
;;
|
|
290
274
|
esac
|
|
291
275
|
|
|
292
|
-
#
|
|
276
|
+
# 检查项目目录的微信模式配置文件
|
|
293
277
|
PROJECT_DIR=$(pwd)
|
|
294
|
-
|
|
278
|
+
CONFIG_FILE="$PROJECT_DIR/.claude/wecom-aibot.json"
|
|
295
279
|
|
|
296
|
-
#
|
|
297
|
-
if [[ ! -f "$
|
|
280
|
+
# 配置文件不存在,不在微信模式
|
|
281
|
+
if [[ ! -f "$CONFIG_FILE" ]]; then
|
|
298
282
|
exit 0
|
|
299
283
|
fi
|
|
300
284
|
|
|
301
|
-
#
|
|
302
|
-
|
|
285
|
+
# 检查 wechatMode 是否为 true(微信模式开关)
|
|
286
|
+
WECHAT_MODE=$(jq -r '.wechatMode // false' "$CONFIG_FILE" 2>/dev/null)
|
|
287
|
+
if [[ "$WECHAT_MODE" != "true" ]]; then
|
|
303
288
|
exit 0
|
|
304
289
|
fi
|
|
305
290
|
|
|
306
|
-
#
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
retry_curl() {
|
|
310
|
-
local url="$1"
|
|
311
|
-
local max_retries="$2"
|
|
312
|
-
local attempt=1
|
|
313
|
-
local response
|
|
314
|
-
|
|
315
|
-
while [[ $attempt -le $max_retries ]]; do
|
|
316
|
-
response=$(curl -s -m 10 "$url" 2>/dev/null)
|
|
317
|
-
local exit_code=$?
|
|
318
|
-
|
|
319
|
-
if [[ $exit_code -eq 0 ]]; then
|
|
320
|
-
echo "$response"
|
|
321
|
-
return 0
|
|
322
|
-
fi
|
|
323
|
-
|
|
324
|
-
echo "[hook] curl 失败 (尝试 $attempt/$max_retries, 退出码: $exit_code)" >&2
|
|
325
|
-
|
|
326
|
-
if [[ $attempt -lt $max_retries ]]; then
|
|
327
|
-
sleep $RETRY_DELAY
|
|
328
|
-
fi
|
|
329
|
-
|
|
330
|
-
((attempt++))
|
|
331
|
-
done
|
|
332
|
-
|
|
333
|
-
return 1
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
# ============================================
|
|
337
|
-
# 检查 MCP Server 健康状态
|
|
338
|
-
# ============================================
|
|
339
|
-
HEALTH=$(retry_curl "http://127.0.0.1:$MCP_PORT/health" "$MAX_RETRIES")
|
|
340
|
-
if [[ $? -ne 0 ]] || ! echo "$HEALTH" | jq -e '.status == "ok"' > /dev/null 2>&1; then
|
|
341
|
-
echo "[hook] MCP Server 不在线,跳过审批" >&2
|
|
291
|
+
# 检查 MCP Server 是否在线
|
|
292
|
+
HEALTH=$(curl -s -m 2 "http://127.0.0.1:$MCP_PORT/health" 2>/dev/null)
|
|
293
|
+
if ! echo "$HEALTH" | jq -e '.status == "ok"' > /dev/null 2>&1; then
|
|
342
294
|
exit 0
|
|
343
295
|
fi
|
|
344
296
|
|
|
345
|
-
#
|
|
346
|
-
# 发送审批请求
|
|
347
|
-
# ============================================
|
|
297
|
+
# 发送审批请求(使用 pwd 作为 projectDir)
|
|
348
298
|
TOOL_INPUT=$(echo "$INPUT" | jq -c '.tool_input // {}')
|
|
349
|
-
|
|
350
|
-
# 验证 tool_input 是有效的 JSON
|
|
351
|
-
if ! echo "$TOOL_INPUT" | jq -e . > /dev/null 2>&1; then
|
|
352
|
-
echo "[hook] 无效的 tool_input JSON" >&2
|
|
353
|
-
exit 0
|
|
354
|
-
fi
|
|
355
|
-
|
|
356
299
|
BODY=$(jq -n --arg tool_name "$TOOL_NAME" --argjson tool_input "$TOOL_INPUT" --arg project_dir "$PROJECT_DIR" \\
|
|
357
300
|
'{"tool_name":$tool_name,"tool_input":$tool_input,"projectDir":$project_dir}')
|
|
358
301
|
|
|
359
302
|
RESPONSE=$(curl -s -m 10 -X POST "http://127.0.0.1:$MCP_PORT/approve" \\
|
|
360
303
|
-H "Content-Type: application/json" \\
|
|
361
|
-
-d "$BODY"
|
|
362
|
-
CURL_EXIT=$?
|
|
363
|
-
|
|
364
|
-
if [[ $CURL_EXIT -ne 0 ]]; then
|
|
365
|
-
echo "[hook] 发送审批请求失败 (退出码: $CURL_EXIT)" >&2
|
|
366
|
-
exit 0
|
|
367
|
-
fi
|
|
304
|
+
-d "$BODY")
|
|
368
305
|
|
|
369
306
|
TASK_ID=$(echo "$RESPONSE" | jq -r '.taskId // empty')
|
|
370
307
|
if [[ -z "$TASK_ID" ]]; then
|
|
371
|
-
echo "[hook] 未获取到 taskId" >&2
|
|
372
308
|
exit 0
|
|
373
309
|
fi
|
|
374
310
|
|
|
375
|
-
#
|
|
376
|
-
|
|
377
|
-
#
|
|
378
|
-
WAIT_RESULT=$(curl -s -m 600 -X GET "http://127.0.0.1:$MCP_PORT/approval_wait/$TASK_ID" 2>/dev/null)
|
|
379
|
-
CURL_EXIT=$?
|
|
311
|
+
# 轮询审批结果(带超时:10 分钟)
|
|
312
|
+
POLL_COUNT=0
|
|
313
|
+
MAX_POLL=300 # 300 * 2秒 = 600秒 = 10分钟
|
|
380
314
|
|
|
381
|
-
|
|
382
|
-
|
|
315
|
+
while [[ $POLL_COUNT -lt $MAX_POLL ]]; do
|
|
316
|
+
sleep 2
|
|
317
|
+
POLL_COUNT=$((POLL_COUNT + 1))
|
|
318
|
+
|
|
319
|
+
STATUS=$(curl -s -m 3 "http://127.0.0.1:$MCP_PORT/approval_status/$TASK_ID" 2>/dev/null)
|
|
320
|
+
RESULT=$(echo "$STATUS" | jq -r '.result // empty')
|
|
383
321
|
|
|
384
322
|
if [[ "$RESULT" == "allow-once" || "$RESULT" == "allow-always" ]]; then
|
|
385
323
|
printf '%s\\n' '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow"}}}'
|
|
@@ -388,33 +326,86 @@ if [[ $CURL_EXIT -eq 0 ]] && [[ -n "$WAIT_RESULT" ]]; then
|
|
|
388
326
|
printf '%s\\n' '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"deny","message":"用户拒绝"}}}'
|
|
389
327
|
exit 0
|
|
390
328
|
fi
|
|
391
|
-
|
|
329
|
+
done
|
|
392
330
|
|
|
393
|
-
#
|
|
394
|
-
#
|
|
395
|
-
#
|
|
396
|
-
echo "[hook] 开始无限轮询审批结果" >&2
|
|
331
|
+
# 超时处理:根据 autoApprove 决定行为
|
|
332
|
+
# autoApprove: false → 继续等待(无限轮询)
|
|
333
|
+
# autoApprove: true → 智能代批
|
|
397
334
|
|
|
398
|
-
|
|
399
|
-
|
|
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')
|
|
400
342
|
|
|
401
|
-
|
|
402
|
-
|
|
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
|
|
403
352
|
|
|
404
|
-
|
|
405
|
-
|
|
353
|
+
# autoApprove: true,执行智能代批
|
|
354
|
+
# 规则:删除命令→拒绝,项目内操作→允许,项目外操作→拒绝
|
|
355
|
+
|
|
356
|
+
# 检查是否是删除命令
|
|
357
|
+
IS_DELETE=0
|
|
358
|
+
if [[ "$TOOL_NAME" == "Bash" ]]; then
|
|
359
|
+
CMD=$(echo "$TOOL_INPUT" | jq -r '.command // empty')
|
|
360
|
+
if [[ "$CMD" == rm* ]] || [[ "$CMD" == *" rm "* ]] || [[ "$CMD" == *"-rf"* ]]; then
|
|
361
|
+
IS_DELETE=1
|
|
406
362
|
fi
|
|
363
|
+
fi
|
|
407
364
|
|
|
408
|
-
|
|
365
|
+
# 删除操作 → 永远拒绝
|
|
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 &
|
|
369
|
+
printf '%s\\n' '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"deny","message":"超时自动拒绝:删除操作需人工确认"}}}'
|
|
370
|
+
exit 0
|
|
371
|
+
fi
|
|
409
372
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
373
|
+
# 检查操作路径是否在项目内
|
|
374
|
+
IS_IN_PROJECT=0
|
|
375
|
+
|
|
376
|
+
case "$TOOL_NAME" in
|
|
377
|
+
Bash)
|
|
378
|
+
CMD=$(echo "$TOOL_INPUT" | jq -r '.command // empty')
|
|
379
|
+
if [[ "$CMD" == *"$PROJECT_DIR"* ]] || [[ "$CMD" == ./* ]] || [[ "$CMD" == npm* ]] || [[ "$CMD" == npx* ]] || [[ "$CMD" == git* ]] || [[ "$CMD" == node* ]]; then
|
|
380
|
+
IS_IN_PROJECT=1
|
|
381
|
+
fi
|
|
382
|
+
;;
|
|
383
|
+
Write|Edit)
|
|
384
|
+
FILE_PATH=$(echo "$TOOL_INPUT" | jq -r '.file_path // empty')
|
|
385
|
+
if [[ "$FILE_PATH" == "$PROJECT_DIR"* ]] || [[ "$FILE_PATH" != /* ]]; then
|
|
386
|
+
IS_IN_PROJECT=1
|
|
387
|
+
fi
|
|
388
|
+
;;
|
|
389
|
+
*)
|
|
390
|
+
FILE_PATH=$(echo "$TOOL_INPUT" | jq -r '.file_path // .path // .directory // empty')
|
|
391
|
+
if [[ -n "$FILE_PATH" ]]; then
|
|
392
|
+
if [[ "$FILE_PATH" == "$PROJECT_DIR"* ]] || [[ "$FILE_PATH" != /* ]]; then
|
|
393
|
+
IS_IN_PROJECT=1
|
|
394
|
+
fi
|
|
395
|
+
fi
|
|
396
|
+
;;
|
|
397
|
+
esac
|
|
398
|
+
|
|
399
|
+
# 根据项目内/外决策
|
|
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 &
|
|
403
|
+
printf '%s\\n' '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow","message":"超时自动允许:项目内操作"}}}'
|
|
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 &
|
|
407
|
+
printf '%s\\n' '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"deny","message":"超时自动拒绝:项目外操作需人工确认"}}}'
|
|
408
|
+
fi
|
|
418
409
|
`;
|
|
419
410
|
ensureConfigDir();
|
|
420
411
|
fs.writeFileSync(HOOK_SCRIPT_PATH, script, { mode: 0o755 });
|
|
@@ -423,7 +414,7 @@ done
|
|
|
423
414
|
// 写入 MCP Server 配置到 ~/.claude.json
|
|
424
415
|
function writeMcpServerConfig(config, instanceName) {
|
|
425
416
|
try {
|
|
426
|
-
// 1. 写入机器人配置到 ~/.wecom-aibot-mcp/
|
|
417
|
+
// 1. 写入机器人配置到 ~/.wecom-aibot-mcp/config.json
|
|
427
418
|
ensureConfigDir();
|
|
428
419
|
const botConfig = {
|
|
429
420
|
botId: config.botId,
|
|
@@ -433,10 +424,8 @@ function writeMcpServerConfig(config, instanceName) {
|
|
|
433
424
|
if (config.nameTag) {
|
|
434
425
|
botConfig.nameTag = config.nameTag;
|
|
435
426
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
fs.writeFileSync(robotConfigPath, JSON.stringify(botConfig, null, 2));
|
|
439
|
-
console.log(`[config] 机器人配置已写入: ${path.basename(robotConfigPath)}`);
|
|
427
|
+
fs.writeFileSync(BOT_CONFIG_FILE, JSON.stringify(botConfig, null, 2));
|
|
428
|
+
console.log('[config] 机器人配置已写入 ~/.wecom-aibot-mcp/config.json');
|
|
440
429
|
// 2. 写入 MCP 配置到 ~/.claude.json(仅 URL)
|
|
441
430
|
let claudeConfig = {};
|
|
442
431
|
if (fs.existsSync(CLAUDE_CONFIG_FILE)) {
|
|
@@ -459,7 +448,7 @@ function writeMcpServerConfig(config, instanceName) {
|
|
|
459
448
|
console.error('[config] 写入配置失败:', err);
|
|
460
449
|
console.log('[config] ⚠️ 请手动配置:');
|
|
461
450
|
console.log('');
|
|
462
|
-
console.log('~/.wecom-aibot-mcp/
|
|
451
|
+
console.log('~/.wecom-aibot-mcp/config.json:');
|
|
463
452
|
console.log(JSON.stringify({
|
|
464
453
|
botId: config.botId,
|
|
465
454
|
secret: config.secret,
|
|
@@ -544,10 +533,19 @@ export async function addMcpConfig() {
|
|
|
544
533
|
};
|
|
545
534
|
// 确保配置目录存在
|
|
546
535
|
ensureConfigDir();
|
|
547
|
-
//
|
|
536
|
+
// 如果是第一个机器人,保存为默认配置
|
|
537
|
+
const defaultConfigPath = BOT_CONFIG_FILE;
|
|
548
538
|
const robotConfigPath = path.join(CONFIG_DIR, `robot-${Date.now()}.json`);
|
|
549
|
-
fs.
|
|
550
|
-
|
|
539
|
+
if (!fs.existsSync(defaultConfigPath)) {
|
|
540
|
+
// 第一个机器人作为默认
|
|
541
|
+
fs.writeFileSync(defaultConfigPath, JSON.stringify(robotConfig, null, 2));
|
|
542
|
+
console.log(`\n[config] ✅ 已设为默认机器人: ${robotName}`);
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
545
|
+
// 后续机器人保存为独立文件
|
|
546
|
+
fs.writeFileSync(robotConfigPath, JSON.stringify(robotConfig, null, 2));
|
|
547
|
+
console.log(`\n[config] ✅ 已添加新机器人: ${robotName}`);
|
|
548
|
+
}
|
|
551
549
|
console.log(`[config] 用户 ID: ${targetUserId}`);
|
|
552
550
|
// 列出所有机器人
|
|
553
551
|
const robots = listAllRobots();
|