openclaw-mem 1.0.3 → 1.2.1

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/package.json CHANGED
@@ -1,58 +1,82 @@
1
1
  {
2
2
  "name": "openclaw-mem",
3
- "version": "1.0.3",
4
- "description": "Persistent memory system for OpenClaw - Give your AI agent long-term memory",
3
+ "version": "1.2.1",
4
+ "description": "Persistent memory system for OpenClaw - captures conversations, generates summaries, and injects context into new sessions",
5
5
  "type": "module",
6
- "main": "lib/handler.js",
6
+ "main": "handler.js",
7
7
  "bin": {
8
- "openclaw-mem": "bin/openclaw-mem.js"
8
+ "openclaw-mem-api": "mcp-http-api.js",
9
+ "openclaw-mem-mcp": "mcp-server.js",
10
+ "openclaw-mem-setup": "setup.js"
9
11
  },
10
12
  "scripts": {
11
- "test": "node scripts/test.js",
12
- "test:e2e": "node scripts/e2e-test.js",
13
- "test:stress": "node scripts/stress-test.js",
14
- "test:all": "npm run test && npm run verify",
15
- "verify": "node scripts/verify.js"
13
+ "test": "vitest run",
14
+ "test:watch": "vitest",
15
+ "test:coverage": "vitest run --coverage",
16
+ "monitor": "node monitor.js",
17
+ "watch": "node session-watcher.js",
18
+ "mcp": "node mcp-server.js",
19
+ "api": "node mcp-http-api.js",
20
+ "api:start": "nohup node mcp-http-api.js > ~/.openclaw-mem/logs/api.log 2>&1 &",
21
+ "debug": "node debug-logger.js",
22
+ "setup": "node setup.js",
23
+ "postinstall": "node setup.js"
16
24
  },
17
25
  "keywords": [
18
26
  "openclaw",
19
27
  "memory",
20
- "agent",
21
- "llm",
22
28
  "ai",
23
- "hooks",
24
- "claude",
25
- "gpt",
26
- "persistent",
27
- "context"
29
+ "llm",
30
+ "context",
31
+ "persistence",
32
+ "mcp",
33
+ "model-context-protocol",
34
+ "sqlite",
35
+ "hooks"
28
36
  ],
29
- "author": "OpenClaw Contributors",
37
+ "author": "Aaron",
30
38
  "license": "MIT",
31
39
  "repository": {
32
40
  "type": "git",
33
41
  "url": "git+https://github.com/wenyupapa-sys/openclaw-mem.git"
34
42
  },
35
- "homepage": "https://github.com/wenyupapa-sys/openclaw-mem#readme",
36
43
  "bugs": {
37
44
  "url": "https://github.com/wenyupapa-sys/openclaw-mem/issues"
38
45
  },
46
+ "homepage": "https://github.com/wenyupapa-sys/openclaw-mem#readme",
39
47
  "engines": {
40
48
  "node": ">=18.0.0"
41
49
  },
50
+ "files": [
51
+ "handler.js",
52
+ "database.js",
53
+ "context-builder.js",
54
+ "extractor.js",
55
+ "gateway-llm.js",
56
+ "mcp-server.js",
57
+ "mcp-http-api.js",
58
+ "mem-search.sh",
59
+ "mem-get.sh",
60
+ "monitor.js",
61
+ "debug-logger.js",
62
+ "realtime-monitor.js",
63
+ "session-watcher.js",
64
+ "sync-recent.js",
65
+ "setup.js",
66
+ "HOOK.md",
67
+ "MCP.json",
68
+ "README.md"
69
+ ],
42
70
  "dependencies": {
71
+ "@modelcontextprotocol/sdk": "^1.25.3",
43
72
  "better-sqlite3": "^11.0.0"
44
73
  },
45
- "files": [
46
- "bin/",
47
- "lib/",
48
- "scripts/init.js",
49
- "scripts/commands.js",
50
- "docs/",
51
- "README.md",
52
- "README_CN.md",
53
- "LICENSE"
54
- ],
74
+ "devDependencies": {
75
+ "vitest": "^2.0.0"
76
+ },
55
77
  "openclaw": {
56
- "hooks": ["."]
78
+ "hooks": [
79
+ "."
80
+ ]
57
81
  }
58
82
  }
@@ -0,0 +1,371 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * OpenClaw 实时监控工具
4
+ *
5
+ * 显示:
6
+ * - 用户消息
7
+ * - 发送给 LLM 的请求
8
+ * - LLM 的响应
9
+ * - 工具调用
10
+ * - API 调用
11
+ *
12
+ * 使用: node realtime-monitor.js
13
+ */
14
+
15
+ import fs from 'fs';
16
+ import path from 'path';
17
+ import os from 'os';
18
+ import readline from 'readline';
19
+
20
+ // 配置
21
+ const OPENCLAW_DIR = path.join(os.homedir(), '.openclaw');
22
+ const GATEWAY_LOG = path.join(OPENCLAW_DIR, 'logs', 'gateway.log');
23
+ const SESSIONS_DIR = path.join(OPENCLAW_DIR, 'agents', 'main', 'sessions');
24
+ const API_LOG = path.join(os.homedir(), '.openclaw-mem', 'logs', 'api.log');
25
+
26
+ // 颜色代码
27
+ const colors = {
28
+ reset: '\x1b[0m',
29
+ bright: '\x1b[1m',
30
+ dim: '\x1b[2m',
31
+ red: '\x1b[31m',
32
+ green: '\x1b[32m',
33
+ yellow: '\x1b[33m',
34
+ blue: '\x1b[34m',
35
+ magenta: '\x1b[35m',
36
+ cyan: '\x1b[36m',
37
+ white: '\x1b[37m',
38
+ bgBlue: '\x1b[44m',
39
+ bgGreen: '\x1b[42m',
40
+ bgYellow: '\x1b[43m',
41
+ bgRed: '\x1b[41m',
42
+ bgMagenta: '\x1b[45m',
43
+ };
44
+
45
+ function colorize(text, color) {
46
+ return `${colors[color]}${text}${colors.reset}`;
47
+ }
48
+
49
+ function timestamp() {
50
+ return new Date().toLocaleTimeString('zh-CN', { hour12: false });
51
+ }
52
+
53
+ function truncate(text, maxLen = 200) {
54
+ if (!text) return '';
55
+ const clean = String(text).replace(/\s+/g, ' ').trim();
56
+ if (clean.length <= maxLen) return clean;
57
+ return clean.slice(0, maxLen - 3) + '...';
58
+ }
59
+
60
+ function printHeader() {
61
+ console.clear();
62
+ console.log(colorize('═'.repeat(80), 'cyan'));
63
+ console.log(colorize(' 🔍 OpenClaw 实时监控工具', 'bright'));
64
+ console.log(colorize('═'.repeat(80), 'cyan'));
65
+ console.log();
66
+ console.log(colorize(' 监控中... (Ctrl+C 退出)', 'dim'));
67
+ console.log();
68
+ console.log(colorize('─'.repeat(80), 'dim'));
69
+ console.log();
70
+ }
71
+
72
+ function printEvent(type, content, extra = '') {
73
+ const ts = colorize(`[${timestamp()}]`, 'dim');
74
+ let icon, label, color;
75
+
76
+ switch (type) {
77
+ case 'user':
78
+ icon = '👤';
79
+ label = '用户消息';
80
+ color = 'green';
81
+ break;
82
+ case 'assistant':
83
+ icon = '🤖';
84
+ label = 'AI 响应';
85
+ color = 'blue';
86
+ break;
87
+ case 'tool_call':
88
+ icon = '🔧';
89
+ label = '工具调用';
90
+ color = 'yellow';
91
+ break;
92
+ case 'tool_result':
93
+ icon = '📋';
94
+ label = '工具结果';
95
+ color = 'cyan';
96
+ break;
97
+ case 'api_call':
98
+ icon = '🌐';
99
+ label = 'API 调用';
100
+ color = 'magenta';
101
+ break;
102
+ case 'bootstrap':
103
+ icon = '🚀';
104
+ label = '会话启动';
105
+ color = 'green';
106
+ break;
107
+ case 'hook':
108
+ icon = '🪝';
109
+ label = 'Hook 事件';
110
+ color = 'cyan';
111
+ break;
112
+ case 'error':
113
+ icon = '❌';
114
+ label = '错误';
115
+ color = 'red';
116
+ break;
117
+ case 'info':
118
+ icon = 'ℹ️';
119
+ label = '信息';
120
+ color = 'white';
121
+ break;
122
+ default:
123
+ icon = '•';
124
+ label = type;
125
+ color = 'white';
126
+ }
127
+
128
+ console.log(`${ts} ${icon} ${colorize(label, color)}${extra ? ` ${colorize(extra, 'dim')}` : ''}`);
129
+ if (content) {
130
+ const lines = content.split('\n').slice(0, 10);
131
+ lines.forEach(line => {
132
+ console.log(` ${colorize('│', 'dim')} ${line}`);
133
+ });
134
+ if (content.split('\n').length > 10) {
135
+ console.log(` ${colorize('│', 'dim')} ${colorize('... (更多内容省略)', 'dim')}`);
136
+ }
137
+ }
138
+ console.log();
139
+ }
140
+
141
+ // 监控最新的 session 文件
142
+ let lastSessionFile = null;
143
+ let lastSessionSize = 0;
144
+ let lastSessionLines = new Set();
145
+
146
+ function findLatestSession() {
147
+ try {
148
+ const files = fs.readdirSync(SESSIONS_DIR)
149
+ .filter(f => f.endsWith('.jsonl'))
150
+ .map(f => ({
151
+ name: f,
152
+ path: path.join(SESSIONS_DIR, f),
153
+ mtime: fs.statSync(path.join(SESSIONS_DIR, f)).mtime
154
+ }))
155
+ .sort((a, b) => b.mtime - a.mtime);
156
+
157
+ return files[0]?.path || null;
158
+ } catch {
159
+ return null;
160
+ }
161
+ }
162
+
163
+ function parseSessionLine(line) {
164
+ try {
165
+ const entry = JSON.parse(line);
166
+
167
+ if (entry.type === 'message' && entry.message) {
168
+ const msg = entry.message;
169
+
170
+ // 用户消息
171
+ if (msg.role === 'user') {
172
+ let content = '';
173
+ if (Array.isArray(msg.content)) {
174
+ const textPart = msg.content.find(c => c.type === 'text');
175
+ content = textPart?.text || '';
176
+ } else {
177
+ content = msg.content || '';
178
+ }
179
+ if (content && !content.startsWith('/')) {
180
+ return { type: 'user', content: truncate(content, 300) };
181
+ }
182
+ }
183
+
184
+ // AI 响应
185
+ if (msg.role === 'assistant') {
186
+ let content = '';
187
+ if (Array.isArray(msg.content)) {
188
+ const textPart = msg.content.find(c => c.type === 'text');
189
+ content = textPart?.text || '';
190
+ } else {
191
+ content = msg.content || '';
192
+ }
193
+ if (content) {
194
+ return { type: 'assistant', content: truncate(content, 300) };
195
+ }
196
+ }
197
+
198
+ // 工具调用
199
+ if (msg.role === 'assistant' && msg.tool_calls) {
200
+ for (const call of msg.tool_calls) {
201
+ const toolName = call.function?.name || call.name || 'unknown';
202
+ const toolArgs = call.function?.arguments || call.arguments || '{}';
203
+ let args;
204
+ try {
205
+ args = JSON.parse(toolArgs);
206
+ } catch {
207
+ args = toolArgs;
208
+ }
209
+
210
+ let summary = toolName;
211
+ if (args.command) summary += `: ${truncate(args.command, 100)}`;
212
+ else if (args.file_path) summary += `: ${args.file_path}`;
213
+ else if (args.query) summary += `: ${truncate(args.query, 100)}`;
214
+ else if (args.url) summary += `: ${args.url}`;
215
+
216
+ return { type: 'tool_call', content: summary, extra: `(${toolName})` };
217
+ }
218
+ }
219
+
220
+ // 工具结果
221
+ if (msg.role === 'toolResult' || msg.role === 'tool') {
222
+ const toolName = msg.toolName || msg.name || 'unknown';
223
+ let result = '';
224
+ if (Array.isArray(msg.content)) {
225
+ const textPart = msg.content.find(c => c.type === 'text');
226
+ result = textPart?.text || '';
227
+ } else {
228
+ result = msg.content || '';
229
+ }
230
+ return { type: 'tool_result', content: truncate(result, 200), extra: `(${toolName})` };
231
+ }
232
+ }
233
+
234
+ return null;
235
+ } catch {
236
+ return null;
237
+ }
238
+ }
239
+
240
+ function watchSession() {
241
+ const latestSession = findLatestSession();
242
+
243
+ if (latestSession !== lastSessionFile) {
244
+ lastSessionFile = latestSession;
245
+ lastSessionSize = 0;
246
+ lastSessionLines.clear();
247
+ if (latestSession) {
248
+ printEvent('info', `监控会话: ${path.basename(latestSession)}`);
249
+ }
250
+ }
251
+
252
+ if (!lastSessionFile) return;
253
+
254
+ try {
255
+ const content = fs.readFileSync(lastSessionFile, 'utf-8');
256
+ const lines = content.trim().split('\n');
257
+
258
+ for (const line of lines) {
259
+ const lineHash = line.slice(0, 100); // 简单去重
260
+ if (lastSessionLines.has(lineHash)) continue;
261
+ lastSessionLines.add(lineHash);
262
+
263
+ const parsed = parseSessionLine(line);
264
+ if (parsed) {
265
+ printEvent(parsed.type, parsed.content, parsed.extra);
266
+ }
267
+ }
268
+ } catch {
269
+ // 文件可能正在被写入
270
+ }
271
+ }
272
+
273
+ // 监控 Gateway 日志
274
+ let lastGatewaySize = 0;
275
+
276
+ function watchGatewayLog() {
277
+ try {
278
+ const stats = fs.statSync(GATEWAY_LOG);
279
+ if (stats.size <= lastGatewaySize) return;
280
+
281
+ const fd = fs.openSync(GATEWAY_LOG, 'r');
282
+ const buffer = Buffer.alloc(stats.size - lastGatewaySize);
283
+ fs.readSync(fd, buffer, 0, buffer.length, lastGatewaySize);
284
+ fs.closeSync(fd);
285
+
286
+ lastGatewaySize = stats.size;
287
+
288
+ const newContent = buffer.toString('utf-8');
289
+ const lines = newContent.split('\n');
290
+
291
+ for (const line of lines) {
292
+ if (!line.trim()) continue;
293
+
294
+ // Hook 事件
295
+ if (line.includes('[openclaw-mem]')) {
296
+ if (line.includes('Agent bootstrap')) {
297
+ printEvent('bootstrap', '新会话开始');
298
+ } else if (line.includes('API server')) {
299
+ printEvent('hook', truncate(line.replace(/.*\[openclaw-mem\]\s*/, ''), 100));
300
+ } else if (line.includes('Tool') || line.includes('tool:post')) {
301
+ printEvent('hook', truncate(line.replace(/.*\[openclaw-mem\]\s*/, ''), 100));
302
+ }
303
+ }
304
+
305
+ // 错误
306
+ if (line.toLowerCase().includes('error')) {
307
+ printEvent('error', truncate(line, 200));
308
+ }
309
+ }
310
+ } catch {
311
+ // 日志文件可能不存在
312
+ }
313
+ }
314
+
315
+ // 监控 API 日志
316
+ let lastApiSize = 0;
317
+
318
+ function watchApiLog() {
319
+ try {
320
+ const stats = fs.statSync(API_LOG);
321
+ if (stats.size <= lastApiSize) return;
322
+
323
+ const fd = fs.openSync(API_LOG, 'r');
324
+ const buffer = Buffer.alloc(stats.size - lastApiSize);
325
+ fs.readSync(fd, buffer, 0, buffer.length, lastApiSize);
326
+ fs.closeSync(fd);
327
+
328
+ lastApiSize = stats.size;
329
+
330
+ const newContent = buffer.toString('utf-8');
331
+ const lines = newContent.split('\n');
332
+
333
+ for (const line of lines) {
334
+ if (!line.trim()) continue;
335
+ if (line.includes('/search') || line.includes('/get_observations') || line.includes('/timeline')) {
336
+ printEvent('api_call', truncate(line, 150));
337
+ }
338
+ }
339
+ } catch {
340
+ // API 日志可能不存在
341
+ }
342
+ }
343
+
344
+ // 主循环
345
+ function main() {
346
+ printHeader();
347
+
348
+ // 初始化文件位置
349
+ try {
350
+ lastGatewaySize = fs.statSync(GATEWAY_LOG).size;
351
+ } catch {}
352
+ try {
353
+ lastApiSize = fs.statSync(API_LOG).size;
354
+ } catch {}
355
+
356
+ // 开始监控
357
+ setInterval(() => {
358
+ watchSession();
359
+ watchGatewayLog();
360
+ watchApiLog();
361
+ }, 500);
362
+
363
+ // 处理退出
364
+ process.on('SIGINT', () => {
365
+ console.log();
366
+ console.log(colorize('监控已停止', 'yellow'));
367
+ process.exit(0);
368
+ });
369
+ }
370
+
371
+ main();
@@ -0,0 +1,192 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * OpenClaw Session Watcher
4
+ * Watches session JSONL files for new messages and records them in real-time
5
+ */
6
+
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import os from 'node:os';
10
+ import Database from 'better-sqlite3';
11
+
12
+ const SESSIONS_DIR = path.join(os.homedir(), '.openclaw', 'agents', 'main', 'sessions');
13
+ const DB_PATH = path.join(os.homedir(), '.openclaw-mem', 'memory.db');
14
+ const POLL_INTERVAL = 2000; // 2 seconds
15
+
16
+ // Track file positions to only read new content
17
+ const filePositions = new Map();
18
+ const processedMessages = new Set();
19
+
20
+ let db;
21
+ try {
22
+ db = new Database(DB_PATH);
23
+ } catch (err) {
24
+ console.error('Cannot open database:', err.message);
25
+ process.exit(1);
26
+ }
27
+
28
+ // Prepare statement for inserting observations
29
+ const insertStmt = db.prepare(`
30
+ INSERT INTO observations (session_id, timestamp, tool_name, tool_input, tool_response, summary, concepts, tokens_discovery, tokens_read)
31
+ VALUES (?, datetime('now'), ?, ?, ?, ?, ?, ?, ?)
32
+ `);
33
+
34
+ function estimateTokens(text) {
35
+ if (!text) return 0;
36
+ return Math.ceil(String(text).length / 4);
37
+ }
38
+
39
+ function formatTime() {
40
+ return new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
41
+ }
42
+
43
+ console.log('\x1b[36m╔══════════════════════════════════════════════════════════════╗\x1b[0m');
44
+ console.log('\x1b[36m║\x1b[0m \x1b[1m📡 OpenClaw Session Watcher\x1b[0m \x1b[36m║\x1b[0m');
45
+ console.log('\x1b[36m╠══════════════════════════════════════════════════════════════╣\x1b[0m');
46
+ console.log('\x1b[36m║\x1b[0m Sessions: ~/.openclaw/agents/main/sessions/ \x1b[36m║\x1b[0m');
47
+ console.log('\x1b[36m║\x1b[0m Database: ~/.openclaw-mem/memory.db \x1b[36m║\x1b[0m');
48
+ console.log('\x1b[36m║\x1b[0m Press Ctrl+C to stop \x1b[36m║\x1b[0m');
49
+ console.log('\x1b[36m╚══════════════════════════════════════════════════════════════╝\x1b[0m');
50
+ console.log('');
51
+
52
+ function processNewLines(sessionKey, newContent) {
53
+ const lines = newContent.trim().split('\n').filter(l => l.trim());
54
+
55
+ for (const line of lines) {
56
+ try {
57
+ const entry = JSON.parse(line);
58
+
59
+ // Skip if already processed (use a hash of content + timestamp)
60
+ const msgId = `${entry.type}-${entry.timestamp || ''}-${(entry.content || entry.message?.content || '').slice(0, 50)}`;
61
+ if (processedMessages.has(msgId)) continue;
62
+ processedMessages.add(msgId);
63
+
64
+ // Process message entries
65
+ if (entry.type === 'message' && entry.message) {
66
+ const msg = entry.message;
67
+ const role = msg.role || 'unknown';
68
+ const content = msg.content || '';
69
+
70
+ if (!content.trim()) continue;
71
+
72
+ const toolName = role === 'user' ? 'UserMessage' : 'AssistantMessage';
73
+ const summary = content.slice(0, 100) + (content.length > 100 ? '...' : '');
74
+ const tokens = estimateTokens(content);
75
+
76
+ // Save to database
77
+ try {
78
+ insertStmt.run(
79
+ sessionKey,
80
+ toolName,
81
+ JSON.stringify({ role, sessionKey }),
82
+ JSON.stringify({ content: content.slice(0, 2000) }),
83
+ summary,
84
+ content.slice(0, 500),
85
+ tokens,
86
+ estimateTokens(summary)
87
+ );
88
+
89
+ const icon = role === 'user' ? '\x1b[33m👤\x1b[0m' : '\x1b[32m🤖\x1b[0m';
90
+ const preview = content.slice(0, 60).replace(/\n/g, ' ');
91
+ console.log(`\x1b[90m${formatTime()}\x1b[0m ${icon} [${role}] ${preview}${content.length > 60 ? '...' : ''}`);
92
+ } catch (dbErr) {
93
+ // Ignore duplicate errors
94
+ }
95
+ }
96
+
97
+ // Process tool call entries
98
+ if (entry.type === 'tool_use' || entry.type === 'tool_result') {
99
+ const toolName = entry.name || entry.tool_name || 'unknown';
100
+ const input = entry.input || entry.tool_input || {};
101
+ const result = entry.result || entry.output || {};
102
+
103
+ const summary = `${toolName}: ${JSON.stringify(input).slice(0, 80)}`;
104
+
105
+ try {
106
+ insertStmt.run(
107
+ sessionKey,
108
+ toolName,
109
+ JSON.stringify(input),
110
+ JSON.stringify(result).slice(0, 2000),
111
+ summary,
112
+ summary,
113
+ estimateTokens(JSON.stringify(input)),
114
+ estimateTokens(summary)
115
+ );
116
+
117
+ console.log(`\x1b[90m${formatTime()}\x1b[0m \x1b[35m🔧\x1b[0m [${toolName}]`);
118
+ } catch (dbErr) {
119
+ // Ignore duplicate errors
120
+ }
121
+ }
122
+ } catch (parseErr) {
123
+ // Skip invalid JSON lines
124
+ }
125
+ }
126
+ }
127
+
128
+ function watchSessions() {
129
+ try {
130
+ if (!fs.existsSync(SESSIONS_DIR)) {
131
+ return;
132
+ }
133
+
134
+ const files = fs.readdirSync(SESSIONS_DIR).filter(f => f.endsWith('.jsonl'));
135
+
136
+ for (const file of files) {
137
+ const filePath = path.join(SESSIONS_DIR, file);
138
+ const sessionKey = file.replace('.jsonl', '');
139
+
140
+ try {
141
+ const stats = fs.statSync(filePath);
142
+ const currentSize = stats.size;
143
+ const lastPosition = filePositions.get(filePath) || 0;
144
+
145
+ if (currentSize > lastPosition) {
146
+ // Read new content
147
+ const fd = fs.openSync(filePath, 'r');
148
+ const buffer = Buffer.alloc(currentSize - lastPosition);
149
+ fs.readSync(fd, buffer, 0, buffer.length, lastPosition);
150
+ fs.closeSync(fd);
151
+
152
+ const newContent = buffer.toString('utf-8');
153
+ processNewLines(sessionKey, newContent);
154
+
155
+ filePositions.set(filePath, currentSize);
156
+ }
157
+ } catch (fileErr) {
158
+ // File might be locked or deleted
159
+ }
160
+ }
161
+ } catch (err) {
162
+ // Directory might not exist yet
163
+ }
164
+ }
165
+
166
+ // Initial scan - just record current positions without processing old content
167
+ try {
168
+ if (fs.existsSync(SESSIONS_DIR)) {
169
+ const files = fs.readdirSync(SESSIONS_DIR).filter(f => f.endsWith('.jsonl'));
170
+ for (const file of files) {
171
+ const filePath = path.join(SESSIONS_DIR, file);
172
+ try {
173
+ const stats = fs.statSync(filePath);
174
+ filePositions.set(filePath, stats.size);
175
+ } catch (e) {}
176
+ }
177
+ console.log(`\x1b[90m监控 ${files.length} 个会话文件...\x1b[0m`);
178
+ }
179
+ } catch (e) {}
180
+
181
+ // Start watching
182
+ const interval = setInterval(watchSessions, POLL_INTERVAL);
183
+
184
+ // Handle graceful shutdown
185
+ process.on('SIGINT', () => {
186
+ console.log('\n\x1b[90m监控已停止\x1b[0m');
187
+ clearInterval(interval);
188
+ db.close();
189
+ process.exit(0);
190
+ });
191
+
192
+ console.log('\x1b[32m开始监控新消息...\x1b[0m\n');