feat: 新增对话汇总视图,支持跨 session 连续性分析
- 新增 /api/agents/:agentName/conversations API,按 sessionKey 分组汇总 - 新增 /api/agents/:agentName/conversations/:sessionKey/timeline API - 前端新增「对话汇总」Tab,展示对话级别的统计信息 - 支持展开查看对话下的所有 session 阶段 - 修复 session 文件不存在时返回 404 的问题 功能: - 显示每个对话的总消息数、Token 数、压缩次数、时长 - 标注原始 session 和压缩后的 session - 显示对话时间跨度
This commit is contained in:
+157
-38
@@ -1,6 +1,9 @@
|
||||
// 状态
|
||||
let currentAgent = null;
|
||||
let currentSession = null;
|
||||
let currentMessage = null;
|
||||
let currentView = 'conversations'; // 'conversations' or 'sessions'
|
||||
let conversationsData = []; // 缓存对话汇总数据
|
||||
|
||||
// API 调用
|
||||
async function fetchAPI(endpoint) {
|
||||
@@ -45,8 +48,9 @@ function formatNumber(num) {
|
||||
}
|
||||
|
||||
// 截断文本
|
||||
function truncate(text, maxLen = 100) {
|
||||
function truncate(text, maxLen = 50) {
|
||||
if (!text) return '';
|
||||
text = text.replace(/\n/g, ' ').trim();
|
||||
if (text.length <= maxLen) return text;
|
||||
return text.substring(0, maxLen) + '...';
|
||||
}
|
||||
@@ -79,21 +83,38 @@ async function loadAgents() {
|
||||
}
|
||||
}
|
||||
|
||||
// 切换视图
|
||||
function switchView(view) {
|
||||
currentView = view;
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.view === view);
|
||||
});
|
||||
document.querySelectorAll('.view-content').forEach(content => {
|
||||
content.classList.remove('active');
|
||||
});
|
||||
document.getElementById(`${view}-view`).classList.add('active');
|
||||
}
|
||||
|
||||
// 选择智能体
|
||||
async function selectAgent(agentName) {
|
||||
currentAgent = agentName;
|
||||
currentSession = null;
|
||||
currentMessage = null;
|
||||
|
||||
// 更新UI
|
||||
document.querySelectorAll('.agent-item').forEach(el => el.classList.remove('active'));
|
||||
event.target.closest('.agent-item')?.classList.add('active');
|
||||
|
||||
document.getElementById('agent-name-display').textContent = `- ${agentName}`;
|
||||
document.getElementById('agent-name-display').textContent = `🤖 ${agentName}`;
|
||||
document.getElementById('session-id-display').textContent = '';
|
||||
document.getElementById('messages-list').innerHTML = '<div class="placeholder">请选择一个会话</div>';
|
||||
document.getElementById('message-detail').innerHTML = '<div class="placeholder">请选择一条消息</div>';
|
||||
|
||||
// 加载会话列表
|
||||
await loadSessions(agentName);
|
||||
// 同时加载对话汇总和会话列表
|
||||
await Promise.all([
|
||||
loadConversations(agentName),
|
||||
loadSessions(agentName)
|
||||
]);
|
||||
}
|
||||
|
||||
// 加载会话列表
|
||||
@@ -112,13 +133,15 @@ async function loadSessions(agentName) {
|
||||
container.innerHTML = sessions.map(session => `
|
||||
<div class="session-item ${currentSession === session.sessionId ? 'active' : ''}"
|
||||
onclick="selectSession('${agentName}', '${session.sessionId}')">
|
||||
<div class="channel ${session.channel}">${session.channel}</div>
|
||||
<span class="status ${session.status}">${session.status}</span>
|
||||
<div class="session-header">
|
||||
<span class="channel ${session.channel}">${session.channel}</span>
|
||||
<span class="status ${session.status}">${session.status}</span>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<span class="model">${session.model}</span>
|
||||
<span>${formatTime(session.updatedAt)}</span>
|
||||
</div>
|
||||
<div class="target">${truncate(session.target, 50)}</div>
|
||||
<div class="target">${truncate(session.target, 40)}</div>
|
||||
<div class="tokens">
|
||||
📊 ${formatNumber(session.totalTokens)} tokens
|
||||
(${formatNumber(session.inputTokens)} in / ${formatNumber(session.outputTokens)} out)
|
||||
@@ -130,22 +153,24 @@ async function loadSessions(agentName) {
|
||||
}
|
||||
}
|
||||
|
||||
// 选择会话
|
||||
// 选择会话 - 显示消息概览列表
|
||||
async function selectSession(agentName, sessionId) {
|
||||
currentSession = sessionId;
|
||||
currentMessage = null;
|
||||
|
||||
// 更新UI
|
||||
document.querySelectorAll('.session-item').forEach(el => el.classList.remove('active'));
|
||||
event.target.closest('.session-item')?.classList.add('active');
|
||||
|
||||
document.getElementById('session-id-display').textContent = `- ${sessionId.substring(0, 8)}...`;
|
||||
document.getElementById('message-detail').innerHTML = '<div class="placeholder">请选择一条消息查看详情</div>';
|
||||
|
||||
// 加载消息
|
||||
await loadMessages(agentName, sessionId);
|
||||
// 加载消息概览列表
|
||||
await loadMessageSummaries(agentName, sessionId);
|
||||
}
|
||||
|
||||
// 加载消息
|
||||
async function loadMessages(agentName, sessionId) {
|
||||
// 加载消息概览列表(不加载完整内容)
|
||||
async function loadMessageSummaries(agentName, sessionId) {
|
||||
const container = document.getElementById('messages-list');
|
||||
container.innerHTML = '<div class="loading">加载中...</div>';
|
||||
|
||||
@@ -158,13 +183,16 @@ async function loadMessages(agentName, sessionId) {
|
||||
}
|
||||
|
||||
container.innerHTML = messages.map((msg, index) => `
|
||||
<div class="message-item ${msg.role}" data-agent="${agentName}" data-session="${sessionId}" data-msg-id="${msg.id}">
|
||||
<div class="header">
|
||||
<span class="role ${msg.role}">${msg.role}</span>
|
||||
<span class="time">${formatFullTime(msg.timestamp)}</span>
|
||||
<div class="message-summary ${msg.role} ${currentMessage === msg.id ? 'active' : ''}"
|
||||
onclick="selectMessage('${agentName}', '${sessionId}', '${msg.id}')">
|
||||
<div class="summary-header">
|
||||
<span class="role-badge ${msg.role}">${msg.role === 'user' ? '👤' : '🤖'} ${msg.role}</span>
|
||||
<span class="time">${formatTime(msg.timestamp)}</span>
|
||||
</div>
|
||||
<div class="summary-text">${escapeHtml(msg.summary)}${msg.hasMoreContent ? '...' : ''}</div>
|
||||
<div class="summary-meta">
|
||||
${msg.hasMoreContent ? `<span class="more-indicator">📝 ${formatNumber(msg.contentLength)} 字符</span>` : ''}
|
||||
</div>
|
||||
<div class="content ${msg.fullContent ? 'truncated' : ''}">${escapeHtml(msg.content)}</div>
|
||||
${msg.fullContent ? `<button class="expand-btn" onclick="showFullMessage('${agentName}', '${sessionId}', '${msg.id}')">查看完整内容</button>` : ''}
|
||||
</div>
|
||||
`).join('');
|
||||
} catch (error) {
|
||||
@@ -172,28 +200,42 @@ async function loadMessages(agentName, sessionId) {
|
||||
}
|
||||
}
|
||||
|
||||
// 显示完整消息
|
||||
async function showFullMessage(agentName, sessionId, messageId) {
|
||||
const modal = document.getElementById('message-modal');
|
||||
const content = document.getElementById('modal-content');
|
||||
// 选择消息 - 显示完整内容
|
||||
async function selectMessage(agentName, sessionId, messageId) {
|
||||
currentMessage = messageId;
|
||||
|
||||
modal.classList.remove('hidden');
|
||||
content.textContent = '加载中...';
|
||||
// 更新UI
|
||||
document.querySelectorAll('.message-summary').forEach(el => el.classList.remove('active'));
|
||||
event.target.closest('.message-summary')?.classList.add('active');
|
||||
|
||||
// 加载完整消息内容
|
||||
await loadMessageDetail(agentName, sessionId, messageId);
|
||||
}
|
||||
|
||||
// 加载消息详情(完整内容)
|
||||
async function loadMessageDetail(agentName, sessionId, messageId) {
|
||||
const container = document.getElementById('message-detail');
|
||||
container.innerHTML = '<div class="loading">加载中...</div>';
|
||||
|
||||
try {
|
||||
const msg = await fetchAPI(`/agents/${agentName}/sessions/${sessionId}/messages/${messageId}`);
|
||||
document.getElementById('modal-title').textContent = `${msg.role.toUpperCase()} - ${formatFullTime(msg.timestamp)}`;
|
||||
content.textContent = msg.content;
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="message-detail-content ${msg.role}">
|
||||
<div class="detail-header">
|
||||
<span class="role-badge large ${msg.role}">${msg.role === 'user' ? '👤' : '🤖'} ${msg.role}</span>
|
||||
<span class="time">${formatFullTime(msg.timestamp)}</span>
|
||||
</div>
|
||||
<div class="detail-body">
|
||||
<pre class="content-text">${escapeHtml(msg.content)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} catch (error) {
|
||||
content.textContent = `加载失败: ${error.message}`;
|
||||
container.innerHTML = `<div class="placeholder">加载失败: ${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭模态框
|
||||
function closeModal() {
|
||||
document.getElementById('message-modal').classList.add('hidden');
|
||||
}
|
||||
|
||||
// HTML 转义
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
@@ -204,16 +246,93 @@ function escapeHtml(text) {
|
||||
// 键盘事件
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
closeModal();
|
||||
// 可以添加关闭操作
|
||||
}
|
||||
});
|
||||
|
||||
// 点击模态框外部关闭
|
||||
document.getElementById('message-modal').addEventListener('click', (e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
closeModal();
|
||||
// 加载对话汇总
|
||||
async function loadConversations(agentName) {
|
||||
const container = document.getElementById('conversations-list');
|
||||
container.innerHTML = '<div class="loading">加载中...</div>';
|
||||
|
||||
try {
|
||||
const conversations = await fetchAPI(`/agents/${agentName}/conversations`);
|
||||
conversationsData = conversations;
|
||||
|
||||
if (conversations.length === 0) {
|
||||
container.innerHTML = '<div class="placeholder">没有找到对话</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = conversations.map((conv, index) => `
|
||||
<div class="conversation-item" onclick="showConversationDetail(${index})">
|
||||
<div class="conv-header">
|
||||
<span class="conv-title">
|
||||
💬 对话 ${index + 1}
|
||||
<span class="conv-badge">${conv.sessionCount} 个会话</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="conv-stats">
|
||||
<span class="stat">📨 消息: <span class="stat-value">${conv.totalMessages}</span></span>
|
||||
<span class="stat">📊 Token: <span class="stat-value">${formatNumber(conv.totalTokens)}</span></span>
|
||||
<span class="stat">🔄 压缩: <span class="stat-value">${conv.compactionCount}</span></span>
|
||||
<span class="stat">⏱️ 时长: <span class="stat-value">${conv.durationHours || '?'}h</span></span>
|
||||
</div>
|
||||
<div class="conv-target">${conv.channel} · ${conv.chatType} · ${truncate(conv.target, 30)}</div>
|
||||
<div class="conv-time">${formatTime(conv.firstStartedAt)} → ${formatTime(conv.lastUpdatedAt)}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
} catch (error) {
|
||||
container.innerHTML = `<div class="placeholder">加载失败: ${error.message}</div>`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 显示对话详情(包含该对话下的所有 session)
|
||||
function showConversationDetail(index) {
|
||||
const conv = conversationsData[index];
|
||||
if (!conv) return;
|
||||
|
||||
// 高亮选中的对话
|
||||
document.querySelectorAll('.conversation-item').forEach((el, i) => {
|
||||
el.classList.toggle('active', i === index);
|
||||
});
|
||||
|
||||
// 切换到会话视图并显示该对话下的 session
|
||||
switchView('sessions');
|
||||
|
||||
const container = document.getElementById('sessions-list');
|
||||
container.innerHTML = `
|
||||
<div class="conv-sessions-header">
|
||||
<span class="back-btn" onclick="backToConversations()">← 返回</span>
|
||||
💬 ${conv.channel} · ${conv.chatType}
|
||||
</div>
|
||||
${conv.sessions.map((session, sIndex) => `
|
||||
<div class="session-item" onclick="selectSession('${currentAgent}', '${session.sessionId}')">
|
||||
<div class="session-header">
|
||||
<span class="channel ${conv.channel}">${conv.channel}</span>
|
||||
<span class="status ${session.status}">${session.status}</span>
|
||||
${sIndex === 0 ? '<span class="conv-badge">原始</span>' : `<span class="conv-badge">压缩 #${sIndex}</span>`}
|
||||
</div>
|
||||
<div class="meta">
|
||||
<span class="model">${session.model}</span>
|
||||
<span>${formatTime(session.startedAt)}</span>
|
||||
</div>
|
||||
<div class="tokens">
|
||||
📊 ${formatNumber(session.totalTokens)} tokens · 📨 ${session.messageCount} 条消息
|
||||
</div>
|
||||
<div class="conv-time">
|
||||
${formatTime(session.startedAt)} → ${formatTime(session.updatedAt)}
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
`;
|
||||
}
|
||||
|
||||
// 返回对话汇总
|
||||
function backToConversations() {
|
||||
switchView('conversations');
|
||||
document.querySelectorAll('.conversation-item').forEach(el => el.classList.remove('active'));
|
||||
}
|
||||
|
||||
// 初始化
|
||||
loadAgents();
|
||||
Reference in New Issue
Block a user