@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.
- package/README.md +52 -307
- package/dist/approval-manager.d.ts +38 -0
- package/dist/approval-manager.js +129 -0
- package/dist/bin.js +233 -65
- package/dist/cc-registry.d.ts +62 -0
- package/dist/cc-registry.js +278 -0
- package/dist/channel-server.d.ts +15 -0
- package/dist/channel-server.js +492 -0
- package/dist/channel-server.test.d.ts +5 -0
- package/dist/channel-server.test.js +324 -0
- package/dist/client-pool.js +4 -3
- package/dist/client.js +49 -49
- package/dist/config-wizard.d.ts +16 -2
- package/dist/config-wizard.js +542 -141
- package/dist/connection-log.js +7 -6
- package/dist/connection-manager.d.ts +2 -8
- package/dist/connection-manager.js +22 -33
- package/dist/daemon.js +7 -6
- package/dist/headless-state.d.ts +0 -12
- package/dist/headless-state.js +11 -35
- package/dist/http-server.d.ts +30 -18
- package/dist/http-server.js +465 -177
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/keepalive-monitor.js +5 -4
- package/dist/logger.d.ts +51 -0
- package/dist/logger.js +84 -0
- package/dist/message-bus.d.ts +13 -1
- package/dist/message-bus.js +56 -3
- package/dist/project-config.d.ts +57 -0
- package/dist/project-config.js +218 -7
- package/dist/tools/headless.d.ts +8 -0
- package/dist/tools/headless.js +248 -0
- package/dist/tools/index.js +271 -115
- package/dist/tools/messaging.d.ts +7 -0
- package/dist/tools/messaging.js +170 -0
- package/dist/tools/utils-tools.d.ts +11 -0
- package/dist/tools/utils-tools.js +249 -0
- package/dist/utils/atomic-write.d.ts +4 -0
- package/dist/utils/atomic-write.js +9 -0
- package/dist/utils/sanitize.d.ts +59 -0
- package/dist/utils/sanitize.js +246 -0
- package/package.json +1 -1
- 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
|
-
|
|
3
|
+
中文 | [English](README_EN.md)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
企业微信智能机器人 MCP 服务 - 让 Claude Code 通过微信远程审批和交互。
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## 功能
|
|
8
8
|
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
239
|
-
```
|
|
48
|
+
## 运行模式
|
|
240
49
|
|
|
241
|
-
|
|
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
|
-
```
|
|
262
|
-
|
|
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
|
-
|
|
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
|
-
|
|
75
|
+
# Channel 不可用("Channels are not currently available")
|
|
76
|
+
# → 使用 API Key 或中转服务,改用 HTTP 模式
|
|
309
77
|
|
|
310
|
-
#
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
### 完全卸载
|
|
78
|
+
# 端口占用
|
|
79
|
+
lsof -i :18963 | grep LISTEN # 找到 PID
|
|
80
|
+
kill <PID>
|
|
316
81
|
|
|
317
|
-
|
|
318
|
-
npx @vrs-soft/wecom-aibot-mcp --
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|