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}`); });