@vrs-soft/wecom-aibot-mcp 1.3.0 → 1.5.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 +33 -1
- package/dist/bin.js +112 -8
- package/dist/client.d.ts +5 -3
- package/dist/client.js +85 -58
- package/dist/config-wizard.d.ts +4 -0
- package/dist/config-wizard.js +175 -37
- package/dist/connection-log.d.ts +1 -2
- package/dist/connection-log.js +0 -10
- package/dist/connection-manager.d.ts +2 -0
- package/dist/connection-manager.js +15 -34
- package/dist/http-server.d.ts +23 -13
- package/dist/http-server.js +215 -160
- package/dist/index.d.ts +1 -3
- package/dist/index.js +1 -3
- package/dist/keepalive-monitor.js +6 -3
- package/dist/project-config.d.ts +41 -0
- package/dist/project-config.js +132 -0
- package/dist/tools/index.js +61 -78
- 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 +94 -42
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# @vrs-soft/wecom-aibot-mcp
|
|
2
2
|
|
|
3
|
+
中文 | [English](README_EN.md)
|
|
4
|
+
|
|
3
5
|
企业微信智能机器人 MCP 服务 - Claude Code 远程审批通道
|
|
4
6
|
|
|
5
7
|
> 通过企业微信智能机器人实现 Claude Code 的远程审批和消息推送,离开电脑也能处理决策请求。
|
|
@@ -9,7 +11,6 @@
|
|
|
9
11
|
- 🔐 **远程审批**:敏感操作通过微信卡片审批,支持"允许一次/拒绝"
|
|
10
12
|
- 💬 **双向通信**:任务进度、完成通知实时推送到微信
|
|
11
13
|
- 📱 **Headless 模式**:离开电脑时切换到微信交互,长轮询实时接收消息
|
|
12
|
-
- 🔄 **智能代批**:超时自动审批,项目内操作允许,删除操作拒绝
|
|
13
14
|
- 🤖 **多机器人支持**:支持配置多个机器人,团队场景下多人独立使用
|
|
14
15
|
- 🌐 **HTTP Transport**:使用 HTTP 传输,支持多实例共享服务
|
|
15
16
|
|
|
@@ -143,6 +144,7 @@ npx @vrs-soft/wecom-aibot-mcp
|
|
|
143
144
|
| `npx @vrs-soft/wecom-aibot-mcp --add` | 添加新机器人 |
|
|
144
145
|
| `npx @vrs-soft/wecom-aibot-mcp --delete` | 删除机器人配置 |
|
|
145
146
|
| `npx @vrs-soft/wecom-aibot-mcp --uninstall` | 完全卸载 |
|
|
147
|
+
| `npx @vrs-soft/wecom-aibot-mcp --debug` | 前台启动(输出调试日志) |
|
|
146
148
|
|
|
147
149
|
### 添加新机器人
|
|
148
150
|
|
|
@@ -290,6 +292,36 @@ Claude:执行命令,发送结果到群聊
|
|
|
290
292
|
}
|
|
291
293
|
```
|
|
292
294
|
|
|
295
|
+
### 超时审批配置
|
|
296
|
+
|
|
297
|
+
在 `~/.wecom-aibot-mcp/config.json` 中可配置审批超时时间:
|
|
298
|
+
|
|
299
|
+
```json
|
|
300
|
+
{
|
|
301
|
+
"botId": "bot-xxx",
|
|
302
|
+
"secret": "sec-yyy",
|
|
303
|
+
"targetUserId": "user1",
|
|
304
|
+
"nameTag": "机器人1",
|
|
305
|
+
"autoApproveTimeout": 600
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
- `autoApproveTimeout`: 审批超时时间(秒),默认 600 秒(10 分钟)
|
|
310
|
+
- 超时后,项目目录内的操作会自动允许,项目外的操作会自动拒绝
|
|
311
|
+
|
|
312
|
+
### 调试模式
|
|
313
|
+
|
|
314
|
+
使用 `--debug` 启动可在终端查看 hook 脚本的调试日志:
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
npx @vrs-soft/wecom-aibot-mcp --debug
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
调试日志会输出到 stderr,包括:
|
|
321
|
+
- 审批请求拦截信息
|
|
322
|
+
- 超时时间配置
|
|
323
|
+
- 操作类型判断详情
|
|
324
|
+
|
|
293
325
|
## 故障排查
|
|
294
326
|
|
|
295
327
|
### 认证失败(错误码 40058)
|
package/dist/bin.js
CHANGED
|
@@ -13,13 +13,15 @@ import { spawn } from 'child_process';
|
|
|
13
13
|
import * as fs from 'fs';
|
|
14
14
|
import * as path from 'path';
|
|
15
15
|
import * as os from 'os';
|
|
16
|
-
import { runConfigWizard, loadConfig, saveConfig, deleteConfig, deleteMcpConfigInteractive, uninstall, addMcpConfig, detectUserIdFromMessage, ensureHookInstalled, listAllRobots, } from './config-wizard.js';
|
|
16
|
+
import { runConfigWizard, loadConfig, saveConfig, deleteConfig, deleteMcpConfigInteractive, uninstall, addMcpConfig, detectUserIdFromMessage, ensureHookInstalled, listAllRobots, ensureGlobalConfigs, } 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';
|
|
22
|
-
const VERSION = '1.2
|
|
24
|
+
const VERSION = '1.4.2';
|
|
23
25
|
const PID_FILE = path.join(os.homedir(), '.wecom-aibot-mcp', 'server.pid');
|
|
24
26
|
function showHelp() {
|
|
25
27
|
console.log(`
|
|
@@ -34,8 +36,11 @@ function showHelp() {
|
|
|
34
36
|
选项:
|
|
35
37
|
--help, -h 显示帮助信息
|
|
36
38
|
--version, -v 显示版本号
|
|
39
|
+
--upgrade 强制升级全局配置(覆盖 MCP 配置、权限、skill)
|
|
40
|
+
--reinstall 重新安装全局配置(删除后重新写入,保留机器人配置)
|
|
37
41
|
--start 启动 MCP Server(后台服务模式)
|
|
38
42
|
--stop 停止 MCP Server
|
|
43
|
+
--debug 前台启动 MCP Server(日志直接输出到终端,用于调试)
|
|
39
44
|
--status 显示服务状态和机器人配置
|
|
40
45
|
--config 重新配置默认机器人(修改 Bot ID / Secret / 目标用户)
|
|
41
46
|
--add 添加新的机器人配置(多机器人场景)
|
|
@@ -116,8 +121,10 @@ function isServerRunning() {
|
|
|
116
121
|
return true;
|
|
117
122
|
}
|
|
118
123
|
catch {
|
|
119
|
-
// 进程不存在,清理 PID
|
|
120
|
-
fs.
|
|
124
|
+
// 进程不存在,清理 PID 文件(可能已被进程自身删除)
|
|
125
|
+
if (fs.existsSync(PID_FILE)) {
|
|
126
|
+
fs.unlinkSync(PID_FILE);
|
|
127
|
+
}
|
|
121
128
|
return false;
|
|
122
129
|
}
|
|
123
130
|
}
|
|
@@ -144,13 +151,18 @@ function stopServer() {
|
|
|
144
151
|
break;
|
|
145
152
|
}
|
|
146
153
|
}
|
|
147
|
-
|
|
154
|
+
// 进程退出后删除 PID 文件(如果还存在)
|
|
155
|
+
if (fs.existsSync(PID_FILE)) {
|
|
156
|
+
fs.unlinkSync(PID_FILE);
|
|
157
|
+
}
|
|
148
158
|
console.log('[mcp] 服务已停止');
|
|
149
159
|
return true;
|
|
150
160
|
}
|
|
151
161
|
catch (err) {
|
|
152
162
|
console.error('[mcp] 停止服务失败:', err);
|
|
153
|
-
fs.
|
|
163
|
+
if (fs.existsSync(PID_FILE)) {
|
|
164
|
+
fs.unlinkSync(PID_FILE);
|
|
165
|
+
}
|
|
154
166
|
return false;
|
|
155
167
|
}
|
|
156
168
|
}
|
|
@@ -184,6 +196,12 @@ async function startMcpServerForeground() {
|
|
|
184
196
|
// 加载统计并清理旧日志
|
|
185
197
|
loadStats();
|
|
186
198
|
cleanupOldLogs(1 / 24);
|
|
199
|
+
// 创建 MCP Server
|
|
200
|
+
const server = new McpServer({
|
|
201
|
+
name: 'wecom-aibot-mcp',
|
|
202
|
+
version: VERSION,
|
|
203
|
+
});
|
|
204
|
+
registerTools(server);
|
|
187
205
|
// 启动 HTTP 服务
|
|
188
206
|
console.log('');
|
|
189
207
|
console.log(' ╔════════════════════════════════════════════════════════╗');
|
|
@@ -192,7 +210,7 @@ async function startMcpServerForeground() {
|
|
|
192
210
|
console.log(' ╚════════════════════════════════════════════════════════╝');
|
|
193
211
|
console.log('');
|
|
194
212
|
console.log(`[mcp] 启动 MCP HTTP Server (端口: ${HTTP_PORT})...`);
|
|
195
|
-
await startHttpServer();
|
|
213
|
+
await startHttpServer(server);
|
|
196
214
|
startKeepaliveMonitor();
|
|
197
215
|
console.log(`[mcp] MCP Server 已就绪`);
|
|
198
216
|
console.log(`[mcp] HTTP endpoint: http://127.0.0.1:${HTTP_PORT}/mcp`);
|
|
@@ -236,9 +254,15 @@ function startMcpServerBackground() {
|
|
|
236
254
|
console.log(`[mcp] HTTP endpoint: http://127.0.0.1:18963/mcp`);
|
|
237
255
|
console.log('[mcp] 健康检查: curl http://127.0.0.1:18963/health');
|
|
238
256
|
console.log('[mcp] 停止服务: npx @vrs-soft/wecom-aibot-mcp --stop');
|
|
257
|
+
console.log('[mcp] 调试模式: npx @vrs-soft/wecom-aibot-mcp --debug');
|
|
239
258
|
}
|
|
240
259
|
async function main() {
|
|
241
260
|
const args = process.argv.slice(2);
|
|
261
|
+
// --reinstall 命令需要先删除再安装,跳过开头的 ensureGlobalConfigs
|
|
262
|
+
if (!args.includes('--reinstall')) {
|
|
263
|
+
// 强制覆盖所有全局配置(不依赖智能体)
|
|
264
|
+
ensureGlobalConfigs();
|
|
265
|
+
}
|
|
242
266
|
// 解析命令行参数
|
|
243
267
|
if (args.includes('--help') || args.includes('-h')) {
|
|
244
268
|
showHelp();
|
|
@@ -248,6 +272,75 @@ async function main() {
|
|
|
248
272
|
showVersion();
|
|
249
273
|
process.exit(0);
|
|
250
274
|
}
|
|
275
|
+
// --upgrade 命令:强制升级全局配置(已在启动时执行,这里显示结果)
|
|
276
|
+
if (args.includes('--upgrade')) {
|
|
277
|
+
console.log('\n[mcp] ✅ 全局配置已更新完成!');
|
|
278
|
+
console.log('[mcp] 配置位置:');
|
|
279
|
+
console.log(' - ~/.claude.json (MCP Server 配置)');
|
|
280
|
+
console.log(' - ~/.claude/settings.local.json (权限和 Hook)');
|
|
281
|
+
console.log(' - ~/.claude/skills/headless-mode/ (Skill)');
|
|
282
|
+
console.log(' - ~/.wecom-aibot-mcp/version.json (版本记录)');
|
|
283
|
+
console.log('\n[mcp] 请重启 Claude Code 以加载最新配置');
|
|
284
|
+
process.exit(0);
|
|
285
|
+
}
|
|
286
|
+
// --reinstall 命令:删除所有全局配置(保留机器人配置)后重新安装
|
|
287
|
+
if (args.includes('--reinstall')) {
|
|
288
|
+
console.log('\n[mcp] 重新安装全局配置...');
|
|
289
|
+
console.log('[mcp] 保留所有机器人配置: ~/.wecom-aibot-mcp/config.json 和 robot-*.json');
|
|
290
|
+
const CLAUDE_CONFIG_FILE = path.join(os.homedir(), '.claude.json');
|
|
291
|
+
const CLAUDE_SETTINGS_FILE = path.join(os.homedir(), '.claude', 'settings.local.json');
|
|
292
|
+
const SKILL_DIR = path.join(os.homedir(), '.claude', 'skills', 'headless-mode');
|
|
293
|
+
const VERSION_FILE = path.join(os.homedir(), '.wecom-aibot-mcp', 'version.json');
|
|
294
|
+
const HOOK_SCRIPT = path.join(os.homedir(), '.wecom-aibot-mcp', 'permission-hook.sh');
|
|
295
|
+
// 1. 删除 ~/.claude.json 中的 wecom-aibot 配置
|
|
296
|
+
if (fs.existsSync(CLAUDE_CONFIG_FILE)) {
|
|
297
|
+
const content = fs.readFileSync(CLAUDE_CONFIG_FILE, 'utf-8');
|
|
298
|
+
const config = JSON.parse(content);
|
|
299
|
+
if (config.mcpServers?.['wecom-aibot']) {
|
|
300
|
+
delete config.mcpServers['wecom-aibot'];
|
|
301
|
+
fs.writeFileSync(CLAUDE_CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
302
|
+
console.log('[mcp] 已删除 ~/.claude.json 中的 wecom-aibot 配置');
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
// 2. 删除 ~/.claude/settings.local.json 中的权限和 Hook
|
|
306
|
+
if (fs.existsSync(CLAUDE_SETTINGS_FILE)) {
|
|
307
|
+
const content = fs.readFileSync(CLAUDE_SETTINGS_FILE, 'utf-8');
|
|
308
|
+
const config = JSON.parse(content);
|
|
309
|
+
if (config.permissions?.allow) {
|
|
310
|
+
config.permissions.allow = config.permissions.allow.filter((p) => !p.startsWith('mcp__wecom-aibot__'));
|
|
311
|
+
console.log('[mcp] 已删除 wecom-aibot 工具权限');
|
|
312
|
+
}
|
|
313
|
+
if (config.hooks?.PermissionRequest) {
|
|
314
|
+
config.hooks.PermissionRequest = config.hooks.PermissionRequest.filter((h) => !h.hooks?.some?.((hook) => hook.command?.includes?.('wecom-aibot-mcp')));
|
|
315
|
+
if (config.hooks.PermissionRequest.length === 0) {
|
|
316
|
+
delete config.hooks.PermissionRequest;
|
|
317
|
+
}
|
|
318
|
+
console.log('[mcp] 已删除 PermissionRequest hook');
|
|
319
|
+
}
|
|
320
|
+
fs.writeFileSync(CLAUDE_SETTINGS_FILE, JSON.stringify(config, null, 2));
|
|
321
|
+
}
|
|
322
|
+
// 3. 删除 skill 目录
|
|
323
|
+
if (fs.existsSync(SKILL_DIR)) {
|
|
324
|
+
fs.rmSync(SKILL_DIR, { recursive: true });
|
|
325
|
+
console.log('[mcp] 已删除 ~/.claude/skills/headless-mode/');
|
|
326
|
+
}
|
|
327
|
+
// 4. 删除版本文件
|
|
328
|
+
if (fs.existsSync(VERSION_FILE)) {
|
|
329
|
+
fs.unlinkSync(VERSION_FILE);
|
|
330
|
+
console.log('[mcp] 已删除 ~/.wecom-aibot-mcp/version.json');
|
|
331
|
+
}
|
|
332
|
+
// 5. 删除 hook 脚本
|
|
333
|
+
if (fs.existsSync(HOOK_SCRIPT)) {
|
|
334
|
+
fs.unlinkSync(HOOK_SCRIPT);
|
|
335
|
+
console.log('[mcp] 已删除 ~/.wecom-aibot-mcp/permission-hook.sh');
|
|
336
|
+
}
|
|
337
|
+
// 6. 重新安装全局配置
|
|
338
|
+
console.log('\n[mcp] 正在重新安装...');
|
|
339
|
+
ensureGlobalConfigs();
|
|
340
|
+
console.log('\n[mcp] ✅ 重新安装完成!');
|
|
341
|
+
console.log('[mcp] 请重启 Claude Code 以加载最新配置');
|
|
342
|
+
process.exit(0);
|
|
343
|
+
}
|
|
251
344
|
if (args.includes('--status') || args.includes('--list')) {
|
|
252
345
|
showStatus();
|
|
253
346
|
process.exit(0);
|
|
@@ -277,11 +370,22 @@ async function main() {
|
|
|
277
370
|
await deleteMcpConfigInteractive(instanceName);
|
|
278
371
|
process.exit(0);
|
|
279
372
|
}
|
|
280
|
-
// --start --foreground
|
|
373
|
+
// --start --foreground:前台启动(内部调用,输出到日志文件)
|
|
281
374
|
if (args.includes('--start') && args.includes('--foreground')) {
|
|
282
375
|
await startMcpServerForeground();
|
|
283
376
|
return; // 保持运行,不 exit
|
|
284
377
|
}
|
|
378
|
+
// --debug:前台启动,日志直接输出到终端
|
|
379
|
+
if (args.includes('--debug')) {
|
|
380
|
+
console.log('[mcp] Debug 模式:前台运行,Ctrl+C 退出');
|
|
381
|
+
// 写入 debug 标记文件,hook 脚本检测后日志输出到 stderr
|
|
382
|
+
const debugFile = path.join(os.homedir(), '.wecom-aibot-mcp', 'debug');
|
|
383
|
+
fs.writeFileSync(debugFile, 'true');
|
|
384
|
+
await startMcpServerForeground();
|
|
385
|
+
// 退出时删除标记文件
|
|
386
|
+
fs.unlinkSync(debugFile);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
285
389
|
// --start:后台启动
|
|
286
390
|
if (args.includes('--start')) {
|
|
287
391
|
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.d.ts
CHANGED
|
@@ -23,6 +23,10 @@ export declare function listAllRobots(): Array<{
|
|
|
23
23
|
targetUserId: string;
|
|
24
24
|
}>;
|
|
25
25
|
export declare function ensureHookInstalled(): void;
|
|
26
|
+
export declare function ensureGlobalConfigs(): {
|
|
27
|
+
upgraded: boolean;
|
|
28
|
+
previousVersion?: string;
|
|
29
|
+
};
|
|
26
30
|
export declare function saveConfig(config: WecomConfig, instanceName?: string): void;
|
|
27
31
|
/**
|
|
28
32
|
* 安装 headless-mode skill 到项目目录
|