@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.
@@ -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
+ });
@@ -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
- console.log(`[client-pool] 已创建 client: ${projectDir}`);
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
- console.log(`[client-pool] 已移除 client: ${projectDir}`);
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
- console.log('[client-pool] 已清空所有 client');
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
- console.error('[wecom] 发送恢复通知失败:', err);
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
- console.error('[wecom] 发送断线通知失败:', err);
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
- console.log('');
97
- console.log(' ⚠️ 机器人未授权或配置有误,请检查以下事项:');
98
- console.log('');
99
- console.log(' 1. 新建机器人需要等待约 2 分钟同步时间,请稍后再试');
100
- console.log(' 2. 确认 Bot ID 和 Secret 是否正确');
101
- console.log(' 3. 完成机器人授权(任选其一):');
102
- console.log(' • 在电脑端企业微信APP中打开:机器人详情 → 可使用权限 → 授权');
103
- console.log(' • 打开浏览器访问以下地址,使用手机企业微信扫码授权:');
104
- console.log(` ${this.getAuthUrl()}`);
105
- console.log('');
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
- console.log('[wecom] 收到 template_card_event 事件,完整 frame:');
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
- console.log('[wecom] 用户进入会话');
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
- console.log('[wecom] 收到消息帧:', JSON.stringify(body, null, 2).substring(0, 500));
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
- console.log('[wecom] 检测到引用内容:', quoteContent.substring(0, 100));
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
- console.warn(`[wecom] 消息队列已满,丢弃旧消息: ${dropped?.msgid}`);
173
+ logger.warn('wecom', `消息队列已满,丢弃旧消息: ${dropped?.msgid}`);
174
174
  }
175
175
  this.messages.push(msgRecord);
176
176
  const source = chattype === 'group' ? `群聊(${chatid})` : '单聊';
177
- console.log(`[wecom] 收到${source}消息: ${from_userid} -> ${content.slice(0, 100)}`);
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
- console.log('[wecom] handleApprovalResponse body.event:', JSON.stringify(event));
193
+ logger.log('wecom', `handleApprovalResponse body.event: ${JSON.stringify(event)}`);
194
194
  if (!event) {
195
- console.log('[wecom] event 为空,frame.body:', JSON.stringify(frame.body));
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
- console.log(`[wecom] taskId=${taskId}, eventKey=${eventKey}, approvals keys:`, [...this.approvals.keys()]);
202
+ logger.log('wecom', `taskId=${taskId}, eventKey=${eventKey}, approvals keys: ${[...this.approvals.keys()].join(',')}`);
203
203
  if (!taskId)
204
204
  return;
205
- console.log(`[wecom] 收到审批响应: taskId=${taskId}, key=${eventKey}`);
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
- console.error('[wecom] 发送审批确认失败:', err);
220
+ logger.error('wecom', `发送审批确认失败: ${err}`);
221
221
  });
222
222
  }
223
223
  else if (approval && approval.resolved) {
224
- console.log(`[wecom] 审批已解决,跳过点击: ${taskId}, resolved=${approval.resolved}, result=${approval.result}`);
224
+ logger.log('wecom', `审批已解决,跳过点击: ${taskId}, resolved=${approval.resolved}, result=${approval.result}`);
225
225
  }
226
226
  else {
227
- console.log(`[wecom] 审批记录不存在: ${taskId}`);
227
+ logger.log('wecom', `审批记录不存在: ${taskId}`);
228
228
  }
229
229
  }
230
230
  // 使用 reply 方法回复审批结果(会有引用效果)
231
231
  async replyApprovalResult(frame, content) {
232
232
  if (!this.connected) {
233
- console.log('[wecom] 未连接,无法回复');
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
- console.log(`[wecom] 已回复审批结果`);
241
+ logger.log('wecom', '已回复审批结果');
242
242
  }
243
243
  catch (err) {
244
- console.error(`[wecom] 回复失败: ${err}`);
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
- console.log(`[wecom] 用户验证成功: ${targetId}`);
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
- console.error(`[wecom] 用户验证失败: ${errorMsg}`);
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
- console.log('[wecom] 未连接,消息已加入队列');
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
- console.log(`[wecom] 已发送消息到 ${userId}`);
316
+ logger.log('wecom', `已发送消息到 ${userId}`);
317
317
  return true;
318
318
  }
319
319
  catch (err) {
320
- console.error(`[wecom] 发送失败: ${err}`);
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
- console.log(`[wecom] 复用已有审批: ${existing.taskId} (hash: ${operationHash.slice(0, 8)}...)`);
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
- console.log('[wecom] 未连接,审批请求已加入队列');
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
- console.log(`[wecom] 已发送审批请求到 ${userId}: ${taskId}`);
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
- console.log(`[wecom] 审批记录不存在: ${taskId}`);
394
+ logger.log(`[wecom] 审批记录不存在: ${taskId}`);
395
395
  return false;
396
396
  }
397
397
  if (approval.resolved) {
398
- console.log(`[wecom] 审批已解决,跳过发送: ${taskId}`);
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
- console.log(`[wecom] 已发送排队审批请求到 ${userId}: ${taskId}`);
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
- console.log(`[wecom] 设置审批结果失败:记录不存在 ${taskId}`);
435
+ logger.log(`[wecom] 设置审批结果失败:记录不存在 ${taskId}`);
436
436
  return false;
437
437
  }
438
438
  if (approval.resolved) {
439
- console.log(`[wecom] 设置审批结果失败:已解决 ${taskId}`);
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
- console.error('[wecom] 发送审批确认失败:', err);
452
+ logger.error('[wecom] 发送审批确认失败:', err);
453
453
  });
454
- console.log(`[wecom] 超时自动决策已设置: ${taskId} → ${result}`);
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
- console.log(`[wecom] 注入恢复审批记录: ${taskId}`);
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
- console.log(`[wecom] 刷新待发送消息队列: ${this.pendingMessages.length} 条`);
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
- console.log(`[wecom] 重发审批请求: ${taskId}`);
594
+ logger.log(`[wecom] 重发审批请求: ${taskId}`);
595
595
  }
596
- console.log(`[wecom] 重发消息成功: ${msg.type}`);
596
+ logger.log(`[wecom] 重发消息成功: ${msg.type}`);
597
597
  }
598
598
  catch (err) {
599
- console.error(`[wecom] 重发消息失败: ${err}`);
599
+ logger.error(`[wecom] 重发消息失败: ${err}`);
600
600
  }
601
601
  }
602
602
  }