@vrs-soft/wecom-aibot-mcp 2.4.21 → 2.4.23

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
@@ -94,13 +94,31 @@ npx @vrs-soft/wecom-aibot-mcp --http-only --start
94
94
  | `--add / --delete` | Add/delete bot |
95
95
  | `--set-token [token]` | Set Auth Token (for remote deployment) |
96
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) |
97
+ | `--debug` | Start in foreground with debug-level logging (writes to `server.log` + stdout) |
98
+ | `--http-only` | Deprecated; identical to `--start` |
99
99
  | `--channel-only` | Configure Channel MCP only (requires `MCP_URL`) |
100
100
  | `--clean-cache` | Clear CC registry cache |
101
101
  | `--upgrade` | Force upgrade global configs |
102
102
  | `--uninstall` | Complete uninstall |
103
103
 
104
+ ## Logs
105
+
106
+ All logs are written to `~/.wecom-aibot-mcp/` as JSON Lines (one JSON object per line):
107
+
108
+ | File | Producer | Levels | Rotation |
109
+ |------|----------|--------|----------|
110
+ | `server.log` | `--start` / `--debug` daemon | `info` always, `debug` when `--debug` | 10MB × 5 |
111
+ | `channel.log` | `--channel` MCP proxy (per agent) | `info` always, `debug` when debug marker exists | 10MB × 5 |
112
+ | `connection.log` | WebSocket layer (connect/disconnect/auth) | n/a | append-only |
113
+ | `debug` (marker file) | `--debug` creates it; channel-server reads it | — | — |
114
+
115
+ Quick inspection:
116
+
117
+ ```bash
118
+ tail -f ~/.wecom-aibot-mcp/server.log | jq .
119
+ grep -h '"level":"error"' ~/.wecom-aibot-mcp/server.log*
120
+ ```
121
+
104
122
  ## Run Modes
105
123
 
106
124
  | | Channel Mode | HTTP Mode |
package/dist/bin.js CHANGED
@@ -42,11 +42,11 @@ function showHelp() {
42
42
  --setup --server --channel 本地完整安装(HTTP + Channel)
43
43
  --upgrade 强制升级全局配置(覆盖 MCP 配置、权限、skill)
44
44
  --reinstall 重新安装全局配置(删除后重新写入,保留机器人配置)
45
- --start 启动 MCP Server(后台服务模式)
45
+ --start 启动 HTTP MCP Server(后台守护进程,日志写 server.log)
46
46
  --stop 停止 MCP Server
47
- --debug 前台启动 MCP Server(日志直接输出到终端,用于调试)
48
- --channel 启动 Channel MCP Proxy(stdio 代理 + SSE 唤醒)
49
- --http-only 仅启动 HTTP Server(远程部署场景,不安装 Channel MCP 配置)
47
+ --debug 前台启动 + debug 级日志(日志同时落 server.log,stdout 实时打印)
48
+ --channel 启动 Channel MCP Proxy(stdio 代理 + SSE 唤醒,日志写 channel.log)
49
+ --http-only 仅写 HTTP-only 配置(已废弃;--start 默认就是 daemon-only 行为)
50
50
  --channel-only 仅配置 Channel MCP(本地连接远程 HTTP Server)
51
51
  --status 显示服务状态和机器人配置
52
52
  --config 重新配置默认机器人(修改 Bot ID / Secret / 目标用户)
@@ -70,8 +70,8 @@ function showHelp() {
70
70
  拆分部署(远程 HTTP + 本地 Channel):
71
71
 
72
72
  远程服务器:
73
- npx @vrs-soft/wecom-aibot-mcp --http-only --start
74
- # 只启动 HTTP Server,不写入本地 MCP 配置
73
+ npx @vrs-soft/wecom-aibot-mcp --start
74
+ # 启动 HTTP Server(daemon 不会写本地 client MCP 配置)
75
75
 
76
76
  本地机器:
77
77
  MCP_URL=http://远程IP:18963 npx @vrs-soft/wecom-aibot-mcp --channel-only
@@ -96,6 +96,24 @@ MCP 配置(默认安装同时配置两种模式):
96
96
  启动 Channel 模式(研究预览):
97
97
  claude --dangerously-load-development-channels server:wecom-aibot-channel
98
98
 
99
+ 日志:
100
+
101
+ Daemon (HTTP MCP):
102
+ ~/.wecom-aibot-mcp/server.log info 级(永久)+ debug 级(仅 --debug 时写)
103
+ ~/.wecom-aibot-mcp/server.log.1..5 自动滚动备份(每份 ≤10MB,共保留 5 份)
104
+
105
+ Channel MCP:
106
+ ~/.wecom-aibot-mcp/channel.log info 级(永久)+ debug 级(仅 debug 模式时写)
107
+ ~/.wecom-aibot-mcp/channel.log.1..5 自动滚动备份
108
+
109
+ WebSocket 连接事件:
110
+ ~/.wecom-aibot-mcp/connection.log 专用文件(connect/disconnect/auth 事件)
111
+
112
+ Debug 标记:
113
+ ~/.wecom-aibot-mcp/debug 存在则启用 debug 级输出(--debug 时自动创建)
114
+
115
+ 日志格式: JSON Lines,每行一个 {ts, level, msg, data?},方便 jq/grep 检索
116
+
99
117
  更多信息: https://github.com/eric2877/wecom-aibot-mcp
100
118
  `);
101
119
  }
@@ -271,6 +289,10 @@ async function startMcpServerForeground(isDebug = false) {
271
289
  fs.writeFileSync(debugFile, 'true');
272
290
  console.log('[mcp] Debug 标记文件已创建');
273
291
  }
292
+ // 配置统一日志输出到文件(JSON Lines + 自动滚动)
293
+ logger.setLogFile(path.join(os.homedir(), '.wecom-aibot-mcp', 'server.log'));
294
+ if (isDebug)
295
+ logger.setDebug(true);
274
296
  // 确保 hook 已安装
275
297
  ensureHookInstalled();
276
298
  // 加载统计并清理旧日志
@@ -299,9 +321,12 @@ async function startMcpServerForeground(isDebug = false) {
299
321
  logger.log(`[mcp] 健康检查: http://127.0.0.1:${HTTP_PORT}/health`);
300
322
  logger.log(`[mcp] 微信模式:enter_headless_mode 时建立连接`);
301
323
  logger.log(`[mcp] PID: ${process.pid}`);
324
+ // 写入启动事件到 server.log(永久记录)
325
+ logger.info('daemon started', { version: VERSION, port: HTTP_PORT, protocol, pid: process.pid, debug: isDebug });
302
326
  // 退出处理
303
327
  const gracefulShutdown = () => {
304
328
  console.log('[mcp] 正在关闭...');
329
+ logger.info('daemon shutdown', { pid: process.pid });
305
330
  stopKeepaliveMonitor();
306
331
  stopHttpServer();
307
332
  if (fs.existsSync(PID_FILE)) {
@@ -605,7 +630,7 @@ async function main() {
605
630
  console.log(`[setup] 如需启用 HTTPS,配置证书后重新运行 --setup --server`);
606
631
  }
607
632
  console.log('\n[setup] Server 配置完成!');
608
- console.log(' 启动: npx @vrs-soft/wecom-aibot-mcp --http-only --start');
633
+ console.log(' 启动: npx @vrs-soft/wecom-aibot-mcp --start');
609
634
  console.log('\n[setup] ─── 步骤 2/2:配置企业微信机器人 ───\n');
610
635
  await addMcpConfig();
611
636
  }
@@ -675,6 +700,10 @@ async function main() {
675
700
  fs.writeFileSync(debugFile, 'true');
676
701
  }
677
702
  }
703
+ // 配置统一日志输出到文件(每个 channel-server 进程都写同一份 channel.log,多进程并发追加)
704
+ logger.setLogFile(path.join(os.homedir(), '.wecom-aibot-mcp', 'channel.log'));
705
+ if (isDebug)
706
+ logger.setDebug(true);
678
707
  console.log('[channel] Starting Channel MCP Proxy...');
679
708
  const { startChannelServer } = await import('./channel-server.js');
680
709
  await startChannelServer();
@@ -691,7 +720,7 @@ async function main() {
691
720
  if (args.includes('--http-only') && !args.includes('--start')) {
692
721
  console.log('[mcp] HTTP-only 模式:仅启动 HTTP Server');
693
722
  console.log('[mcp] 不写入 MCP 配置(远程部署场景)');
694
- console.log('[mcp] 使用 --http-only --start 启动服务');
723
+ console.log('[mcp] 使用 --start 启动服务');
695
724
  process.exit(0);
696
725
  }
697
726
  // --channel-only:仅配置 Channel MCP(本地连接远程 HTTP Server)
@@ -12,11 +12,9 @@
12
12
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
13
13
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
14
14
  import { z } from 'zod';
15
- import * as fs from 'fs';
16
- import * as path from 'path';
17
- import * as os from 'os';
18
15
  import { VERSION, installSkill } from './config-wizard.js';
19
16
  import { addPermissionHook, registerActiveProject, unregisterActiveProject, updateWechatModeConfig } from './project-config.js';
17
+ import { logger } from './logger.js';
20
18
  const MCP_URL = process.env.MCP_URL || 'http://127.0.0.1:18963';
21
19
  const MCP_AUTH_TOKEN = process.env.MCP_AUTH_TOKEN;
22
20
  // 构建带 auth 的 fetch headers
@@ -28,22 +26,14 @@ function getAuthHeaders() {
28
26
  return headers;
29
27
  }
30
28
  // Channel 日志文件
31
- const CHANNEL_LOG_FILE = path.join(os.homedir(), '.wecom-aibot-mcp', 'channel.log');
32
29
  /**
33
30
  * 写入 Channel 日志
31
+ *
32
+ * 默认走 logger.debug(仅 --debug 时落盘)。关键事件请直接调用 logger.info(),
33
+ * 它们会永久落盘到 ~/.wecom-aibot-mcp/channel.log(自动滚动)。
34
34
  */
35
35
  function logChannel(message, data) {
36
- const timestamp = new Date().toISOString();
37
- const logLine = `[${timestamp}] ${message}${data ? ` | ${JSON.stringify(data)}` : ''}\n`;
38
- // 写入日志文件
39
- try {
40
- fs.appendFileSync(CHANNEL_LOG_FILE, logLine);
41
- }
42
- catch (err) {
43
- console.error(`[channel] 日志写入失败: ${err}`);
44
- }
45
- // 同时输出到 stderr
46
- console.error(`[channel] ${message}${data ? ` | ${JSON.stringify(data).slice(0, 200)}` : ''}`);
36
+ logger.debug(message, data);
47
37
  }
48
38
  // SSE 连接状态
49
39
  let sseConnected = false;
@@ -192,7 +182,7 @@ function connectSSE(ccId) {
192
182
  sseCurrentCcId = ccId;
193
183
  // SSE URL 添加 ccId 查询参数用于授权验证
194
184
  const sseUrl = ccId ? `${MCP_URL}/sse/${ccId}?ccId=${ccId}` : `${MCP_URL}/sse`;
195
- logChannel('Connecting to SSE', { url: sseUrl, ccId, mcpServerReady: mcpServer ? 'yes' : 'no' });
185
+ logger.info('Connecting to SSE', { url: sseUrl, ccId, mcpServerReady: mcpServer ? 'yes' : 'no' });
196
186
  sseAbortController = new AbortController();
197
187
  // SSE fetch 配置:添加 keep-alive headers 确保连接稳定
198
188
  fetch(sseUrl, {
@@ -228,7 +218,7 @@ function connectSSE(ccId) {
228
218
  }
229
219
  return;
230
220
  }
231
- logChannel('SSE connected, waiting for messages', { status: res.status });
221
+ logger.info('SSE connected', { ccId, status: res.status });
232
222
  const reader = res.body?.getReader();
233
223
  if (!reader) {
234
224
  logChannel('No response body');
@@ -250,7 +240,7 @@ function connectSSE(ccId) {
250
240
  sseConnected = false;
251
241
  // 非主动断开时自动重连
252
242
  if (!sseAbortController?.signal.aborted) {
253
- logChannel('SSE 断线,3 秒后重连', { ccId });
243
+ logger.info('SSE 断线,3 秒后重连', { ccId });
254
244
  setTimeout(() => { httpSessionId = null; connectSSE(ccId); }, 3000);
255
245
  }
256
246
  break;
@@ -321,11 +311,11 @@ function connectSSE(ccId) {
321
311
  }
322
312
  clearInterval(heartbeatInterval);
323
313
  }).catch((err) => {
324
- logChannel('SSE error', { error: String(err) });
314
+ logger.error('SSE error', { error: String(err) });
325
315
  sseConnected = false;
326
316
  // 非主动断开时自动重连
327
317
  if (!sseAbortController?.signal.aborted) {
328
- logChannel('SSE 出错,3 秒后重连', { ccId });
318
+ logger.info('SSE 出错,3 秒后重连', { ccId });
329
319
  setTimeout(() => { httpSessionId = null; connectSSE(ccId); }, 3000);
330
320
  }
331
321
  });
@@ -438,7 +428,7 @@ function registerChannelTools(server) {
438
428
  try {
439
429
  const parsed = JSON.parse(content[0].text);
440
430
  if (parsed.ccId) {
441
- logChannel('Got ccId, connecting SSE', { ccId: parsed.ccId, mode });
431
+ logger.info('Got ccId, connecting SSE', { ccId: parsed.ccId, mode });
442
432
  // 保存连接参数供重连复用
443
433
  sseRobotId = robot_id || parsed.robotName;
444
434
  sseProjectDir = project_dir || process.cwd();
@@ -446,10 +436,10 @@ function registerChannelTools(server) {
446
436
  // Channel 模式:在本地项目写入 PermissionRequest hook
447
437
  const localProjectDir = project_dir || process.cwd();
448
438
  const hookResult = addPermissionHook(localProjectDir);
449
- logChannel('本地 PermissionRequest hook 已写入', { path: hookResult.path, success: hookResult.success });
439
+ logger.info('本地 PermissionRequest hook 已写入', { path: hookResult.path, success: hookResult.success });
450
440
  // 注册本地 PID → projectDir(供本地 permission-hook.sh 通过进程树匹配项目)
451
441
  registerActiveProject(process.ppid ?? process.pid, localProjectDir);
452
- logChannel('本地 active-projects 已注册', { pid: process.ppid ?? process.pid, projectDir: localProjectDir });
442
+ logger.info('本地 active-projects 已注册', { pid: process.ppid ?? process.pid, projectDir: localProjectDir });
453
443
  // 写入本地 wecom-aibot.json(远程 HTTP MCP 写在远端 fs,agent 本地需要自己落地)
454
444
  updateWechatModeConfig(localProjectDir, {
455
445
  wechatMode: true,
@@ -458,10 +448,10 @@ function registerChannelTools(server) {
458
448
  mode: parsed.mode || mode,
459
449
  autoApproveTimeout: auto_approve_timeout,
460
450
  });
461
- logChannel('本地 wecom-aibot.json 已写入', { projectDir: localProjectDir, robotName: parsed.robotName, ccId: parsed.ccId });
451
+ logger.info('本地 wecom-aibot.json 已写入', { projectDir: localProjectDir, robotName: parsed.robotName, ccId: parsed.ccId });
462
452
  // 安装 skill 到本地(同上)
463
453
  const skillResult = installSkill(localProjectDir);
464
- logChannel('本地 skill 安装', { success: skillResult.success, skillUrl: skillResult.skillUrl });
454
+ logger.info('本地 skill 安装', { success: skillResult.success, skillUrl: skillResult.skillUrl });
465
455
  // Channel 模式:过滤 heartbeat 信息,简化消息
466
456
  if (mode === 'channel' || parsed.mode === 'channel') {
467
457
  delete parsed.heartbeat; // Channel 模式不需要 heartbeat loop
@@ -492,7 +482,7 @@ function registerChannelTools(server) {
492
482
  sseAbortController = null;
493
483
  sseConnected = false;
494
484
  sseCurrentCcId = undefined;
495
- logChannel('SSE disconnected', { cc_id });
485
+ logger.info('SSE disconnected', { cc_id });
496
486
  }
497
487
  // 注销本地 active-projects 记录
498
488
  unregisterActiveProject(localProjectDir);
@@ -1111,7 +1111,8 @@ export function ensureGlobalConfigs(mode = 'full', remoteOptions) {
1111
1111
  fs.writeFileSync(VERSION_FILE, JSON.stringify({ version: VERSION, installedAt: Date.now() }, null, 2));
1112
1112
  return { upgraded, previousVersion };
1113
1113
  }
1114
- // remote-channel 模式:写入远程 HTTP MCP(带 token)+ Channel MCP
1114
+ // remote-channel 模式:远程部署的 Channel 客户端——只写 Channel MCP,不写 HTTP MCP
1115
+ // (HTTP MCP daemon 在远端,本地不需要 HTTP transport client config)
1115
1116
  if (mode === 'remote-channel') {
1116
1117
  if (!remoteOptions?.url) {
1117
1118
  console.log('[config] ❌ 远程模式需要提供 URL');
@@ -1123,13 +1124,7 @@ export function ensureGlobalConfigs(mode = 'full', remoteOptions) {
1123
1124
  }
1124
1125
  if (!claudeConfig.mcpServers)
1125
1126
  claudeConfig.mcpServers = {};
1126
- // HTTP MCP 配置(带 token,可选)
1127
- const mcpEndpointUrl = remoteOptions.url.replace(/\/+$/, '') + '/mcp';
1128
- const httpMcpConfig = { type: 'http', url: mcpEndpointUrl };
1129
- if (remoteOptions.token)
1130
- httpMcpConfig.headers = { Authorization: `Bearer ${remoteOptions.token}` };
1131
- claudeConfig.mcpServers['wecom-aibot'] = httpMcpConfig;
1132
- // Channel MCP 配置(带 MCP_URL + MCP_AUTH_TOKEN)
1127
+ // 只写 Channel MCP 配置(带 MCP_URL + MCP_AUTH_TOKEN),HTTP MCP 由远端 daemon 提供,本地无需 client 配置
1133
1128
  const channelEnvRemote = { MCP_URL: remoteOptions.url.replace(/\/+$/, '') };
1134
1129
  if (remoteOptions.token)
1135
1130
  channelEnvRemote.MCP_AUTH_TOKEN = remoteOptions.token;
@@ -1138,8 +1133,13 @@ export function ensureGlobalConfigs(mode = 'full', remoteOptions) {
1138
1133
  args: ['-y', '@vrs-soft/wecom-aibot-mcp', '--channel'],
1139
1134
  env: channelEnvRemote,
1140
1135
  };
1136
+ // 移除可能残留的 HTTP MCP client 配置(远程模式 HTTP/Channel 完全分离)
1137
+ if (claudeConfig.mcpServers['wecom-aibot']) {
1138
+ delete claudeConfig.mcpServers['wecom-aibot'];
1139
+ console.log('[config] 已移除残留的 HTTP MCP client 配置(远程模式只用 Channel)');
1140
+ }
1141
1141
  fs.writeFileSync(CLAUDE_CONFIG_FILE, JSON.stringify(claudeConfig, null, 2));
1142
- console.log('[config] remote-channel 模式:已写入 HTTP MCP + Channel MCP 配置(带 Token)');
1142
+ console.log('[config] remote-channel 模式:仅写入 Channel MCP 配置');
1143
1143
  // Channel 模式需要权限配置
1144
1144
  writeMcpPermissions();
1145
1145
  console.log('[config] 已写入权限配置到 ~/.claude/settings.local.json');
@@ -1235,19 +1235,20 @@ export async function runRemoteInstallWizard() {
1235
1235
  // Server 不写入 ~/.claude.json,只提示启动命令
1236
1236
  console.log('\n─────────────────────────────────────');
1237
1237
  console.log('Server 安装完成!');
1238
- console.log(' 启动命令: npx @anthropic/wecom-aibot-mcp --http-only --start');
1238
+ console.log(' 启动命令: npx @vrs-soft/wecom-aibot-mcp --start');
1239
1239
  console.log(' 或者: npm run start:http');
1240
1240
  console.log('─────────────────────────────────────\n');
1241
1241
  console.log('[config] Client 端请在其他机器运行安装程序连接本服务器\n');
1242
1242
  return 'server';
1243
1243
  }
1244
1244
  // Client 安装模式:本机有 ~/.claude.json,作为客户端
1245
+ // 远程模式 = HTTP/Channel 完全分离:本地只装 Client 配置,daemon 在远端
1245
1246
  console.log('\n检测到本机有 ~/.claude.json → Client 安装模式\n');
1246
1247
  console.log(' 请选择连接远程服务器的方式:\n');
1247
- console.log(' 1. HTTP MCP(轮询模式)');
1248
- console.log(' 2. HTTP MCP + Channel MCP(推荐,消息自动唤醒)\n');
1248
+ console.log(' 1. Channel MCP(推荐:SSE 自动推送,消息到达立即唤醒 agent)');
1249
+ console.log(' 2. HTTP MCP(轮询模式,兼容不支持 Channel 的 Claude Code)\n');
1249
1250
  const choice = await question(rl, '请选择 (1/2): ');
1250
- const mode = choice === '2' ? 'remote-channel' : 'remote';
1251
+ const mode = choice === '2' ? 'remote' : 'remote-channel';
1251
1252
  let serverUrl = await question(rl, '远程服务器地址(如 https://your-server:18963): ');
1252
1253
  while (!serverUrl) {
1253
1254
  console.log('服务器地址不能为空');
@@ -1264,12 +1265,13 @@ export async function runRemoteInstallWizard() {
1264
1265
  ensureGlobalConfigs(mode, { url: serverUrl, token });
1265
1266
  console.log('\n─────────────────────────────────────');
1266
1267
  console.log('Client 配置完成!');
1267
- console.log(` 模式: ${mode === 'remote-channel' ? 'HTTP + Channel' : ' HTTP'}`);
1268
+ console.log(` 模式: ${mode === 'remote-channel' ? 'Channel(仅 Channel MCP)' : 'HTTP(仅 HTTP MCP)'}`);
1268
1269
  console.log(` 服务器: ${serverUrl}`);
1269
1270
  console.log(` Auth Token: ${token.slice(0, 8)}...${token.slice(-4)}`);
1270
1271
  console.log('─────────────────────────────────────\n');
1271
1272
  if (mode === 'remote-channel') {
1272
- console.log('Channel 模式优势:微信消息自动唤醒 agent,无需主动轮询');
1273
+ console.log('Channel 模式优势:微信消息通过 SSE 自动唤醒 agent,无需主动轮询');
1274
+ console.log('启动方式:claude --dangerously-load-development-channels server:wecom-aibot-channel');
1273
1275
  }
1274
1276
  console.log('[config] 请重启 Claude Code 以加载最新配置\n');
1275
1277
  return mode;
@@ -113,7 +113,7 @@ export async function connectRobot(robotName, agentName) {
113
113
  agentName,
114
114
  };
115
115
  connectionPool.set(robot.name, state);
116
- logger.log(`[connection] 已连接机器人: ${robot.name}`);
116
+ logger.info('robot connected', { robotName: robot.name });
117
117
  return {
118
118
  success: true,
119
119
  client,
@@ -127,7 +127,7 @@ export function disconnectRobot(robotName) {
127
127
  if (state) {
128
128
  state.client.disconnect();
129
129
  connectionPool.delete(robotName);
130
- logger.log(`[connection] 已断开机器人: ${robotName}`);
130
+ logger.info('robot disconnected', { robotName });
131
131
  }
132
132
  }
133
133
  /**
@@ -145,7 +145,7 @@ export async function getClient(robotName) {
145
145
  // 断开了,尝试重连
146
146
  const robot = await findRobotConfig(state.robotName);
147
147
  if (robot) {
148
- logger.log(`[connection] 重连机器人: ${robot.name}`);
148
+ logger.info('robot reconnecting', { robotName: robot.name });
149
149
  const oldClient = state.client;
150
150
  // 迁移旧 client 的未解决审批记录和待发送审批消息
151
151
  const pendingApprovals = oldClient.getUnresolvedApprovalMap();
@@ -165,11 +165,11 @@ export async function getClient(robotName) {
165
165
  state.client.connect();
166
166
  const connected = await waitForConnection(state.client, 5000);
167
167
  if (connected) {
168
- logger.log(`[connection] 重连成功: ${robot.name}`);
168
+ logger.info('robot reconnected', { robotName: robot.name });
169
169
  return state.client;
170
170
  }
171
171
  else {
172
- logger.log(`[connection] 重连失败: ${robot.name}`);
172
+ logger.error('robot reconnect failed', { robotName: robot.name });
173
173
  return null;
174
174
  }
175
175
  }
@@ -141,7 +141,7 @@ function cleanStaleCcIds() {
141
141
  for (const [id, entry] of ccIdRegistry) {
142
142
  if (now - entry.lastOnline > CCID_STALE_TIMEOUT) {
143
143
  ccIdRegistry.delete(id);
144
- logger.log(`[ccid] 清理超时条目: ${id} (离线 ${Math.round((now - entry.lastOnline) / 60000)} 分钟)`);
144
+ logger.info('ccId stale cleanup', { ccId: id, offlineMins: Math.round((now - entry.lastOnline) / 60000) });
145
145
  }
146
146
  }
147
147
  }
@@ -150,24 +150,24 @@ function cleanStaleCcIds() {
150
150
  export function registerCcId(ccId, robotName, agentName, mode, projectDir, isReconnect) {
151
151
  if (isReconnect || ccIdRegistry.has(ccId)) {
152
152
  // 重连:直接覆盖,更新 lastOnline
153
- logger.log(`[ccid] 重连: ${ccId} ${robotName}`);
153
+ logger.info('ccId reconnect', { ccId, robotName });
154
154
  }
155
155
  else {
156
156
  // 首次注册:先清理超时条目
157
157
  cleanStaleCcIds();
158
- logger.log(`[ccid] 注册: ${ccId} → ${robotName} (${agentName || 'unknown'}, mode: ${mode || 'http'})`);
158
+ logger.info('ccId registered', { ccId, robotName, agentName: agentName || null, mode: mode || 'http' });
159
159
  }
160
160
  ccIdRegistry.set(ccId, { robotName, agentName, mode, projectDir, lastOnline: Date.now() });
161
161
  return { success: true, ccId };
162
162
  }
163
163
  export function unregisterCcId(ccId) {
164
164
  ccIdRegistry.delete(ccId);
165
- logger.log(`[ccid] 注销: ${ccId}`);
165
+ logger.info('ccId unregistered', { ccId });
166
166
  }
167
167
  export function clearCcIdRegistry() {
168
168
  const entries = Array.from(ccIdRegistry.keys());
169
169
  ccIdRegistry.clear();
170
- logger.log(`[ccid] 清空注册表: 共清理 ${entries.length} (${entries.join(', ')})`);
170
+ logger.info('ccId registry cleared', { count: entries.length, entries });
171
171
  return { cleared: entries.length, entries };
172
172
  }
173
173
  export function getRobotByCcId(ccId) {
package/dist/logger.d.ts CHANGED
@@ -1,51 +1,33 @@
1
1
  /**
2
2
  * 统一日志模块
3
3
  *
4
- * 通过 --debug 参数控制日志输出:
5
- * - debug 模式:所有日志输出到 stderr
6
- * - 正常模式:只输出必要信息(如错误、启动消息)
4
+ * 级别:error / info / debug
5
+ * - error:永远写文件 + stderr
6
+ * - info:永远写文件,--debug 时也写 stdout
7
+ * - debug:仅 --debug 模式写文件 + stdout
7
8
  *
8
- * Debug 标记文件:~/.wecom-aibot-mcp/debug
9
+ * Debug 标记:~/.wecom-aibot-mcp/debug 文件存在 → debug 模式
9
10
  *
10
- * 用法与 console 完全一致:
11
- * - logger.log() - debug 模式才输出
12
- * - logger.error() - 始终输出到 stderr
13
- * - logger.warn() - 始终输出到 stderr
14
- * - logger.info() - 始终输出到 stdout
15
- */
16
- /**
17
- * 检查是否处于 debug 模式
11
+ * 文件输出:JSON Lines 格式,自动按 10MB 滚动,保留最近 5 份
12
+ * <file> 当前
13
+ * <file>.1 上一份
14
+ * ...
15
+ * <file>.5 最旧
16
+ *
17
+ * 调用方在入口处通过 setLogFile() 指定写入文件:
18
+ * - daemon (`--start` / `--debug`): server.log
19
+ * - channel-server (`--channel`): channel.log
18
20
  */
19
21
  export declare function isDebugMode(): boolean;
20
- /**
21
- * 设置 debug 模式
22
- */
23
22
  export declare function setDebugMode(enabled: boolean): void;
24
- /**
25
- * 日志输出(仅 debug 模式)
26
- * 与 console.log 用法完全一致
27
- */
28
- export declare function log(...args: unknown[]): void;
29
- /**
30
- * 错误日志(始终输出到 stderr)
31
- * 与 console.error 用法完全一致
32
- */
33
- export declare function error(...args: unknown[]): void;
34
- /**
35
- * 信息日志(始终输出到 stdout)
36
- * 与 console.info 用法完全一致
37
- */
38
- export declare function info(...args: unknown[]): void;
39
- /**
40
- * 警告日志(始终输出到 stderr)
41
- * 与 console.warn 用法完全一致
42
- */
43
- export declare function warn(...args: unknown[]): void;
23
+ export declare function setLogFile(file: string): void;
44
24
  export declare const logger: {
45
- log: typeof log;
46
- error: typeof error;
47
- info: typeof info;
48
- warn: typeof warn;
25
+ info: (msg: string, data?: unknown) => void;
26
+ debug: (msg: string, data?: unknown) => void;
27
+ error: (msg: string, data?: unknown) => void;
28
+ log: (...args: unknown[]) => void;
29
+ warn: (...args: unknown[]) => void;
49
30
  isDebug: typeof isDebugMode;
50
31
  setDebug: typeof setDebugMode;
32
+ setLogFile: typeof setLogFile;
51
33
  };
package/dist/logger.js CHANGED
@@ -1,84 +1,114 @@
1
1
  /**
2
2
  * 统一日志模块
3
3
  *
4
- * 通过 --debug 参数控制日志输出:
5
- * - debug 模式:所有日志输出到 stderr
6
- * - 正常模式:只输出必要信息(如错误、启动消息)
4
+ * 级别:error / info / debug
5
+ * - error:永远写文件 + stderr
6
+ * - info:永远写文件,--debug 时也写 stdout
7
+ * - debug:仅 --debug 模式写文件 + stdout
7
8
  *
8
- * Debug 标记文件:~/.wecom-aibot-mcp/debug
9
+ * Debug 标记:~/.wecom-aibot-mcp/debug 文件存在 → debug 模式
9
10
  *
10
- * 用法与 console 完全一致:
11
- * - logger.log() - debug 模式才输出
12
- * - logger.error() - 始终输出到 stderr
13
- * - logger.warn() - 始终输出到 stderr
14
- * - logger.info() - 始终输出到 stdout
11
+ * 文件输出:JSON Lines 格式,自动按 10MB 滚动,保留最近 5 份
12
+ * <file> 当前
13
+ * <file>.1 上一份
14
+ * ...
15
+ * <file>.5 最旧
16
+ *
17
+ * 调用方在入口处通过 setLogFile() 指定写入文件:
18
+ * - daemon (`--start` / `--debug`): server.log
19
+ * - channel-server (`--channel`): channel.log
15
20
  */
16
21
  import * as fs from 'fs';
17
22
  import * as path from 'path';
18
23
  import * as os from 'os';
19
- const DEBUG_FILE = path.join(os.homedir(), '.wecom-aibot-mcp', 'debug');
24
+ const CONFIG_DIR = path.join(os.homedir(), '.wecom-aibot-mcp');
25
+ const DEBUG_FILE = path.join(CONFIG_DIR, 'debug');
26
+ const MAX_SIZE = 10 * 1024 * 1024; // 10 MB
27
+ const KEEP_FILES = 5;
28
+ let logFile = null;
20
29
  let debugMode = false;
21
- /**
22
- * 检查是否处于 debug 模式
23
- */
24
30
  export function isDebugMode() {
25
31
  if (debugMode)
26
32
  return true;
27
- // 检查标记文件
28
33
  if (fs.existsSync(DEBUG_FILE)) {
29
34
  debugMode = true;
30
35
  return true;
31
36
  }
32
37
  return false;
33
38
  }
34
- /**
35
- * 设置 debug 模式
36
- */
37
39
  export function setDebugMode(enabled) {
38
40
  debugMode = enabled;
39
41
  if (enabled && !fs.existsSync(DEBUG_FILE)) {
42
+ if (!fs.existsSync(CONFIG_DIR))
43
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
40
44
  fs.writeFileSync(DEBUG_FILE, 'true');
41
45
  }
42
46
  else if (!enabled && fs.existsSync(DEBUG_FILE)) {
43
47
  fs.unlinkSync(DEBUG_FILE);
44
48
  }
45
49
  }
46
- /**
47
- * 日志输出(仅 debug 模式)
48
- * console.log 用法完全一致
49
- */
50
- export function log(...args) {
51
- if (isDebugMode()) {
52
- console.log(...args);
53
- }
54
- }
55
- /**
56
- * 错误日志(始终输出到 stderr)
57
- * 与 console.error 用法完全一致
58
- */
59
- export function error(...args) {
60
- console.error(...args);
50
+ export function setLogFile(file) {
51
+ logFile = file;
52
+ const dir = path.dirname(file);
53
+ if (!fs.existsSync(dir))
54
+ fs.mkdirSync(dir, { recursive: true });
61
55
  }
62
- /**
63
- * 信息日志(始终输出到 stdout)
64
- * console.info 用法完全一致
65
- */
66
- export function info(...args) {
67
- console.info(...args);
56
+ function rotate(file) {
57
+ try {
58
+ for (let i = KEEP_FILES - 1; i >= 1; i--) {
59
+ const src = `${file}.${i}`;
60
+ const dst = `${file}.${i + 1}`;
61
+ if (fs.existsSync(src))
62
+ fs.renameSync(src, dst);
63
+ }
64
+ if (fs.existsSync(file))
65
+ fs.renameSync(file, `${file}.1`);
66
+ }
67
+ catch {
68
+ // 滚动失败忽略,避免阻塞日志写入
69
+ }
68
70
  }
69
- /**
70
- * 警告日志(始终输出到 stderr)
71
- * 与 console.warn 用法完全一致
72
- */
73
- export function warn(...args) {
74
- console.warn(...args);
71
+ function write(level, msg, data) {
72
+ if (level === 'debug' && !isDebugMode())
73
+ return;
74
+ const entry = JSON.stringify({
75
+ ts: new Date().toISOString(),
76
+ level,
77
+ msg,
78
+ ...(data !== undefined ? { data } : {}),
79
+ }) + '\n';
80
+ // 文件
81
+ if (logFile) {
82
+ try {
83
+ if (fs.existsSync(logFile) && fs.statSync(logFile).size >= MAX_SIZE) {
84
+ rotate(logFile);
85
+ }
86
+ fs.appendFileSync(logFile, entry);
87
+ }
88
+ catch {
89
+ // 写文件失败不抛
90
+ }
91
+ }
92
+ // 控制台
93
+ if (level === 'error') {
94
+ process.stderr.write(entry);
95
+ }
96
+ else if (debugMode) {
97
+ process.stdout.write(entry);
98
+ }
75
99
  }
76
- // 导出 logger 对象,用法与 console 完全一致
100
+ // 公共 API
77
101
  export const logger = {
78
- log,
79
- error,
80
- info,
81
- warn,
102
+ info: (msg, data) => write('info', msg, data),
103
+ debug: (msg, data) => write('debug', msg, data),
104
+ error: (msg, data) => write('error', msg, data),
105
+ // 兼容旧 API(散落在各处的调用)
106
+ log: (...args) => {
107
+ if (debugMode)
108
+ console.log(...args);
109
+ },
110
+ warn: (...args) => console.warn(...args),
82
111
  isDebug: isDebugMode,
83
112
  setDebug: setDebugMode,
113
+ setLogFile,
84
114
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vrs-soft/wecom-aibot-mcp",
3
- "version": "2.4.21",
3
+ "version": "2.4.23",
4
4
  "description": "企业微信智能机器人 MCP 服务 - Claude Code 审批通道",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",