/** * KBModal - 知识库管理面板 * * 支持:创建/删除集合、上传/删除文档、选择嵌入模型、启用 RAG 检索 */ import { state, KEYS } from '../state.js'; import { initVectorStore, getVectorStore, addDocumentToKB, removeDocumentFromKB, getDocumentsInCollection, retrieveContext, buildRagSystemPrompt } from '../rag.js'; import { fileToText, formatSize, escapeHtml, truncate } from '../utils.js'; import { showToast } from './toast.js'; let kbModalEl; let currentColId = null; /** 当前是否启用 RAG */ let ragEnabled = false; let ragCollectionId = null; export function initKBModal() { kbModalEl = document.querySelector('#kbModal'); document.querySelector('#btnKB').addEventListener('click', openKBModal); document.querySelector('#btnCloseKB').addEventListener('click', closeKBModal); kbModalEl.addEventListener('click', (e) => { if (e.target === kbModalEl) closeKBModal(); }); // 新建集合 document.querySelector('#btnNewCollection').addEventListener('click', createCollection); // 上传文档 document.querySelector('#btnUploadDoc').addEventListener('click', () => { if (!currentColId) { showToast('请先选择一个知识库集合', 'warning'); return; } document.querySelector('#kbFileInput').click(); }); document.querySelector('#kbFileInput').addEventListener('change', handleDocUpload); // RAG 开关 document.querySelector('#toggleRAG').addEventListener('change', (e) => { ragEnabled = e.target.checked; if (ragEnabled && currentColId) { ragCollectionId = currentColId; showToast('RAG 知识检索已开启', 'success'); } else if (ragEnabled && !currentColId) { e.target.checked = false; ragEnabled = false; showToast('请先选择一个知识库集合', 'warning'); return; } else { ragCollectionId = null; showToast('RAG 知识检索已关闭', 'info'); } updateRagBadge(); }); // 嵌入模型变更 document.querySelector('#selectEmbedModel').addEventListener('change', async (e) => { if (!currentColId) return; const vs = getVectorStore(); const col = await vs.getCollection(currentColId); if (col) { col.embeddingModel = e.target.value; await vs.updateCollection(col); } }); } // ── RAG 接口 ── export function isRagEnabled() { return ragEnabled && !!ragCollectionId; } export function getRagCollectionId() { return ragCollectionId; } export async function performRagRetrieval(query) { if (!isRagEnabled()) return null; try { const { context, results } = await retrieveContext(query, ragCollectionId, 5); if (!context) return null; return { context, results, ragPrompt: buildRagSystemPrompt(context) }; } catch (err) { console.warn('[KB] RAG 检索失败:', err); return null; } } // ── UI ── function updateRagBadge() { const badge = document.querySelector('#badgeRAG'); if (!badge) return; badge.style.display = ragEnabled ? '' : 'none'; } async function openKBModal() { kbModalEl.style.display = ''; await refreshCollections(); await populateEmbedModels(); } function closeKBModal() { kbModalEl.style.display = 'none'; } /** * 刷新集合列表 */ async function refreshCollections() { const vs = await initVectorStore(); const collections = await vs.getCollections(); const listEl = document.querySelector('#kbCollectionList'); if (collections.length === 0) { listEl.innerHTML = '

暂无知识库,点击上方「新建集合」创建

'; currentColId = null; refreshDocList(); return; } listEl.innerHTML = collections.map(c => `
${escapeHtml(c.name)} ${c.docCount || 0} 文档 · ${(c.chunkCount || 0)} 分块
`).join(''); // 点击选中集合 listEl.querySelectorAll('.kb-collection-item').forEach(el => { el.addEventListener('click', async (e) => { if (e.target.closest('.kb-col-delete')) return; currentColId = el.dataset.id; await refreshCollections(); await refreshDocList(); // 设置嵌入模型下拉 const vs = getVectorStore(); const col = await vs.getCollection(currentColId); if (col?.embeddingModel) { document.querySelector('#selectEmbedModel').value = col.embeddingModel; } }); }); // 删除集合 listEl.querySelectorAll('.kb-col-delete').forEach(btn => { btn.addEventListener('click', async (e) => { e.stopPropagation(); const id = btn.dataset.id; if (!confirm('确定删除此知识库集合?所有文档将被清除。')) return; const vs = getVectorStore(); await vs.deleteCollection(id); if (currentColId === id) currentColId = null; if (ragCollectionId === id) { ragEnabled = false; ragCollectionId = null; document.querySelector('#toggleRAG').checked = false; updateRagBadge(); } showToast('集合已删除', 'success'); await refreshCollections(); await refreshDocList(); }); }); } /** * 刷新文档列表 */ async function refreshDocList() { const docListEl = document.querySelector('#kbDocList'); if (!currentColId) { docListEl.innerHTML = '

← 选择一个知识库集合

'; return; } const docs = await getDocumentsInCollection(currentColId); if (docs.length === 0) { docListEl.innerHTML = '

此集合暂无文档,上传文件开始构建知识库

'; return; } docListEl.innerHTML = docs.map(d => `
📄
${escapeHtml(d.filename)} ${d.chunkCount} 个分块
`).join(''); docListEl.querySelectorAll('.kb-doc-delete').forEach(btn => { btn.addEventListener('click', async () => { const docId = btn.dataset.docid; if (!confirm('确定删除此文档?')) return; await removeDocumentFromKB(currentColId, docId); showToast('文档已删除', 'success'); await refreshCollections(); await refreshDocList(); }); }); } /** * 创建新集合 */ async function createCollection() { const nameInput = document.querySelector('#inputKBName'); const name = nameInput.value.trim(); if (!name) { showToast('请输入知识库名称', 'warning'); return; } const vs = await initVectorStore(); const col = await vs.createCollection(name); currentColId = col.id; nameInput.value = ''; showToast(`知识库「${name}」已创建`, 'success'); await refreshCollections(); await refreshDocList(); } /** * 上传文档处理 */ async function handleDocUpload(e) { const files = e.target.value; e.target.value = ''; const embedModel = document.querySelector('#selectEmbedModel').value; if (!embedModel) { showToast('请先选择一个嵌入模型', 'warning'); return; } const fileArr = Array.from(e.target.files || []); if (fileArr.length === 0) return; const progressEl = document.querySelector('#kbProgress'); const progressTextEl = document.querySelector('#kbProgressText'); for (const file of fileArr) { if (file.size > 5 * 1024 * 1024) { showToast(`${file.name} 超过 5MB 限制`, 'warning'); continue; } try { progressEl.style.display = ''; progressTextEl.textContent = `正在处理 ${file.name}...`; const content = await fileToText(file); await addDocumentToKB( currentColId, file.name, content, embedModel, (done, total, msg) => { progressTextEl.textContent = `${file.name}: ${msg} (${done}/${total})`; } ); showToast(`${file.name} 已添加到知识库`, 'success'); } catch (err) { console.error('[KB] 文档处理失败:', err); showToast(`${file.name} 处理失败: ${err.message}`, 'error'); } } progressEl.style.display = 'none'; await refreshCollections(); await refreshDocList(); } /** * 填充嵌入模型下拉 */ async function populateEmbedModels() { const select = document.querySelector('#selectEmbedModel'); const api = state.get(KEYS.API); if (!api) return; try { const data = await api.listModels(); select.innerHTML = ''; if (data.models) { // 推荐的嵌入模型优先显示 const embedKeywords = ['embed', 'nomic', 'mxbai', 'bge', 'e5', 'snowflake']; const recommended = []; const others = []; for (const m of data.models) { const name = m.name.toLowerCase(); if (embedKeywords.some(kw => name.includes(kw))) { recommended.push(m); } else { others.push(m); } } if (recommended.length > 0) { const optgroup = document.createElement('optgroup'); optgroup.label = '推荐(嵌入模型)'; recommended.forEach(m => { const opt = document.createElement('option'); opt.value = m.name; opt.textContent = m.name; optgroup.appendChild(opt); }); select.appendChild(optgroup); } const optgroup2 = document.createElement('optgroup'); optgroup2.label = '其他模型'; others.forEach(m => { const opt = document.createElement('option'); opt.value = m.name; opt.textContent = m.name; optgroup2.appendChild(opt); }); select.appendChild(optgroup2); } } catch (err) { console.warn('[KB] 加载模型列表失败:', err); } }