核心 dashboard.js 缩减至266行,只保留:
- TSPDashboard class 定义(constructor、init、bindEvents、switchTab)
- 分页组件、缓存、状态管理、自动刷新、WebSocket、i18n
功能模块拆分到 modules/ 目录:
- chat.js (207行) 智能对话
- agent.js (424行) Agent管理
- alerts.js (348行) 预警管理
- knowledge.js (699行) 知识库管理
- workorders.js (约500行) 工单管理
- conversations.js (671行) 对话历史
- monitoring.js (455行) Token/AI监控
- system.js (1388行) 系统优化+设置+数据分析图表
- tenants.js (165行) 租户管理
- utils.js (573行) 工具函数
- dashboard-home.js (286行) 仪表板首页
- feishu-sync.js (698行) 飞书同步管理器
拆分方式:Object.assign(TSPDashboard.prototype, {...})
this 引用完全不会断,所有模块方法互相调用正常
672 lines
31 KiB
JavaScript
672 lines
31 KiB
JavaScript
// 对话历史模块
|
|
Object.assign(TSPDashboard.prototype, {
|
|
async loadConversationTenantList() {
|
|
this.conversationCurrentTenantId = null;
|
|
this.renderConversationBreadcrumb(null);
|
|
|
|
// 加载全局统计
|
|
this.loadConversationStats(null);
|
|
|
|
// 显示租户列表容器,隐藏详情容器
|
|
const tenantListEl = document.getElementById('conversation-tenant-list');
|
|
const tenantDetailEl = document.getElementById('conversation-tenant-detail');
|
|
|
|
if (tenantListEl) tenantListEl.style.display = '';
|
|
if (tenantDetailEl) tenantDetailEl.style.display = 'none';
|
|
|
|
// 显示加载中 spinner
|
|
if (tenantListEl) {
|
|
tenantListEl.innerHTML = '<div class="col-12 text-center py-4"><i class="fas fa-spinner fa-spin fa-2x"></i><p class="mt-2 text-muted">加载中...</p></div>';
|
|
}
|
|
|
|
try {
|
|
const response = await fetch('/api/conversations/tenants');
|
|
const tenants = await response.json();
|
|
|
|
if (!Array.isArray(tenants) || tenants.length === 0) {
|
|
if (tenantListEl) {
|
|
tenantListEl.innerHTML = '<div class="col-12 text-center py-4"><i class="fas fa-comments fa-2x text-muted"></i><p class="mt-2 text-muted">暂无对话会话数据</p></div>';
|
|
}
|
|
return;
|
|
}
|
|
|
|
const cardsHtml = tenants.map(t => {
|
|
const lastActive = t.last_active_time ? new Date(t.last_active_time).toLocaleString() : '无';
|
|
return `
|
|
<div class="col-md-4 col-sm-6 mb-3">
|
|
<div class="card h-100 shadow-sm" style="cursor:pointer" onclick="dashboard.loadConversationTenantDetail('${t.tenant_id}')">
|
|
<div class="card-body">
|
|
<h6 class="card-title"><i class="fas fa-building me-2"></i>${t.tenant_id}</h6>
|
|
<div class="d-flex justify-content-between mt-3">
|
|
<div>
|
|
<small class="text-muted">会话总数</small>
|
|
<h5 class="mb-0">${t.session_count}</h5>
|
|
</div>
|
|
<div>
|
|
<small class="text-muted">消息总数</small>
|
|
<h5 class="mb-0">${t.message_count}</h5>
|
|
</div>
|
|
<div>
|
|
<small class="text-muted">活跃会话</small>
|
|
<h5 class="mb-0 text-success">${t.active_session_count}</h5>
|
|
</div>
|
|
</div>
|
|
<div class="mt-3">
|
|
<small class="text-muted"><i class="fas fa-clock me-1"></i>最近活跃: ${lastActive}</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
|
|
if (tenantListEl) {
|
|
tenantListEl.innerHTML = cardsHtml;
|
|
}
|
|
} catch (error) {
|
|
console.error('加载对话租户列表失败:', error);
|
|
if (tenantListEl) {
|
|
tenantListEl.innerHTML = '<div class="col-12 text-center py-4 text-danger"><i class="fas fa-exclamation-triangle"></i> 加载失败</div>';
|
|
}
|
|
this.showNotification('加载对话租户列表失败', 'error');
|
|
}
|
|
},
|
|
|
|
// Task 7.1: 加载对话历史租户详情视图
|
|
async loadConversationTenantDetail(tenantId, page = 1) {
|
|
this.conversationCurrentTenantId = tenantId;
|
|
this.paginationConfig.currentConversationPage = page;
|
|
|
|
// 隐藏租户列表,显示详情容器
|
|
const tenantListEl = document.getElementById('conversation-tenant-list');
|
|
const tenantDetailEl = document.getElementById('conversation-tenant-detail');
|
|
|
|
if (tenantListEl) tenantListEl.style.display = 'none';
|
|
if (tenantDetailEl) tenantDetailEl.style.display = '';
|
|
|
|
// 渲染面包屑(如果 renderConversationBreadcrumb 已实现)
|
|
if (typeof this.renderConversationBreadcrumb === 'function') {
|
|
this.renderConversationBreadcrumb(tenantId);
|
|
}
|
|
|
|
// 加载租户级统计(如果 loadConversationStats 已实现)
|
|
if (typeof this.loadConversationStats === 'function') {
|
|
this.loadConversationStats(tenantId);
|
|
}
|
|
|
|
// 显示加载中 spinner
|
|
const sessionListEl = document.getElementById('conversation-session-list');
|
|
if (sessionListEl) {
|
|
sessionListEl.innerHTML = '<div class="text-center py-4"><i class="fas fa-spinner fa-spin fa-2x"></i><p class="mt-2 text-muted">加载中...</p></div>';
|
|
}
|
|
|
|
try {
|
|
const perPage = this.getPageSize('conversation-session-pagination');
|
|
const statusFilter = document.getElementById('conversation-status-filter')?.value || '';
|
|
const dateFilter = document.getElementById('conversation-detail-date-filter')?.value || '';
|
|
|
|
let url = `/api/conversations/sessions?tenant_id=${encodeURIComponent(tenantId)}&page=${page}&per_page=${perPage}`;
|
|
if (statusFilter) url += `&status=${encodeURIComponent(statusFilter)}`;
|
|
if (dateFilter) url += `&date_filter=${encodeURIComponent(dateFilter)}`;
|
|
|
|
const response = await fetch(url);
|
|
const data = await response.json();
|
|
|
|
if (data.sessions) {
|
|
this.renderConversationSessionTable(data.sessions, tenantId);
|
|
this.updateConversationTenantPagination(data);
|
|
} else {
|
|
if (sessionListEl) {
|
|
sessionListEl.innerHTML = '<div class="text-center py-4"><i class="fas fa-comments fa-2x text-muted"></i><p class="mt-2 text-muted">暂无会话数据</p></div>';
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('加载租户会话列表失败:', error);
|
|
if (sessionListEl) {
|
|
sessionListEl.innerHTML = '<div class="text-center py-4 text-danger"><i class="fas fa-exclamation-triangle"></i> 加载失败</div>';
|
|
}
|
|
this.showNotification('加载租户会话列表失败', 'error');
|
|
}
|
|
},
|
|
|
|
// Task 7.1: 渲染会话表格
|
|
renderConversationSessionTable(sessions, tenantId) {
|
|
const sessionListEl = document.getElementById('conversation-session-list');
|
|
if (!sessionListEl) return;
|
|
|
|
if (!sessions || sessions.length === 0) {
|
|
sessionListEl.innerHTML = '<div class="text-center py-4"><i class="fas fa-comments fa-2x text-muted"></i><p class="mt-2 text-muted">暂无会话数据</p></div>';
|
|
return;
|
|
}
|
|
|
|
const statusBadge = (status) => {
|
|
if (status === 'active') return '<span class="badge bg-success">活跃</span>';
|
|
if (status === 'ended') return '<span class="badge bg-secondary">已结束</span>';
|
|
return `<span class="badge bg-info">${status || '未知'}</span>`;
|
|
};
|
|
|
|
const sourceBadge = (source) => {
|
|
if (source === 'feishu') return '<span class="badge bg-purple">飞书</span>';
|
|
if (source === 'websocket') return '<span class="badge bg-blue">WebSocket</span>';
|
|
if (source === 'api') return '<span class="badge bg-indigo">API</span>';
|
|
return `<span class="badge bg-secondary">${source || '未知'}</span>`;
|
|
};
|
|
|
|
const formatTime = (isoStr) => {
|
|
if (!isoStr) return '-';
|
|
return new Date(isoStr).toLocaleString();
|
|
};
|
|
|
|
const rowsHtml = sessions.map(s => `
|
|
<tr style="cursor:pointer" onclick="dashboard.viewSessionMessages('${s.session_id}', '${(s.title || '').replace(/'/g, "\\'")}')">
|
|
<td>${s.title || s.session_id || '-'}</td>
|
|
<td class="text-center">${s.message_count || 0}</td>
|
|
<td class="text-center">${statusBadge(s.status)}</td>
|
|
<td class="text-center">${sourceBadge(s.source)}</td>
|
|
<td>${formatTime(s.created_at)}</td>
|
|
<td>${formatTime(s.updated_at)}</td>
|
|
<td class="text-center" onclick="event.stopPropagation()">
|
|
<button class="btn btn-outline-danger btn-sm" onclick="dashboard.deleteConversationSession('${s.session_id}')">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
|
|
sessionListEl.innerHTML = `
|
|
<div class="table-responsive">
|
|
<table class="table table-hover table-striped mb-0">
|
|
<thead>
|
|
<tr>
|
|
<th>会话标题</th>
|
|
<th class="text-center">消息数</th>
|
|
<th class="text-center">状态</th>
|
|
<th class="text-center">来源</th>
|
|
<th>创建时间</th>
|
|
<th>更新时间</th>
|
|
<th class="text-center">操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${rowsHtml}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
`;
|
|
},
|
|
|
|
// Task 7.1: 查看会话消息详情
|
|
async viewSessionMessages(sessionId, sessionTitle) {
|
|
try {
|
|
const response = await fetch(`/api/conversations/sessions/${encodeURIComponent(sessionId)}`);
|
|
const data = await response.json();
|
|
|
|
if (data.success && data.messages) {
|
|
// 更新面包屑到第三层(如果已实现)
|
|
if (typeof this.renderConversationBreadcrumb === 'function') {
|
|
this.renderConversationBreadcrumb(this.conversationCurrentTenantId, sessionTitle || sessionId);
|
|
}
|
|
this.showSessionMessagesModal(data.session, data.messages);
|
|
} else {
|
|
throw new Error(data.error || '获取会话消息失败');
|
|
}
|
|
} catch (error) {
|
|
console.error('获取会话消息失败:', error);
|
|
this.showNotification('获取会话消息失败: ' + error.message, 'error');
|
|
}
|
|
},
|
|
|
|
// Task 7.1: 显示会话消息模态框
|
|
showSessionMessagesModal(session, messages) {
|
|
const messagesHtml = messages.map(msg => `
|
|
<div class="mb-3 p-3 border rounded">
|
|
<div class="mb-2">
|
|
<strong class="text-primary"><i class="fas fa-user me-1"></i>用户:</strong>
|
|
<div class="mt-1">${msg.user_message || ''}</div>
|
|
</div>
|
|
<div>
|
|
<strong class="text-success"><i class="fas fa-robot me-1"></i>助手:</strong>
|
|
<div class="mt-1">${msg.assistant_response || ''}</div>
|
|
</div>
|
|
<div class="mt-2 text-muted">
|
|
<small>
|
|
${msg.timestamp ? new Date(msg.timestamp).toLocaleString() : ''}
|
|
${msg.response_time ? ` | 响应: ${msg.response_time}s` : ''}
|
|
${msg.confidence_score ? ` | 置信度: ${Math.round(msg.confidence_score * 100)}%` : ''}
|
|
</small>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
const modalHtml = `
|
|
<div class="modal fade" id="sessionMessagesModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title"><i class="fas fa-comments me-2"></i>${session?.title || session?.session_id || '会话详情'}</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="mb-3 text-muted">
|
|
<small>
|
|
会话ID: ${session?.session_id || '-'} |
|
|
消息数: ${session?.message_count || 0} |
|
|
状态: ${session?.status || '-'} |
|
|
来源: ${session?.source || '-'}
|
|
</small>
|
|
</div>
|
|
${messages.length > 0 ? messagesHtml : '<div class="text-center text-muted py-3">暂无消息记录</div>'}
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// 移除已存在的模态框
|
|
const existingModal = document.getElementById('sessionMessagesModal');
|
|
if (existingModal) existingModal.remove();
|
|
|
|
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
|
const modal = new bootstrap.Modal(document.getElementById('sessionMessagesModal'));
|
|
modal.show();
|
|
|
|
// 模态框关闭后清理
|
|
document.getElementById('sessionMessagesModal').addEventListener('hidden.bs.modal', function () {
|
|
this.remove();
|
|
});
|
|
},
|
|
|
|
// Task 7.1: 对话租户详情分页
|
|
updateConversationTenantPagination(data) {
|
|
this.createPaginationComponent(data, 'conversation-session-pagination', 'loadConversationTenantDetailPage', '条会话');
|
|
},
|
|
|
|
// Task 7.2: 面包屑导航
|
|
renderConversationBreadcrumb(tenantId, sessionTitle) {
|
|
const breadcrumbEl = document.getElementById('conversation-breadcrumb');
|
|
if (!breadcrumbEl) return;
|
|
|
|
if (!tenantId) {
|
|
breadcrumbEl.innerHTML = '';
|
|
return;
|
|
}
|
|
|
|
if (!sessionTitle) {
|
|
// 租户详情视图: "对话历史 > {tenant_id}"
|
|
breadcrumbEl.innerHTML = `
|
|
<nav aria-label="breadcrumb">
|
|
<ol class="breadcrumb mb-0">
|
|
<li class="breadcrumb-item"><a href="#" onclick="dashboard.loadConversationTenantList(); return false;"><i class="fas fa-history me-1"></i>对话历史</a></li>
|
|
<li class="breadcrumb-item active" aria-current="page">${tenantId}</li>
|
|
</ol>
|
|
</nav>
|
|
`;
|
|
} else {
|
|
// 消息详情视图: "对话历史 > {tenant_id} > {session_title}"
|
|
breadcrumbEl.innerHTML = `
|
|
<nav aria-label="breadcrumb">
|
|
<ol class="breadcrumb mb-0">
|
|
<li class="breadcrumb-item"><a href="#" onclick="dashboard.loadConversationTenantList(); return false;"><i class="fas fa-history me-1"></i>对话历史</a></li>
|
|
<li class="breadcrumb-item"><a href="#" onclick="dashboard.loadConversationTenantDetail('${tenantId}'); return false;">${tenantId}</a></li>
|
|
<li class="breadcrumb-item active" aria-current="page">${sessionTitle}</li>
|
|
</ol>
|
|
</nav>
|
|
`;
|
|
}
|
|
|
|
this.conversationCurrentTenantId = tenantId;
|
|
},
|
|
|
|
// Task 7.1: 应用对话筛选条件
|
|
applyConversationFilters() {
|
|
if (this.conversationCurrentTenantId) {
|
|
this.loadConversationTenantDetail(this.conversationCurrentTenantId, 1);
|
|
}
|
|
},
|
|
|
|
// Task 7.3: 删除会话并处理空租户自动返回
|
|
async deleteConversationSession(sessionId) {
|
|
if (!confirm('确定要删除这个会话及其所有消息吗?')) return;
|
|
try {
|
|
const response = await fetch(`/api/conversations/sessions/${encodeURIComponent(sessionId)}`, { method: 'DELETE' });
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
this.showNotification('会话已删除', 'success');
|
|
// 刷新当前租户详情视图并检查是否还有剩余会话
|
|
if (this.conversationCurrentTenantId) {
|
|
const tenantId = this.conversationCurrentTenantId;
|
|
const perPage = this.getPageSize('conversation-session-pagination');
|
|
const checkUrl = `/api/conversations/sessions?tenant_id=${encodeURIComponent(tenantId)}&page=1&per_page=${perPage}`;
|
|
const checkResp = await fetch(checkUrl);
|
|
const checkData = await checkResp.json();
|
|
|
|
if (!checkData.sessions || checkData.sessions.length === 0 || checkData.total === 0) {
|
|
// 该租户下已无会话,自动返回租户列表视图
|
|
this.loadConversationTenantList();
|
|
} else {
|
|
// 仍有会话,刷新当前详情视图(调整页码避免越界)
|
|
const maxPage = checkData.total_pages || 1;
|
|
const currentPage = Math.min(this.paginationConfig.currentConversationPage, maxPage);
|
|
await this.loadConversationTenantDetail(tenantId, currentPage);
|
|
}
|
|
}
|
|
} else {
|
|
throw new Error(data.error || '删除失败');
|
|
}
|
|
} catch (error) {
|
|
console.error('删除会话失败:', error);
|
|
this.showNotification('删除会话失败: ' + error.message, 'error');
|
|
}
|
|
},
|
|
|
|
// 对话历史管理
|
|
async loadConversationHistory(page = 1) {
|
|
try {
|
|
const pageSize = this.getPageSize('conversations-pagination');
|
|
const response = await fetch(`/api/conversations?page=${page}&per_page=${pageSize}`);
|
|
const data = await response.json();
|
|
|
|
if (data.conversations) {
|
|
this.renderConversationList(data.conversations || []);
|
|
this.updateConversationPagination(data);
|
|
this.updateConversationStats(data.stats || {});
|
|
} else {
|
|
throw new Error(data.error || '加载对话历史失败');
|
|
}
|
|
} catch (error) {
|
|
console.error('加载对话历史失败:', error);
|
|
this.showNotification('加载对话历史失败: ' + error.message, 'error');
|
|
}
|
|
},
|
|
|
|
renderConversationList(conversations) {
|
|
const container = document.getElementById('conversation-list');
|
|
if (!conversations || conversations.length === 0) {
|
|
container.innerHTML = `
|
|
<div class="empty-state">
|
|
<i class="fas fa-comments"></i>
|
|
<p>暂无对话记录</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
const html = conversations.map(conv => {
|
|
// 识别来源和类型
|
|
let sourceBadge = '';
|
|
const method = conv.invocation_method || '';
|
|
const userId = conv.user_id || '';
|
|
|
|
// 判断是否来自飞书 (根据调用方式或ID格式)
|
|
if (method.includes('feishu') || userId.startsWith('ou_')) {
|
|
sourceBadge = '<span class="badge bg-purple me-1">飞书</span>';
|
|
} else {
|
|
sourceBadge = '<span class="badge bg-blue me-1">Web</span>';
|
|
}
|
|
|
|
// 判断群聊/私聊
|
|
let typeBadge = '';
|
|
if (method.includes('group')) {
|
|
typeBadge = '<span class="badge bg-azure me-1">群聊</span>';
|
|
} else if (method.includes('p2p')) {
|
|
typeBadge = '<span class="badge bg-indigo me-1">私聊</span>';
|
|
}
|
|
|
|
return `
|
|
<div class="card mb-3 conversation-item" data-conversation-id="${conv.id}">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-start mb-2">
|
|
<div>
|
|
<h6 class="mb-1" style="font-family: var(--font-family-primary); font-weight: 600;">
|
|
${sourceBadge}
|
|
${typeBadge}
|
|
用户: ${userId || '匿名'}
|
|
</h6>
|
|
<small class="text-muted">${new Date(conv.timestamp).toLocaleString()}</small>
|
|
</div>
|
|
<div class="btn-group btn-group-sm">
|
|
<button class="btn btn-outline-primary" onclick="dashboard.viewConversation(${conv.id})">
|
|
<i class="fas fa-eye"></i>
|
|
</button>
|
|
<button class="btn btn-outline-danger" onclick="dashboard.deleteConversation(${conv.id})">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="conversation-preview">
|
|
<p class="mb-1"><strong>用户:</strong> ${conv.user_message?.substring(0, 100)}${conv.user_message?.length > 100 ? '...' : ''}</p>
|
|
<p class="mb-0"><strong>助手:</strong> ${conv.assistant_response?.substring(0, 100)}${conv.assistant_response?.length > 100 ? '...' : ''}</p>
|
|
</div>
|
|
<div class="mt-2">
|
|
<span class="badge bg-secondary">响应时间: ${conv.response_time || 0}ms</span>
|
|
${conv.work_order_id ? `<span class="badge bg-info">工单: ${conv.work_order_id}</span>` : ''}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
|
|
container.innerHTML = html;
|
|
},
|
|
|
|
updateConversationPagination(data) {
|
|
this.createPaginationComponent(data, 'conversations-pagination', 'loadConversationHistory', '条对话');
|
|
},
|
|
|
|
updateConversationStats(stats) {
|
|
document.getElementById('conversation-total').textContent = stats.total || 0;
|
|
document.getElementById('conversation-today').textContent = stats.today || 0;
|
|
document.getElementById('conversation-avg-response').textContent = `${stats.avg_response_time || 0}ms`;
|
|
document.getElementById('conversation-active-users').textContent = stats.active_users || 0;
|
|
},
|
|
|
|
// Task 8.3: 加载对话统计(支持租户级别)
|
|
async loadConversationStats(tenantId) {
|
|
try {
|
|
let url = '/api/conversations/analytics';
|
|
if (tenantId) {
|
|
url += `?tenant_id=${encodeURIComponent(tenantId)}`;
|
|
}
|
|
const response = await fetch(url);
|
|
const data = await response.json();
|
|
|
|
const analytics = data.analytics || {};
|
|
const convStats = analytics.conversations || {};
|
|
|
|
const totalEl = document.getElementById('conversation-total');
|
|
const todayEl = document.getElementById('conversation-today');
|
|
const avgResponseEl = document.getElementById('conversation-avg-response');
|
|
const activeUsersEl = document.getElementById('conversation-active-users');
|
|
|
|
if (totalEl) totalEl.textContent = convStats.total || 0;
|
|
if (todayEl) todayEl.textContent = convStats.today || 0;
|
|
if (avgResponseEl) avgResponseEl.textContent = `${Math.round(convStats.avg_response_time || 0)}ms`;
|
|
if (activeUsersEl) activeUsersEl.textContent = convStats.active_users || 0;
|
|
} catch (error) {
|
|
console.error('加载对话统计失败:', error);
|
|
}
|
|
},
|
|
|
|
async refreshConversationHistory() {
|
|
// 先尝试触发一次合并迁移(幂等,重复调用也安全)
|
|
try {
|
|
await fetch('/api/conversations/migrate-merge', { method: 'POST' });
|
|
} catch (e) { /* 忽略迁移失败 */ }
|
|
// 根据当前视图状态刷新:租户详情视图或租户列表视图
|
|
if (this.conversationCurrentTenantId) {
|
|
await this.loadConversationTenantDetail(this.conversationCurrentTenantId, this.paginationConfig.currentConversationPage);
|
|
} else {
|
|
await this.loadConversationTenantList();
|
|
}
|
|
this.showNotification('对话历史已刷新', 'success');
|
|
},
|
|
|
|
async clearAllConversations() {
|
|
if (!confirm('确定要清空所有对话历史吗?此操作不可恢复!')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch('/api/conversations/clear', { method: 'DELETE' });
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
this.showNotification('对话历史已清空', 'success');
|
|
await this.loadConversationHistory();
|
|
} else {
|
|
throw new Error(data.error || '清空对话历史失败');
|
|
}
|
|
} catch (error) {
|
|
console.error('清空对话历史失败:', error);
|
|
this.showNotification('清空对话历史失败: ' + error.message, 'error');
|
|
}
|
|
},
|
|
|
|
async deleteConversation(conversationId) {
|
|
if (!confirm('确定要删除这条对话记录吗?')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`/api/conversations/${conversationId}`, { method: 'DELETE' });
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
this.showNotification('对话记录已删除', 'success');
|
|
await this.loadConversationHistory();
|
|
} else {
|
|
throw new Error(data.error || '删除对话记录失败');
|
|
}
|
|
} catch (error) {
|
|
console.error('删除对话记录失败:', error);
|
|
this.showNotification('删除对话记录失败: ' + error.message, 'error');
|
|
}
|
|
},
|
|
|
|
async viewConversation(conversationId) {
|
|
try {
|
|
const response = await fetch(`/api/conversations/${conversationId}`);
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
data.user_id = data.user_id || '匿名';
|
|
this.showConversationModal(data);
|
|
} else {
|
|
throw new Error(data.error || '获取对话详情失败');
|
|
}
|
|
} catch (error) {
|
|
console.error('获取对话详情失败:', error);
|
|
this.showNotification('获取对话详情失败: ' + error.message, 'error');
|
|
}
|
|
},
|
|
|
|
showConversationModal(conversation) {
|
|
const modalHtml = `
|
|
<div class="modal fade" id="conversationModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">对话详情</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<strong>用户:</strong> ${conversation.user_id || '匿名'}<br>
|
|
<strong>时间:</strong> ${new Date(conversation.timestamp).toLocaleString()}<br>
|
|
<strong>响应时间:</strong> ${conversation.response_time || 0}ms
|
|
</div>
|
|
<div class="mb-3">
|
|
<h6>用户消息:</h6>
|
|
<div class="border p-3 rounded">${conversation.user_message || ''}</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<h6>助手回复:</h6>
|
|
<div class="border p-3 rounded">${conversation.assistant_response || ''}</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// 移除已存在的模态框
|
|
const existingModal = document.getElementById('conversationModal');
|
|
if (existingModal) {
|
|
existingModal.remove();
|
|
}
|
|
|
|
// 添加新模态框
|
|
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
|
|
|
// 显示模态框
|
|
const modal = new bootstrap.Modal(document.getElementById('conversationModal'));
|
|
modal.show();
|
|
},
|
|
|
|
async filterConversations() {
|
|
const search = document.getElementById('conversation-search').value.trim();
|
|
const userFilter = document.getElementById('conversation-user-filter')?.value || '';
|
|
const dateFilter = document.getElementById('conversation-date-filter')?.value || '';
|
|
|
|
// Task 8.1: 在 Tenant_Detail_View 中搜索时自动附加 tenant_id 参数
|
|
if (this.conversationCurrentTenantId) {
|
|
if (!search) {
|
|
// 清空搜索时恢复当前租户的完整分页列表
|
|
this.loadConversationTenantDetail(this.conversationCurrentTenantId);
|
|
return;
|
|
}
|
|
try {
|
|
const perPage = this.getPageSize('conversation-session-pagination');
|
|
let url = `/api/conversations/sessions?search=${encodeURIComponent(search)}&tenant_id=${encodeURIComponent(this.conversationCurrentTenantId)}&page=1&per_page=${perPage}`;
|
|
if (dateFilter) url += `&date_filter=${encodeURIComponent(dateFilter)}`;
|
|
|
|
const response = await fetch(url);
|
|
const data = await response.json();
|
|
|
|
if (data.sessions) {
|
|
this.renderConversationSessionTable(data.sessions, this.conversationCurrentTenantId);
|
|
this.updateConversationTenantPagination(data);
|
|
} else {
|
|
const sessionListEl = document.getElementById('conversation-session-list');
|
|
if (sessionListEl) {
|
|
sessionListEl.innerHTML = '<div class="text-center py-4"><i class="fas fa-search fa-2x text-muted"></i><p class="mt-2 text-muted">未找到匹配的会话</p></div>';
|
|
}
|
|
const paginationEl = document.getElementById('conversation-session-pagination');
|
|
if (paginationEl) paginationEl.innerHTML = '';
|
|
}
|
|
} catch (error) {
|
|
console.error('搜索租户会话失败:', error);
|
|
this.showNotification('搜索会话失败: ' + error.message, 'error');
|
|
}
|
|
return;
|
|
}
|
|
|
|
// 非租户详情视图:保持原有行为
|
|
try {
|
|
const params = new URLSearchParams();
|
|
if (search) params.append('search', search);
|
|
if (userFilter) params.append('user_id', userFilter);
|
|
if (dateFilter) params.append('date_filter', dateFilter);
|
|
|
|
const response = await fetch(`/api/conversations?${params.toString()}`);
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
this.renderConversationList(data.conversations || []);
|
|
this.renderConversationPagination(data.pagination || {});
|
|
} else {
|
|
throw new Error(data.error || '筛选对话失败');
|
|
}
|
|
} catch (error) {
|
|
console.error('筛选对话失败:', error);
|
|
this.showNotification('筛选对话失败: ' + error.message, 'error');
|
|
}
|
|
}
|
|
});
|