Files
CyberStrikeAI/web/static/js/knowledge.js
2025-12-27 19:42:21 +08:00

1878 lines
77 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 知识库管理相关功能
let knowledgeCategories = [];
let knowledgeItems = [];
let currentEditingItemId = null;
let isSavingKnowledgeItem = false; // 防止重复提交
let retrievalLogsData = []; // 存储检索日志数据,用于详情查看
// 加载知识分类
async function loadKnowledgeCategories() {
try {
// 添加时间戳参数避免缓存
const timestamp = Date.now();
const response = await apiFetch(`/api/knowledge/categories?_t=${timestamp}`, {
method: 'GET',
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0'
}
});
if (!response.ok) {
throw new Error('获取分类失败');
}
const data = await response.json();
// 检查知识库功能是否启用
if (data.enabled === false) {
// 功能未启用,显示友好提示
const container = document.getElementById('knowledge-items-list');
if (container) {
container.innerHTML = `
<div class="empty-state" style="text-align: center; padding: 40px 20px;">
<div style="font-size: 48px; margin-bottom: 20px;">📚</div>
<h3 style="margin-bottom: 10px; color: #666;">知识库功能未启用</h3>
<p style="color: #999; margin-bottom: 20px;">${data.message || '请前往系统设置启用知识检索功能'}</p>
<button onclick="switchToSettings()" style="
background: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
">前往设置</button>
</div>
`;
}
return [];
}
knowledgeCategories = data.categories || [];
// 更新分类筛选下拉框
const filterDropdown = document.getElementById('knowledge-category-filter-dropdown');
if (filterDropdown) {
filterDropdown.innerHTML = '<div class="custom-select-option" data-value="" onclick="selectKnowledgeCategory(\'\')">全部</div>';
knowledgeCategories.forEach(category => {
const option = document.createElement('div');
option.className = 'custom-select-option';
option.setAttribute('data-value', category);
option.textContent = category;
option.onclick = function() {
selectKnowledgeCategory(category);
};
filterDropdown.appendChild(option);
});
}
return knowledgeCategories;
} catch (error) {
console.error('加载分类失败:', error);
// 只在非功能未启用的情况下显示错误
if (!error.message.includes('知识库功能未启用')) {
showNotification('加载分类失败: ' + error.message, 'error');
}
return [];
}
}
// 加载知识项列表
async function loadKnowledgeItems(category = '') {
try {
// 添加时间戳参数避免缓存
const timestamp = Date.now();
const url = category
? `/api/knowledge/items?category=${encodeURIComponent(category)}&_t=${timestamp}`
: `/api/knowledge/items?_t=${timestamp}`;
const response = await apiFetch(url, {
method: 'GET',
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0'
}
});
if (!response.ok) {
throw new Error('获取知识项失败');
}
const data = await response.json();
// 检查知识库功能是否启用
if (data.enabled === false) {
// 功能未启用,显示友好提示(如果还没有显示的话)
const container = document.getElementById('knowledge-items-list');
if (container && !container.querySelector('.empty-state')) {
container.innerHTML = `
<div class="empty-state" style="text-align: center; padding: 40px 20px;">
<div style="font-size: 48px; margin-bottom: 20px;">📚</div>
<h3 style="margin-bottom: 10px; color: #666;">知识库功能未启用</h3>
<p style="color: #999; margin-bottom: 20px;">${data.message || '请前往系统设置启用知识检索功能'}</p>
<button onclick="switchToSettings()" style="
background: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
">前往设置</button>
</div>
`;
}
knowledgeItems = [];
return [];
}
knowledgeItems = data.items || [];
renderKnowledgeItems(knowledgeItems);
return knowledgeItems;
} catch (error) {
console.error('加载知识项失败:', error);
// 只在非功能未启用的情况下显示错误
if (!error.message.includes('知识库功能未启用')) {
showNotification('加载知识项失败: ' + error.message, 'error');
}
return [];
}
}
// 渲染知识项列表
function renderKnowledgeItems(items) {
const container = document.getElementById('knowledge-items-list');
if (!container) return;
if (items.length === 0) {
container.innerHTML = '<div class="empty-state">暂无知识项</div>';
return;
}
// 按分类分组
const groupedByCategory = {};
items.forEach(item => {
const category = item.category || '未分类';
if (!groupedByCategory[category]) {
groupedByCategory[category] = [];
}
groupedByCategory[category].push(item);
});
// 更新统计信息
updateKnowledgeStats(items, Object.keys(groupedByCategory).length);
// 渲染分组后的内容
const categories = Object.keys(groupedByCategory).sort();
let html = '<div class="knowledge-categories-container">';
categories.forEach(category => {
const categoryItems = groupedByCategory[category];
const categoryCount = categoryItems.length;
html += `
<div class="knowledge-category-section" data-category="${escapeHtml(category)}">
<div class="knowledge-category-header">
<div class="knowledge-category-info">
<h3 class="knowledge-category-title">${escapeHtml(category)}</h3>
<span class="knowledge-category-count">${categoryCount} 项</span>
</div>
</div>
<div class="knowledge-items-grid">
${categoryItems.map(item => renderKnowledgeItemCard(item)).join('')}
</div>
</div>
`;
});
html += '</div>';
container.innerHTML = html;
}
// 渲染单个知识项卡片
function renderKnowledgeItemCard(item) {
// 提取内容预览去除markdown格式取前150字符
let preview = item.content || '';
// 移除markdown标题标记
preview = preview.replace(/^#+\s+/gm, '');
// 移除代码块
preview = preview.replace(/```[\s\S]*?```/g, '');
// 移除行内代码
preview = preview.replace(/`[^`]+`/g, '');
// 移除链接
preview = preview.replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1');
// 清理多余空白
preview = preview.replace(/\n+/g, ' ').replace(/\s+/g, ' ').trim();
const previewText = preview.length > 150 ? preview.substring(0, 150) + '...' : preview;
// 提取文件路径显示
const filePath = item.filePath || '';
const relativePath = filePath.split(/[/\\]/).slice(-2).join('/'); // 显示最后两级路径
// 格式化时间
const createdTime = formatTime(item.createdAt);
const updatedTime = formatTime(item.updatedAt);
// 优先显示更新时间,如果没有更新时间则显示创建时间
const displayTime = updatedTime || createdTime;
const timeLabel = updatedTime ? '更新时间' : '创建时间';
// 判断是否为最近更新7天内
let isRecent = false;
if (item.updatedAt && updatedTime) {
const updateDate = new Date(item.updatedAt);
if (!isNaN(updateDate.getTime())) {
isRecent = (Date.now() - updateDate.getTime()) < 7 * 24 * 60 * 60 * 1000;
}
}
return `
<div class="knowledge-item-card" data-id="${item.id}" data-category="${escapeHtml(item.category)}">
<div class="knowledge-item-card-header">
<div class="knowledge-item-card-title-row">
<h4 class="knowledge-item-card-title" title="${escapeHtml(item.title)}">${escapeHtml(item.title)}</h4>
<div class="knowledge-item-card-actions">
<button class="knowledge-item-action-btn" onclick="editKnowledgeItem('${item.id}')" title="编辑">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<button class="knowledge-item-action-btn knowledge-item-delete-btn" onclick="deleteKnowledgeItem('${item.id}')" title="删除">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</div>
</div>
${relativePath ? `<div class="knowledge-item-path">📁 ${escapeHtml(relativePath)}</div>` : ''}
</div>
<div class="knowledge-item-card-content">
<p class="knowledge-item-preview">${escapeHtml(previewText || '无内容预览')}</p>
</div>
<div class="knowledge-item-card-footer">
<div class="knowledge-item-meta">
${displayTime ? `<span class="knowledge-item-time" title="${timeLabel}">🕒 ${displayTime}</span>` : ''}
${isRecent ? '<span class="knowledge-item-badge-new">新</span>' : ''}
</div>
</div>
</div>
`;
}
// 更新统计信息
function updateKnowledgeStats(items, categoryCount) {
const statsContainer = document.getElementById('knowledge-stats');
if (!statsContainer) return;
const totalItems = items.length;
const totalSize = items.reduce((sum, item) => sum + (item.content?.length || 0), 0);
const sizeKB = (totalSize / 1024).toFixed(1);
statsContainer.innerHTML = `
<div class="knowledge-stat-item">
<span class="knowledge-stat-label">总知识项</span>
<span class="knowledge-stat-value">${totalItems}</span>
</div>
<div class="knowledge-stat-item">
<span class="knowledge-stat-label">分类数</span>
<span class="knowledge-stat-value">${categoryCount}</span>
</div>
<div class="knowledge-stat-item">
<span class="knowledge-stat-label">总内容</span>
<span class="knowledge-stat-value">${sizeKB} KB</span>
</div>
`;
// 更新索引进度
updateIndexProgress();
}
// 更新索引进度
let indexProgressInterval = null;
async function updateIndexProgress() {
try {
const response = await apiFetch('/api/knowledge/index-status', {
method: 'GET',
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0'
}
});
if (!response.ok) {
return; // 静默失败,不影响主界面
}
const status = await response.json();
const progressContainer = document.getElementById('knowledge-index-progress');
if (!progressContainer) return;
// 检查知识库功能是否启用
if (status.enabled === false) {
// 功能未启用,隐藏进度条
progressContainer.style.display = 'none';
if (indexProgressInterval) {
clearInterval(indexProgressInterval);
indexProgressInterval = null;
}
return;
}
const totalItems = status.total_items || 0;
const indexedItems = status.indexed_items || 0;
const progressPercent = status.progress_percent || 0;
const isComplete = status.is_complete || false;
if (totalItems === 0) {
// 没有知识项,隐藏进度条
progressContainer.style.display = 'none';
if (indexProgressInterval) {
clearInterval(indexProgressInterval);
indexProgressInterval = null;
}
return;
}
// 显示进度条
progressContainer.style.display = 'block';
if (isComplete) {
progressContainer.innerHTML = `
<div class="knowledge-index-progress-complete">
<span class="progress-icon">✅</span>
<span class="progress-text">索引构建完成 (${indexedItems}/${totalItems})</span>
</div>
`;
// 完成后停止轮询
if (indexProgressInterval) {
clearInterval(indexProgressInterval);
indexProgressInterval = null;
}
} else {
progressContainer.innerHTML = `
<div class="knowledge-index-progress">
<div class="progress-header">
<span class="progress-icon">🔨</span>
<span class="progress-text">正在构建索引: ${indexedItems}/${totalItems} (${progressPercent.toFixed(1)}%)</span>
</div>
<div class="progress-bar-container">
<div class="progress-bar" style="width: ${progressPercent}%"></div>
</div>
<div class="progress-hint">索引构建完成后,语义搜索功能将可用</div>
</div>
`;
// 如果还没有开始轮询,开始轮询
if (!indexProgressInterval) {
indexProgressInterval = setInterval(updateIndexProgress, 3000); // 每3秒刷新一次
}
}
} catch (error) {
// 静默失败
console.debug('获取索引状态失败:', error);
}
}
// 选择知识分类
function selectKnowledgeCategory(category) {
const trigger = document.getElementById('knowledge-category-filter-trigger');
const wrapper = document.getElementById('knowledge-category-filter-wrapper');
const dropdown = document.getElementById('knowledge-category-filter-dropdown');
if (trigger && wrapper && dropdown) {
const displayText = category || '全部';
trigger.querySelector('span').textContent = displayText;
wrapper.classList.remove('open');
// 更新选中状态
dropdown.querySelectorAll('.custom-select-option').forEach(opt => {
opt.classList.remove('selected');
if (opt.getAttribute('data-value') === category) {
opt.classList.add('selected');
}
});
}
loadKnowledgeItems(category);
}
// 筛选知识项
function filterKnowledgeItems() {
const wrapper = document.getElementById('knowledge-category-filter-wrapper');
if (wrapper) {
const selectedOption = wrapper.querySelector('.custom-select-option.selected');
const category = selectedOption ? selectedOption.getAttribute('data-value') : '';
loadKnowledgeItems(category);
}
}
// 搜索知识项
function searchKnowledgeItems() {
const searchTerm = document.getElementById('knowledge-search').value.toLowerCase().trim();
if (!searchTerm) {
// 恢复原始列表
const wrapper = document.getElementById('knowledge-category-filter-wrapper');
let category = '';
if (wrapper) {
const selectedOption = wrapper.querySelector('.custom-select-option.selected');
category = selectedOption ? selectedOption.getAttribute('data-value') : '';
}
loadKnowledgeItems(category);
return;
}
const filtered = knowledgeItems.filter(item =>
item.title.toLowerCase().includes(searchTerm) ||
item.content.toLowerCase().includes(searchTerm) ||
item.category.toLowerCase().includes(searchTerm) ||
(item.filePath && item.filePath.toLowerCase().includes(searchTerm))
);
renderKnowledgeItems(filtered);
}
// 刷新知识库
async function refreshKnowledgeBase() {
try {
showNotification('正在扫描知识库...', 'info');
const response = await apiFetch('/api/knowledge/scan', {
method: 'POST'
});
if (!response.ok) {
throw new Error('扫描知识库失败');
}
const data = await response.json();
// 根据返回的消息显示不同的提示
if (data.items_to_index && data.items_to_index > 0) {
showNotification(`扫描完成,开始索引 ${data.items_to_index} 个新添加或更新的知识项`, 'success');
} else {
showNotification(data.message || '扫描完成,没有需要索引的新项或更新项', 'success');
}
// 重新加载知识项
await loadKnowledgeCategories();
await loadKnowledgeItems();
// 停止现有的轮询
if (indexProgressInterval) {
clearInterval(indexProgressInterval);
indexProgressInterval = null;
}
// 如果有需要索引的项,等待一小段时间后立即更新进度
if (data.items_to_index && data.items_to_index > 0) {
await new Promise(resolve => setTimeout(resolve, 500));
updateIndexProgress();
// 开始轮询进度每2秒刷新一次
if (!indexProgressInterval) {
indexProgressInterval = setInterval(updateIndexProgress, 2000);
}
} else {
// 没有需要索引的项,也更新一次以显示当前状态
updateIndexProgress();
}
} catch (error) {
console.error('刷新知识库失败:', error);
showNotification('刷新知识库失败: ' + error.message, 'error');
}
}
// 重建索引
async function rebuildKnowledgeIndex() {
try {
if (!confirm('确定要重建索引吗?这可能需要一些时间。')) {
return;
}
showNotification('正在重建索引...', 'info');
// 先停止现有的轮询
if (indexProgressInterval) {
clearInterval(indexProgressInterval);
indexProgressInterval = null;
}
// 立即显示"正在重建"状态,因为重建开始时会清空旧索引
const progressContainer = document.getElementById('knowledge-index-progress');
if (progressContainer) {
progressContainer.style.display = 'block';
progressContainer.innerHTML = `
<div class="knowledge-index-progress">
<div class="progress-header">
<span class="progress-icon">🔨</span>
<span class="progress-text">正在重建索引: 准备中...</span>
</div>
<div class="progress-bar-container">
<div class="progress-bar" style="width: 0%"></div>
</div>
<div class="progress-hint">索引构建完成后,语义搜索功能将可用</div>
</div>
`;
}
const response = await apiFetch('/api/knowledge/index', {
method: 'POST'
});
if (!response.ok) {
throw new Error('重建索引失败');
}
showNotification('索引重建已开始,将在后台进行', 'success');
// 等待一小段时间,确保后端已经开始处理并清空了旧索引
await new Promise(resolve => setTimeout(resolve, 500));
// 立即更新一次进度
updateIndexProgress();
// 开始轮询进度每2秒刷新一次比默认的3秒更频繁
if (!indexProgressInterval) {
indexProgressInterval = setInterval(updateIndexProgress, 2000);
}
} catch (error) {
console.error('重建索引失败:', error);
showNotification('重建索引失败: ' + error.message, 'error');
}
}
// 显示添加知识项模态框
function showAddKnowledgeItemModal() {
currentEditingItemId = null;
document.getElementById('knowledge-item-modal-title').textContent = '添加知识';
document.getElementById('knowledge-item-category').value = '';
document.getElementById('knowledge-item-title').value = '';
document.getElementById('knowledge-item-content').value = '';
document.getElementById('knowledge-item-modal').style.display = 'block';
}
// 编辑知识项
async function editKnowledgeItem(id) {
try {
const response = await apiFetch(`/api/knowledge/items/${id}`);
if (!response.ok) {
throw new Error('获取知识项失败');
}
const item = await response.json();
currentEditingItemId = id;
document.getElementById('knowledge-item-modal-title').textContent = '编辑知识';
document.getElementById('knowledge-item-category').value = item.category;
document.getElementById('knowledge-item-title').value = item.title;
document.getElementById('knowledge-item-content').value = item.content;
document.getElementById('knowledge-item-modal').style.display = 'block';
} catch (error) {
console.error('编辑知识项失败:', error);
showNotification('编辑知识项失败: ' + error.message, 'error');
}
}
// 保存知识项
async function saveKnowledgeItem() {
// 防止重复提交
if (isSavingKnowledgeItem) {
showNotification('正在保存中,请勿重复点击...', 'warning');
return;
}
const category = document.getElementById('knowledge-item-category').value.trim();
const title = document.getElementById('knowledge-item-title').value.trim();
const content = document.getElementById('knowledge-item-content').value.trim();
if (!category || !title || !content) {
showNotification('请填写所有必填字段', 'error');
return;
}
// 设置保存中标志
isSavingKnowledgeItem = true;
// 获取保存按钮和取消按钮
const saveButton = document.querySelector('#knowledge-item-modal .modal-footer .btn-primary');
const cancelButton = document.querySelector('#knowledge-item-modal .modal-footer .btn-secondary');
const modal = document.getElementById('knowledge-item-modal');
const originalButtonText = saveButton ? saveButton.textContent : '保存';
const originalButtonDisabled = saveButton ? saveButton.disabled : false;
// 禁用所有输入字段和按钮
const categoryInput = document.getElementById('knowledge-item-category');
const titleInput = document.getElementById('knowledge-item-title');
const contentInput = document.getElementById('knowledge-item-content');
if (categoryInput) categoryInput.disabled = true;
if (titleInput) titleInput.disabled = true;
if (contentInput) contentInput.disabled = true;
if (cancelButton) cancelButton.disabled = true;
// 设置保存按钮加载状态
if (saveButton) {
saveButton.disabled = true;
saveButton.style.opacity = '0.6';
saveButton.style.cursor = 'not-allowed';
saveButton.textContent = '保存中...';
}
try {
const url = currentEditingItemId
? `/api/knowledge/items/${currentEditingItemId}`
: '/api/knowledge/items';
const method = currentEditingItemId ? 'PUT' : 'POST';
const response = await apiFetch(url, {
method: method,
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
category,
title,
content
})
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || '保存知识项失败');
}
const item = await response.json();
const action = currentEditingItemId ? '更新' : '创建';
const newItemCategory = item.category || category; // 保存新添加的知识项分类
// 获取当前筛选状态,以便刷新后保持
const currentCategory = document.getElementById('knowledge-category-filter-wrapper');
let selectedCategory = '';
if (currentCategory) {
const selectedOption = currentCategory.querySelector('.custom-select-option.selected');
if (selectedOption) {
selectedCategory = selectedOption.getAttribute('data-value') || '';
}
}
// 立即关闭模态框,给用户明确的反馈
closeKnowledgeItemModal();
// 显示加载状态并刷新数据(等待完成以确保数据同步)
const itemsListContainer = document.getElementById('knowledge-items-list');
const originalContent = itemsListContainer ? itemsListContainer.innerHTML : '';
if (itemsListContainer) {
itemsListContainer.innerHTML = '<div class="loading-spinner">刷新中...</div>';
}
try {
// 先刷新分类,再刷新知识项
console.log('开始刷新知识库数据...');
await loadKnowledgeCategories();
console.log('分类刷新完成,开始刷新知识项...');
// 如果新添加的知识项不在当前筛选的分类中,切换到该分类显示
let categoryToShow = selectedCategory;
if (!currentEditingItemId && selectedCategory && selectedCategory !== '' && newItemCategory !== selectedCategory) {
// 新添加的知识项,如果当前筛选的不是该分类,切换到新知识项的分类
categoryToShow = newItemCategory;
// 更新筛选器显示(不触发加载,因为我们下面会手动加载)
const trigger = document.getElementById('knowledge-category-filter-trigger');
const wrapper = document.getElementById('knowledge-category-filter-wrapper');
const dropdown = document.getElementById('knowledge-category-filter-dropdown');
if (trigger && wrapper && dropdown) {
trigger.querySelector('span').textContent = newItemCategory || '全部';
dropdown.querySelectorAll('.custom-select-option').forEach(opt => {
opt.classList.remove('selected');
if (opt.getAttribute('data-value') === newItemCategory) {
opt.classList.add('selected');
}
});
}
showNotification(`${action}成功!已切换到分类"${newItemCategory}"查看新添加的知识项。`, 'success');
}
// 刷新知识项列表
await loadKnowledgeItems(categoryToShow);
console.log('知识项刷新完成');
} catch (err) {
console.error('刷新数据失败:', err);
// 如果刷新失败,恢复原内容
if (itemsListContainer && originalContent) {
itemsListContainer.innerHTML = originalContent;
}
showNotification('⚠️ 知识项已保存,但刷新列表失败,请手动刷新页面查看', 'warning');
}
} catch (error) {
console.error('保存知识项失败:', error);
showNotification('❌ 保存知识项失败: ' + error.message, 'error');
// 如果通知系统不可用使用alert
if (typeof window.showNotification !== 'function') {
alert('❌ 保存知识项失败: ' + error.message);
}
// 恢复输入字段和按钮状态(错误时不关闭模态框,让用户修改后重试)
if (categoryInput) categoryInput.disabled = false;
if (titleInput) titleInput.disabled = false;
if (contentInput) contentInput.disabled = false;
if (cancelButton) cancelButton.disabled = false;
if (saveButton) {
saveButton.disabled = false;
saveButton.style.opacity = '';
saveButton.style.cursor = '';
saveButton.textContent = originalButtonText;
}
} finally {
// 清除保存中标志
isSavingKnowledgeItem = false;
}
}
// 删除知识项
async function deleteKnowledgeItem(id) {
if (!confirm('确定要删除这个知识项吗?')) {
return;
}
// 找到要删除的知识项卡片和删除按钮
const itemCard = document.querySelector(`.knowledge-item-card[data-id="${id}"]`);
const deleteButton = itemCard ? itemCard.querySelector('.knowledge-item-delete-btn') : null;
const categorySection = itemCard ? itemCard.closest('.knowledge-category-section') : null;
let originalDisplay = '';
let originalOpacity = '';
let originalButtonOpacity = '';
// 设置删除按钮的加载状态
if (deleteButton) {
originalButtonOpacity = deleteButton.style.opacity;
deleteButton.style.opacity = '0.5';
deleteButton.style.cursor = 'not-allowed';
deleteButton.disabled = true;
// 添加加载动画
const svg = deleteButton.querySelector('svg');
if (svg) {
svg.style.animation = 'spin 1s linear infinite';
}
}
// 立即从UI中移除该项乐观更新
if (itemCard) {
originalDisplay = itemCard.style.display;
originalOpacity = itemCard.style.opacity;
itemCard.style.transition = 'opacity 0.3s ease-out, transform 0.3s ease-out';
itemCard.style.opacity = '0';
itemCard.style.transform = 'translateX(-20px)';
// 等待动画完成后移除
setTimeout(() => {
if (itemCard.parentElement) {
itemCard.remove();
// 检查分类是否还有项目,如果没有则隐藏分类标题
if (categorySection) {
const remainingItems = categorySection.querySelectorAll('.knowledge-item-card');
if (remainingItems.length === 0) {
categorySection.style.transition = 'opacity 0.3s ease-out';
categorySection.style.opacity = '0';
setTimeout(() => {
if (categorySection.parentElement) {
categorySection.remove();
}
}, 300);
} else {
// 更新分类计数
const categoryCount = categorySection.querySelector('.knowledge-category-count');
if (categoryCount) {
const newCount = remainingItems.length;
categoryCount.textContent = `${newCount}`;
}
}
}
// 更新统计信息(临时更新,稍后会重新加载)
updateKnowledgeStatsAfterDelete();
}
}, 300);
}
try {
const response = await apiFetch(`/api/knowledge/items/${id}`, {
method: 'DELETE'
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || '删除知识项失败');
}
// 显示成功通知
showNotification('✅ 删除成功!知识项已从系统中移除。', 'success');
// 重新加载数据以确保数据同步
await loadKnowledgeCategories();
await loadKnowledgeItems();
} catch (error) {
console.error('删除知识项失败:', error);
// 如果删除失败,恢复该项显示
if (itemCard && originalDisplay !== 'none') {
itemCard.style.display = originalDisplay || '';
itemCard.style.opacity = originalOpacity || '1';
itemCard.style.transform = '';
itemCard.style.transition = '';
// 如果分类被移除了,需要恢复
if (categorySection && !categorySection.parentElement) {
// 需要重新加载来恢复
await loadKnowledgeItems();
}
}
// 恢复删除按钮状态
if (deleteButton) {
deleteButton.style.opacity = originalButtonOpacity || '';
deleteButton.style.cursor = '';
deleteButton.disabled = false;
const svg = deleteButton.querySelector('svg');
if (svg) {
svg.style.animation = '';
}
}
showNotification('❌ 删除知识项失败: ' + error.message, 'error');
}
}
// 临时更新统计信息(删除后)
function updateKnowledgeStatsAfterDelete() {
const statsContainer = document.getElementById('knowledge-stats');
if (!statsContainer) return;
const allItems = document.querySelectorAll('.knowledge-item-card');
const allCategories = document.querySelectorAll('.knowledge-category-section');
const totalItems = allItems.length;
const categoryCount = allCategories.length;
// 计算总内容大小(这里简化处理,实际应该从服务器获取)
const statsItems = statsContainer.querySelectorAll('.knowledge-stat-item');
if (statsItems.length >= 2) {
const totalItemsSpan = statsItems[0].querySelector('.knowledge-stat-value');
const categoryCountSpan = statsItems[1].querySelector('.knowledge-stat-value');
if (totalItemsSpan) {
totalItemsSpan.textContent = totalItems;
}
if (categoryCountSpan) {
categoryCountSpan.textContent = categoryCount;
}
}
}
// 关闭知识项模态框
function closeKnowledgeItemModal() {
const modal = document.getElementById('knowledge-item-modal');
if (modal) {
modal.style.display = 'none';
}
// 重置编辑状态
currentEditingItemId = null;
isSavingKnowledgeItem = false;
// 恢复所有输入字段和按钮状态
const categoryInput = document.getElementById('knowledge-item-category');
const titleInput = document.getElementById('knowledge-item-title');
const contentInput = document.getElementById('knowledge-item-content');
const saveButton = document.querySelector('#knowledge-item-modal .modal-footer .btn-primary');
const cancelButton = document.querySelector('#knowledge-item-modal .modal-footer .btn-secondary');
if (categoryInput) {
categoryInput.disabled = false;
categoryInput.value = '';
}
if (titleInput) {
titleInput.disabled = false;
titleInput.value = '';
}
if (contentInput) {
contentInput.disabled = false;
contentInput.value = '';
}
if (saveButton) {
saveButton.disabled = false;
saveButton.style.opacity = '';
saveButton.style.cursor = '';
saveButton.textContent = '保存';
}
if (cancelButton) {
cancelButton.disabled = false;
}
}
// 加载检索日志
async function loadRetrievalLogs(conversationId = '', messageId = '') {
try {
let url = '/api/knowledge/retrieval-logs?limit=100';
if (conversationId) {
url += `&conversationId=${encodeURIComponent(conversationId)}`;
}
if (messageId) {
url += `&messageId=${encodeURIComponent(messageId)}`;
}
const response = await apiFetch(url);
if (!response.ok) {
throw new Error('获取检索日志失败');
}
const data = await response.json();
renderRetrievalLogs(data.logs || []);
} catch (error) {
console.error('加载检索日志失败:', error);
// 即使加载失败,也显示空状态而不是一直显示"加载中..."
renderRetrievalLogs([]);
// 只在非空筛选条件下才显示错误通知(避免在没有数据时显示错误)
if (conversationId || messageId) {
showNotification('加载检索日志失败: ' + error.message, 'error');
}
}
}
// 渲染检索日志
function renderRetrievalLogs(logs) {
const container = document.getElementById('retrieval-logs-list');
if (!container) return;
// 更新统计信息(即使为空数组也要更新)
updateRetrievalStats(logs);
if (logs.length === 0) {
container.innerHTML = '<div class="empty-state">暂无检索记录</div>';
retrievalLogsData = [];
return;
}
// 保存日志数据供详情查看使用
retrievalLogsData = logs;
container.innerHTML = logs.map((log, index) => {
// 处理retrievedItems可能是数组、字符串数组或者特殊标记
let itemCount = 0;
let hasResults = false;
if (log.retrievedItems) {
if (Array.isArray(log.retrievedItems)) {
// 过滤掉特殊标记
const realItems = log.retrievedItems.filter(id => id !== '_has_results');
itemCount = realItems.length;
// 如果有特殊标记表示有结果但ID未知显示为"有结果"
if (log.retrievedItems.includes('_has_results')) {
hasResults = true;
// 如果有真实ID使用真实数量否则显示为"有结果"(不显示具体数量)
if (itemCount === 0) {
itemCount = -1; // -1 表示有结果但数量未知
}
} else {
hasResults = itemCount > 0;
}
} else if (typeof log.retrievedItems === 'string') {
// 如果是字符串尝试解析JSON
try {
const parsed = JSON.parse(log.retrievedItems);
if (Array.isArray(parsed)) {
const realItems = parsed.filter(id => id !== '_has_results');
itemCount = realItems.length;
if (parsed.includes('_has_results')) {
hasResults = true;
if (itemCount === 0) {
itemCount = -1;
}
} else {
hasResults = itemCount > 0;
}
}
} catch (e) {
// 解析失败,忽略
}
}
}
const timeAgo = getTimeAgo(log.createdAt);
return `
<div class="retrieval-log-card ${hasResults ? 'has-results' : 'no-results'}" data-index="${index}">
<div class="retrieval-log-card-header">
<div class="retrieval-log-icon">
${hasResults ? '🔍' : '⚠️'}
</div>
<div class="retrieval-log-main-info">
<div class="retrieval-log-query">
${escapeHtml(log.query || '无查询内容')}
</div>
<div class="retrieval-log-meta">
<span class="retrieval-log-time" title="${formatTime(log.createdAt)}">
🕒 ${timeAgo}
</span>
${log.riskType ? `<span class="retrieval-log-risk-type">📁 ${escapeHtml(log.riskType)}</span>` : ''}
</div>
</div>
<div class="retrieval-log-result-badge ${hasResults ? 'success' : 'empty'}">
${hasResults ? (itemCount > 0 ? `${itemCount}` : '有结果') : '无结果'}
</div>
</div>
<div class="retrieval-log-card-body">
<div class="retrieval-log-details-grid">
${log.conversationId ? `
<div class="retrieval-log-detail-item">
<span class="detail-label">对话ID</span>
<code class="detail-value" title="点击复制" onclick="navigator.clipboard.writeText('${escapeHtml(log.conversationId)}'); this.title='已复制!'; setTimeout(() => this.title='点击复制', 2000);" style="cursor: pointer;">${escapeHtml(log.conversationId)}</code>
</div>
` : ''}
${log.messageId ? `
<div class="retrieval-log-detail-item">
<span class="detail-label">消息ID</span>
<code class="detail-value" title="点击复制" onclick="navigator.clipboard.writeText('${escapeHtml(log.messageId)}'); this.title='已复制!'; setTimeout(() => this.title='点击复制', 2000);" style="cursor: pointer;">${escapeHtml(log.messageId)}</code>
</div>
` : ''}
<div class="retrieval-log-detail-item">
<span class="detail-label">检索结果</span>
<span class="detail-value ${hasResults ? 'text-success' : 'text-muted'}">
${hasResults ? (itemCount > 0 ? `找到 ${itemCount} 个相关知识项` : '找到相关知识项(数量未知)') : '未找到匹配的知识项'}
</span>
</div>
</div>
${hasResults && log.retrievedItems && log.retrievedItems.length > 0 ? `
<div class="retrieval-log-items-preview">
<div class="retrieval-log-items-label">检索到的知识项:</div>
<div class="retrieval-log-items-list">
${log.retrievedItems.slice(0, 3).map((itemId, idx) => `
<span class="retrieval-log-item-tag">${idx + 1}</span>
`).join('')}
${log.retrievedItems.length > 3 ? `<span class="retrieval-log-item-tag more">+${log.retrievedItems.length - 3}</span>` : ''}
</div>
</div>
` : ''}
<div class="retrieval-log-actions">
<button class="btn-secondary btn-sm" onclick="showRetrievalLogDetails(${index})" style="margin-top: 12px; display: inline-flex; align-items: center; gap: 4px;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
查看详情
</button>
<button class="btn-secondary btn-sm retrieval-log-delete-btn" onclick="deleteRetrievalLog('${escapeHtml(log.id)}', ${index})" style="margin-top: 12px; margin-left: 8px; display: inline-flex; align-items: center; gap: 4px; color: var(--error-color, #dc3545); border-color: var(--error-color, #dc3545);" onmouseover="this.style.backgroundColor='rgba(220, 53, 69, 0.1)'; this.style.color='#dc3545';" onmouseout="this.style.backgroundColor=''; this.style.color='var(--error-color, #dc3545)';" title="删除">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
删除
</button>
</div>
</div>
</div>
`;
}).join('');
}
// 更新检索统计信息
function updateRetrievalStats(logs) {
const statsContainer = document.getElementById('retrieval-stats');
if (!statsContainer) return;
const totalLogs = logs.length;
// 判断是否有结果检查retrievedItems数组过滤掉特殊标记后长度>0或者包含特殊标记
const successfulLogs = logs.filter(log => {
if (!log.retrievedItems) return false;
if (Array.isArray(log.retrievedItems)) {
const realItems = log.retrievedItems.filter(id => id !== '_has_results');
return realItems.length > 0 || log.retrievedItems.includes('_has_results');
}
return false;
}).length;
// 计算总知识项数只计算真实ID不包括特殊标记
const totalItems = logs.reduce((sum, log) => {
if (!log.retrievedItems) return sum;
if (Array.isArray(log.retrievedItems)) {
const realItems = log.retrievedItems.filter(id => id !== '_has_results');
return sum + realItems.length;
}
return sum;
}, 0);
const successRate = totalLogs > 0 ? ((successfulLogs / totalLogs) * 100).toFixed(1) : 0;
statsContainer.innerHTML = `
<div class="retrieval-stat-item">
<span class="retrieval-stat-label">总检索次数</span>
<span class="retrieval-stat-value">${totalLogs}</span>
</div>
<div class="retrieval-stat-item">
<span class="retrieval-stat-label">成功检索</span>
<span class="retrieval-stat-value text-success">${successfulLogs}</span>
</div>
<div class="retrieval-stat-item">
<span class="retrieval-stat-label">成功率</span>
<span class="retrieval-stat-value">${successRate}%</span>
</div>
<div class="retrieval-stat-item">
<span class="retrieval-stat-label">检索到知识项</span>
<span class="retrieval-stat-value">${totalItems}</span>
</div>
`;
}
// 获取相对时间
function getTimeAgo(timeStr) {
if (!timeStr) return '';
// 处理时间字符串,支持多种格式
let date;
if (typeof timeStr === 'string') {
// 首先尝试直接解析支持RFC3339/ISO8601格式
date = new Date(timeStr);
// 如果解析失败,尝试其他格式
if (isNaN(date.getTime())) {
// SQLite格式: "2006-01-02 15:04:05" 或带时区
const sqliteMatch = timeStr.match(/(\d{4}-\d{2}-\d{2}[\sT]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[+-]\d{2}:\d{2}|Z)?)/);
if (sqliteMatch) {
let timeStr2 = sqliteMatch[1].replace(' ', 'T');
// 如果没有时区信息添加Z表示UTC
if (!timeStr2.includes('Z') && !timeStr2.match(/[+-]\d{2}:\d{2}$/)) {
timeStr2 += 'Z';
}
date = new Date(timeStr2);
}
}
// 如果还是失败,尝试更宽松的格式
if (isNaN(date.getTime())) {
// 尝试匹配 "YYYY-MM-DD HH:MM:SS" 格式
const match = timeStr.match(/(\d{4})-(\d{2})-(\d{2})[\sT](\d{2}):(\d{2}):(\d{2})/);
if (match) {
date = new Date(
parseInt(match[1]),
parseInt(match[2]) - 1,
parseInt(match[3]),
parseInt(match[4]),
parseInt(match[5]),
parseInt(match[6])
);
}
}
} else {
date = new Date(timeStr);
}
// 检查日期是否有效
if (isNaN(date.getTime())) {
return formatTime(timeStr);
}
// 检查日期是否合理不在1970年之前不在未来太远
const year = date.getFullYear();
if (year < 1970 || year > 2100) {
return formatTime(timeStr);
}
const now = new Date();
const diff = now - date;
// 如果时间差为负数或过大(可能是解析错误),返回格式化时间
if (diff < 0 || diff > 365 * 24 * 60 * 60 * 1000 * 10) { // 超过10年认为是错误
return formatTime(timeStr);
}
const seconds = Math.floor(diff / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (days > 0) return `${days}天前`;
if (hours > 0) return `${hours}小时前`;
if (minutes > 0) return `${minutes}分钟前`;
return '刚刚';
}
// 截断ID显示
function truncateId(id) {
if (!id || id.length <= 16) return id;
return id.substring(0, 8) + '...' + id.substring(id.length - 8);
}
// 筛选检索日志
function filterRetrievalLogs() {
const conversationId = document.getElementById('retrieval-logs-conversation-id').value.trim();
const messageId = document.getElementById('retrieval-logs-message-id').value.trim();
loadRetrievalLogs(conversationId, messageId);
}
// 刷新检索日志
function refreshRetrievalLogs() {
filterRetrievalLogs();
}
// 删除检索日志
async function deleteRetrievalLog(id, index) {
if (!confirm('确定要删除这条检索记录吗?')) {
return;
}
// 找到要删除的日志卡片和删除按钮
const logCard = document.querySelector(`.retrieval-log-card[data-index="${index}"]`);
const deleteButton = logCard ? logCard.querySelector('.retrieval-log-delete-btn') : null;
let originalButtonOpacity = '';
let originalButtonDisabled = false;
// 设置删除按钮的加载状态
if (deleteButton) {
originalButtonOpacity = deleteButton.style.opacity;
originalButtonDisabled = deleteButton.disabled;
deleteButton.style.opacity = '0.5';
deleteButton.style.cursor = 'not-allowed';
deleteButton.disabled = true;
// 添加加载动画
const svg = deleteButton.querySelector('svg');
if (svg) {
svg.style.animation = 'spin 1s linear infinite';
}
}
// 立即从UI中移除该项乐观更新
if (logCard) {
logCard.style.transition = 'opacity 0.3s ease-out, transform 0.3s ease-out';
logCard.style.opacity = '0';
logCard.style.transform = 'translateX(-20px)';
// 等待动画完成后移除
setTimeout(() => {
if (logCard.parentElement) {
logCard.remove();
// 更新统计信息(临时更新,稍后会重新加载)
updateRetrievalStatsAfterDelete();
}
}, 300);
}
try {
const response = await apiFetch(`/api/knowledge/retrieval-logs/${id}`, {
method: 'DELETE'
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || '删除检索日志失败');
}
// 显示成功通知
showNotification('✅ 删除成功!检索记录已从系统中移除。', 'success');
// 从内存中移除该项
if (retrievalLogsData && index >= 0 && index < retrievalLogsData.length) {
retrievalLogsData.splice(index, 1);
}
// 重新加载数据以确保数据同步
const conversationId = document.getElementById('retrieval-logs-conversation-id')?.value.trim() || '';
const messageId = document.getElementById('retrieval-logs-message-id')?.value.trim() || '';
await loadRetrievalLogs(conversationId, messageId);
} catch (error) {
console.error('删除检索日志失败:', error);
// 如果删除失败,恢复该项显示
if (logCard) {
logCard.style.opacity = '1';
logCard.style.transform = '';
logCard.style.transition = '';
}
// 恢复删除按钮状态
if (deleteButton) {
deleteButton.style.opacity = originalButtonOpacity || '';
deleteButton.style.cursor = '';
deleteButton.disabled = originalButtonDisabled;
const svg = deleteButton.querySelector('svg');
if (svg) {
svg.style.animation = '';
}
}
showNotification('❌ 删除检索日志失败: ' + error.message, 'error');
}
}
// 临时更新统计信息(删除后)
function updateRetrievalStatsAfterDelete() {
const statsContainer = document.getElementById('retrieval-stats');
if (!statsContainer) return;
const allLogs = document.querySelectorAll('.retrieval-log-card');
const totalLogs = allLogs.length;
// 计算成功检索数
const successfulLogs = Array.from(allLogs).filter(card => {
return card.classList.contains('has-results');
}).length;
// 计算总知识项数(简化处理,实际应该从服务器获取)
const totalItems = Array.from(allLogs).reduce((sum, card) => {
const badge = card.querySelector('.retrieval-log-result-badge');
if (badge && badge.classList.contains('success')) {
const text = badge.textContent.trim();
const match = text.match(/(\d+)\s*项/);
if (match) {
return sum + parseInt(match[1]);
} else if (text === '有结果') {
return sum + 1; // 简化处理假设为1
}
}
return sum;
}, 0);
const successRate = totalLogs > 0 ? ((successfulLogs / totalLogs) * 100).toFixed(1) : 0;
statsContainer.innerHTML = `
<div class="retrieval-stat-item">
<span class="retrieval-stat-label">总检索次数</span>
<span class="retrieval-stat-value">${totalLogs}</span>
</div>
<div class="retrieval-stat-item">
<span class="retrieval-stat-label">成功检索</span>
<span class="retrieval-stat-value text-success">${successfulLogs}</span>
</div>
<div class="retrieval-stat-item">
<span class="retrieval-stat-label">成功率</span>
<span class="retrieval-stat-value">${successRate}%</span>
</div>
<div class="retrieval-stat-item">
<span class="retrieval-stat-label">检索到知识项</span>
<span class="retrieval-stat-value">${totalItems}</span>
</div>
`;
}
// 显示检索日志详情
async function showRetrievalLogDetails(index) {
if (!retrievalLogsData || index < 0 || index >= retrievalLogsData.length) {
showNotification('无法获取检索详情', 'error');
return;
}
const log = retrievalLogsData[index];
// 获取检索到的知识项详情
let retrievedItemsDetails = [];
if (log.retrievedItems && Array.isArray(log.retrievedItems)) {
const realItemIds = log.retrievedItems.filter(id => id !== '_has_results');
if (realItemIds.length > 0) {
try {
// 批量获取知识项详情
const itemPromises = realItemIds.map(async (itemId) => {
try {
const response = await apiFetch(`/api/knowledge/items/${itemId}`);
if (response.ok) {
return await response.json();
}
return null;
} catch (err) {
console.error(`获取知识项 ${itemId} 失败:`, err);
return null;
}
});
const items = await Promise.all(itemPromises);
retrievedItemsDetails = items.filter(item => item !== null);
} catch (err) {
console.error('批量获取知识项详情失败:', err);
}
}
}
// 显示详情模态框
showRetrievalLogDetailsModal(log, retrievedItemsDetails);
}
// 显示检索日志详情模态框
function showRetrievalLogDetailsModal(log, retrievedItems) {
// 创建或获取模态框
let modal = document.getElementById('retrieval-log-details-modal');
if (!modal) {
modal = document.createElement('div');
modal.id = 'retrieval-log-details-modal';
modal.className = 'modal';
modal.innerHTML = `
<div class="modal-content" style="max-width: 900px; max-height: 90vh; overflow-y: auto;">
<div class="modal-header">
<h2>检索详情</h2>
<span class="modal-close" onclick="closeRetrievalLogDetailsModal()">&times;</span>
</div>
<div class="modal-body" id="retrieval-log-details-content">
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="closeRetrievalLogDetailsModal()">关闭</button>
</div>
</div>
`;
document.body.appendChild(modal);
}
// 填充内容
const content = document.getElementById('retrieval-log-details-content');
const timeAgo = getTimeAgo(log.createdAt);
const fullTime = formatTime(log.createdAt);
let itemsHtml = '';
if (retrievedItems.length > 0) {
itemsHtml = retrievedItems.map((item, idx) => {
// 提取内容预览
let preview = item.content || '';
preview = preview.replace(/^#+\s+/gm, '');
preview = preview.replace(/```[\s\S]*?```/g, '');
preview = preview.replace(/`[^`]+`/g, '');
preview = preview.replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1');
preview = preview.replace(/\n+/g, ' ').replace(/\s+/g, ' ').trim();
const previewText = preview.length > 200 ? preview.substring(0, 200) + '...' : preview;
return `
<div class="retrieval-detail-item-card" style="margin-bottom: 16px; padding: 16px; border: 1px solid var(--border-color); border-radius: 8px; background: var(--bg-secondary);">
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 8px;">
<h4 style="margin: 0; color: var(--text-primary);">${idx + 1}. ${escapeHtml(item.title || '未命名')}</h4>
<span style="font-size: 0.875rem; color: var(--text-secondary);">${escapeHtml(item.category || '未分类')}</span>
</div>
${item.filePath ? `<div style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 8px;">📁 ${escapeHtml(item.filePath)}</div>` : ''}
<div style="font-size: 0.875rem; color: var(--text-secondary); line-height: 1.6;">
${escapeHtml(previewText || '无内容预览')}
</div>
</div>
`;
}).join('');
} else {
itemsHtml = '<div style="padding: 16px; text-align: center; color: var(--text-muted);">未找到知识项详情</div>';
}
content.innerHTML = `
<div style="display: flex; flex-direction: column; gap: 20px;">
<div class="retrieval-detail-section">
<h3 style="margin: 0 0 12px 0; font-size: 1.125rem; color: var(--text-primary);">查询信息</h3>
<div style="padding: 12px; background: var(--bg-secondary); border-radius: 6px; border-left: 3px solid var(--accent-color);">
<div style="font-weight: 500; margin-bottom: 8px; color: var(--text-primary);">查询内容:</div>
<div style="color: var(--text-primary); line-height: 1.6; word-break: break-word;">${escapeHtml(log.query || '无查询内容')}</div>
</div>
</div>
<div class="retrieval-detail-section">
<h3 style="margin: 0 0 12px 0; font-size: 1.125rem; color: var(--text-primary);">检索信息</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px;">
${log.riskType ? `
<div style="padding: 12px; background: var(--bg-secondary); border-radius: 6px;">
<div style="font-size: 0.875rem; color: var(--text-secondary); margin-bottom: 4px;">风险类型</div>
<div style="font-weight: 500; color: var(--text-primary);">${escapeHtml(log.riskType)}</div>
</div>
` : ''}
<div style="padding: 12px; background: var(--bg-secondary); border-radius: 6px;">
<div style="font-size: 0.875rem; color: var(--text-secondary); margin-bottom: 4px;">检索时间</div>
<div style="font-weight: 500; color: var(--text-primary);" title="${fullTime}">${timeAgo}</div>
</div>
<div style="padding: 12px; background: var(--bg-secondary); border-radius: 6px;">
<div style="font-size: 0.875rem; color: var(--text-secondary); margin-bottom: 4px;">检索结果</div>
<div style="font-weight: 500; color: var(--text-primary);">${retrievedItems.length} 个知识项</div>
</div>
</div>
</div>
${log.conversationId || log.messageId ? `
<div class="retrieval-detail-section">
<h3 style="margin: 0 0 12px 0; font-size: 1.125rem; color: var(--text-primary);">关联信息</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px;">
${log.conversationId ? `
<div style="padding: 12px; background: var(--bg-secondary); border-radius: 6px;">
<div style="font-size: 0.875rem; color: var(--text-secondary); margin-bottom: 4px;">对话ID</div>
<code style="font-size: 0.8125rem; color: var(--text-primary); word-break: break-all; cursor: pointer;"
onclick="navigator.clipboard.writeText('${escapeHtml(log.conversationId)}'); this.title='已复制!'; setTimeout(() => this.title='点击复制', 2000);"
title="点击复制">${escapeHtml(log.conversationId)}</code>
</div>
` : ''}
${log.messageId ? `
<div style="padding: 12px; background: var(--bg-secondary); border-radius: 6px;">
<div style="font-size: 0.875rem; color: var(--text-secondary); margin-bottom: 4px;">消息ID</div>
<code style="font-size: 0.8125rem; color: var(--text-primary); word-break: break-all; cursor: pointer;"
onclick="navigator.clipboard.writeText('${escapeHtml(log.messageId)}'); this.title='已复制!'; setTimeout(() => this.title='点击复制', 2000);"
title="点击复制">${escapeHtml(log.messageId)}</code>
</div>
` : ''}
</div>
</div>
` : ''}
<div class="retrieval-detail-section">
<h3 style="margin: 0 0 12px 0; font-size: 1.125rem; color: var(--text-primary);">检索到的知识项 (${retrievedItems.length})</h3>
${itemsHtml}
</div>
</div>
`;
modal.style.display = 'block';
}
// 关闭检索日志详情模态框
function closeRetrievalLogDetailsModal() {
const modal = document.getElementById('retrieval-log-details-modal');
if (modal) {
modal.style.display = 'none';
}
}
// 点击模态框外部关闭
window.addEventListener('click', function(event) {
const modal = document.getElementById('retrieval-log-details-modal');
if (event.target === modal) {
closeRetrievalLogDetailsModal();
}
});
// 页面切换时加载数据
if (typeof switchPage === 'function') {
const originalSwitchPage = switchPage;
window.switchPage = function(page) {
originalSwitchPage(page);
if (page === 'knowledge-management') {
loadKnowledgeCategories();
loadKnowledgeItems();
updateIndexProgress(); // 更新索引进度
} else if (page === 'knowledge-retrieval-logs') {
loadRetrievalLogs();
// 切换到其他页面时停止轮询
if (indexProgressInterval) {
clearInterval(indexProgressInterval);
indexProgressInterval = null;
}
} else {
// 切换到其他页面时停止轮询
if (indexProgressInterval) {
clearInterval(indexProgressInterval);
indexProgressInterval = null;
}
}
};
}
// 页面卸载时清理定时器
window.addEventListener('beforeunload', function() {
if (indexProgressInterval) {
clearInterval(indexProgressInterval);
indexProgressInterval = null;
}
});
// 工具函数
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function formatTime(timeStr) {
if (!timeStr) return '';
// 处理时间字符串,支持多种格式
let date;
if (typeof timeStr === 'string') {
// 首先尝试直接解析支持RFC3339/ISO8601格式
date = new Date(timeStr);
// 如果解析失败,尝试其他格式
if (isNaN(date.getTime())) {
// SQLite格式: "2006-01-02 15:04:05" 或带时区
const sqliteMatch = timeStr.match(/(\d{4}-\d{2}-\d{2}[\sT]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[+-]\d{2}:\d{2}|Z)?)/);
if (sqliteMatch) {
let timeStr2 = sqliteMatch[1].replace(' ', 'T');
// 如果没有时区信息添加Z表示UTC
if (!timeStr2.includes('Z') && !timeStr2.match(/[+-]\d{2}:\d{2}$/)) {
timeStr2 += 'Z';
}
date = new Date(timeStr2);
}
}
// 如果还是失败,尝试更宽松的格式
if (isNaN(date.getTime())) {
// 尝试匹配 "YYYY-MM-DD HH:MM:SS" 格式
const match = timeStr.match(/(\d{4})-(\d{2})-(\d{2})[\sT](\d{2}):(\d{2}):(\d{2})/);
if (match) {
date = new Date(
parseInt(match[1]),
parseInt(match[2]) - 1,
parseInt(match[3]),
parseInt(match[4]),
parseInt(match[5]),
parseInt(match[6])
);
}
}
} else {
date = new Date(timeStr);
}
// 如果日期无效,检查是否是零值时间
if (isNaN(date.getTime())) {
// 检查是否是零值时间的字符串形式
if (typeof timeStr === 'string' && (timeStr.includes('0001-01-01') || timeStr.startsWith('0001'))) {
return '';
}
console.warn('无法解析时间:', timeStr);
return '';
}
// 检查日期是否合理不在1970年之前不在未来太远
const year = date.getFullYear();
if (year < 1970 || year > 2100) {
// 如果是零值时间0001-01-01返回空字符串不显示
if (year === 1) {
return '';
}
console.warn('时间值不合理:', timeStr, '解析为:', date);
return '';
}
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});
}
// 显示通知
function showNotification(message, type = 'info') {
// 如果存在全局通知系统(且不是当前函数),使用它
if (typeof window.showNotification === 'function' && window.showNotification !== showNotification) {
window.showNotification(message, type);
return;
}
// 否则使用自定义的toast通知
showToastNotification(message, type);
}
// 显示Toast通知
function showToastNotification(message, type = 'info') {
// 创建通知容器(如果不存在)
let container = document.getElementById('toast-notification-container');
if (!container) {
container = document.createElement('div');
container.id = 'toast-notification-container';
container.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
z-index: 10000;
display: flex;
flex-direction: column;
gap: 12px;
pointer-events: none;
`;
document.body.appendChild(container);
}
// 创建通知元素
const toast = document.createElement('div');
toast.className = `toast-notification toast-${type}`;
// 根据类型设置颜色
const typeStyles = {
success: {
background: '#28a745',
color: '#fff',
icon: '✅'
},
error: {
background: '#dc3545',
color: '#fff',
icon: '❌'
},
info: {
background: '#17a2b8',
color: '#fff',
icon: ''
},
warning: {
background: '#ffc107',
color: '#000',
icon: '⚠️'
}
};
const style = typeStyles[type] || typeStyles.info;
toast.style.cssText = `
background: ${style.background};
color: ${style.color};
padding: 14px 20px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
min-width: 300px;
max-width: 500px;
pointer-events: auto;
animation: slideInRight 0.3s ease-out;
display: flex;
align-items: center;
gap: 12px;
font-size: 0.9375rem;
line-height: 1.5;
word-wrap: break-word;
`;
toast.innerHTML = `
<span style="font-size: 1.2em; flex-shrink: 0;">${style.icon}</span>
<span style="flex: 1;">${escapeHtml(message)}</span>
<button onclick="this.parentElement.remove()" style="
background: transparent;
border: none;
color: ${style.color};
cursor: pointer;
font-size: 1.2em;
padding: 0;
margin-left: 8px;
opacity: 0.7;
flex-shrink: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
" onmouseover="this.style.opacity='1'" onmouseout="this.style.opacity='0.7'">×</button>
`;
container.appendChild(toast);
// 自动移除成功消息显示5秒错误消息显示7秒其他显示4秒
const duration = type === 'success' ? 5000 : type === 'error' ? 7000 : 4000;
setTimeout(() => {
if (toast.parentElement) {
toast.style.animation = 'slideOutRight 0.3s ease-out';
setTimeout(() => {
if (toast.parentElement) {
toast.remove();
}
}, 300);
}
}, duration);
}
// 添加CSS动画如果不存在
if (!document.getElementById('toast-notification-styles')) {
const style = document.createElement('style');
style.id = 'toast-notification-styles';
style.textContent = `
@keyframes slideInRight {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOutRight {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(100%);
opacity: 0;
}
}
`;
document.head.appendChild(style);
}
// 点击模态框外部关闭
window.addEventListener('click', function(event) {
const modal = document.getElementById('knowledge-item-modal');
if (event.target === modal) {
closeKnowledgeItemModal();
}
});
// 切换到设置页面(用于功能未启用时的提示)
function switchToSettings() {
if (typeof switchPage === 'function') {
switchPage('settings');
// 等待设置页面加载后,切换到知识库配置部分
setTimeout(() => {
if (typeof switchSettingsSection === 'function') {
// 查找知识库配置部分(通常在基本设置中)
const knowledgeSection = document.querySelector('[data-section="knowledge"]');
if (knowledgeSection) {
switchSettingsSection('knowledge');
} else {
// 如果没有独立的知识库部分,切换到基本设置
switchSettingsSection('basic');
// 滚动到知识库配置区域
setTimeout(() => {
const knowledgeEnabledCheckbox = document.getElementById('knowledge-enabled');
if (knowledgeEnabledCheckbox) {
knowledgeEnabledCheckbox.scrollIntoView({ behavior: 'smooth', block: 'center' });
// 高亮显示
knowledgeEnabledCheckbox.parentElement.style.transition = 'background-color 0.3s';
knowledgeEnabledCheckbox.parentElement.style.backgroundColor = '#e3f2fd';
setTimeout(() => {
knowledgeEnabledCheckbox.parentElement.style.backgroundColor = '';
}, 2000);
}
}, 300);
}
}
}, 100);
}
}
// 自定义下拉组件交互
document.addEventListener('DOMContentLoaded', function() {
const wrapper = document.getElementById('knowledge-category-filter-wrapper');
const trigger = document.getElementById('knowledge-category-filter-trigger');
if (wrapper && trigger) {
// 点击触发器打开/关闭下拉菜单
trigger.addEventListener('click', function(e) {
e.stopPropagation();
wrapper.classList.toggle('open');
});
// 点击外部关闭下拉菜单
document.addEventListener('click', function(e) {
if (!wrapper.contains(e.target)) {
wrapper.classList.remove('open');
}
});
// 选择选项时更新选中状态
const dropdown = document.getElementById('knowledge-category-filter-dropdown');
if (dropdown) {
// 默认选中"全部"选项
const defaultOption = dropdown.querySelector('.custom-select-option[data-value=""]');
if (defaultOption) {
defaultOption.classList.add('selected');
}
dropdown.addEventListener('click', function(e) {
const option = e.target.closest('.custom-select-option');
if (option) {
// 移除之前的选中状态
dropdown.querySelectorAll('.custom-select-option').forEach(opt => {
opt.classList.remove('selected');
});
// 添加选中状态
option.classList.add('selected');
}
});
}
}
});