@vrs-soft/wecom-aibot-mcp 2.4.22 → 2.4.24
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 +20 -2
- package/dist/bin.js +33 -4
- package/dist/channel-server.js +66 -28
- package/dist/connection-manager.js +5 -5
- package/dist/http-server.js +5 -5
- package/dist/logger.d.ts +21 -39
- package/dist/logger.js +79 -49
- package/package.json +1 -1
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
|
|
98
|
-
| `--http-only` |
|
|
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 前台启动
|
|
48
|
-
--channel 启动 Channel MCP Proxy(stdio 代理 + SSE
|
|
49
|
-
--http-only
|
|
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 / 目标用户)
|
|
@@ -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)) {
|
|
@@ -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();
|
package/dist/channel-server.js
CHANGED
|
@@ -12,11 +12,50 @@
|
|
|
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
|
|
16
|
-
import * as path from 'path';
|
|
17
|
-
import * as os from 'os';
|
|
15
|
+
import { execSync } from 'child_process';
|
|
18
16
|
import { VERSION, installSkill } from './config-wizard.js';
|
|
19
17
|
import { addPermissionHook, registerActiveProject, unregisterActiveProject, updateWechatModeConfig } from './project-config.js';
|
|
18
|
+
import { logger } from './logger.js';
|
|
19
|
+
/**
|
|
20
|
+
* 沿进程树向上查找 Claude Code TUI 的 PID。
|
|
21
|
+
*
|
|
22
|
+
* 背景:本地 dev (`command: "node"`) 时 channel-server 是 Claude TUI 的直接子进程,
|
|
23
|
+
* process.ppid = Claude TUI ✓
|
|
24
|
+
* 但 npx 部署 (`command: "npx"`) 时多了一层 npx:
|
|
25
|
+
* Claude TUI → npx → node bin.js (channel-server)
|
|
26
|
+
* process.ppid = npx ❌
|
|
27
|
+
* permission-hook.sh 从 hook 自身向上查 active-projects.json 时只能命中 Claude TUI
|
|
28
|
+
* 这条祖先链。如果注册的是 npx 的 PID,hook 永远找不到 → 静默 exit 0 → 跳过审批。
|
|
29
|
+
*
|
|
30
|
+
* 此函数从 startPid 起向上遍历,找到第一个命令名为 "claude" 的进程,返回其 PID。
|
|
31
|
+
* 找不到时回退到 startPid(保持旧行为,至少 dev 场景不退化)。
|
|
32
|
+
*/
|
|
33
|
+
function findClaudePid(startPid) {
|
|
34
|
+
let pid = startPid;
|
|
35
|
+
for (let i = 0; i < 8; i++) {
|
|
36
|
+
if (!pid || pid <= 1)
|
|
37
|
+
break;
|
|
38
|
+
try {
|
|
39
|
+
const comm = execSync(`ps -p ${pid} -o comm=`, { stdio: ['ignore', 'pipe', 'ignore'] })
|
|
40
|
+
.toString()
|
|
41
|
+
.trim();
|
|
42
|
+
// ps comm= 返回执行文件 basename。Claude Code TUI 安装名就是 "claude"
|
|
43
|
+
if (comm === 'claude' || comm.endsWith('/claude'))
|
|
44
|
+
return pid;
|
|
45
|
+
const ppidStr = execSync(`ps -p ${pid} -o ppid=`, { stdio: ['ignore', 'pipe', 'ignore'] })
|
|
46
|
+
.toString()
|
|
47
|
+
.trim();
|
|
48
|
+
const ppid = parseInt(ppidStr, 10);
|
|
49
|
+
if (!ppid || ppid === pid)
|
|
50
|
+
break;
|
|
51
|
+
pid = ppid;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return startPid;
|
|
58
|
+
}
|
|
20
59
|
const MCP_URL = process.env.MCP_URL || 'http://127.0.0.1:18963';
|
|
21
60
|
const MCP_AUTH_TOKEN = process.env.MCP_AUTH_TOKEN;
|
|
22
61
|
// 构建带 auth 的 fetch headers
|
|
@@ -28,22 +67,14 @@ function getAuthHeaders() {
|
|
|
28
67
|
return headers;
|
|
29
68
|
}
|
|
30
69
|
// Channel 日志文件
|
|
31
|
-
const CHANNEL_LOG_FILE = path.join(os.homedir(), '.wecom-aibot-mcp', 'channel.log');
|
|
32
70
|
/**
|
|
33
71
|
* 写入 Channel 日志
|
|
72
|
+
*
|
|
73
|
+
* 默认走 logger.debug(仅 --debug 时落盘)。关键事件请直接调用 logger.info(),
|
|
74
|
+
* 它们会永久落盘到 ~/.wecom-aibot-mcp/channel.log(自动滚动)。
|
|
34
75
|
*/
|
|
35
76
|
function logChannel(message, data) {
|
|
36
|
-
|
|
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)}` : ''}`);
|
|
77
|
+
logger.debug(message, data);
|
|
47
78
|
}
|
|
48
79
|
// SSE 连接状态
|
|
49
80
|
let sseConnected = false;
|
|
@@ -192,7 +223,7 @@ function connectSSE(ccId) {
|
|
|
192
223
|
sseCurrentCcId = ccId;
|
|
193
224
|
// SSE URL 添加 ccId 查询参数用于授权验证
|
|
194
225
|
const sseUrl = ccId ? `${MCP_URL}/sse/${ccId}?ccId=${ccId}` : `${MCP_URL}/sse`;
|
|
195
|
-
|
|
226
|
+
logger.info('Connecting to SSE', { url: sseUrl, ccId, mcpServerReady: mcpServer ? 'yes' : 'no' });
|
|
196
227
|
sseAbortController = new AbortController();
|
|
197
228
|
// SSE fetch 配置:添加 keep-alive headers 确保连接稳定
|
|
198
229
|
fetch(sseUrl, {
|
|
@@ -228,7 +259,7 @@ function connectSSE(ccId) {
|
|
|
228
259
|
}
|
|
229
260
|
return;
|
|
230
261
|
}
|
|
231
|
-
|
|
262
|
+
logger.info('SSE connected', { ccId, status: res.status });
|
|
232
263
|
const reader = res.body?.getReader();
|
|
233
264
|
if (!reader) {
|
|
234
265
|
logChannel('No response body');
|
|
@@ -250,7 +281,7 @@ function connectSSE(ccId) {
|
|
|
250
281
|
sseConnected = false;
|
|
251
282
|
// 非主动断开时自动重连
|
|
252
283
|
if (!sseAbortController?.signal.aborted) {
|
|
253
|
-
|
|
284
|
+
logger.info('SSE 断线,3 秒后重连', { ccId });
|
|
254
285
|
setTimeout(() => { httpSessionId = null; connectSSE(ccId); }, 3000);
|
|
255
286
|
}
|
|
256
287
|
break;
|
|
@@ -321,11 +352,11 @@ function connectSSE(ccId) {
|
|
|
321
352
|
}
|
|
322
353
|
clearInterval(heartbeatInterval);
|
|
323
354
|
}).catch((err) => {
|
|
324
|
-
|
|
355
|
+
logger.error('SSE error', { error: String(err) });
|
|
325
356
|
sseConnected = false;
|
|
326
357
|
// 非主动断开时自动重连
|
|
327
358
|
if (!sseAbortController?.signal.aborted) {
|
|
328
|
-
|
|
359
|
+
logger.info('SSE 出错,3 秒后重连', { ccId });
|
|
329
360
|
setTimeout(() => { httpSessionId = null; connectSSE(ccId); }, 3000);
|
|
330
361
|
}
|
|
331
362
|
});
|
|
@@ -438,7 +469,7 @@ function registerChannelTools(server) {
|
|
|
438
469
|
try {
|
|
439
470
|
const parsed = JSON.parse(content[0].text);
|
|
440
471
|
if (parsed.ccId) {
|
|
441
|
-
|
|
472
|
+
logger.info('Got ccId, connecting SSE', { ccId: parsed.ccId, mode });
|
|
442
473
|
// 保存连接参数供重连复用
|
|
443
474
|
sseRobotId = robot_id || parsed.robotName;
|
|
444
475
|
sseProjectDir = project_dir || process.cwd();
|
|
@@ -446,10 +477,17 @@ function registerChannelTools(server) {
|
|
|
446
477
|
// Channel 模式:在本地项目写入 PermissionRequest hook
|
|
447
478
|
const localProjectDir = project_dir || process.cwd();
|
|
448
479
|
const hookResult = addPermissionHook(localProjectDir);
|
|
449
|
-
|
|
450
|
-
//
|
|
451
|
-
|
|
452
|
-
|
|
480
|
+
logger.info('本地 PermissionRequest hook 已写入', { path: hookResult.path, success: hookResult.success });
|
|
481
|
+
// 注册 Claude TUI 的 PID(不能用 process.ppid,npx 部署时 ppid 是 npx 不是 Claude)
|
|
482
|
+
const startPid = process.ppid ?? process.pid;
|
|
483
|
+
const claudePid = findClaudePid(startPid);
|
|
484
|
+
registerActiveProject(claudePid, localProjectDir);
|
|
485
|
+
logger.info('本地 active-projects 已注册', {
|
|
486
|
+
pid: claudePid,
|
|
487
|
+
rawPpid: startPid,
|
|
488
|
+
resolvedClaudePid: claudePid !== startPid,
|
|
489
|
+
projectDir: localProjectDir,
|
|
490
|
+
});
|
|
453
491
|
// 写入本地 wecom-aibot.json(远程 HTTP MCP 写在远端 fs,agent 本地需要自己落地)
|
|
454
492
|
updateWechatModeConfig(localProjectDir, {
|
|
455
493
|
wechatMode: true,
|
|
@@ -458,10 +496,10 @@ function registerChannelTools(server) {
|
|
|
458
496
|
mode: parsed.mode || mode,
|
|
459
497
|
autoApproveTimeout: auto_approve_timeout,
|
|
460
498
|
});
|
|
461
|
-
|
|
499
|
+
logger.info('本地 wecom-aibot.json 已写入', { projectDir: localProjectDir, robotName: parsed.robotName, ccId: parsed.ccId });
|
|
462
500
|
// 安装 skill 到本地(同上)
|
|
463
501
|
const skillResult = installSkill(localProjectDir);
|
|
464
|
-
|
|
502
|
+
logger.info('本地 skill 安装', { success: skillResult.success, skillUrl: skillResult.skillUrl });
|
|
465
503
|
// Channel 模式:过滤 heartbeat 信息,简化消息
|
|
466
504
|
if (mode === 'channel' || parsed.mode === 'channel') {
|
|
467
505
|
delete parsed.heartbeat; // Channel 模式不需要 heartbeat loop
|
|
@@ -492,7 +530,7 @@ function registerChannelTools(server) {
|
|
|
492
530
|
sseAbortController = null;
|
|
493
531
|
sseConnected = false;
|
|
494
532
|
sseCurrentCcId = undefined;
|
|
495
|
-
|
|
533
|
+
logger.info('SSE disconnected', { cc_id });
|
|
496
534
|
}
|
|
497
535
|
// 注销本地 active-projects 记录
|
|
498
536
|
unregisterActiveProject(localProjectDir);
|
|
@@ -113,7 +113,7 @@ export async function connectRobot(robotName, agentName) {
|
|
|
113
113
|
agentName,
|
|
114
114
|
};
|
|
115
115
|
connectionPool.set(robot.name, state);
|
|
116
|
-
logger.
|
|
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.
|
|
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.
|
|
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.
|
|
168
|
+
logger.info('robot reconnected', { robotName: robot.name });
|
|
169
169
|
return state.client;
|
|
170
170
|
}
|
|
171
171
|
else {
|
|
172
|
-
logger.
|
|
172
|
+
logger.error('robot reconnect failed', { robotName: robot.name });
|
|
173
173
|
return null;
|
|
174
174
|
}
|
|
175
175
|
}
|
package/dist/http-server.js
CHANGED
|
@@ -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.
|
|
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.
|
|
153
|
+
logger.info('ccId reconnect', { ccId, robotName });
|
|
154
154
|
}
|
|
155
155
|
else {
|
|
156
156
|
// 首次注册:先清理超时条目
|
|
157
157
|
cleanStaleCcIds();
|
|
158
|
-
logger.
|
|
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.
|
|
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.
|
|
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
|
-
*
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
4
|
+
* 级别:error / info / debug
|
|
5
|
+
* - error:永远写文件 + stderr
|
|
6
|
+
* - info:永远写文件,--debug 时也写 stdout
|
|
7
|
+
* - debug:仅 --debug 模式写文件 + stdout
|
|
7
8
|
*
|
|
8
|
-
* Debug
|
|
9
|
+
* Debug 标记:~/.wecom-aibot-mcp/debug 文件存在 → debug 模式
|
|
9
10
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
*
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
*
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
4
|
+
* 级别:error / info / debug
|
|
5
|
+
* - error:永远写文件 + stderr
|
|
6
|
+
* - info:永远写文件,--debug 时也写 stdout
|
|
7
|
+
* - debug:仅 --debug 模式写文件 + stdout
|
|
7
8
|
*
|
|
8
|
-
* Debug
|
|
9
|
+
* Debug 标记:~/.wecom-aibot-mcp/debug 文件存在 → debug 模式
|
|
9
10
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
//
|
|
100
|
+
// 公共 API
|
|
77
101
|
export const logger = {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
};
|