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 格式 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 || session.authProfileOverride || '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`); // 如果文件不存在,返回空数组而不是 404 if (!fs.existsSync(sessionFile)) { return res.json([]); } 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'); } // 生成摘要(前150字符) const summary = text.substring(0, 150); const hasMoreContent = text.length > 150; messages.push({ id: data.id, timestamp: data.timestamp, role: data.message.role, summary: summary, hasMoreContent: hasMoreContent, contentLength: text.length }); } } 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`); // 如果文件不存在,返回 404 if (!fs.existsSync(sessionFile)) { return res.status(404).json({ error: 'Session file not found (may have been reset or deleted)' }); } 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 }); } }); // API: 获取智能体的"对话"列表(按 sessionKey 分组,包含连续性信息) app.get('/api/agents/:agentName/conversations', 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 conversations = {}; // 按 sessionKey 分组 for (const [sessionKey, session] of Object.entries(sessionsData)) { if (!conversations[sessionKey]) { // 解析 sessionKey 获取元信息 const parts = sessionKey.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(':'); } } } conversations[sessionKey] = { sessionKey, channel, chatType, target, sessions: [], totalTokens: 0, totalInputTokens: 0, totalOutputTokens: 0, totalMessages: 0, firstStartedAt: null, lastUpdatedAt: null, compactionCount: 0 }; } const conv = conversations[sessionKey]; // 获取 session 文件中的消息数 let messageCount = 0; const sessionFile = session.sessionFile; if (sessionFile && fs.existsSync(sessionFile)) { try { const content = fs.readFileSync(sessionFile, 'utf8'); const lines = content.trim().split('\n'); messageCount = lines.filter(line => { try { const d = JSON.parse(line); return d.type === 'message'; } catch { return false; } }).length; } catch { messageCount = 0; } } conv.sessions.push({ sessionId: session.sessionId, status: session.status || 'unknown', model: session.model || session.authProfileOverride || 'unknown', startedAt: session.sessionStartedAt ? new Date(session.sessionStartedAt).toISOString() : null, updatedAt: session.updatedAt ? new Date(session.updatedAt).toISOString() : null, lastInteractionAt: session.lastInteractionAt ? new Date(session.lastInteractionAt).toISOString() : null, inputTokens: session.inputTokens || 0, outputTokens: session.outputTokens || 0, totalTokens: session.totalTokens || 0, compactionCount: session.compactionCount || 0, messageCount, sessionFile: session.sessionFile }); conv.totalTokens += session.totalTokens || 0; conv.totalInputTokens += session.inputTokens || 0; conv.totalOutputTokens += session.outputTokens || 0; conv.totalMessages += messageCount; conv.compactionCount = Math.max(conv.compactionCount, session.compactionCount || 0); // 更新最早开始时间和最新更新时间 const startedAt = session.sessionStartedAt ? new Date(session.sessionStartedAt) : null; const updatedAt = session.updatedAt ? new Date(session.updatedAt) : null; if (startedAt && (!conv.firstStartedAt || startedAt < new Date(conv.firstStartedAt))) { conv.firstStartedAt = startedAt.toISOString(); } if (updatedAt && (!conv.lastUpdatedAt || updatedAt > new Date(conv.lastUpdatedAt))) { conv.lastUpdatedAt = updatedAt.toISOString(); } } // 转换为数组并排序 const result = Object.values(conversations).map(conv => { // 按时间排序 sessions conv.sessions.sort((a, b) => { if (!a.startedAt) return 1; if (!b.startedAt) return -1; return new Date(a.startedAt) - new Date(b.startedAt); }); return { ...conv, sessionCount: conv.sessions.length, durationHours: conv.firstStartedAt && conv.lastUpdatedAt ? ((new Date(conv.lastUpdatedAt) - new Date(conv.firstStartedAt)) / 3600000).toFixed(1) : null }; }); // 按最后活跃时间排序 result.sort((a, b) => { if (!a.lastUpdatedAt) return 1; if (!b.lastUpdatedAt) return -1; return new Date(b.lastUpdatedAt) - new Date(a.lastUpdatedAt); }); res.json(result); } catch (error) { res.status(500).json({ error: error.message }); } }); // API: 获取对话的详细信息(包含所有 session 的消息时间线) app.get('/api/agents/:agentName/conversations/:sessionKey/timeline', async (req, res) => { try { const { agentName, sessionKey } = req.params; const sessionsPath = path.join(AGENTS_DIR, agentName, 'sessions'); const sessionsFile = path.join(sessionsPath, 'sessions.json'); if (!fs.existsSync(sessionsFile)) { return res.status(404).json({ error: 'Sessions file not found' }); } const sessionsData = JSON.parse(fs.readFileSync(sessionsFile, 'utf8')); const session = sessionsData[sessionKey]; if (!session) { return res.status(404).json({ error: 'Conversation not found' }); } // 获取 session 文件中的消息时间线 const timeline = []; const sessionFile = session.sessionFile; if (sessionFile && fs.existsSync(sessionFile)) { const content = fs.readFileSync(sessionFile, 'utf8'); const lines = content.trim().split('\n'); for (const line of lines) { 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 ''; }).filter(Boolean).join('\n'); } timeline.push({ id: data.id, timestamp: data.timestamp, role: data.message.role, type: 'message', summary: text.substring(0, 100), contentLength: text.length }); } else if (data.type === 'session') { timeline.push({ id: data.id, timestamp: data.timestamp, type: 'session_start', summary: 'Session started' }); } else if (data.type === 'custom' && data.customType === 'compaction') { timeline.push({ id: data.id, timestamp: data.timestamp, type: 'compaction', summary: 'Context compacted' }); } } catch { /* skip */ } } } } // 按时间排序 timeline.sort((a, b) => { if (!a.timestamp) return 1; if (!b.timestamp) return -1; return new Date(a.timestamp) - new Date(b.timestamp); }); res.json({ sessionKey, sessionId: session.sessionId, timeline }); } catch (error) { res.status(500).json({ error: error.message }); } }); app.listen(PORT, () => { console.log(`Agent Sessions Viewer running at http://localhost:${PORT}`); });