@vrs-soft/wecom-aibot-mcp 1.2.0 → 1.2.1

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
@@ -44,7 +44,7 @@ PermissionRequest Hook 拦截
44
44
 
45
45
  ┌────────────────────────┐
46
46
  │ 检查 headless 模式状态 │
47
- │ (检查 .claude/headless.json)
47
+ │ (检查 .claude/wecom-aibot.json)
48
48
  └────────────────────────┘
49
49
 
50
50
  ┌───────┴───────┐
@@ -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
+ }
package/dist/bin.js CHANGED
@@ -86,11 +86,11 @@ function showStatus() {
86
86
  console.log('尚未配置机器人,请运行 npx @vrs-soft/wecom-aibot-mcp 启动配置向导');
87
87
  return;
88
88
  }
89
- // 构建机器人占用信息
89
+ // 构建机器人占用信息(v3.0: ccId 由 cc-registry 管理,连接状态不再含 ccId)
90
90
  const robotUsage = new Map();
91
91
  for (const conn of connections) {
92
- if (conn.agentName) {
93
- robotUsage.set(conn.robotName, { agentName: conn.agentName });
92
+ if (conn.connected) {
93
+ robotUsage.set(conn.robotName, { ccId: '' });
94
94
  }
95
95
  }
96
96
  console.log(`已配置 ${allRobots.length} 个机器人:\n`);
@@ -101,7 +101,7 @@ function showStatus() {
101
101
  console.log(` Bot ID: ${robot.botId}`);
102
102
  console.log(` 目标用户: ${robot.targetUserId}`);
103
103
  if (usage) {
104
- console.log(` 使用者: ${usage.agentName}`);
104
+ console.log(` 使用者: ${usage.ccId}`);
105
105
  }
106
106
  console.log('');
107
107
  }
@@ -174,11 +174,13 @@ async function waitForConnection(client, timeoutMs = 10000) {
174
174
  }
175
175
  // 启动 MCP Server(前台运行,供 --start 使用)
176
176
  async function startMcpServerForeground() {
177
- const savedConfig = loadConfig();
178
- if (!savedConfig || !savedConfig.botId || !savedConfig.secret || !savedConfig.targetUserId) {
177
+ // 检查是否有任何机器人配置(支持 config.json 和 robot-*.json)
178
+ const allRobots = listAllRobots();
179
+ if (allRobots.length === 0) {
179
180
  console.error('[mcp] 未找到配置,请先运行: npx @vrs-soft/wecom-aibot-mcp');
180
181
  process.exit(1);
181
182
  }
183
+ console.log(`[mcp] 发现 ${allRobots.length} 个机器人配置: ${allRobots.map(r => r.name).join(', ')}`);
182
184
  // 写入 PID 文件
183
185
  fs.writeFileSync(PID_FILE, String(process.pid));
184
186
  // 确保 hook 已安装
@@ -222,9 +224,9 @@ async function startMcpServerForeground() {
222
224
  }
223
225
  // 后台启动 MCP Server(使用 spawn)
224
226
  function startMcpServerBackground() {
225
- // 检查配置是否存在
226
- const savedConfig = loadConfig();
227
- if (!savedConfig || !savedConfig.botId || !savedConfig.secret || !savedConfig.targetUserId) {
227
+ // 检查是否有任何机器人配置
228
+ const allRobots = listAllRobots();
229
+ if (allRobots.length === 0) {
228
230
  console.error('[mcp] 未找到配置,请先运行: npx @vrs-soft/wecom-aibot-mcp');
229
231
  process.exit(1);
230
232
  }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * ccId 注册表
3
+ *
4
+ * 管理 ~/.wecom-aibot-mcp/cc-registry.json
5
+ * 维护 ccId → { robotName, lastActive, createdAt } 的映射
6
+ *
7
+ * 文件锁通过 .lock 文件实现(EEXIST 原子性)
8
+ */
9
+ /**
10
+ * 设置配置目录(仅用于测试)
11
+ */
12
+ export declare function setConfigDir(dir: string): void;
13
+ export interface CcRegistryEntry {
14
+ robotName: string;
15
+ lastActive: number;
16
+ createdAt: number;
17
+ lastNotified?: number;
18
+ }
19
+ type Registry = Record<string, CcRegistryEntry>;
20
+ /**
21
+ * 公开接口:独立调用时使用(内部会获取锁)
22
+ */
23
+ export declare function cleanupExpiredEntries(): void;
24
+ export type RegisterResult = 'registered' | 'renewed' | 'occupied';
25
+ /**
26
+ * 注册 ccId
27
+ * - 新 ccId → registered
28
+ * - 已存在且 robotName 相同 → renewed(续期)
29
+ * - 已存在且 robotName 不同 → occupied(被占用)
30
+ */
31
+ export declare function registerCcId(ccId: string, robotName: string): RegisterResult;
32
+ /**
33
+ * 注销 ccId
34
+ */
35
+ export declare function unregisterCcId(ccId: string): void;
36
+ /**
37
+ * 检查 ccId 是否已注册
38
+ */
39
+ export declare function isCcIdRegistered(ccId: string): boolean;
40
+ /**
41
+ * 更新 ccId 的最后活跃时间
42
+ */
43
+ export declare function touchCcId(ccId: string): void;
44
+ /**
45
+ * 获取 ccId 绑定的机器人名称
46
+ */
47
+ export declare function getCcIdBinding(ccId: string): {
48
+ robotName: string;
49
+ } | null;
50
+ /**
51
+ * 获取完整注册表(调试用)
52
+ */
53
+ export declare function getRegistry(): Registry;
54
+ /**
55
+ * 启动心跳检测(5 分钟扫描一次)
56
+ */
57
+ export declare function startHeartbeatMonitor(): void;
58
+ /**
59
+ * 停止心跳检测
60
+ */
61
+ export declare function stopHeartbeatMonitor(): void;
62
+ export {};
@@ -0,0 +1,272 @@
1
+ /**
2
+ * ccId 注册表
3
+ *
4
+ * 管理 ~/.wecom-aibot-mcp/cc-registry.json
5
+ * 维护 ccId → { robotName, lastActive, createdAt } 的映射
6
+ *
7
+ * 文件锁通过 .lock 文件实现(EEXIST 原子性)
8
+ */
9
+ import * as fs from 'fs';
10
+ import * as path from 'path';
11
+ import * as os from 'os';
12
+ import { atomicWriteFileSync } from './utils/atomic-write.js';
13
+ const EXPIRY_MS = 14 * 24 * 60 * 60 * 1000;
14
+ // 心跳检测常量
15
+ const OFFLINE_THRESHOLD = 10 * 60 * 1000; // 10 分钟无心跳视为离线
16
+ const NOTIFICATION_INTERVAL = 30 * 60 * 1000; // 30 分钟最多通知一次
17
+ // 支持测试环境覆盖
18
+ let CONFIG_DIR = path.join(os.homedir(), '.wecom-aibot-mcp');
19
+ let REGISTRY_FILE = path.join(CONFIG_DIR, 'cc-registry.json');
20
+ let LOCK_FILE = path.join(CONFIG_DIR, 'cc-registry.lock');
21
+ /**
22
+ * 设置配置目录(仅用于测试)
23
+ */
24
+ export function setConfigDir(dir) {
25
+ CONFIG_DIR = dir;
26
+ REGISTRY_FILE = path.join(CONFIG_DIR, 'cc-registry.json');
27
+ LOCK_FILE = path.join(CONFIG_DIR, 'cc-registry.lock');
28
+ }
29
+ // ────────────────────────────────────────────
30
+ // 文件锁(基于 EEXIST 原子性)
31
+ // ────────────────────────────────────────────
32
+ function acquireLock() {
33
+ try {
34
+ fs.openSync(LOCK_FILE, 'wx');
35
+ return true;
36
+ }
37
+ catch (e) {
38
+ if (e.code === 'EEXIST')
39
+ return false;
40
+ throw e;
41
+ }
42
+ }
43
+ function releaseLock() {
44
+ try {
45
+ fs.unlinkSync(LOCK_FILE);
46
+ }
47
+ catch { /* ignore */ }
48
+ }
49
+ function withLock(fn) {
50
+ const deadline = Date.now() + 3000;
51
+ while (!acquireLock()) {
52
+ if (Date.now() > deadline)
53
+ throw new Error('cc-registry: 获取锁超时');
54
+ // 自旋等待(锁持有时间极短)
55
+ const start = Date.now();
56
+ while (Date.now() - start < 50) { /* busy wait */ }
57
+ }
58
+ try {
59
+ return fn();
60
+ }
61
+ finally {
62
+ releaseLock();
63
+ }
64
+ }
65
+ // ────────────────────────────────────────────
66
+ // 注册表 I/O
67
+ // ────────────────────────────────────────────
68
+ function readRegistry() {
69
+ try {
70
+ return JSON.parse(fs.readFileSync(REGISTRY_FILE, 'utf-8'));
71
+ }
72
+ catch {
73
+ return {};
74
+ }
75
+ }
76
+ function writeRegistry(registry) {
77
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
78
+ atomicWriteFileSync(REGISTRY_FILE, JSON.stringify(registry, null, 2));
79
+ }
80
+ // ────────────────────────────────────────────
81
+ // 过期清理
82
+ // ────────────────────────────────────────────
83
+ /**
84
+ * 内部清理:在已持锁的上下文中直接操作传入的 registry 对象。
85
+ * 不获取锁,调用方负责锁安全。
86
+ */
87
+ function pruneExpired(registry) {
88
+ const now = Date.now();
89
+ let changed = false;
90
+ for (const [ccId, entry] of Object.entries(registry)) {
91
+ if (now - entry.lastActive > EXPIRY_MS) {
92
+ console.log(`[cc-registry] 清理过期条目: ccId=${ccId}, robot=${entry.robotName}`);
93
+ delete registry[ccId];
94
+ changed = true;
95
+ }
96
+ }
97
+ return changed;
98
+ }
99
+ /**
100
+ * 公开接口:独立调用时使用(内部会获取锁)
101
+ */
102
+ export function cleanupExpiredEntries() {
103
+ withLock(() => {
104
+ const registry = readRegistry();
105
+ if (pruneExpired(registry))
106
+ writeRegistry(registry);
107
+ });
108
+ }
109
+ /**
110
+ * 注册 ccId
111
+ * - 新 ccId → registered
112
+ * - 已存在且 robotName 相同 → renewed(续期)
113
+ * - 已存在且 robotName 不同 → occupied(被占用)
114
+ */
115
+ export function registerCcId(ccId, robotName) {
116
+ return withLock(() => {
117
+ const registry = readRegistry();
118
+ pruneExpired(registry); // 已在锁内,不二次获取
119
+ const existing = registry[ccId];
120
+ if (existing) {
121
+ if (existing.robotName === robotName) {
122
+ // 同 ccId + 同 robotName → 续期
123
+ existing.lastActive = Date.now();
124
+ writeRegistry(registry);
125
+ return 'renewed';
126
+ }
127
+ else {
128
+ return 'occupied';
129
+ }
130
+ }
131
+ registry[ccId] = {
132
+ robotName,
133
+ lastActive: Date.now(),
134
+ createdAt: Date.now(),
135
+ };
136
+ writeRegistry(registry);
137
+ return 'registered';
138
+ });
139
+ }
140
+ /**
141
+ * 注销 ccId
142
+ */
143
+ export function unregisterCcId(ccId) {
144
+ withLock(() => {
145
+ const registry = readRegistry();
146
+ delete registry[ccId];
147
+ writeRegistry(registry);
148
+ });
149
+ }
150
+ /**
151
+ * 检查 ccId 是否已注册
152
+ */
153
+ export function isCcIdRegistered(ccId) {
154
+ const registry = readRegistry();
155
+ return ccId in registry;
156
+ }
157
+ /**
158
+ * 更新 ccId 的最后活跃时间
159
+ */
160
+ export function touchCcId(ccId) {
161
+ withLock(() => {
162
+ const registry = readRegistry();
163
+ if (registry[ccId]) {
164
+ registry[ccId].lastActive = Date.now();
165
+ writeRegistry(registry);
166
+ }
167
+ });
168
+ }
169
+ /**
170
+ * 获取 ccId 绑定的机器人名称
171
+ */
172
+ export function getCcIdBinding(ccId) {
173
+ const registry = readRegistry();
174
+ const entry = registry[ccId];
175
+ if (!entry)
176
+ return null;
177
+ return { robotName: entry.robotName };
178
+ }
179
+ /**
180
+ * 获取完整注册表(调试用)
181
+ */
182
+ export function getRegistry() {
183
+ return readRegistry();
184
+ }
185
+ // ────────────────────────────────────────────
186
+ // 心跳检测
187
+ // ────────────────────────────────────────────
188
+ let heartbeatInterval = null;
189
+ /**
190
+ * 检查 CC 心跳并发送离线通知
191
+ */
192
+ async function checkCcHeartbeat() {
193
+ const registry = readRegistry();
194
+ const now = Date.now();
195
+ let changed = false;
196
+ for (const [ccId, entry] of Object.entries(registry)) {
197
+ const inactive = now - entry.lastActive;
198
+ if (inactive > OFFLINE_THRESHOLD) {
199
+ // 检查是否需要通知(避免重复)
200
+ const shouldNotify = !entry.lastNotified ||
201
+ (now - entry.lastNotified > NOTIFICATION_INTERVAL);
202
+ if (shouldNotify) {
203
+ await sendOfflineNotification(ccId, entry.robotName);
204
+ entry.lastNotified = now;
205
+ changed = true;
206
+ }
207
+ }
208
+ }
209
+ if (changed) {
210
+ withLock(() => {
211
+ const reg = readRegistry();
212
+ for (const [ccId, entry] of Object.entries(registry)) {
213
+ if (entry.lastNotified && reg[ccId]) {
214
+ reg[ccId].lastNotified = entry.lastNotified;
215
+ }
216
+ }
217
+ writeRegistry(reg);
218
+ });
219
+ }
220
+ }
221
+ /**
222
+ * 发送离线通知到微信
223
+ */
224
+ async function sendOfflineNotification(ccId, robotName) {
225
+ try {
226
+ // 动态导入避免循环依赖
227
+ const { getClient } = await import('./connection-manager.js');
228
+ const client = await getClient(robotName);
229
+ if (!client) {
230
+ console.log(`[heartbeat] 机器人 ${robotName} 未连接,无法发送离线通知`);
231
+ return;
232
+ }
233
+ const inactiveMinutes = Math.floor((Date.now() - getRegistry()[ccId]?.lastActive || 0) / 60000);
234
+ const message = `【系统警告】CC "${ccId}" 已超过 ${inactiveMinutes} 分钟无心跳,可能已离线。
235
+
236
+ 可能原因:
237
+ • CC 进程已退出
238
+ • 网络连接中断
239
+ • CC 正在执行长时间任务
240
+
241
+ 建议:请检查终端状态或重新启动 CC。`;
242
+ await client.sendText(message);
243
+ console.log(`[heartbeat] 已发送离线通知: ccId=${ccId}, robot=${robotName}`);
244
+ }
245
+ catch (err) {
246
+ console.error(`[heartbeat] 发送离线通知失败:`, err);
247
+ }
248
+ }
249
+ /**
250
+ * 启动心跳检测(5 分钟扫描一次)
251
+ */
252
+ export function startHeartbeatMonitor() {
253
+ if (heartbeatInterval)
254
+ return;
255
+ // 测试期间改为 1 分钟,正式环境改为 5 * 60 * 1000
256
+ heartbeatInterval = setInterval(() => {
257
+ checkCcHeartbeat().catch(err => {
258
+ console.error('[heartbeat] 心跳检测错误:', err);
259
+ });
260
+ }, 1 * 60 * 1000);
261
+ console.log('[heartbeat] 心跳检测已启动(测试模式:1 分钟周期)');
262
+ }
263
+ /**
264
+ * 停止心跳检测
265
+ */
266
+ export function stopHeartbeatMonitor() {
267
+ if (heartbeatInterval) {
268
+ clearInterval(heartbeatInterval);
269
+ heartbeatInterval = null;
270
+ console.log('[heartbeat] 心跳检测已停止');
271
+ }
272
+ }
package/dist/client.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { EventEmitter } from 'events';
2
+ declare const MAX_PENDING_MESSAGES = 100;
2
3
  interface ApprovalRecord {
3
4
  taskId: string;
4
5
  resolved: boolean;
@@ -9,11 +10,15 @@ interface ApprovalRecord {
9
10
  projectDir?: string;
10
11
  lastKeepaliveMinute?: number;
11
12
  keepaliveCount?: number;
13
+ operationHash?: string;
14
+ consumed?: boolean;
12
15
  }
13
16
  interface MessageRecord {
17
+ seq: number;
14
18
  msgid: string;
15
19
  content: string;
16
20
  timestamp: number;
21
+ serverTime: number;
17
22
  from_userid: string;
18
23
  chatid: string;
19
24
  chattype: 'single' | 'group';
@@ -44,7 +49,8 @@ declare class WecomClient extends EventEmitter {
44
49
  error?: string;
45
50
  }>;
46
51
  sendText(content: string, targetUser?: string): Promise<boolean>;
47
- sendApprovalRequest(title: string, description: string, requestId: string, targetUser?: string): Promise<string>;
52
+ sendApprovalRequest(title: string, description: string, requestId: string, targetUser?: string, toolInput?: Record<string, unknown>, // v3.0: 用于去重
53
+ ccId?: string): Promise<string>;
48
54
  sendQueuedApproval(taskId: string, title: string, description: string, targetUser?: string): Promise<boolean>;
49
55
  getApprovalResult(taskId: string): 'pending' | 'allow-once' | 'allow-always' | 'deny';
50
56
  getPendingApprovals(): string[];
@@ -52,6 +58,28 @@ declare class WecomClient extends EventEmitter {
52
58
  getApprovalRecord(taskId: string): ApprovalRecord | undefined;
53
59
  getLatestMessage(afterTimestamp: number): MessageRecord | undefined;
54
60
  getPendingMessages(clear?: boolean): MessageRecord[];
61
+ /**
62
+ * 根据操作哈希查找已有审批
63
+ */
64
+ findApprovalByHash(operationHash: string): ApprovalRecord | null;
65
+ /**
66
+ * 检查审批是否可以消费
67
+ * allow-once 只能消费一次
68
+ */
69
+ canConsumeApproval(taskId: string): boolean;
70
+ /**
71
+ * 消费审批结果
72
+ * allow-once 消费后标记为已消费
73
+ */
74
+ consumeApproval(taskId: string): 'allow-once' | 'allow-always' | 'deny' | null;
75
+ /**
76
+ * 注入审批记录(MCP 重启恢复用)
77
+ * 如果 taskId 已存在则跳过,避免覆盖用户已点击的结果
78
+ */
79
+ injectApprovalRecord(taskId: string, partial: {
80
+ toolName?: string;
81
+ toolInput?: Record<string, unknown>;
82
+ }): void;
55
83
  cleanupMessages(maxAgeMs?: number): void;
56
84
  private flushPendingMessages;
57
85
  getPendingMessageCount(): number;
@@ -63,4 +91,4 @@ declare class WecomClient extends EventEmitter {
63
91
  }
64
92
  export declare function initClient(botId: string, secret: string, targetUserId: string, robotName: string): WecomClient;
65
93
  export declare function getClient(): WecomClient;
66
- export { WecomClient, ApprovalRecord, MessageRecord };
94
+ export { WecomClient, ApprovalRecord, MessageRecord, MAX_PENDING_MESSAGES };