openclaw-mem 1.0.4 → 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/HOOK.md +125 -0
- package/LICENSE +1 -1
- package/MCP.json +11 -0
- package/README.md +146 -168
- package/context-builder.js +703 -0
- package/database.js +520 -0
- package/debug-logger.js +280 -0
- package/extractor.js +211 -0
- package/gateway-llm.js +155 -0
- package/handler.js +1122 -0
- package/mcp-http-api.js +356 -0
- package/mcp-server.js +525 -0
- package/mem-get.sh +24 -0
- package/mem-search.sh +17 -0
- package/monitor.js +112 -0
- package/package.json +53 -29
- package/realtime-monitor.js +371 -0
- package/session-watcher.js +192 -0
- package/setup.js +114 -0
- package/sync-recent.js +63 -0
- package/README_CN.md +0 -201
- package/bin/openclaw-mem.js +0 -117
- package/docs/locales/README_AR.md +0 -35
- package/docs/locales/README_DE.md +0 -35
- package/docs/locales/README_ES.md +0 -35
- package/docs/locales/README_FR.md +0 -35
- package/docs/locales/README_HE.md +0 -35
- package/docs/locales/README_HI.md +0 -35
- package/docs/locales/README_ID.md +0 -35
- package/docs/locales/README_IT.md +0 -35
- package/docs/locales/README_JA.md +0 -57
- package/docs/locales/README_KO.md +0 -35
- package/docs/locales/README_NL.md +0 -35
- package/docs/locales/README_PL.md +0 -35
- package/docs/locales/README_PT.md +0 -35
- package/docs/locales/README_RU.md +0 -35
- package/docs/locales/README_TH.md +0 -35
- package/docs/locales/README_TR.md +0 -35
- package/docs/locales/README_UK.md +0 -35
- package/docs/locales/README_VI.md +0 -35
- package/docs/logo.svg +0 -32
- package/lib/context-builder.js +0 -415
- package/lib/database.js +0 -309
- package/lib/handler.js +0 -494
- package/scripts/commands.js +0 -141
- package/scripts/init.js +0 -248
package/package.json
CHANGED
|
@@ -1,58 +1,82 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-mem",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Persistent memory system for OpenClaw -
|
|
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": "
|
|
6
|
+
"main": "handler.js",
|
|
7
7
|
"bin": {
|
|
8
|
-
"openclaw-mem": "
|
|
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": "
|
|
12
|
-
"test:
|
|
13
|
-
"test:
|
|
14
|
-
"
|
|
15
|
-
"
|
|
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
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"context"
|
|
29
|
+
"llm",
|
|
30
|
+
"context",
|
|
31
|
+
"persistence",
|
|
32
|
+
"mcp",
|
|
33
|
+
"model-context-protocol",
|
|
34
|
+
"sqlite",
|
|
35
|
+
"hooks"
|
|
28
36
|
],
|
|
29
|
-
"author": "
|
|
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
|
-
"
|
|
46
|
-
"
|
|
47
|
-
|
|
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');
|