@vrs-soft/wecom-aibot-mcp 1.4.0 → 2.3.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.
Files changed (44) hide show
  1. package/README.md +52 -307
  2. package/dist/approval-manager.d.ts +38 -0
  3. package/dist/approval-manager.js +129 -0
  4. package/dist/bin.js +233 -65
  5. package/dist/cc-registry.d.ts +62 -0
  6. package/dist/cc-registry.js +278 -0
  7. package/dist/channel-server.d.ts +15 -0
  8. package/dist/channel-server.js +492 -0
  9. package/dist/channel-server.test.d.ts +5 -0
  10. package/dist/channel-server.test.js +324 -0
  11. package/dist/client-pool.js +4 -3
  12. package/dist/client.js +49 -49
  13. package/dist/config-wizard.d.ts +16 -2
  14. package/dist/config-wizard.js +542 -141
  15. package/dist/connection-log.js +7 -6
  16. package/dist/connection-manager.d.ts +2 -8
  17. package/dist/connection-manager.js +22 -33
  18. package/dist/daemon.js +7 -6
  19. package/dist/headless-state.d.ts +0 -12
  20. package/dist/headless-state.js +11 -35
  21. package/dist/http-server.d.ts +30 -18
  22. package/dist/http-server.js +465 -177
  23. package/dist/index.d.ts +1 -1
  24. package/dist/index.js +1 -1
  25. package/dist/keepalive-monitor.js +5 -4
  26. package/dist/logger.d.ts +51 -0
  27. package/dist/logger.js +84 -0
  28. package/dist/message-bus.d.ts +13 -1
  29. package/dist/message-bus.js +56 -3
  30. package/dist/project-config.d.ts +57 -0
  31. package/dist/project-config.js +218 -7
  32. package/dist/tools/headless.d.ts +8 -0
  33. package/dist/tools/headless.js +248 -0
  34. package/dist/tools/index.js +271 -115
  35. package/dist/tools/messaging.d.ts +7 -0
  36. package/dist/tools/messaging.js +170 -0
  37. package/dist/tools/utils-tools.d.ts +11 -0
  38. package/dist/tools/utils-tools.js +249 -0
  39. package/dist/utils/atomic-write.d.ts +4 -0
  40. package/dist/utils/atomic-write.js +9 -0
  41. package/dist/utils/sanitize.d.ts +59 -0
  42. package/dist/utils/sanitize.js +246 -0
  43. package/package.json +1 -1
  44. package/skills/headless-mode/SKILL.md +144 -134
package/README.md CHANGED
@@ -1,355 +1,100 @@
1
1
  # @vrs-soft/wecom-aibot-mcp
2
2
 
3
- 企业微信智能机器人 MCP 服务 - Claude Code 远程审批通道
3
+ 中文 | [English](README_EN.md)
4
4
 
5
- > 通过企业微信智能机器人实现 Claude Code 的远程审批和消息推送,离开电脑也能处理决策请求。
5
+ 企业微信智能机器人 MCP 服务 - 让 Claude Code 通过微信远程审批和交互。
6
6
 
7
- ## 功能特性
7
+ ## 功能
8
8
 
9
- - 🔐 **远程审批**:敏感操作通过微信卡片审批,支持"允许一次/拒绝"
10
- - 💬 **双向通信**:任务进度、完成通知实时推送到微信
11
- - 📱 **Headless 模式**:离开电脑时切换到微信交互,长轮询实时接收消息
12
- - 🔄 **智能代批**:超时自动审批,项目内操作允许,删除操作拒绝
13
- - 🤖 **多机器人支持**:支持配置多个机器人,团队场景下多人独立使用
14
- - 🌐 **HTTP Transport**:使用 HTTP 传输,支持多实例共享服务
15
-
16
- ## 架构
17
-
18
- ```
19
- ┌─────────────────┐ MCP (HTTP) ┌──────────────────┐
20
- │ Claude Code │ ──────────────────▶ │ wecom-aibot-mcp │
21
- │ (MCP Client) │ ◀────────────────── │ MCP Server │
22
- └─────────────────┘ └──────────────────┘
23
-
24
- WebSocket 长连接
25
-
26
- ┌───────────────────┐
27
- │ 企业微信服务器 │
28
- │ wss://openws... │
29
- └───────────────────┘
30
-
31
-
32
- ┌───────────────────┐
33
- │ 用户企业微信客户端 │
34
- │ (手机/桌面) │
35
- └───────────────────┘
36
- ```
37
-
38
- ### 审批流程
39
-
40
- ```
41
- Claude 请求执行敏感操作(Bash/Write/Edit 等)
42
-
43
- PermissionRequest Hook 拦截
44
-
45
- ┌────────────────────────┐
46
- │ 检查 headless 模式状态 │
47
- │ (检查 .claude/headless.json)
48
- └────────────────────────┘
49
-
50
- ┌───────┴───────┐
51
- ↓ ↓
52
- 非 headless headless
53
- ↓ ↓
54
- 终端确认框 发送微信审批卡片
55
-
56
- 用户点击按钮
57
-
58
- 通过 HTTP /approval_status
59
-
60
- 执行或拒绝操作
61
- ```
62
-
63
- ### Headless 模式
64
-
65
- ```
66
- 用户:现在开始通过微信联系
67
-
68
- Claude → enter_headless_mode()
69
-
70
- ├─ 连接 WebSocket
71
- ├─ 写入 .claude/settings.json (PermissionRequest hook)
72
- ├─ 发送微信确认消息
73
- └─ 返回 { status: 'entered', headless: true }
74
-
75
- Claude 开始长轮询 get_pending_messages(timeout_ms=30000)
76
-
77
- ┌─────────────────────────────────────────┐
78
- │ loop: │
79
- │ 1. 等待用户消息(30秒超时) │
80
- │ 2. 收到消息 → 理解意图 → 执行操作 │
81
- │ 3. Hook 自动拦截审批 → 发送微信卡片 │
82
- │ 4. 用户审批 → 操作完成 → 汇报结果 │
83
- │ 5. 继续轮询 │
84
- └─────────────────────────────────────────┘
85
-
86
- 用户:我回来了
87
-
88
- Claude → exit_headless_mode()
89
- ├─ 断开 WebSocket
90
- ├─ 删除 .claude/settings.json hook
91
- └─ 发送微信确认消息
92
- ```
9
+ - 远程审批敏感操作(Bash/Write/Edit),微信卡片一键通过/拒绝
10
+ - 离开电脑后通过微信下达任务,实时接收进度通知
11
+ - 支持 Channel 模式(SSE 推送唤醒)和 HTTP 模式(心跳轮询)
12
+ - 支持群聊 @机器人,自动回复到对应会话
13
+ - 支持多机器人、多用户
93
14
 
94
15
  ## 安装
95
16
 
96
- ### 前置要求
97
-
98
- - **Node.js >= 18**
99
- - 企业微信账号(有创建机器人权限)
100
- - Claude Code
101
-
102
- ### 第一步:创建企业微信机器人
103
-
104
- 1. 登录企业微信管理后台:https://work.weixin.qq.com
105
- 2. 进入「管理工具」→「智能机器人」
106
- 3. 点击「创建机器人」→「手动创建」
107
- 4. 填写机器人名称(如"Claude 审批助手")
108
- 5. 在「API 配置」区域:
109
- - 连接方式选择「**使用长连接**」
110
- - 点击「获取 Secret」
111
- 6. 记录 **Bot ID** 和 **Secret**
112
-
113
- > ⚠️ 每个机器人同时只能保持一个 WebSocket 长连接
114
-
115
- ### 第二步:运行配置向导
116
-
117
17
  ```bash
118
18
  npx @vrs-soft/wecom-aibot-mcp
119
19
  ```
120
20
 
121
- 配置向导会引导您:
122
- 1. 输入**机器人名称**(用于识别,如"工作机器人")
123
- 2. 输入 **Bot ID**
124
- 3. 输入 **Secret**
125
- 4. 在企业微信中给机器人发送消息,自动识别用户 ID
21
+ 首次运行进入配置向导,完成后自动启动服务并写入 Claude Code MCP 配置。
126
22
 
127
- 配置完成后会自动:
128
- - 写入机器人配置到 `~/.wecom-aibot-mcp/config.json`
129
- - 写入 MCP 配置到 `~/.claude.json`
130
- - 注册 PermissionRequest hook 到 `~/.claude/settings.local.json`
131
- - 安装 headless-mode skill 到 `~/.claude/skills/`
132
- - 后台启动 MCP 服务
23
+ **前置条件**:企业微信管理后台创建智能机器人,连接方式选「使用长连接」,记录 Bot ID 和 Secret。
133
24
 
134
- ## 常用命令
135
-
136
- | 命令 | 说明 |
137
- |------|------|
138
- | `npx @vrs-soft/wecom-aibot-mcp` | 首次配置向导 |
139
- | `npx @vrs-soft/wecom-aibot-mcp --start` | 后台启动 MCP 服务 |
140
- | `npx @vrs-soft/wecom-aibot-mcp --stop` | 停止 MCP 服务 |
141
- | `npx @vrs-soft/wecom-aibot-mcp --status` | 查看状态 |
142
- | `npx @vrs-soft/wecom-aibot-mcp --config` | 修改配置 |
143
- | `npx @vrs-soft/wecom-aibot-mcp --add` | 添加新机器人 |
144
- | `npx @vrs-soft/wecom-aibot-mcp --delete` | 删除机器人配置 |
145
- | `npx @vrs-soft/wecom-aibot-mcp --uninstall` | 完全卸载 |
146
-
147
- ### 添加新机器人
148
-
149
- 适用于团队多人场景:
150
-
151
- ```bash
152
- npx @vrs-soft/wecom-aibot-mcp --add
153
- # 输入机器人名称(如"张三的机器人")
154
- # 输入 Bot ID 和 Secret
155
- # 发送消息识别用户
156
- ```
157
-
158
- ## 快速开始
159
-
160
- ### 配置 Claude Code
161
-
162
- 配置向导会自动写入 `~/.claude.json`:
163
-
164
- ```json
165
- {
166
- "mcpServers": {
167
- "wecom-aibot": {
168
- "type": "http",
169
- "url": "http://127.0.0.1:18963/mcp"
170
- }
171
- }
172
- }
173
- ```
174
-
175
- ### 启动服务
25
+ ## 启动服务
176
26
 
177
27
  ```bash
28
+ # 后台启动(常用)
178
29
  npx @vrs-soft/wecom-aibot-mcp --start
179
- ```
180
30
 
181
- 输出:
182
- ```
183
- [mcp] MCP Server 已在后台启动
184
- [mcp] HTTP endpoint: http://127.0.0.1:18963/mcp
185
- [mcp] 健康检查: curl http://127.0.0.1:18963/health
186
- [mcp] 停止服务: npx @vrs-soft/wecom-aibot-mcp --stop
31
+ # Channel 模式(需 claude.ai 直连账号)
32
+ claude --dangerously-load-development-channels server:wecom-aibot-channel
187
33
  ```
188
34
 
189
- ### 重启 Claude Code
190
-
191
- 运行 `/mcp` 命令,选择「Reconnect」重新连接 MCP 服务。
192
-
193
- ## 使用示例
194
-
195
- ### Headless 模式(远程审批)
196
-
197
- ```
198
- 你:现在开始通过微信联系
199
-
200
- Claude:已进入微信模式,所有交互将通过企业微信进行。
201
- 微信收到:【cc-1】已进入微信模式,使用机器人「工作机器人」。
202
-
203
- [你离开电脑,Claude 需要执行删除文件操作]
204
-
205
- 微信收到审批卡片:
206
- ┌─────────────────────────┐
207
- │ 【待审批】Bash │
208
- │ 执行命令: rm -rf dist │
209
- │ [允许一次] [拒绝] │
210
- └─────────────────────────┘
211
-
212
- [你在手机点击"允许一次"]
213
-
214
- Claude 继续执行,发送结果到微信。
215
-
216
- 你:我回来了
217
-
218
- Claude:已退出微信模式,恢复终端交互。
219
- ```
220
-
221
- ### 发送任务通知
222
-
223
- ```
224
- 你:帮我重构这个函数,完成后微信通知我
225
-
226
- Claude:[执行重构...]
227
- 微信收到:【完成】函数重构完成!
228
- ```
229
-
230
- ### 群聊机器人
231
-
232
- 将机器人拉入群聊:
35
+ ## 常用命令
233
36
 
234
- ```
235
- 群聊中:
236
- 张三:@Claude助手 查看服务器日志
37
+ | 命令 | 说明 |
38
+ |------|------|
39
+ | `--start / --stop` | 启动/停止后台服务 |
40
+ | `--status` | 查看服务状态和机器人列表 |
41
+ | `--config` | 修改默认机器人配置 |
42
+ | `--add / --delete` | 添加/删除机器人 |
43
+ | `--debug` | 前台启动,输出调试日志 |
44
+ | `--clean-cache` | 清空 CC 注册表缓存 |
45
+ | `--upgrade` | 强制升级全局配置 |
46
+ | `--uninstall` | 完全卸载 |
237
47
 
238
- Claude:执行命令,发送结果到群聊
239
- ```
48
+ ## 运行模式
240
49
 
241
- ## MCP 工具
50
+ | | Channel 模式 | HTTP 模式 |
51
+ |-|-------------|----------|
52
+ | 消息接收 | SSE 自动推送唤醒 | `/loop` 心跳轮询 |
53
+ | 响应延迟 | 即时 | ≤1 分钟 |
54
+ | 账号要求 | claude.ai 直连 | 任意(含 API 中转)|
242
55
 
243
- | 工具 | 说明 | 参数 |
244
- |------|------|------|
245
- | `send_message` | 发送消息到微信 | `content`, `target_user` |
246
- | `get_pending_messages` | 获取待处理消息(长轮询) | `clear`, `timeout_ms` |
247
- | `enter_headless_mode` | 进入微信模式 | `agent_name`, `robot_id` |
248
- | `exit_headless_mode` | 退出微信模式 | `agent_name` |
249
- | `check_connection` | 检查连接状态 | - |
250
- | `list_robots` | 列出所有机器人 | - |
251
- | `get_connection_stats` | 获取连接统计 | `recent_logs` |
252
- | `detect_user_from_message` | 从消息识别用户 | `timeout` |
253
- | `get_setup_guide` | 获取安装指南 | - |
56
+ 使用微信模式时告诉 Claude「现在开始通过微信联系」,会自动触发 `headless-mode` skill。
254
57
 
255
58
  ## 配置说明
256
59
 
257
- ### 多机器人配置
258
-
259
- 支持多个机器人独立使用:
60
+ 机器人配置保存在 `~/.wecom-aibot-mcp/`,支持多个机器人并发:
260
61
 
261
- ```json
262
- // ~/.wecom-aibot-mcp/config.json
263
- {
264
- "botId": "bot-xxx",
265
- "secret": "sec-yyy",
266
- "targetUserId": "user1",
267
- "nameTag": "机器人1"
268
- }
269
-
270
- // ~/.wecom-aibot-mcp/robot-1234567890.json
271
- {
272
- "botId": "bot-zzz",
273
- "secret": "sec-www",
274
- "targetUserId": "user2",
275
- "nameTag": "机器人2"
276
- }
62
+ ```bash
63
+ npx @vrs-soft/wecom-aibot-mcp --add # 添加机器人
64
+ npx @vrs-soft/wecom-aibot-mcp --status # 查看占用情况
277
65
  ```
278
66
 
279
- 使用 `list_robots` 查看所有机器人状态:
280
-
281
- ```json
282
- {
283
- "robots": [
284
- {"name": "机器人1", "status": "connected"},
285
- {"name": "机器人2", "status": "available"}
286
- ],
287
- "total": 2,
288
- "connected": 1,
289
- "occupied": 0
290
- }
291
- ```
67
+ 超时自动审批(默认 10 分钟):在机器人配置中设置 `"autoApproveTimeout": 600`。
292
68
 
293
69
  ## 故障排查
294
70
 
295
- ### 认证失败(错误码 40058)
296
-
297
- 1. 新建机器人需等待约 2 分钟同步
298
- 2. 完成授权:机器人详情 → 可使用权限 → 授权
299
- 3. 检查 Bot ID 和 Secret 是否正确
300
-
301
- ### 连接问题
302
-
303
71
  ```bash
304
- # 检查服务状态
72
+ # 检查服务
305
73
  curl http://127.0.0.1:18963/health
306
74
 
307
- # 查看日志
308
- tail -f ~/.wecom-aibot-mcp/connection.log
75
+ # Channel 不可用("Channels are not currently available")
76
+ # 使用 API Key 或中转服务,改用 HTTP 模式
309
77
 
310
- # 重启服务
311
- npx @vrs-soft/wecom-aibot-mcp --stop
312
- npx @vrs-soft/wecom-aibot-mcp --start
313
- ```
314
-
315
- ### 完全卸载
78
+ # 端口占用
79
+ lsof -i :18963 | grep LISTEN # 找到 PID
80
+ kill <PID>
316
81
 
317
- ```bash
318
- npx @vrs-soft/wecom-aibot-mcp --uninstall
82
+ # 清理断线残留
83
+ npx @vrs-soft/wecom-aibot-mcp --clean-cache
319
84
  ```
320
85
 
321
- 这会删除:
322
- - `~/.wecom-aibot-mcp/`
323
- - `~/.claude.json` 中的 wecom-aibot 配置
324
- - `~/.claude/settings.local.json` 中的 hook
325
- - `~/.claude/skills/headless-mode/`
86
+ ## 拆分部署
326
87
 
327
- ## 开发
88
+ HTTP MCP 跑在远程服务器,Channel 代理跑在本地:
328
89
 
329
90
  ```bash
330
- # 克隆仓库
331
- git clone https://github.com/eric2877/wecom-aibot-mcp.git
332
- cd wecom-aibot-mcp
333
-
334
- # 安装依赖
335
- npm install
91
+ # 远程服务器
92
+ npx @vrs-soft/wecom-aibot-mcp --http-only --start
336
93
 
337
- # 开发模式
338
- npm run dev
339
-
340
- # 构建
341
- npm run build
342
-
343
- # 测试
344
- npm test
94
+ # 本地
95
+ MCP_URL=http://远程IP:18963 npx @vrs-soft/wecom-aibot-mcp --channel-only
345
96
  ```
346
97
 
347
98
  ## License
348
99
 
349
- MIT
350
-
351
- ## 相关链接
352
-
353
- - [企业微信智能机器人文档](https://developer.work.weixin.qq.com/document/path/101039)
354
- - [Claude Code 文档](https://docs.anthropic.com/claude-code)
355
- - [MCP 协议规范](https://modelcontextprotocol.io)
100
+ MIT · [企业微信机器人文档](https://developer.work.weixin.qq.com/document/path/101039) · [Channels 文档](https://code.claude.com/docs/en/channels-reference)
@@ -0,0 +1,38 @@
1
+ /**
2
+ * 审批管理器
3
+ *
4
+ * 负责:
5
+ * 1. 存储 pendingApprovals Map(http-server 审批记录)
6
+ * 2. 持久化到 approval-state.json,MCP 重启后恢复
7
+ * 3. 恢复时将 approvalRecord 注入对应的 WecomClient
8
+ *
9
+ * 与 WecomClient.approvals 的关系:
10
+ * - WecomClient.approvals 记录企业微信卡片状态(用户点击后更新)
11
+ * - approval-manager 记录 http-server 层的审批条目
12
+ * - MCP 重启后,WecomClient 实例是全新的,需要 injectApprovalRecord 恢复
13
+ */
14
+ export interface ApprovalEntry {
15
+ taskId: string;
16
+ status: 'pending' | 'allow-once' | 'allow-always' | 'deny';
17
+ timestamp: number;
18
+ tool_name: string;
19
+ tool_input: Record<string, unknown>;
20
+ description: string;
21
+ robotName: string;
22
+ }
23
+ /**
24
+ * 设置配置目录(仅用于测试)
25
+ */
26
+ export declare function setConfigDir(dir: string): void;
27
+ export declare function addApproval(entry: ApprovalEntry): void;
28
+ export declare function getApproval(taskId: string): ApprovalEntry | undefined;
29
+ export declare function updateApprovalStatus(taskId: string, status: 'allow-once' | 'allow-always' | 'deny'): void;
30
+ export declare function getPendingApprovals(): Map<string, ApprovalEntry>;
31
+ export declare function saveApprovalState(): void;
32
+ /**
33
+ * 从文件恢复审批状态,并将审批记录注入对应的 WecomClient
34
+ * 需在 connectAllRobots() 完成后调用,确保 client 已存在
35
+ */
36
+ export declare function loadApprovalState(getClientFn: (robotName: string) => Promise<import('./client.js').WecomClient | null>): Promise<void>;
37
+ export declare function startAutoSave(): void;
38
+ export declare function stopAutoSave(): void;
@@ -0,0 +1,129 @@
1
+ /**
2
+ * 审批管理器
3
+ *
4
+ * 负责:
5
+ * 1. 存储 pendingApprovals Map(http-server 审批记录)
6
+ * 2. 持久化到 approval-state.json,MCP 重启后恢复
7
+ * 3. 恢复时将 approvalRecord 注入对应的 WecomClient
8
+ *
9
+ * 与 WecomClient.approvals 的关系:
10
+ * - WecomClient.approvals 记录企业微信卡片状态(用户点击后更新)
11
+ * - approval-manager 记录 http-server 层的审批条目
12
+ * - MCP 重启后,WecomClient 实例是全新的,需要 injectApprovalRecord 恢复
13
+ */
14
+ import * as fs from 'fs';
15
+ import * as path from 'path';
16
+ import * as os from 'os';
17
+ import { atomicWriteFileSync } from './utils/atomic-write.js';
18
+ const pendingApprovals = new Map();
19
+ // 支持测试环境覆盖
20
+ let CONFIG_DIR = path.join(os.homedir(), '.wecom-aibot-mcp');
21
+ let APPROVAL_STATE_FILE = path.join(CONFIG_DIR, 'approval-state.json');
22
+ /**
23
+ * 设置配置目录(仅用于测试)
24
+ */
25
+ export function setConfigDir(dir) {
26
+ CONFIG_DIR = dir;
27
+ APPROVAL_STATE_FILE = path.join(CONFIG_DIR, 'approval-state.json');
28
+ }
29
+ let saveInterval = null;
30
+ // ────────────────────────────────────────────
31
+ // 审批 CRUD
32
+ // ────────────────────────────────────────────
33
+ export function addApproval(entry) {
34
+ pendingApprovals.set(entry.taskId, entry);
35
+ }
36
+ export function getApproval(taskId) {
37
+ return pendingApprovals.get(taskId);
38
+ }
39
+ export function updateApprovalStatus(taskId, status) {
40
+ const entry = pendingApprovals.get(taskId);
41
+ if (entry) {
42
+ entry.status = status;
43
+ // 审批完成后从 Map 中移除,避免 pendingApprovals.size 持续增长
44
+ pendingApprovals.delete(taskId);
45
+ }
46
+ }
47
+ export function getPendingApprovals() {
48
+ return pendingApprovals;
49
+ }
50
+ // ────────────────────────────────────────────
51
+ // 持久化
52
+ // ────────────────────────────────────────────
53
+ export function saveApprovalState() {
54
+ const approvals = [];
55
+ for (const [taskId, entry] of pendingApprovals) {
56
+ if (entry.status === 'pending') {
57
+ approvals.push({ taskId, entry });
58
+ }
59
+ }
60
+ // 无待处理审批时不创建文件
61
+ if (approvals.length === 0)
62
+ return;
63
+ try {
64
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
65
+ atomicWriteFileSync(APPROVAL_STATE_FILE, JSON.stringify({ approvals, savedAt: Date.now() }, null, 2));
66
+ console.log(`[approval-manager] 已保存 ${approvals.length} 个待处理审批`);
67
+ }
68
+ catch (err) {
69
+ console.error('[approval-manager] 保存审批状态失败:', err);
70
+ }
71
+ }
72
+ /**
73
+ * 从文件恢复审批状态,并将审批记录注入对应的 WecomClient
74
+ * 需在 connectAllRobots() 完成后调用,确保 client 已存在
75
+ */
76
+ export async function loadApprovalState(getClientFn) {
77
+ if (!fs.existsSync(APPROVAL_STATE_FILE))
78
+ return;
79
+ try {
80
+ const content = fs.readFileSync(APPROVAL_STATE_FILE, 'utf-8');
81
+ const state = JSON.parse(content);
82
+ // 只恢复 10 分钟内的 pending 审批(超时的不再有效)
83
+ const now = Date.now();
84
+ const maxAge = 10 * 60 * 1000;
85
+ let restored = 0;
86
+ for (const { taskId, entry } of state.approvals) {
87
+ if (entry.status === 'pending' && now - entry.timestamp < maxAge) {
88
+ pendingApprovals.set(taskId, entry);
89
+ // 将审批记录注入对应 WecomClient,使用户点击后能正确路由
90
+ const client = await getClientFn(entry.robotName);
91
+ if (client) {
92
+ client.injectApprovalRecord(taskId, {
93
+ toolName: entry.tool_name,
94
+ toolInput: entry.tool_input,
95
+ });
96
+ console.log(`[approval-manager] 恢复审批: ${taskId} → robot=${entry.robotName}`);
97
+ restored++;
98
+ }
99
+ else {
100
+ console.warn(`[approval-manager] 恢复审批 ${taskId} 失败:机器人 ${entry.robotName} 不在线`);
101
+ }
102
+ }
103
+ }
104
+ // 恢复完成,删除持久化文件
105
+ fs.unlinkSync(APPROVAL_STATE_FILE);
106
+ console.log(`[approval-manager] 共恢复 ${restored} 个审批`);
107
+ }
108
+ catch (err) {
109
+ console.warn('[approval-manager] 恢复审批状态失败:', err);
110
+ }
111
+ }
112
+ // ────────────────────────────────────────────
113
+ // 定时保存
114
+ // ────────────────────────────────────────────
115
+ export function startAutoSave() {
116
+ if (saveInterval)
117
+ return;
118
+ saveInterval = setInterval(() => {
119
+ if (pendingApprovals.size > 0) {
120
+ saveApprovalState();
121
+ }
122
+ }, 30000);
123
+ }
124
+ export function stopAutoSave() {
125
+ if (saveInterval) {
126
+ clearInterval(saveInterval);
127
+ saveInterval = null;
128
+ }
129
+ }