@vrs-soft/wecom-aibot-mcp 1.5.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 +50 -337
- package/dist/bin.js +172 -85
- 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 +13 -3
- package/dist/config-wizard.js +424 -141
- package/dist/connection-log.js +7 -6
- package/dist/connection-manager.d.ts +0 -8
- package/dist/connection-manager.js +11 -23
- 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 +21 -2
- package/dist/http-server.js +421 -88
- 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 +16 -0
- package/dist/project-config.js +91 -12
- package/dist/tools/index.js +226 -57
- package/package.json +1 -1
- package/skills/headless-mode/SKILL.md +136 -181
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel MCP Proxy 单元测试
|
|
3
|
+
* 测试 SSE 消息接收和 notification 发送
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
6
|
+
// 模拟 fetch
|
|
7
|
+
const mockFetch = vi.fn();
|
|
8
|
+
global.fetch = mockFetch;
|
|
9
|
+
// 模拟 McpServer
|
|
10
|
+
const mockNotification = vi.fn();
|
|
11
|
+
const mockMcpServer = {
|
|
12
|
+
server: {
|
|
13
|
+
notification: mockNotification,
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
describe('Channel Server - SSE 消息接收', () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
vi.clearAllMocks();
|
|
19
|
+
mockFetch.mockReset();
|
|
20
|
+
});
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
vi.restoreAllMocks();
|
|
23
|
+
});
|
|
24
|
+
it('应该正确建立 SSE 连接', async () => {
|
|
25
|
+
// 模拟 SSE 响应
|
|
26
|
+
const mockSSEStream = new ReadableStream({
|
|
27
|
+
start(controller) {
|
|
28
|
+
controller.enqueue(new TextEncoder().encode('event: connected\ndata: {"clientId":"test-123","ccId":"test"}\n\n'));
|
|
29
|
+
controller.close();
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
mockFetch.mockResolvedValueOnce({
|
|
33
|
+
ok: true,
|
|
34
|
+
status: 200,
|
|
35
|
+
body: mockSSEStream,
|
|
36
|
+
headers: new Headers({
|
|
37
|
+
'Content-Type': 'text/event-stream',
|
|
38
|
+
}),
|
|
39
|
+
});
|
|
40
|
+
const response = await fetch('http://127.0.0.1:18963/sse/test-channel');
|
|
41
|
+
expect(response.ok).toBe(true);
|
|
42
|
+
expect(response.status).toBe(200);
|
|
43
|
+
});
|
|
44
|
+
it('应该正确解析 SSE 消息', async () => {
|
|
45
|
+
// 模拟 SSE 数据格式
|
|
46
|
+
const sseData = `event: message\ndata: {"type":"wecom_message","robotName":"CC","ccId":"test-channel","message":{"content":"测试消息","from":"LiuYang"}}\n\n`;
|
|
47
|
+
const lines = sseData.split('\n');
|
|
48
|
+
const messages = [];
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
if (line.startsWith('data: ')) {
|
|
51
|
+
const data = line.slice(6);
|
|
52
|
+
try {
|
|
53
|
+
const msg = JSON.parse(data);
|
|
54
|
+
messages.push(msg);
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
// 解析失败
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
expect(messages.length).toBe(1);
|
|
62
|
+
expect(messages[0].type).toBe('wecom_message');
|
|
63
|
+
expect(messages[0].message.content).toBe('测试消息');
|
|
64
|
+
});
|
|
65
|
+
it('应该发送正确的 notification 格式', async () => {
|
|
66
|
+
// 模拟接收到的 SSE 消息
|
|
67
|
+
const sseMessage = {
|
|
68
|
+
type: 'wecom_message',
|
|
69
|
+
robotName: 'CC',
|
|
70
|
+
ccId: 'test-channel',
|
|
71
|
+
message: {
|
|
72
|
+
content: '你好',
|
|
73
|
+
from: 'LiuYang',
|
|
74
|
+
chatid: 'LiuYang',
|
|
75
|
+
chattype: 'single',
|
|
76
|
+
time: '2026-04-12T16:00:00.000Z',
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
// 构造 notification
|
|
80
|
+
const notification = {
|
|
81
|
+
method: 'notifications/claude/channel',
|
|
82
|
+
params: {
|
|
83
|
+
type: sseMessage.type,
|
|
84
|
+
content: sseMessage,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
// 验证 notification 格式
|
|
88
|
+
expect(notification.method).toBe('notifications/claude/channel');
|
|
89
|
+
expect(notification.params.type).toBe('wecom_message');
|
|
90
|
+
expect(notification.params.content.message.content).toBe('你好');
|
|
91
|
+
// 模拟发送 notification
|
|
92
|
+
mockMcpServer.server.notification(notification);
|
|
93
|
+
expect(mockNotification).toHaveBeenCalledOnce();
|
|
94
|
+
expect(mockNotification).toHaveBeenCalledWith(notification);
|
|
95
|
+
});
|
|
96
|
+
it('应该处理多条 SSE 消息', async () => {
|
|
97
|
+
// 模拟多条 SSE 消息
|
|
98
|
+
const sseData = `
|
|
99
|
+
event: message
|
|
100
|
+
data: {"type":"wecom_message","message":{"content":"消息1"}}
|
|
101
|
+
|
|
102
|
+
event: message
|
|
103
|
+
data: {"type":"wecom_message","message":{"content":"消息2"}}
|
|
104
|
+
|
|
105
|
+
`;
|
|
106
|
+
const lines = sseData.split('\n');
|
|
107
|
+
const messages = [];
|
|
108
|
+
for (const line of lines) {
|
|
109
|
+
if (line.startsWith('data: ')) {
|
|
110
|
+
const data = line.slice(6);
|
|
111
|
+
try {
|
|
112
|
+
messages.push(JSON.parse(data));
|
|
113
|
+
}
|
|
114
|
+
catch (e) { }
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
expect(messages.length).toBe(2);
|
|
118
|
+
expect(messages[0].message.content).toBe('消息1');
|
|
119
|
+
expect(messages[1].message.content).toBe('消息2');
|
|
120
|
+
});
|
|
121
|
+
it('应该处理 SSE 连接失败', async () => {
|
|
122
|
+
mockFetch.mockResolvedValueOnce({
|
|
123
|
+
ok: false,
|
|
124
|
+
status: 404,
|
|
125
|
+
});
|
|
126
|
+
const response = await fetch('http://127.0.0.1:18963/sse/invalid-ccid');
|
|
127
|
+
expect(response.ok).toBe(false);
|
|
128
|
+
expect(response.status).toBe(404);
|
|
129
|
+
});
|
|
130
|
+
it('应该处理 SSE 流结束', async () => {
|
|
131
|
+
// 模拟 SSE 流正常结束
|
|
132
|
+
const mockSSEStream = new ReadableStream({
|
|
133
|
+
start(controller) {
|
|
134
|
+
controller.enqueue(new TextEncoder().encode('event: connected\ndata: {"status":"ok"}\n\n'));
|
|
135
|
+
controller.close(); // 流结束
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
mockFetch.mockResolvedValueOnce({
|
|
139
|
+
ok: true,
|
|
140
|
+
body: mockSSEStream,
|
|
141
|
+
});
|
|
142
|
+
const response = await fetch('http://127.0.0.1:18963/sse/test');
|
|
143
|
+
const reader = response.body?.getReader();
|
|
144
|
+
if (reader) {
|
|
145
|
+
const { done, value } = await reader.read();
|
|
146
|
+
expect(done).toBe(false); // 第一条消息
|
|
147
|
+
const { done: done2 } = await reader.read();
|
|
148
|
+
expect(done2).toBe(true); // 流结束
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
describe('Channel Server - HTTP MCP 转发', () => {
|
|
153
|
+
it('应该正确初始化 HTTP MCP session', async () => {
|
|
154
|
+
// 模拟 HTTP MCP initialize 响应
|
|
155
|
+
mockFetch.mockResolvedValueOnce({
|
|
156
|
+
ok: true,
|
|
157
|
+
headers: new Headers({
|
|
158
|
+
'mcp-session-id': 'session-test-123',
|
|
159
|
+
}),
|
|
160
|
+
});
|
|
161
|
+
const response = await fetch('http://127.0.0.1:18963/mcp', {
|
|
162
|
+
method: 'POST',
|
|
163
|
+
headers: {
|
|
164
|
+
'Content-Type': 'application/json',
|
|
165
|
+
},
|
|
166
|
+
body: JSON.stringify({
|
|
167
|
+
jsonrpc: '2.0',
|
|
168
|
+
method: 'initialize',
|
|
169
|
+
params: {
|
|
170
|
+
protocolVersion: '2024-11-05',
|
|
171
|
+
capabilities: {},
|
|
172
|
+
clientInfo: { name: 'channel-proxy', version: '1.0' },
|
|
173
|
+
},
|
|
174
|
+
id: 1,
|
|
175
|
+
}),
|
|
176
|
+
});
|
|
177
|
+
const sessionId = response.headers.get('mcp-session-id');
|
|
178
|
+
expect(sessionId).toBe('session-test-123');
|
|
179
|
+
});
|
|
180
|
+
it('应该正确转发工具调用请求', async () => {
|
|
181
|
+
// 模拟 HTTP MCP tools/call 响应
|
|
182
|
+
mockFetch.mockResolvedValueOnce({
|
|
183
|
+
ok: true,
|
|
184
|
+
text: () => Promise.resolve('data: {"result":{"content":[{"type":"text","text":"消息已发送"}]}}\n\n'),
|
|
185
|
+
});
|
|
186
|
+
const response = await fetch('http://127.0.0.1:18963/mcp', {
|
|
187
|
+
method: 'POST',
|
|
188
|
+
headers: {
|
|
189
|
+
'Content-Type': 'application/json',
|
|
190
|
+
'mcp-session-id': 'session-test-123',
|
|
191
|
+
},
|
|
192
|
+
body: JSON.stringify({
|
|
193
|
+
jsonrpc: '2.0',
|
|
194
|
+
method: 'tools/call',
|
|
195
|
+
params: {
|
|
196
|
+
name: 'send_message',
|
|
197
|
+
arguments: {
|
|
198
|
+
content: '测试消息',
|
|
199
|
+
cc_id: 'test-channel',
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
id: 2,
|
|
203
|
+
}),
|
|
204
|
+
});
|
|
205
|
+
expect(response.ok).toBe(true);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
describe('Channel Server - Notification 转发测试', () => {
|
|
209
|
+
it('应该正确转发 SSE 消息到 Claude Code notification', async () => {
|
|
210
|
+
// 模拟完整的 SSE → Notification 流程
|
|
211
|
+
// 1. SSE 消息数据
|
|
212
|
+
const sseRawData = `event: message\ndata: {"type":"wecom_message","robotName":"CC","ccId":"test-channel","message":{"content":"你好","from":"LiuYang","chatid":"LiuYang","chattype":"single","time":"2026-04-12T16:00:00.000Z"}}\n\n`;
|
|
213
|
+
// 2. 解析 SSE 数据
|
|
214
|
+
const lines = sseRawData.split('\n');
|
|
215
|
+
let parsedMessage = null;
|
|
216
|
+
for (const line of lines) {
|
|
217
|
+
if (line.startsWith('data: ')) {
|
|
218
|
+
const data = line.slice(6);
|
|
219
|
+
parsedMessage = JSON.parse(data);
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
expect(parsedMessage).not.toBeNull();
|
|
224
|
+
expect(parsedMessage.type).toBe('wecom_message');
|
|
225
|
+
// 3. 构造 notification
|
|
226
|
+
const notification = {
|
|
227
|
+
method: 'notifications/claude/channel',
|
|
228
|
+
params: {
|
|
229
|
+
type: parsedMessage.type,
|
|
230
|
+
content: parsedMessage,
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
// 4. 模拟发送 notification
|
|
234
|
+
mockMcpServer.server.notification(notification);
|
|
235
|
+
expect(mockNotification).toHaveBeenCalledOnce();
|
|
236
|
+
expect(mockNotification).toHaveBeenCalledWith(expect.objectContaining({
|
|
237
|
+
method: 'notifications/claude/channel',
|
|
238
|
+
params: expect.objectContaining({
|
|
239
|
+
type: 'wecom_message',
|
|
240
|
+
content: expect.objectContaining({
|
|
241
|
+
message: expect.objectContaining({
|
|
242
|
+
content: '你好',
|
|
243
|
+
}),
|
|
244
|
+
}),
|
|
245
|
+
}),
|
|
246
|
+
}));
|
|
247
|
+
});
|
|
248
|
+
it('notification 应该包含完整的消息信息', async () => {
|
|
249
|
+
// 测试 notification 内容完整性
|
|
250
|
+
const fullMessage = {
|
|
251
|
+
type: 'wecom_message',
|
|
252
|
+
robotName: 'CC',
|
|
253
|
+
ccId: 'test-channel',
|
|
254
|
+
message: {
|
|
255
|
+
content: '完整测试消息',
|
|
256
|
+
from: 'LiuYang',
|
|
257
|
+
chatid: 'LiuYang',
|
|
258
|
+
chattype: 'single',
|
|
259
|
+
time: '2026-04-12T17:00:00.000Z',
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
const notification = {
|
|
263
|
+
method: 'notifications/claude/channel',
|
|
264
|
+
params: {
|
|
265
|
+
type: fullMessage.type,
|
|
266
|
+
content: fullMessage,
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
// 验证所有字段都存在
|
|
270
|
+
expect(notification.params.content.robotName).toBe('CC');
|
|
271
|
+
expect(notification.params.content.message.from).toBe('LiuYang');
|
|
272
|
+
expect(notification.params.content.message.chattype).toBe('single');
|
|
273
|
+
});
|
|
274
|
+
it('应该处理不同类型的 SSE 消息', async () => {
|
|
275
|
+
// 测试不同消息类型
|
|
276
|
+
const messageTypes = [
|
|
277
|
+
{ type: 'wecom_message', expected: 'wecom_message' },
|
|
278
|
+
{ type: 'approval_request', expected: 'approval_request' },
|
|
279
|
+
{ type: 'connection_event', expected: 'connection_event' },
|
|
280
|
+
];
|
|
281
|
+
for (const { type, expected } of messageTypes) {
|
|
282
|
+
const notification = {
|
|
283
|
+
method: 'notifications/claude/channel',
|
|
284
|
+
params: {
|
|
285
|
+
type: type,
|
|
286
|
+
content: { type },
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
expect(notification.params.type).toBe(expected);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
it('应该在 mcpServer 为 null 时不发送 notification', async () => {
|
|
293
|
+
// 清除之前的 mock 调用
|
|
294
|
+
mockNotification.mockClear();
|
|
295
|
+
// 测试 mcpServer 未初始化的情况
|
|
296
|
+
// 当 mcpServer 为 null 时,代码会跳过 notification 发送
|
|
297
|
+
// 这里验证不会调用 mockNotification
|
|
298
|
+
// 预期不会调用 notification
|
|
299
|
+
expect(mockNotification).not.toHaveBeenCalled();
|
|
300
|
+
});
|
|
301
|
+
it('应该正确处理 SSE chunk 分片', async () => {
|
|
302
|
+
// 测试 SSE 数据分片到达的情况
|
|
303
|
+
const chunk1 = 'event: message\ndata: ';
|
|
304
|
+
const chunk2 = '{"type":"wecom_message","message":{"content":"分片测试"}}';
|
|
305
|
+
const chunk3 = '\n\n';
|
|
306
|
+
// 合并分片
|
|
307
|
+
const fullChunk = chunk1 + chunk2 + chunk3;
|
|
308
|
+
const lines = fullChunk.split('\n');
|
|
309
|
+
let parsedMessage = null;
|
|
310
|
+
for (const line of lines) {
|
|
311
|
+
if (line.startsWith('data: ')) {
|
|
312
|
+
const data = line.slice(6);
|
|
313
|
+
try {
|
|
314
|
+
parsedMessage = JSON.parse(data);
|
|
315
|
+
}
|
|
316
|
+
catch (e) {
|
|
317
|
+
// 可能是不完整的 JSON
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
expect(parsedMessage).not.toBeNull();
|
|
322
|
+
expect(parsedMessage.message.content).toBe('分片测试');
|
|
323
|
+
});
|
|
324
|
+
});
|
package/dist/client-pool.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* - 支持多机器人配置
|
|
10
10
|
*/
|
|
11
11
|
import { WecomClient } from './client.js';
|
|
12
|
+
import { logger } from './logger.js';
|
|
12
13
|
// ClientPool 存储
|
|
13
14
|
const clientPool = new Map();
|
|
14
15
|
// key: projectDir
|
|
@@ -38,7 +39,7 @@ export function getOrCreateClient(projectDir, config) {
|
|
|
38
39
|
// 存入 pool
|
|
39
40
|
clientPool.set(projectDir, client);
|
|
40
41
|
configCache.set(projectDir, config);
|
|
41
|
-
|
|
42
|
+
logger.log(`[client-pool] 已创建 client: ${projectDir}`);
|
|
42
43
|
}
|
|
43
44
|
return client;
|
|
44
45
|
}
|
|
@@ -97,7 +98,7 @@ export function removeClient(projectDir) {
|
|
|
97
98
|
if (client) {
|
|
98
99
|
client.disconnect();
|
|
99
100
|
clientPool.delete(projectDir);
|
|
100
|
-
|
|
101
|
+
logger.log(`[client-pool] 已移除 client: ${projectDir}`);
|
|
101
102
|
}
|
|
102
103
|
}
|
|
103
104
|
/**
|
|
@@ -109,7 +110,7 @@ export function clearAll() {
|
|
|
109
110
|
}
|
|
110
111
|
clientPool.clear();
|
|
111
112
|
configCache.clear();
|
|
112
|
-
|
|
113
|
+
logger.log('[client-pool] 已清空所有 client');
|
|
113
114
|
}
|
|
114
115
|
/**
|
|
115
116
|
* 获取 client 状态统计
|
package/dist/client.js
CHANGED
|
@@ -14,6 +14,7 @@ import { EventEmitter } from 'events';
|
|
|
14
14
|
import { logConnected, logAuthenticated, logDisconnected, logReconnecting, logError, } from './connection-log.js';
|
|
15
15
|
import { publishWecomMessage } from './message-bus.js';
|
|
16
16
|
import { hashOperation } from './utils/hash.js';
|
|
17
|
+
import { logger } from './logger.js';
|
|
17
18
|
// 最大待处理消息数量
|
|
18
19
|
const MAX_PENDING_MESSAGES = 100;
|
|
19
20
|
// 全局消息序号计数器
|
|
@@ -66,7 +67,7 @@ class WecomClient extends EventEmitter {
|
|
|
66
67
|
// 重连成功后发送通知
|
|
67
68
|
if (wasReconnecting) {
|
|
68
69
|
this.sendText('【系统】连接已恢复').catch(err => {
|
|
69
|
-
|
|
70
|
+
logger.error('wecom', `发送恢复通知失败: ${err}`);
|
|
70
71
|
});
|
|
71
72
|
// 刷新待发送消息队列
|
|
72
73
|
this.flushPendingMessages();
|
|
@@ -81,7 +82,7 @@ class WecomClient extends EventEmitter {
|
|
|
81
82
|
if (this.disconnectNotifyCount < 1) {
|
|
82
83
|
this.disconnectNotifyCount++;
|
|
83
84
|
this.sendText('【系统】连接中断,正在重连...').catch(err => {
|
|
84
|
-
|
|
85
|
+
logger.error('wecom', `发送断线通知失败: ${err}`);
|
|
85
86
|
});
|
|
86
87
|
}
|
|
87
88
|
});
|
|
@@ -93,16 +94,16 @@ class WecomClient extends EventEmitter {
|
|
|
93
94
|
logError(err.message);
|
|
94
95
|
// 检测授权相关错误(40058: invalid Request Parameter)
|
|
95
96
|
if (err.message.includes('40058') || err.message.includes('invalid Request Parameter')) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
97
|
+
logger.info('wecom', '');
|
|
98
|
+
logger.info('wecom', ' ⚠️ 机器人未授权或配置有误,请检查以下事项:');
|
|
99
|
+
logger.info('wecom', '');
|
|
100
|
+
logger.info('wecom', ' 1. 新建机器人需要等待约 2 分钟同步时间,请稍后再试');
|
|
101
|
+
logger.info('wecom', ' 2. 确认 Bot ID 和 Secret 是否正确');
|
|
102
|
+
logger.info('wecom', ' 3. 完成机器人授权(任选其一):');
|
|
103
|
+
logger.info('wecom', ' • 在电脑端企业微信APP中打开:机器人详情 → 可使用权限 → 授权');
|
|
104
|
+
logger.info('wecom', ' • 打开浏览器访问以下地址,使用手机企业微信扫码授权:');
|
|
105
|
+
logger.info('wecom', ` ${this.getAuthUrl()}`);
|
|
106
|
+
logger.info('wecom', '');
|
|
106
107
|
}
|
|
107
108
|
});
|
|
108
109
|
// 监听所有消息(存储到队列)
|
|
@@ -111,13 +112,12 @@ class WecomClient extends EventEmitter {
|
|
|
111
112
|
});
|
|
112
113
|
// 监听模板卡片事件(审批结果)
|
|
113
114
|
this.wsClient.on('event.template_card_event', (frame) => {
|
|
114
|
-
|
|
115
|
-
console.log(JSON.stringify(frame, null, 2));
|
|
115
|
+
logger.log('wecom', `收到 template_card_event 事件: ${JSON.stringify(frame)}`);
|
|
116
116
|
this.handleApprovalResponse(frame);
|
|
117
117
|
});
|
|
118
118
|
// 监听进入会话事件
|
|
119
119
|
this.wsClient.on('event.enter_chat', (frame) => {
|
|
120
|
-
|
|
120
|
+
logger.log('wecom', '用户进入会话');
|
|
121
121
|
this.wsClient.replyWelcome(frame, {
|
|
122
122
|
msgtype: 'text',
|
|
123
123
|
text: { content: '您好!Claude Code 审批通道已就绪。' },
|
|
@@ -129,7 +129,7 @@ class WecomClient extends EventEmitter {
|
|
|
129
129
|
if (!body)
|
|
130
130
|
return;
|
|
131
131
|
// 打印完整消息结构(调试用)
|
|
132
|
-
|
|
132
|
+
logger.log('wecom', `收到消息帧: ${JSON.stringify(body).substring(0, 500)}`);
|
|
133
133
|
const msgid = body.msgid;
|
|
134
134
|
const from_userid = body.from?.userid || '';
|
|
135
135
|
const msgtype = body.msgtype;
|
|
@@ -152,7 +152,7 @@ class WecomClient extends EventEmitter {
|
|
|
152
152
|
quoteContent = body.quote.text.content;
|
|
153
153
|
}
|
|
154
154
|
if (quoteContent) {
|
|
155
|
-
|
|
155
|
+
logger.log('wecom', `检测到引用内容: ${quoteContent.substring(0, 100)}`);
|
|
156
156
|
}
|
|
157
157
|
if (content) {
|
|
158
158
|
// v3.0: 添加消息序号和队列上限检查
|
|
@@ -170,11 +170,11 @@ class WecomClient extends EventEmitter {
|
|
|
170
170
|
// v3.0: 检查队列上限
|
|
171
171
|
if (this.messages.length >= MAX_PENDING_MESSAGES) {
|
|
172
172
|
const dropped = this.messages.shift();
|
|
173
|
-
|
|
173
|
+
logger.warn('wecom', `消息队列已满,丢弃旧消息: ${dropped?.msgid}`);
|
|
174
174
|
}
|
|
175
175
|
this.messages.push(msgRecord);
|
|
176
176
|
const source = chattype === 'group' ? `群聊(${chatid})` : '单聊';
|
|
177
|
-
|
|
177
|
+
logger.log('wecom', `收到${source}消息: ${from_userid} -> ${content.slice(0, 100)}`);
|
|
178
178
|
// 发布到消息总线(用于 SSE 推送)
|
|
179
179
|
publishWecomMessage({
|
|
180
180
|
robotName: this.robotName,
|
|
@@ -190,19 +190,19 @@ class WecomClient extends EventEmitter {
|
|
|
190
190
|
}
|
|
191
191
|
handleApprovalResponse(frame) {
|
|
192
192
|
const event = frame.body?.event;
|
|
193
|
-
|
|
193
|
+
logger.log('wecom', `handleApprovalResponse body.event: ${JSON.stringify(event)}`);
|
|
194
194
|
if (!event) {
|
|
195
|
-
|
|
195
|
+
logger.log('wecom', `event 为空,frame.body: ${JSON.stringify(frame.body)}`);
|
|
196
196
|
return;
|
|
197
197
|
}
|
|
198
198
|
// task_id 和 event_key 在 event.template_card_event 内部
|
|
199
199
|
const cardEvent = event.template_card_event;
|
|
200
200
|
const taskId = cardEvent?.task_id;
|
|
201
201
|
const eventKey = cardEvent?.event_key; // 用户点击的按钮 key
|
|
202
|
-
|
|
202
|
+
logger.log('wecom', `taskId=${taskId}, eventKey=${eventKey}, approvals keys: ${[...this.approvals.keys()].join(',')}`);
|
|
203
203
|
if (!taskId)
|
|
204
204
|
return;
|
|
205
|
-
|
|
205
|
+
logger.log('wecom', `收到审批响应: taskId=${taskId}, key=${eventKey}`);
|
|
206
206
|
const approval = this.approvals.get(taskId);
|
|
207
207
|
if (approval && !approval.resolved) {
|
|
208
208
|
approval.resolved = true;
|
|
@@ -217,20 +217,20 @@ class WecomClient extends EventEmitter {
|
|
|
217
217
|
const descInfo = approval.description ? `\n\n> ${approval.description}` : '';
|
|
218
218
|
const content = `**审批结果**${toolInfo}\n\n${resultText}${descInfo}`;
|
|
219
219
|
this.sendText(content).catch(err => {
|
|
220
|
-
|
|
220
|
+
logger.error('wecom', `发送审批确认失败: ${err}`);
|
|
221
221
|
});
|
|
222
222
|
}
|
|
223
223
|
else if (approval && approval.resolved) {
|
|
224
|
-
|
|
224
|
+
logger.log('wecom', `审批已解决,跳过点击: ${taskId}, resolved=${approval.resolved}, result=${approval.result}`);
|
|
225
225
|
}
|
|
226
226
|
else {
|
|
227
|
-
|
|
227
|
+
logger.log('wecom', `审批记录不存在: ${taskId}`);
|
|
228
228
|
}
|
|
229
229
|
}
|
|
230
230
|
// 使用 reply 方法回复审批结果(会有引用效果)
|
|
231
231
|
async replyApprovalResult(frame, content) {
|
|
232
232
|
if (!this.connected) {
|
|
233
|
-
|
|
233
|
+
logger.log('wecom', '未连接,无法回复');
|
|
234
234
|
return;
|
|
235
235
|
}
|
|
236
236
|
try {
|
|
@@ -238,10 +238,10 @@ class WecomClient extends EventEmitter {
|
|
|
238
238
|
msgtype: 'markdown',
|
|
239
239
|
markdown: { content },
|
|
240
240
|
});
|
|
241
|
-
|
|
241
|
+
logger.log('wecom', '已回复审批结果');
|
|
242
242
|
}
|
|
243
243
|
catch (err) {
|
|
244
|
-
|
|
244
|
+
logger.error('wecom', `回复失败: ${err}`);
|
|
245
245
|
}
|
|
246
246
|
}
|
|
247
247
|
// 连接
|
|
@@ -272,12 +272,12 @@ class WecomClient extends EventEmitter {
|
|
|
272
272
|
msgtype: 'markdown',
|
|
273
273
|
markdown: { content: '【系统消息】机器人配置验证成功,此消息可忽略。' },
|
|
274
274
|
});
|
|
275
|
-
|
|
275
|
+
logger.log('wecom', `用户验证成功: ${targetId}`);
|
|
276
276
|
return { valid: true };
|
|
277
277
|
}
|
|
278
278
|
catch (err) {
|
|
279
279
|
const errorMsg = err.message || String(err);
|
|
280
|
-
|
|
280
|
+
logger.error('wecom', `用户验证失败: ${errorMsg}`);
|
|
281
281
|
// 解析错误类型
|
|
282
282
|
if (errorMsg.includes('93006') || errorMsg.includes('invalid chatid')) {
|
|
283
283
|
return { valid: false, error: '用户 ID 格式无效,请使用企业微信通讯录中的"账号"字段(通常是拼音格式,如 liuyang),不是中文名称' };
|
|
@@ -299,7 +299,7 @@ class WecomClient extends EventEmitter {
|
|
|
299
299
|
const userId = targetUser || this.targetUserId;
|
|
300
300
|
// 断线时将消息加入队列,等待重连后发送
|
|
301
301
|
if (!this.connected) {
|
|
302
|
-
|
|
302
|
+
logger.log('wecom', '未连接,消息已加入队列');
|
|
303
303
|
this.pendingMessages.push({
|
|
304
304
|
type: 'text',
|
|
305
305
|
content,
|
|
@@ -313,11 +313,11 @@ class WecomClient extends EventEmitter {
|
|
|
313
313
|
msgtype: 'markdown',
|
|
314
314
|
markdown: { content },
|
|
315
315
|
});
|
|
316
|
-
|
|
316
|
+
logger.log('wecom', `已发送消息到 ${userId}`);
|
|
317
317
|
return true;
|
|
318
318
|
}
|
|
319
319
|
catch (err) {
|
|
320
|
-
|
|
320
|
+
logger.error('wecom', `发送失败: ${err}`);
|
|
321
321
|
// 发送失败也加入队列
|
|
322
322
|
this.pendingMessages.push({
|
|
323
323
|
type: 'text',
|
|
@@ -340,7 +340,7 @@ class WecomClient extends EventEmitter {
|
|
|
340
340
|
const operationHash = hashOperation(ccId ?? '', toolName, toolInput);
|
|
341
341
|
const existing = this.findApprovalByHash(operationHash);
|
|
342
342
|
if (existing) {
|
|
343
|
-
|
|
343
|
+
logger.log(`[wecom] 复用已有审批: ${existing.taskId} (hash: ${operationHash.slice(0, 8)}...)`);
|
|
344
344
|
return existing.taskId;
|
|
345
345
|
}
|
|
346
346
|
}
|
|
@@ -358,7 +358,7 @@ class WecomClient extends EventEmitter {
|
|
|
358
358
|
});
|
|
359
359
|
// 断线时将审批请求加入队列,等待重连后发送
|
|
360
360
|
if (!this.connected) {
|
|
361
|
-
|
|
361
|
+
logger.log('[wecom] 未连接,审批请求已加入队列');
|
|
362
362
|
this.pendingMessages.push({
|
|
363
363
|
type: 'approval',
|
|
364
364
|
content: { title, description, requestId, targetUser: userId, taskId },
|
|
@@ -383,7 +383,7 @@ class WecomClient extends EventEmitter {
|
|
|
383
383
|
task_id: taskId,
|
|
384
384
|
},
|
|
385
385
|
});
|
|
386
|
-
|
|
386
|
+
logger.log(`[wecom] 已发送审批请求到 ${userId}: ${taskId}`);
|
|
387
387
|
return taskId;
|
|
388
388
|
}
|
|
389
389
|
// 发送排队的审批请求(使用已存在的 taskId)
|
|
@@ -391,11 +391,11 @@ class WecomClient extends EventEmitter {
|
|
|
391
391
|
// 检查审批是否已解决
|
|
392
392
|
const approval = this.approvals.get(taskId);
|
|
393
393
|
if (!approval) {
|
|
394
|
-
|
|
394
|
+
logger.log(`[wecom] 审批记录不存在: ${taskId}`);
|
|
395
395
|
return false;
|
|
396
396
|
}
|
|
397
397
|
if (approval.resolved) {
|
|
398
|
-
|
|
398
|
+
logger.log(`[wecom] 审批已解决,跳过发送: ${taskId}`);
|
|
399
399
|
return false;
|
|
400
400
|
}
|
|
401
401
|
const userId = targetUser || this.targetUserId;
|
|
@@ -414,7 +414,7 @@ class WecomClient extends EventEmitter {
|
|
|
414
414
|
task_id: taskId,
|
|
415
415
|
},
|
|
416
416
|
});
|
|
417
|
-
|
|
417
|
+
logger.log(`[wecom] 已发送排队审批请求到 ${userId}: ${taskId}`);
|
|
418
418
|
return true;
|
|
419
419
|
}
|
|
420
420
|
// 获取审批结果(非阻塞,立即返回当前状态)
|
|
@@ -432,11 +432,11 @@ class WecomClient extends EventEmitter {
|
|
|
432
432
|
setApprovalResult(taskId, result, reason) {
|
|
433
433
|
const approval = this.approvals.get(taskId);
|
|
434
434
|
if (!approval) {
|
|
435
|
-
|
|
435
|
+
logger.log(`[wecom] 设置审批结果失败:记录不存在 ${taskId}`);
|
|
436
436
|
return false;
|
|
437
437
|
}
|
|
438
438
|
if (approval.resolved) {
|
|
439
|
-
|
|
439
|
+
logger.log(`[wecom] 设置审批结果失败:已解决 ${taskId}`);
|
|
440
440
|
return false;
|
|
441
441
|
}
|
|
442
442
|
approval.resolved = true;
|
|
@@ -449,9 +449,9 @@ class WecomClient extends EventEmitter {
|
|
|
449
449
|
const toolInfo = approval.toolName ? `: ${approval.toolName}` : '';
|
|
450
450
|
const descInfo = approval.description ? `\n\n> ${approval.description}` : '';
|
|
451
451
|
this.sendText(`**审批结果(超时自动决策)**${toolInfo}\n\n${resultText}${reasonText}${descInfo}`).catch(err => {
|
|
452
|
-
|
|
452
|
+
logger.error('[wecom] 发送审批确认失败:', err);
|
|
453
453
|
});
|
|
454
|
-
|
|
454
|
+
logger.log(`[wecom] 超时自动决策已设置: ${taskId} → ${result}`);
|
|
455
455
|
return true;
|
|
456
456
|
}
|
|
457
457
|
// 获取所有待处理的审批任务 ID(供 hook 轮询使用)
|
|
@@ -542,7 +542,7 @@ class WecomClient extends EventEmitter {
|
|
|
542
542
|
toolName: partial.toolName,
|
|
543
543
|
toolInput: partial.toolInput,
|
|
544
544
|
});
|
|
545
|
-
|
|
545
|
+
logger.log(`[wecom] 注入恢复审批记录: ${taskId}`);
|
|
546
546
|
}
|
|
547
547
|
// 清理过期消息
|
|
548
548
|
cleanupMessages(maxAgeMs = 300000) {
|
|
@@ -560,7 +560,7 @@ class WecomClient extends EventEmitter {
|
|
|
560
560
|
if (this.pendingMessages.length === 0) {
|
|
561
561
|
return;
|
|
562
562
|
}
|
|
563
|
-
|
|
563
|
+
logger.log(`[wecom] 刷新待发送消息队列: ${this.pendingMessages.length} 条`);
|
|
564
564
|
while (this.pendingMessages.length > 0 && this.connected) {
|
|
565
565
|
const msg = this.pendingMessages.shift();
|
|
566
566
|
if (!msg)
|
|
@@ -591,12 +591,12 @@ class WecomClient extends EventEmitter {
|
|
|
591
591
|
task_id: taskId,
|
|
592
592
|
},
|
|
593
593
|
});
|
|
594
|
-
|
|
594
|
+
logger.log(`[wecom] 重发审批请求: ${taskId}`);
|
|
595
595
|
}
|
|
596
|
-
|
|
596
|
+
logger.log(`[wecom] 重发消息成功: ${msg.type}`);
|
|
597
597
|
}
|
|
598
598
|
catch (err) {
|
|
599
|
-
|
|
599
|
+
logger.error(`[wecom] 重发消息失败: ${err}`);
|
|
600
600
|
}
|
|
601
601
|
}
|
|
602
602
|
}
|