@vrs-soft/wecom-aibot-mcp 2.4.22 → 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 +20 -2
- package/dist/bin.js +33 -4
- package/dist/channel-server.js +16 -26
- 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,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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
314
|
+
logger.error('SSE error', { error: String(err) });
|
|
325
315
|
sseConnected = false;
|
|
326
316
|
// 非主动断开时自动重连
|
|
327
317
|
if (!sseAbortController?.signal.aborted) {
|
|
328
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
451
|
+
logger.info('本地 wecom-aibot.json 已写入', { projectDir: localProjectDir, robotName: parsed.robotName, ccId: parsed.ccId });
|
|
462
452
|
// 安装 skill 到本地(同上)
|
|
463
453
|
const skillResult = installSkill(localProjectDir);
|
|
464
|
-
|
|
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
|
-
|
|
485
|
+
logger.info('SSE disconnected', { cc_id });
|
|
496
486
|
}
|
|
497
487
|
// 注销本地 active-projects 记录
|
|
498
488
|
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
|
};
|