Initial commit: Agent Sessions Viewer
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
const express = require('express');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const readline = require('readline');
|
||||
|
||||
const app = express();
|
||||
const PORT = 16039;
|
||||
const AGENTS_DIR = '/home/openclaw/.openclaw/agents';
|
||||
|
||||
// 静态文件
|
||||
app.use(express.static('public'));
|
||||
|
||||
// API: 获取所有智能体
|
||||
app.get('/api/agents', async (req, res) => {
|
||||
try {
|
||||
const agents = [];
|
||||
const dirs = fs.readdirSync(AGENTS_DIR, { withFileTypes: true });
|
||||
|
||||
for (const dir of dirs) {
|
||||
if (dir.isDirectory()) {
|
||||
const agentPath = path.join(AGENTS_DIR, dir.name);
|
||||
const sessionsPath = path.join(agentPath, 'sessions');
|
||||
|
||||
if (fs.existsSync(sessionsPath)) {
|
||||
const sessionsFile = path.join(sessionsPath, 'sessions.json');
|
||||
let sessionCount = 0;
|
||||
let lastActive = null;
|
||||
|
||||
if (fs.existsSync(sessionsFile)) {
|
||||
const sessionsData = JSON.parse(fs.readFileSync(sessionsFile, 'utf8'));
|
||||
const sessionKeys = Object.keys(sessionsData);
|
||||
sessionCount = sessionKeys.length;
|
||||
|
||||
// 找到最近的活跃时间
|
||||
for (const key of sessionKeys) {
|
||||
const session = sessionsData[key];
|
||||
if (session.updatedAt) {
|
||||
if (!lastActive || session.updatedAt > lastActive) {
|
||||
lastActive = session.updatedAt;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
agents.push({
|
||||
name: dir.name,
|
||||
path: agentPath,
|
||||
sessionCount,
|
||||
lastActive: lastActive ? new Date(lastActive).toISOString() : null
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 按最后活跃时间排序
|
||||
agents.sort((a, b) => {
|
||||
if (!a.lastActive) return 1;
|
||||
if (!b.lastActive) return -1;
|
||||
return new Date(b.lastActive) - new Date(a.lastActive);
|
||||
});
|
||||
|
||||
res.json(agents);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// API: 获取智能体的会话列表
|
||||
app.get('/api/agents/:agentName/sessions', async (req, res) => {
|
||||
try {
|
||||
const { agentName } = req.params;
|
||||
const sessionsPath = path.join(AGENTS_DIR, agentName, 'sessions');
|
||||
const sessionsFile = path.join(sessionsPath, 'sessions.json');
|
||||
|
||||
if (!fs.existsSync(sessionsFile)) {
|
||||
return res.json([]);
|
||||
}
|
||||
|
||||
const sessionsData = JSON.parse(fs.readFileSync(sessionsFile, 'utf8'));
|
||||
const sessions = [];
|
||||
|
||||
for (const [key, session] of Object.entries(sessionsData)) {
|
||||
// 解析 sessionKey 格式
|
||||
// 例如: agent:hudi:matrix:direct:@huangzhuang_bro:matrix.tphai.com
|
||||
// 或者: agent:hudi:main
|
||||
const parts = key.split(':');
|
||||
let channel = 'unknown';
|
||||
let chatType = 'unknown';
|
||||
let target = 'unknown';
|
||||
|
||||
if (parts.length >= 3) {
|
||||
channel = parts[2] || 'unknown';
|
||||
if (parts.length >= 4) {
|
||||
chatType = parts[3] || 'unknown';
|
||||
if (parts.length >= 5) {
|
||||
target = parts.slice(4).join(':');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sessions.push({
|
||||
sessionKey: key,
|
||||
sessionId: session.sessionId,
|
||||
channel,
|
||||
chatType,
|
||||
target,
|
||||
status: session.status || 'unknown',
|
||||
model: session.model || 'unknown',
|
||||
modelProvider: session.modelProvider || 'unknown',
|
||||
updatedAt: session.updatedAt ? new Date(session.updatedAt).toISOString() : null,
|
||||
startedAt: session.startedAt ? new Date(session.startedAt).toISOString() : null,
|
||||
endedAt: session.endedAt ? new Date(session.endedAt).toISOString() : null,
|
||||
lastInteractionAt: session.lastInteractionAt ? new Date(session.lastInteractionAt).toISOString() : null,
|
||||
inputTokens: session.inputTokens || 0,
|
||||
outputTokens: session.outputTokens || 0,
|
||||
totalTokens: session.totalTokens || 0,
|
||||
sessionFile: session.sessionFile
|
||||
});
|
||||
}
|
||||
|
||||
// 按更新时间排序
|
||||
sessions.sort((a, b) => {
|
||||
if (!a.updatedAt) return 1;
|
||||
if (!b.updatedAt) return -1;
|
||||
return new Date(b.updatedAt) - new Date(a.updatedAt);
|
||||
});
|
||||
|
||||
res.json(sessions);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// API: 获取会话详情(消息列表)
|
||||
app.get('/api/agents/:agentName/sessions/:sessionId/messages', async (req, res) => {
|
||||
try {
|
||||
const { agentName, sessionId } = req.params;
|
||||
const sessionFile = path.join(AGENTS_DIR, agentName, 'sessions', `${sessionId}.jsonl`);
|
||||
|
||||
if (!fs.existsSync(sessionFile)) {
|
||||
return res.status(404).json({ error: 'Session file not found' });
|
||||
}
|
||||
|
||||
const messages = [];
|
||||
const fileStream = fs.createReadStream(sessionFile);
|
||||
const rl = readline.createInterface({
|
||||
input: fileStream,
|
||||
crlfDelay: Infinity
|
||||
});
|
||||
|
||||
for await (const line of rl) {
|
||||
if (line.trim()) {
|
||||
try {
|
||||
const data = JSON.parse(line);
|
||||
|
||||
// 只提取消息类型
|
||||
if (data.type === 'message' && data.message) {
|
||||
const content = data.message.content;
|
||||
let text = '';
|
||||
|
||||
if (typeof content === 'string') {
|
||||
text = content;
|
||||
} else if (Array.isArray(content)) {
|
||||
text = content.map(c => {
|
||||
if (typeof c === 'string') return c;
|
||||
if (c.type === 'text') return c.text;
|
||||
if (c.type === 'image_url') return '[Image]';
|
||||
return JSON.stringify(c);
|
||||
}).join('\n');
|
||||
}
|
||||
|
||||
messages.push({
|
||||
id: data.id,
|
||||
timestamp: data.timestamp,
|
||||
role: data.message.role,
|
||||
content: text.substring(0, 10000), // 限制内容长度
|
||||
fullContent: text.length > 10000
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// 跳过解析错误的行
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.json(messages);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// API: 获取会话完整消息
|
||||
app.get('/api/agents/:agentName/sessions/:sessionId/messages/:messageId', async (req, res) => {
|
||||
try {
|
||||
const { agentName, sessionId, messageId } = req.params;
|
||||
const sessionFile = path.join(AGENTS_DIR, agentName, 'sessions', `${sessionId}.jsonl`);
|
||||
|
||||
if (!fs.existsSync(sessionFile)) {
|
||||
return res.status(404).json({ error: 'Session file not found' });
|
||||
}
|
||||
|
||||
const fileStream = fs.createReadStream(sessionFile);
|
||||
const rl = readline.createInterface({
|
||||
input: fileStream,
|
||||
crlfDelay: Infinity
|
||||
});
|
||||
|
||||
for await (const line of rl) {
|
||||
if (line.trim()) {
|
||||
try {
|
||||
const data = JSON.parse(line);
|
||||
if (data.id === messageId && data.message) {
|
||||
const content = data.message.content;
|
||||
let text = '';
|
||||
|
||||
if (typeof content === 'string') {
|
||||
text = content;
|
||||
} else if (Array.isArray(content)) {
|
||||
text = content.map(c => {
|
||||
if (typeof c === 'string') return c;
|
||||
if (c.type === 'text') return c.text;
|
||||
return JSON.stringify(c, null, 2);
|
||||
}).join('\n');
|
||||
}
|
||||
|
||||
return res.json({
|
||||
id: data.id,
|
||||
timestamp: data.timestamp,
|
||||
role: data.message.role,
|
||||
content: text
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// 跳过
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.status(404).json({ error: 'Message not found' });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Agent Sessions Viewer running at http://localhost:${PORT}`);
|
||||
});
|
||||
Reference in New Issue
Block a user