openclaw-mem 1.0.4 → 1.3.0
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 +158 -167
- package/backfill-embeddings.js +79 -0
- package/context-builder.js +703 -0
- package/database.js +625 -0
- package/debug-logger.js +280 -0
- package/extractor.js +268 -0
- package/gateway-llm.js +250 -0
- package/handler.js +941 -0
- package/mcp-http-api.js +424 -0
- package/mcp-server.js +605 -0
- package/mem-get.sh +24 -0
- package/mem-search.sh +17 -0
- package/monitor.js +112 -0
- package/package.json +58 -30
- 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/monitor.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* OpenClaw-Mem Real-time Monitor
|
|
4
|
+
* Watches for new observations and displays them in real-time
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import Database from 'better-sqlite3';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
|
|
11
|
+
const DB_PATH = path.join(os.homedir(), '.openclaw-mem', 'memory.db');
|
|
12
|
+
const POLL_INTERVAL = 1000; // 1 second
|
|
13
|
+
|
|
14
|
+
const db = new Database(DB_PATH, { readonly: true });
|
|
15
|
+
|
|
16
|
+
// Get the latest observation ID at startup
|
|
17
|
+
let lastId = db.prepare('SELECT MAX(id) as maxId FROM observations').get()?.maxId || 0;
|
|
18
|
+
let lastSummaryId = db.prepare('SELECT MAX(id) as maxId FROM summaries').get()?.maxId || 0;
|
|
19
|
+
|
|
20
|
+
console.log('\x1b[36m╔══════════════════════════════════════════════════════════════╗\x1b[0m');
|
|
21
|
+
console.log('\x1b[36m║\x1b[0m \x1b[1m🧠 OpenClaw-Mem Real-time Monitor\x1b[0m \x1b[36m║\x1b[0m');
|
|
22
|
+
console.log('\x1b[36m╠══════════════════════════════════════════════════════════════╣\x1b[0m');
|
|
23
|
+
console.log('\x1b[36m║\x1b[0m Database: ~/.openclaw-mem/memory.db \x1b[36m║\x1b[0m');
|
|
24
|
+
console.log('\x1b[36m║\x1b[0m Starting from observation #' + lastId.toString().padEnd(33) + '\x1b[36m║\x1b[0m');
|
|
25
|
+
console.log('\x1b[36m║\x1b[0m Press Ctrl+C to stop \x1b[36m║\x1b[0m');
|
|
26
|
+
console.log('\x1b[36m╚══════════════════════════════════════════════════════════════╝\x1b[0m');
|
|
27
|
+
console.log('');
|
|
28
|
+
|
|
29
|
+
const TYPE_COLORS = {
|
|
30
|
+
'discovery': '\x1b[34m🔵\x1b[0m',
|
|
31
|
+
'refactor': '\x1b[35m🔄\x1b[0m',
|
|
32
|
+
'bugfix': '\x1b[31m🔴\x1b[0m',
|
|
33
|
+
'feature': '\x1b[32m🟢\x1b[0m',
|
|
34
|
+
'decision': '\x1b[33m⚖️\x1b[0m',
|
|
35
|
+
'session-request': '\x1b[36m📝\x1b[0m',
|
|
36
|
+
'problem-solution': '\x1b[33m💡\x1b[0m'
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function formatTime(timestamp) {
|
|
40
|
+
if (!timestamp) return '';
|
|
41
|
+
const date = new Date(timestamp);
|
|
42
|
+
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function truncate(text, max = 60) {
|
|
46
|
+
if (!text) return '';
|
|
47
|
+
const clean = String(text).replace(/\s+/g, ' ').trim();
|
|
48
|
+
if (clean.length <= max) return clean;
|
|
49
|
+
return clean.slice(0, max - 3) + '...';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getTypeIcon(type) {
|
|
53
|
+
return TYPE_COLORS[type] || '\x1b[37m⚪\x1b[0m';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function checkNewObservations() {
|
|
57
|
+
try {
|
|
58
|
+
const newObs = db.prepare(`
|
|
59
|
+
SELECT id, timestamp, tool_name, type, summary, narrative
|
|
60
|
+
FROM observations
|
|
61
|
+
WHERE id > ?
|
|
62
|
+
ORDER BY id ASC
|
|
63
|
+
`).all(lastId);
|
|
64
|
+
|
|
65
|
+
for (const obs of newObs) {
|
|
66
|
+
const time = formatTime(obs.timestamp);
|
|
67
|
+
const icon = getTypeIcon(obs.type);
|
|
68
|
+
const tool = obs.tool_name || 'unknown';
|
|
69
|
+
const summary = truncate(obs.narrative || obs.summary || `${tool} operation`, 50);
|
|
70
|
+
|
|
71
|
+
console.log(`\x1b[90m${time}\x1b[0m ${icon} \x1b[1m#${obs.id}\x1b[0m [\x1b[33m${tool}\x1b[0m] ${summary}`);
|
|
72
|
+
lastId = obs.id;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check for new summaries
|
|
76
|
+
const newSummaries = db.prepare(`
|
|
77
|
+
SELECT id, created_at, request, learned, completed, next_steps
|
|
78
|
+
FROM summaries
|
|
79
|
+
WHERE id > ?
|
|
80
|
+
ORDER BY id ASC
|
|
81
|
+
`).all(lastSummaryId);
|
|
82
|
+
|
|
83
|
+
for (const sum of newSummaries) {
|
|
84
|
+
const time = formatTime(sum.created_at);
|
|
85
|
+
console.log('');
|
|
86
|
+
console.log(`\x1b[90m${time}\x1b[0m \x1b[32m📋 Session Summary #${sum.id}\x1b[0m`);
|
|
87
|
+
if (sum.request) console.log(` \x1b[36m请求:\x1b[0m ${truncate(sum.request, 70)}`);
|
|
88
|
+
if (sum.learned) console.log(` \x1b[35m学到:\x1b[0m ${truncate(sum.learned, 70)}`);
|
|
89
|
+
if (sum.completed) console.log(` \x1b[32m完成:\x1b[0m ${truncate(sum.completed, 70)}`);
|
|
90
|
+
if (sum.next_steps) console.log(` \x1b[33m下步:\x1b[0m ${truncate(sum.next_steps, 70)}`);
|
|
91
|
+
console.log('');
|
|
92
|
+
lastSummaryId = sum.id;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
} catch (err) {
|
|
96
|
+
// Database might be locked, retry next interval
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Start polling
|
|
101
|
+
const interval = setInterval(checkNewObservations, POLL_INTERVAL);
|
|
102
|
+
|
|
103
|
+
// Handle graceful shutdown
|
|
104
|
+
process.on('SIGINT', () => {
|
|
105
|
+
console.log('\n\x1b[90m监控已停止\x1b[0m');
|
|
106
|
+
clearInterval(interval);
|
|
107
|
+
db.close();
|
|
108
|
+
process.exit(0);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Initial check
|
|
112
|
+
checkNewObservations();
|
package/package.json
CHANGED
|
@@ -1,58 +1,86 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-mem",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "Persistent memory system for OpenClaw -
|
|
3
|
+
"version": "1.3.0",
|
|
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",
|
|
24
|
+
"backfill-embeddings": "node backfill-embeddings.js"
|
|
16
25
|
},
|
|
17
26
|
"keywords": [
|
|
18
27
|
"openclaw",
|
|
19
28
|
"memory",
|
|
20
|
-
"agent",
|
|
21
|
-
"llm",
|
|
22
29
|
"ai",
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"context"
|
|
30
|
+
"llm",
|
|
31
|
+
"context",
|
|
32
|
+
"persistence",
|
|
33
|
+
"mcp",
|
|
34
|
+
"model-context-protocol",
|
|
35
|
+
"sqlite",
|
|
36
|
+
"hooks"
|
|
28
37
|
],
|
|
29
|
-
"author": "
|
|
38
|
+
"author": "Aaron",
|
|
30
39
|
"license": "MIT",
|
|
31
40
|
"repository": {
|
|
32
41
|
"type": "git",
|
|
33
42
|
"url": "git+https://github.com/wenyupapa-sys/openclaw-mem.git"
|
|
34
43
|
},
|
|
35
|
-
"homepage": "https://github.com/wenyupapa-sys/openclaw-mem#readme",
|
|
36
44
|
"bugs": {
|
|
37
45
|
"url": "https://github.com/wenyupapa-sys/openclaw-mem/issues"
|
|
38
46
|
},
|
|
47
|
+
"homepage": "https://github.com/wenyupapa-sys/openclaw-mem#readme",
|
|
39
48
|
"engines": {
|
|
40
49
|
"node": ">=18.0.0"
|
|
41
50
|
},
|
|
42
|
-
"dependencies": {
|
|
43
|
-
"better-sqlite3": "^11.0.0"
|
|
44
|
-
},
|
|
45
51
|
"files": [
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
52
|
+
"handler.js",
|
|
53
|
+
"database.js",
|
|
54
|
+
"context-builder.js",
|
|
55
|
+
"extractor.js",
|
|
56
|
+
"gateway-llm.js",
|
|
57
|
+
"mcp-server.js",
|
|
58
|
+
"mcp-http-api.js",
|
|
59
|
+
"mem-search.sh",
|
|
60
|
+
"mem-get.sh",
|
|
61
|
+
"monitor.js",
|
|
62
|
+
"debug-logger.js",
|
|
63
|
+
"realtime-monitor.js",
|
|
64
|
+
"session-watcher.js",
|
|
65
|
+
"sync-recent.js",
|
|
66
|
+
"setup.js",
|
|
67
|
+
"backfill-embeddings.js",
|
|
68
|
+
"HOOK.md",
|
|
69
|
+
"MCP.json",
|
|
70
|
+
"README.md"
|
|
54
71
|
],
|
|
72
|
+
"dependencies": {
|
|
73
|
+
"@huggingface/transformers": "^3.8.1",
|
|
74
|
+
"@modelcontextprotocol/sdk": "^1.25.3",
|
|
75
|
+
"better-sqlite3": "^12.6.2",
|
|
76
|
+
"sqlite-vec": "^0.1.7-alpha.2"
|
|
77
|
+
},
|
|
78
|
+
"devDependencies": {
|
|
79
|
+
"vitest": "^2.0.0"
|
|
80
|
+
},
|
|
55
81
|
"openclaw": {
|
|
56
|
-
"hooks": [
|
|
82
|
+
"hooks": [
|
|
83
|
+
"."
|
|
84
|
+
]
|
|
57
85
|
}
|
|
58
86
|
}
|
|
@@ -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();
|