@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 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.0';
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.unlinkSync(PID_FILE);
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
- fs.unlinkSync(PID_FILE);
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.unlinkSync(PID_FILE);
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(robotName?: string): WecomClient;
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.sendText('【系统】连接中断,正在重连...').catch(err => {
80
- console.error('[wecom] 发送断线通知失败:', err);
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
- if (!event)
188
- return;
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
- this.sendText(`**审批结果**${toolInfo}\n\n${resultText}`).catch(err => {
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 && !existing.resolved) {
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
- // 多实例支持(按 robotName 索引)
564
- const instances = new Map();
616
+ // 单例实例
617
+ let instance = null;
565
618
  export function initClient(botId, secret, targetUserId, robotName) {
566
- // 如果该机器人已存在,先断开旧连接
567
- if (instances.has(robotName)) {
568
- instances.get(robotName).disconnect();
619
+ if (instance) {
620
+ instance.disconnect();
569
621
  }
570
- const client = new WecomClient(botId, secret, targetUserId, robotName);
571
- client.connect();
572
- instances.set(robotName, client);
573
- return client;
622
+ instance = new WecomClient(botId, secret, targetUserId, robotName);
623
+ instance.connect();
624
+ return instance;
574
625
  }
575
- export function getClient(robotName) {
576
- if (robotName) {
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
- const first = instances.values().next().value;
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 };
@@ -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 到项目目录