@vrs-soft/wecom-aibot-mcp 2.4.12 → 2.4.17
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 +143 -91
- package/dist/channel-server.js +26 -2
- package/dist/client.js +6 -4
- package/dist/http-server.js +10 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,149 +1,201 @@
|
|
|
1
|
-
#
|
|
1
|
+
# wecom-aibot-mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
English | [中文文档](README_ZH.md)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Enterprise WeChat AI Bot MCP Service - Remote Approval Channel for Claude Code
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- 远程审批敏感操作(Bash/Write/Edit),微信卡片一键通过/拒绝
|
|
9
|
-
- 离开电脑后通过微信下达任务,实时接收进度通知
|
|
10
|
-
- 支持群聊 @机器人,多机器人、多用户并发
|
|
11
|
-
- 代理企业微信文档 MCP,支持文档和智能表格操作
|
|
7
|
+
> Handle Claude Code approval requests via WeChat, even when away from your computer.
|
|
12
8
|
|
|
13
|
-
|
|
9
|
+
## Features
|
|
14
10
|
|
|
15
|
-
|
|
11
|
+
- 🔐 **Remote Approval**: Approve/deny sensitive operations (Bash/Write/Edit) via WeChat cards
|
|
12
|
+
- 🔍 **Full Command View**: Approval cards include a detail link — open in browser to view the complete command
|
|
13
|
+
- 💬 **Bidirectional Communication**: Real-time task progress notifications
|
|
14
|
+
- 📱 **Headless Mode**: Switch to WeChat interaction when leaving terminal
|
|
15
|
+
- 🤖 **Multi-bot Support**: Multiple bots for team and group chat scenarios
|
|
16
|
+
- 🌐 **Remote Deployment**: MCP server can be deployed on a remote host with Bearer Token auth
|
|
17
|
+
- 🔄 **Auto-reconnect**: Channel mode automatically reconnects after network interruption or server restart
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
## Architecture
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
```
|
|
22
|
+
┌─────────────────┐ MCP (stdio) ┌──────────────────────┐
|
|
23
|
+
│ Claude Code │ ──────────────────▶ │ Channel MCP Proxy │
|
|
24
|
+
│ (MCP Client) │ ◀────────────────── │ (local, SSE client) │
|
|
25
|
+
└─────────────────┘ └──────────────────────┘
|
|
26
|
+
│ SSE
|
|
27
|
+
▼
|
|
28
|
+
┌─────────────────────┐
|
|
29
|
+
│ wecom-aibot-mcp │
|
|
30
|
+
│ HTTP MCP Server │
|
|
31
|
+
│ (local or remote) │
|
|
32
|
+
└─────────────────────┘
|
|
33
|
+
│
|
|
34
|
+
WebSocket Connection
|
|
35
|
+
↓
|
|
36
|
+
┌─────────────────────┐
|
|
37
|
+
│ Enterprise WeChat │
|
|
38
|
+
└─────────────────────┘
|
|
39
|
+
│
|
|
40
|
+
↓
|
|
41
|
+
┌─────────────────────┐
|
|
42
|
+
│ User WeChat Client │
|
|
43
|
+
│ (Mobile/Desktop) │
|
|
44
|
+
└─────────────────────┘
|
|
45
|
+
```
|
|
20
46
|
|
|
21
|
-
|
|
47
|
+
## Installation
|
|
22
48
|
|
|
23
|
-
|
|
49
|
+
### Prerequisites
|
|
24
50
|
|
|
25
|
-
|
|
51
|
+
- **Node.js >= 18**
|
|
52
|
+
- Enterprise WeChat account (with bot creation permission)
|
|
53
|
+
- Claude Code
|
|
26
54
|
|
|
27
|
-
|
|
55
|
+
### Step 1: Create Enterprise WeChat Bot
|
|
28
56
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
57
|
+
1. Login to WeChat Work admin portal: https://work.weixin.qq.com
|
|
58
|
+
2. Go to "Management Tools" → "Smart Bot"
|
|
59
|
+
3. Click "Create Bot" → "Manual Creation"
|
|
60
|
+
4. Fill in bot name (e.g., "Claude Approval Assistant")
|
|
61
|
+
5. In "API Configuration":
|
|
62
|
+
- Select "Use Long Connection"
|
|
63
|
+
- Click "Get Secret"
|
|
64
|
+
6. Record **Bot ID** and **Secret**
|
|
32
65
|
|
|
33
|
-
###
|
|
66
|
+
### Step 2: Run Configuration Wizard
|
|
34
67
|
|
|
35
|
-
|
|
68
|
+
```bash
|
|
69
|
+
npx @vrs-soft/wecom-aibot-mcp --setup
|
|
70
|
+
```
|
|
36
71
|
|
|
37
|
-
|
|
38
|
-
- 「把 src/index.ts 里的 TODO 都处理掉」
|
|
39
|
-
- 「最近有什么错误日志?」
|
|
72
|
+
Choose the appropriate role flag:
|
|
40
73
|
|
|
41
|
-
|
|
74
|
+
| Command | Role | Description |
|
|
75
|
+
|---------|------|-------------|
|
|
76
|
+
| `--setup` | Interactive | Guides through local or remote setup |
|
|
77
|
+
| `--setup --server` | Server-side | Configure bot + Token, no local MCP config |
|
|
78
|
+
| `--setup --channel` | Channel client | Connect to remote server, write Channel MCP config |
|
|
79
|
+
| `--setup --server --channel` | Full local | HTTP + Channel full install |
|
|
42
80
|
|
|
43
|
-
|
|
81
|
+
**Start server after setup**:
|
|
44
82
|
|
|
45
|
-
|
|
83
|
+
```bash
|
|
84
|
+
npx @vrs-soft/wecom-aibot-mcp --http-only --start
|
|
85
|
+
```
|
|
46
86
|
|
|
47
|
-
|
|
48
|
-
- 触发 CI 任务
|
|
49
|
-
- 审批自己负责的操作(审批请求精确路由到对应 Claude 窗口)
|
|
87
|
+
## Commands
|
|
50
88
|
|
|
51
|
-
|
|
89
|
+
| Command | Description |
|
|
90
|
+
|---------|-------------|
|
|
91
|
+
| `--start / --stop` | Start/stop background service |
|
|
92
|
+
| `--status` | View service status and bot list |
|
|
93
|
+
| `--config` | Modify default bot configuration |
|
|
94
|
+
| `--add / --delete` | Add/delete bot |
|
|
95
|
+
| `--set-token [token]` | Set Auth Token (for remote deployment) |
|
|
96
|
+
| `--set-token --clear` | Clear Auth Token |
|
|
97
|
+
| `--debug` | Start in foreground with debug output |
|
|
98
|
+
| `--http-only` | Start HTTP MCP Server only (server-side use) |
|
|
99
|
+
| `--channel-only` | Configure Channel MCP only (requires `MCP_URL`) |
|
|
100
|
+
| `--clean-cache` | Clear CC registry cache |
|
|
101
|
+
| `--upgrade` | Force upgrade global configs |
|
|
102
|
+
| `--uninstall` | Complete uninstall |
|
|
52
103
|
|
|
53
|
-
##
|
|
104
|
+
## Run Modes
|
|
54
105
|
|
|
55
|
-
|
|
106
|
+
| | Channel Mode | HTTP Mode |
|
|
107
|
+
|-|-------------|-----------|
|
|
108
|
+
| Message delivery | SSE push (instant) | `/loop` heartbeat polling |
|
|
109
|
+
| Latency | Immediate | ≤1 minute |
|
|
110
|
+
| Claude account | claude.ai direct only | Any (including API relay) |
|
|
111
|
+
| Reconnect | Auto (including server restart) | Auto via heartbeat |
|
|
56
112
|
|
|
57
|
-
|
|
113
|
+
To enter WeChat mode, tell Claude: **"Now contact me via WeChat"** — this triggers the `headless-mode` skill automatically.
|
|
58
114
|
|
|
59
|
-
|
|
115
|
+
**Claude startup command for Channel mode**:
|
|
60
116
|
|
|
61
117
|
```bash
|
|
62
|
-
|
|
118
|
+
claude --dangerously-load-development-channels server:wecom-aibot-channel
|
|
63
119
|
```
|
|
64
120
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
| 命令 | 角色 | 说明 |
|
|
68
|
-
|------|------|------|
|
|
69
|
-
| `--setup` | 交互式 | 询问本地 / 远程,自动引导 |
|
|
70
|
-
| `--setup --server` | 服务器端 | 配置机器人 + Token,不写本机 MCP 配置 |
|
|
71
|
-
| `--setup --channel` | Channel 客户端 | 连接远程 Server,写入 Channel MCP |
|
|
72
|
-
| `--setup --server --channel` | 本地完整 | HTTP + Channel 全安装 |
|
|
121
|
+
## Usage Example
|
|
73
122
|
|
|
74
|
-
|
|
123
|
+
### Headless Mode (Remote Approval)
|
|
75
124
|
|
|
76
|
-
```bash
|
|
77
|
-
npx @vrs-soft/wecom-aibot-mcp --http-only --start
|
|
78
125
|
```
|
|
126
|
+
You: Now contact me via WeChat
|
|
79
127
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
```bash
|
|
83
|
-
npx @vrs-soft/wecom-aibot-mcp --start # 后台启动
|
|
84
|
-
npx @vrs-soft/wecom-aibot-mcp --stop # 停止
|
|
85
|
-
```
|
|
128
|
+
Claude: Entered WeChat mode. All interactions will go through Enterprise WeChat.
|
|
86
129
|
|
|
87
|
-
|
|
130
|
+
[You leave the computer. Claude needs to run a command.]
|
|
88
131
|
|
|
89
|
-
|
|
132
|
+
WeChat receives approval card:
|
|
133
|
+
┌──────────────────────────────┐
|
|
134
|
+
│ 【Pending Approval】Bash │
|
|
135
|
+
│ Command: npm run build... │
|
|
136
|
+
│ 📋 TaskID: approval_xxx │
|
|
137
|
+
│ [Allow Once] [Default] [Deny]│
|
|
138
|
+
│ Details: View full command │
|
|
139
|
+
└──────────────────────────────┘
|
|
90
140
|
|
|
91
|
-
|
|
92
|
-
|-|-------------|----------|
|
|
93
|
-
| 消息接收 | SSE 自动推送唤醒 | `/loop` 心跳轮询 |
|
|
94
|
-
| 响应延迟 | 即时 | ≤1 分钟 |
|
|
95
|
-
| 账号要求 | claude.ai 直连 | 任意(含 API 中转)|
|
|
141
|
+
[Tap "Allow Once" on phone, or open "View full command" to see complete output]
|
|
96
142
|
|
|
97
|
-
|
|
143
|
+
Claude continues execution and sends the result to WeChat.
|
|
98
144
|
|
|
99
|
-
|
|
145
|
+
You: I'm back
|
|
100
146
|
|
|
101
|
-
|
|
102
|
-
claude --dangerously-load-development-channels server:wecom-aibot-channel
|
|
147
|
+
Claude: Exited WeChat mode.
|
|
103
148
|
```
|
|
104
149
|
|
|
105
|
-
|
|
150
|
+
### Timeout Auto-Approval
|
|
106
151
|
|
|
107
|
-
|
|
152
|
+
Configure in the bot config file or via `wecom-aibot.json`:
|
|
108
153
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
| `--add / --delete` | 添加/删除机器人 |
|
|
115
|
-
| `--set-token [token]` | 设置 Auth Token(远程部署用) |
|
|
116
|
-
| `--set-token --clear` | 清除 Auth Token |
|
|
117
|
-
| `--debug` | 前台启动,输出调试日志 |
|
|
118
|
-
| `--http-only` | 仅启动 HTTP MCP Server(服务器端用) |
|
|
119
|
-
| `--channel-only` | 仅配置 Channel MCP(需 `MCP_URL` 环境变量) |
|
|
120
|
-
| `--clean-cache` | 清空 CC 注册表缓存 |
|
|
121
|
-
| `--upgrade` | 强制升级全局配置 |
|
|
122
|
-
| `--uninstall` | 完全卸载 |
|
|
123
|
-
|
|
124
|
-
超时自动审批(默认 10 分钟):在机器人配置中设置 `"autoApproveTimeout": 600`。
|
|
154
|
+
```json
|
|
155
|
+
{
|
|
156
|
+
"autoApproveTimeout": 600
|
|
157
|
+
}
|
|
158
|
+
```
|
|
125
159
|
|
|
126
|
-
|
|
160
|
+
- `autoApproveTimeout`: Timeout in seconds (default 600s = 10 minutes)
|
|
161
|
+
- After timeout: operations **within** the project directory are auto-allowed; operations outside or delete commands are auto-denied
|
|
127
162
|
|
|
128
|
-
##
|
|
163
|
+
## Troubleshooting
|
|
129
164
|
|
|
130
165
|
```bash
|
|
131
|
-
#
|
|
166
|
+
# Check if service is running
|
|
132
167
|
curl http://127.0.0.1:18963/health
|
|
133
168
|
|
|
134
|
-
# Channel
|
|
135
|
-
# →
|
|
169
|
+
# Channel unavailable ("Channels are not currently available")
|
|
170
|
+
# → Using API key or relay service? Switch to HTTP mode instead.
|
|
171
|
+
|
|
172
|
+
# Channel fails to reconnect after server restart
|
|
173
|
+
# → Auto-reconnect triggers within 5 seconds; no manual action needed.
|
|
174
|
+
# Requires v2.4.13 or later.
|
|
136
175
|
|
|
137
|
-
#
|
|
176
|
+
# Approval detail page shows "Unauthorized"
|
|
177
|
+
# → Upgrade to v2.4.14 or later; the /approval/ path is now auth-exempt.
|
|
178
|
+
|
|
179
|
+
# Port conflict
|
|
138
180
|
lsof -i :18963 | grep LISTEN
|
|
139
181
|
kill <PID>
|
|
140
182
|
|
|
141
|
-
#
|
|
183
|
+
# Clean up stale ccId registrations after disconnect
|
|
142
184
|
npx @vrs-soft/wecom-aibot-mcp --clean-cache
|
|
143
185
|
```
|
|
144
186
|
|
|
145
|
-
|
|
187
|
+
## MCP Tools
|
|
188
|
+
|
|
189
|
+
| Tool | Description | Key Parameters |
|
|
190
|
+
|------|-------------|----------------|
|
|
191
|
+
| `send_message` | Send message to WeChat | `content`, `cc_id`, `target_user` |
|
|
192
|
+
| `get_pending_messages` | Get pending messages (long poll) | `cc_id`, `timeout_ms` |
|
|
193
|
+
| `enter_headless_mode` | Enter WeChat mode | `cc_id`, `robot_id`, `mode` |
|
|
194
|
+
| `exit_headless_mode` | Exit WeChat mode | `cc_id` |
|
|
195
|
+
| `check_connection` | Check WebSocket connection status | - |
|
|
196
|
+
| `list_robots` | List all configured bots | - |
|
|
197
|
+
| `get_connection_stats` | Get connection stats and logs | `recent_logs` |
|
|
146
198
|
|
|
147
199
|
## License
|
|
148
200
|
|
|
149
|
-
MIT · [
|
|
201
|
+
MIT · [Enterprise WeChat Bot Docs](https://developer.work.weixin.qq.com/document/path/101039) · [Channels Reference](https://code.claude.com/docs/en/channels-reference)
|
package/dist/channel-server.js
CHANGED
|
@@ -50,6 +50,9 @@ let sseConnected = false;
|
|
|
50
50
|
let sseAbortController = null;
|
|
51
51
|
let mcpServer = null;
|
|
52
52
|
let sseCurrentCcId = undefined;
|
|
53
|
+
// 保存首次 enter_headless_mode 的参数,重连时原样复用
|
|
54
|
+
let sseRobotId = undefined;
|
|
55
|
+
let sseProjectDir = undefined;
|
|
53
56
|
// HTTP MCP session ID(需要在转发请求前初始化)
|
|
54
57
|
let httpSessionId = null;
|
|
55
58
|
/**
|
|
@@ -205,6 +208,24 @@ function connectSSE(ccId) {
|
|
|
205
208
|
if (!res.ok) {
|
|
206
209
|
logChannel('SSE connect failed', { status: res.status });
|
|
207
210
|
sseConnected = false;
|
|
211
|
+
// server 重启后 ccId 注册丢失(404),需重新注册再重连
|
|
212
|
+
if (!sseAbortController?.signal.aborted) {
|
|
213
|
+
const delay = res.status === 404 ? 5000 : 3000;
|
|
214
|
+
logChannel(`SSE 连接失败(${res.status}),${delay / 1000} 秒后重连`, { ccId });
|
|
215
|
+
setTimeout(async () => {
|
|
216
|
+
httpSessionId = null; // 重置 session,防止使用 server 重启前的旧 session
|
|
217
|
+
if (ccId) {
|
|
218
|
+
// 重新调 enter_headless_mode 恢复 server 端 ccId 注册
|
|
219
|
+
await forwardToHttpMcp('enter_headless_mode', {
|
|
220
|
+
cc_id: ccId,
|
|
221
|
+
robot_id: sseRobotId,
|
|
222
|
+
mode: 'channel',
|
|
223
|
+
project_dir: sseProjectDir || process.cwd(),
|
|
224
|
+
}).catch((e) => logChannel('重注册 ccId 失败', { error: String(e) }));
|
|
225
|
+
}
|
|
226
|
+
connectSSE(ccId);
|
|
227
|
+
}, delay);
|
|
228
|
+
}
|
|
208
229
|
return;
|
|
209
230
|
}
|
|
210
231
|
logChannel('SSE connected, waiting for messages', { status: res.status });
|
|
@@ -230,7 +251,7 @@ function connectSSE(ccId) {
|
|
|
230
251
|
// 非主动断开时自动重连
|
|
231
252
|
if (!sseAbortController?.signal.aborted) {
|
|
232
253
|
logChannel('SSE 断线,3 秒后重连', { ccId });
|
|
233
|
-
setTimeout(() => connectSSE(ccId), 3000);
|
|
254
|
+
setTimeout(() => { httpSessionId = null; connectSSE(ccId); }, 3000);
|
|
234
255
|
}
|
|
235
256
|
break;
|
|
236
257
|
}
|
|
@@ -305,7 +326,7 @@ function connectSSE(ccId) {
|
|
|
305
326
|
// 非主动断开时自动重连
|
|
306
327
|
if (!sseAbortController?.signal.aborted) {
|
|
307
328
|
logChannel('SSE 出错,3 秒后重连', { ccId });
|
|
308
|
-
setTimeout(() => connectSSE(ccId), 3000);
|
|
329
|
+
setTimeout(() => { httpSessionId = null; connectSSE(ccId); }, 3000);
|
|
309
330
|
}
|
|
310
331
|
});
|
|
311
332
|
}
|
|
@@ -418,6 +439,9 @@ function registerChannelTools(server) {
|
|
|
418
439
|
const parsed = JSON.parse(content[0].text);
|
|
419
440
|
if (parsed.ccId) {
|
|
420
441
|
logChannel('Got ccId, connecting SSE', { ccId: parsed.ccId, mode });
|
|
442
|
+
// 保存连接参数供重连复用
|
|
443
|
+
sseRobotId = robot_id || parsed.robotName;
|
|
444
|
+
sseProjectDir = project_dir || process.cwd();
|
|
421
445
|
connectSSE(parsed.ccId);
|
|
422
446
|
// Channel 模式:在本地项目写入 PermissionRequest hook
|
|
423
447
|
const localProjectDir = project_dir || process.cwd();
|
package/dist/client.js
CHANGED
|
@@ -252,8 +252,9 @@ class WecomClient extends EventEmitter {
|
|
|
252
252
|
: eventKey === 'allow-always' ? '✅ 已允许(永久)'
|
|
253
253
|
: '❌ 已拒绝';
|
|
254
254
|
const toolInfo = approval.toolName ? `: ${approval.toolName}` : '';
|
|
255
|
-
const
|
|
256
|
-
const
|
|
255
|
+
const desc = approval.description || '';
|
|
256
|
+
const descSnippet = desc ? `\n执行命令: ${desc.slice(0, 100)}${desc.length > 100 ? '…' : ''}` : '';
|
|
257
|
+
const content = `**审批结果**${toolInfo}\n\n${resultText}${descSnippet}`;
|
|
257
258
|
this.sendText(content).catch(err => {
|
|
258
259
|
logger.error('wecom', `发送审批确认失败: ${err}`);
|
|
259
260
|
});
|
|
@@ -461,8 +462,9 @@ class WecomClient extends EventEmitter {
|
|
|
461
462
|
const resultText = result === 'deny' ? '❌ 已拒绝' : '✅ 已允许';
|
|
462
463
|
const reasonText = reason ? `\n\n原因:${reason}` : '';
|
|
463
464
|
const toolInfo = approval.toolName ? `: ${approval.toolName}` : '';
|
|
464
|
-
const
|
|
465
|
-
|
|
465
|
+
const desc = approval.description || '';
|
|
466
|
+
const descBlock = desc ? `\n\n**执行命令**\n\`\`\`\n${desc}\n\`\`\`` : '';
|
|
467
|
+
this.sendText(`**审批结果(超时自动决策)**${toolInfo}\n\n${resultText}${reasonText}${descBlock}`).catch(err => {
|
|
466
468
|
logger.error('[wecom] 发送审批确认失败:', err);
|
|
467
469
|
});
|
|
468
470
|
logger.log(`[wecom] 超时自动决策已设置: ${taskId} → ${result}`);
|
package/dist/http-server.js
CHANGED
|
@@ -204,9 +204,9 @@ function initMcpServer() {
|
|
|
204
204
|
subscribeWecomMessage((msg) => {
|
|
205
205
|
handleWecomMessage(msg);
|
|
206
206
|
});
|
|
207
|
-
// 定时清理过期审批条目(每 5 分钟清理超过
|
|
207
|
+
// 定时清理过期审批条目(每 5 分钟清理超过 30 分钟的条目)
|
|
208
208
|
setInterval(() => {
|
|
209
|
-
const cutoff = Date.now() -
|
|
209
|
+
const cutoff = Date.now() - 30 * 60 * 1000;
|
|
210
210
|
let cleaned = 0;
|
|
211
211
|
for (const [id, entry] of pendingApprovals) {
|
|
212
212
|
if (entry.createdAt < cutoff) {
|
|
@@ -506,9 +506,9 @@ export async function startHttpServer(_server, port = HTTP_PORT, httpsConfig) {
|
|
|
506
506
|
return;
|
|
507
507
|
}
|
|
508
508
|
const url = req.url || '/';
|
|
509
|
-
// Auth token 校验(排除 /health
|
|
509
|
+
// Auth token 校验(排除 /health 和 /approval/ 详情页,后者由浏览器直接访问)
|
|
510
510
|
const authToken = getAuthToken();
|
|
511
|
-
if (authToken && url !== '/health') {
|
|
511
|
+
if (authToken && url !== '/health' && !url.startsWith('/approval/')) {
|
|
512
512
|
const authHeader = req.headers['authorization'];
|
|
513
513
|
if (authHeader !== `Bearer ${authToken}`) {
|
|
514
514
|
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
@@ -996,7 +996,7 @@ function handleApprovalDetail(_req, res, url) {
|
|
|
996
996
|
</div>
|
|
997
997
|
<h3>完整参数</h3>
|
|
998
998
|
<pre>${escapeHtml(inputPretty)}</pre>
|
|
999
|
-
<footer
|
|
999
|
+
<footer>审批记录保留 30 分钟后自动清理 · 请回到企业微信卡片点击审批按钮</footer>
|
|
1000
1000
|
</body>
|
|
1001
1001
|
</html>`;
|
|
1002
1002
|
respondHtml(200, html);
|
|
@@ -1056,7 +1056,11 @@ async function handleApprovalTimeout(req, res, url) {
|
|
|
1056
1056
|
const success = client.setApprovalResult(taskId, result, reason);
|
|
1057
1057
|
if (success) {
|
|
1058
1058
|
entry.status = result;
|
|
1059
|
-
|
|
1059
|
+
// 保留 30 分钟供事后查看详情,不立即删除
|
|
1060
|
+
setTimeout(() => {
|
|
1061
|
+
pendingApprovals.delete(taskId);
|
|
1062
|
+
logger.log(`[http] 超时审批条目已清理: taskId=${taskId}`);
|
|
1063
|
+
}, 30 * 60 * 1000);
|
|
1060
1064
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1061
1065
|
res.end(JSON.stringify({ success: true, taskId, result }));
|
|
1062
1066
|
}
|