// js/chatbot/ui/embedding-config-ui.js // Embedding API 配置面板 (function(window, document) { 'use strict'; if (window.EmbeddingConfigUILoaded) return; const PRESETS = { openai: { name: 'OpenAI格式', endpoint: 'https://api.openai.com/v1/embeddings' }, jina: { name: 'Jina AI', endpoint: 'https://api.jina.ai/v1/embeddings' }, zhipu: { name: '智谱AI', endpoint: 'https://open.bigmodel.cn/api/paas/v4/embeddings' }, alibaba: { name: '阿里云百炼', endpoint: 'https://dashscope.aliyuncs.com/compatible-mode/v1/embeddings' } }; // 阿里云百炼支持的模型和维度 const ALIBABA_MODELS = { 'text-embedding-v1': { name: 'text-embedding-v1 (中文)', dims: 1536 }, 'text-embedding-v2': { name: 'text-embedding-v2 (多语言)', dims: 1536 }, 'text-embedding-v3': { name: 'text-embedding-v3 (高性能)', dims: 1024 }, 'text-embedding-v4': { name: 'text-embedding-v4 (多语言,支持2048维)', dims: 2048 } }; function createModal() { let modal = document.getElementById('embedding-config-modal'); if (modal) return modal; modal = document.createElement('div'); modal.id = 'embedding-config-modal'; modal.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 520px; max-width: 90vw; max-height: 80vh; background: #fff; border-radius: 12px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); display: none; z-index: 100001; padding: 0; overflow: hidden; `; modal.innerHTML = `

向量搜索与重排配置

启用后将使用语义相似度检索,提升检索准确率

请输入服务商支持的嵌入模型ID

索引管理

未建立索引
`; // 遮罩层 const overlay = document.createElement('div'); overlay.id = 'embedding-config-overlay'; overlay.style.cssText = ` position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); display: none; z-index: 9999; `; document.body.appendChild(overlay); document.body.appendChild(modal); // 绑定事件 bindEvents(modal, overlay); return modal; } function bindEvents(modal, overlay) { const $ = (id) => document.getElementById(id); // Tab切换 const tabs = document.querySelectorAll('.emb-tab'); const tabContents = document.querySelectorAll('.emb-tab-content'); tabs.forEach(tab => { tab.addEventListener('click', () => { // 更新tab样式 tabs.forEach(t => { t.classList.remove('active'); t.style.color = '#6b7280'; t.style.fontWeight = '500'; t.style.borderBottomColor = 'transparent'; }); tab.classList.add('active'); tab.style.color = '#3b82f6'; tab.style.fontWeight = '600'; tab.style.borderBottomColor = '#3b82f6'; // 切换内容 const targetId = tab.id.replace('-tab-', '-') + '-content'; tabContents.forEach(content => { content.style.display = 'none'; }); const targetContent = document.getElementById(targetId); if (targetContent) { targetContent.style.display = 'block'; } }); }); // 关闭 $('emb-close-btn').onclick = () => close(); $('emb-cancel-btn').onclick = () => close(); $('rerank-cancel-btn').onclick = () => close(); overlay.onclick = () => close(); // 服务商切换 $('emb-provider').onchange = function() { const provider = this.value; updateProviderUI(provider); }; // 获取模型列表(仅 OpenAI格式) $('emb-fetch-models-btn').onclick = async () => { await fetchModels(); }; // 测试连接 $('emb-test-btn').onclick = async () => { await testConnection(); }; // 保存 $('emb-save-btn').onclick = () => { saveConfig(); }; // 建立索引 $('emb-build-index-btn').onclick = async () => { await buildIndex(false); }; // 重建索引 $('emb-rebuild-index-btn').onclick = async () => { if (confirm('重建索引将删除现有索引,确定继续吗?')) { await buildIndex(true); } }; // 重排tab事件 // 测试连接 $('rerank-test-btn').onclick = async () => { await testRerankConnection(); }; // 保存配置 $('rerank-save-btn').onclick = () => { saveRerankConfig(); }; } function updateProviderUI(provider) { const $ = (id) => document.getElementById(id); const preset = PRESETS[provider]; // 显示/隐藏获取模型列表按钮(仅 OpenAI格式支持) const fetchBtn = $('emb-fetch-models-btn'); const modelHint = $('emb-model-hint'); if (provider === 'openai') { fetchBtn.style.display = 'block'; modelHint.textContent = '可手动输入模型ID,或点击"获取列表"从服务器获取'; } else { fetchBtn.style.display = 'none'; modelHint.textContent = '请输入服务商支持的嵌入模型ID'; } // 更新端点(所有服务商都显示,允许修改) $('emb-endpoint-wrap').style.display = 'block'; $('emb-endpoint').value = preset?.endpoint || window.EmbeddingClient?.config.endpoint || ''; // OpenAI和阿里云百炼支持自定义维度 $('emb-dims-wrap').style.display = (provider === 'openai' || provider === 'alibaba') ? 'block' : 'none'; // 当选择阿里云百炼时,更新维度提示 if (provider === 'alibaba') { const dimsInput = $('emb-dimensions'); const dimsHint = dimsInput.nextElementSibling; const modelInput = $('emb-model'); // 根据当前模型更新默认维度 const updateDimensionsForModel = () => { const modelId = modelInput.value.trim(); const modelInfo = ALIBABA_MODELS[modelId]; if (modelInfo) { dimsInput.placeholder = `默认: ${modelInfo.dims}`; dimsHint.textContent = `默认维度: ${modelInfo.dims}。可输入1-${modelInfo.dims}之间的整数,留空使用默认。`; } }; // 初始化时更新一次 updateDimensionsForModel(); // 模型改变时更新 modelInput.addEventListener('change', updateDimensionsForModel); } } async function fetchModels() { const $ = (id) => document.getElementById(id); const btn = $('emb-fetch-models-btn'); const modelInput = $('emb-model'); const modelHint = $('emb-model-hint'); const provider = $('emb-provider').value; const apiKey = $('emb-api-key').value; let endpoint = $('emb-endpoint').value; if (!apiKey) { modelHint.style.color = '#dc2626'; modelHint.textContent = '❌ 请先输入 API Key'; setTimeout(() => { modelHint.style.color = '#6b7280'; modelHint.textContent = '可手动输入模型ID,或点击"获取列表"从服务器获取'; }, 3000); return; } if (!endpoint) { endpoint = PRESETS[provider]?.endpoint || ''; } // 构建 models 端点 let modelsEndpoint = endpoint; if (modelsEndpoint.endsWith('/embeddings')) { modelsEndpoint = modelsEndpoint.replace('/embeddings', '/models'); } else if (modelsEndpoint.endsWith('/v1')) { modelsEndpoint = modelsEndpoint + '/models'; } else if (!modelsEndpoint.endsWith('/models')) { modelsEndpoint = modelsEndpoint.replace(/\/$/, '') + '/v1/models'; } btn.textContent = '获取中...'; btn.disabled = true; modelHint.style.color = '#6b7280'; modelHint.textContent = '正在获取模型列表...'; try { const response = await fetch(modelsEndpoint, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); const models = data.data || []; // 过滤出嵌入模型(支持多种命名模式) const embeddingModels = models.filter(m => { const id = (m.id || '').toLowerCase(); return id.includes('embedding') || id.includes('embed') || id.includes('bge') || id.includes('text-similarity') || id.includes('sentence') || id.includes('vector'); }); if (embeddingModels.length === 0) { modelHint.style.color = '#f59e0b'; modelHint.textContent = `⚠️ 未找到嵌入模型(共 ${models.length} 个模型)`; setTimeout(() => { modelHint.style.color = '#6b7280'; modelHint.textContent = '可手动输入模型ID,或点击"获取列表"从服务器获取'; }, 3000); return; } // 创建选择对话框 showModelSelector(embeddingModels, modelInput); modelHint.style.color = '#059669'; modelHint.textContent = `✅ 找到 ${embeddingModels.length} 个嵌入模型`; setTimeout(() => { modelHint.style.color = '#6b7280'; modelHint.textContent = '可手动输入模型ID,或点击"获取列表"从服务器获取'; }, 3000); } catch (error) { modelHint.style.color = '#dc2626'; modelHint.textContent = `❌ 获取失败: ${error.message}`; setTimeout(() => { modelHint.style.color = '#6b7280'; modelHint.textContent = '可手动输入模型ID,或点击"获取列表"从服务器获取'; }, 3000); } finally { btn.textContent = '获取列表'; btn.disabled = false; } } function showModelSelector(models, targetInput) { // 创建一个简单的选择对话框 const container = document.createElement('div'); container.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 400px; max-width: 90vw; max-height: 60vh; background: #fff; border-radius: 12px; box-shadow: 0 20px 60px rgba(0,0,0,0.4); z-index: 100002; padding: 0; overflow: hidden; `; const header = document.createElement('div'); header.style.cssText = 'padding: 16px 20px; border-bottom: 1px solid #e5e7eb; display: flex; justify-content: space-between; align-items: center;'; header.innerHTML = `

选择嵌入模型

`; const list = document.createElement('div'); list.style.cssText = 'max-height: 400px; overflow-y: auto; padding: 8px;'; models.forEach(model => { const item = document.createElement('div'); item.style.cssText = ` padding: 12px 16px; margin: 4px 0; border-radius: 8px; cursor: pointer; transition: all 0.2s; border: 1px solid #e5e7eb; `; item.innerHTML = `
${model.id}
${model.owned_by ? `
by ${model.owned_by}
` : ''} `; item.onmouseover = () => { item.style.background = '#f3f4f6'; item.style.borderColor = '#3b82f6'; }; item.onmouseout = () => { item.style.background = '#fff'; item.style.borderColor = '#e5e7eb'; }; item.onclick = () => { targetInput.value = model.id; document.body.removeChild(overlay); document.body.removeChild(container); }; list.appendChild(item); }); container.appendChild(header); container.appendChild(list); const overlay = document.createElement('div'); overlay.style.cssText = ` position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); z-index: 100001; `; const closeHandler = () => { document.body.removeChild(overlay); document.body.removeChild(container); }; overlay.onclick = closeHandler; header.querySelector('#model-selector-close').onclick = closeHandler; document.body.appendChild(overlay); document.body.appendChild(container); } async function testConnection() { const $ = (id) => document.getElementById(id); const resultDiv = $('emb-test-result'); const btn = $('emb-test-btn'); const config = { provider: $('emb-provider').value, apiKey: $('emb-api-key').value, endpoint: $('emb-endpoint').value || PRESETS[$('emb-provider').value].endpoint, model: $('emb-model').value, dimensions: parseInt($('emb-dimensions').value) || null }; if (!config.apiKey) { resultDiv.style.display = 'block'; resultDiv.style.color = '#dc2626'; resultDiv.textContent = '❌ 请输入API Key'; return; } btn.textContent = '测试中...'; btn.disabled = true; resultDiv.style.display = 'none'; try { // 确保 EmbeddingClient 已加载 if (!window.EmbeddingClient || typeof window.EmbeddingClient.saveConfig !== 'function') { // 尝试动态加载 try { const scripts = Array.from(document.getElementsByTagName('script')); const sem = scripts.find(s => (s.src || '').includes('semantic-vector-search.js')); const candidates = []; if (sem && sem.src) candidates.push(sem.src.replace('semantic-vector-search.js', 'embedding-client.js')); const rer = scripts.find(s => (s.src || '').includes('rerank-client.js')); if (rer && rer.src) candidates.push(rer.src.replace('rerank-client.js', 'embedding-client.js')); candidates.push('../../js/chatbot/agents/embedding-client.js'); for (const url of Array.from(new Set(candidates))) { await new Promise((resolve, reject) => { const s=document.createElement('script'); s.src=url; s.async=true; s.onload=resolve; s.onerror=reject; document.head.appendChild(s); }); if (window.EmbeddingClient && typeof window.EmbeddingClient.saveConfig === 'function') break; } } catch(_) {} } if (!window.EmbeddingClient || typeof window.EmbeddingClient.saveConfig !== 'function') { throw new Error('EmbeddingClient 未加载'); } window.EmbeddingClient.saveConfig({ ...config, enabled: true }); // 测试调用 const testText = '测试文本'; const vector = await window.EmbeddingClient.embed(testText); resultDiv.style.display = 'block'; resultDiv.style.color = '#059669'; resultDiv.textContent = `✅ 连接成功!向量维度: ${vector.length}`; } catch (error) { resultDiv.style.display = 'block'; resultDiv.style.color = '#dc2626'; resultDiv.textContent = `❌ 连接失败: ${error.message}`; } finally { btn.textContent = '测试连接'; btn.disabled = false; } } function saveConfig() { const $ = (id) => document.getElementById(id); const config = { enabled: $('emb-enabled').checked, provider: $('emb-provider').value, apiKey: $('emb-api-key').value, endpoint: $('emb-endpoint').value || PRESETS[$('emb-provider').value].endpoint, model: $('emb-model').value, dimensions: parseInt($('emb-dimensions').value) || null }; if (!window.EmbeddingClient || typeof window.EmbeddingClient.saveConfig !== 'function') { if (window.ChatbotUtils && window.ChatbotUtils.showToast) { window.ChatbotUtils.showToast('保存失败:EmbeddingClient 未加载', 'error', 3000); } else { alert('❌ 保存失败:EmbeddingClient 未加载'); } return; } if (!window.EmbeddingClient || typeof window.EmbeddingClient.saveConfig !== 'function') { if (window.ChatbotUtils && window.ChatbotUtils.showToast) { window.ChatbotUtils.showToast('保存失败:EmbeddingClient 未加载', 'error', 3000); } else { alert('❌ 保存失败:EmbeddingClient 未加载'); } return; } window.EmbeddingClient.saveConfig(config); if (window.ChatbotUtils?.showToast) { window.ChatbotUtils.showToast('配置已保存', 'success', 2000); } close(); } // 重排相关函数 async function testRerankConnection() { const $ = (id) => document.getElementById(id); const resultDiv = $('rerank-test-result'); const btn = $('rerank-test-btn'); const config = { provider: $('rerank-provider').value, apiKey: $('rerank-api-key').value, endpoint: $('rerank-endpoint').value, model: $('rerank-model').value, topN: parseInt($('rerank-top-n').value) || 10 }; if (!config.apiKey) { resultDiv.style.display = 'block'; resultDiv.style.color = '#dc2626'; resultDiv.textContent = '❌ 请输入API Key'; return; } btn.textContent = '测试中...'; btn.disabled = true; resultDiv.style.display = 'none'; try { // 临时保存配置并测试 if (!window.RerankClient) { throw new Error('RerankClient 未加载'); } window.RerankClient.saveConfig({ ...config, enabled: true }); // 测试调用 const testQuery = '测试查询'; const testDocs = ['文档1内容', '文档2内容', '文档3内容']; const results = await window.RerankClient.rerank(testQuery, testDocs); resultDiv.style.display = 'block'; resultDiv.style.color = '#059669'; resultDiv.textContent = `✅ 连接成功!返回 ${results.length} 个结果`; } catch (error) { resultDiv.style.display = 'block'; resultDiv.style.color = '#dc2626'; resultDiv.textContent = `❌ 连接失败: ${error.message}`; } finally { btn.textContent = '测试连接'; btn.disabled = false; } } function saveRerankConfig() { const $ = (id) => document.getElementById(id); // 获取选中的scope const scopeRadios = document.getElementsByName('rerank-scope'); let scope = 'vector-only'; for (const radio of scopeRadios) { if (radio.checked) { scope = radio.value; break; } } const config = { enabled: $('rerank-enabled').checked, scope: scope, provider: $('rerank-provider').value, apiKey: $('rerank-api-key').value, endpoint: $('rerank-endpoint').value, model: $('rerank-model').value, topN: parseInt($('rerank-top-n').value) || 10 }; if (!window.RerankClient) { alert('RerankClient 未加载'); return; } window.RerankClient.saveConfig(config); if (window.ChatbotUtils?.showToast) { window.ChatbotUtils.showToast('重排配置已保存', 'success', 2000); } close(); } async function buildIndex(forceRebuild) { const groups = window.data?.semanticGroups; if (!groups || groups.length === 0) { alert('当前文档没有意群数据,请先生成意群'); return; } const docId = window.ChatbotCore?.getCurrentDocId() || window.data?.id || 'default'; try { await window.SemanticVectorSearch.indexGroups(groups, docId, { showProgress: true, forceRebuild }); await updateIndexStatus(); } catch (error) { alert(`建立索引失败: ${error.message}`); } } async function updateIndexStatus() { const $ = (id) => document.getElementById(id); const statusDiv = $('emb-index-status'); if (!window.SemanticVectorSearch) { statusDiv.textContent = '向量搜索模块未加载'; return; } const docId = window.ChatbotCore?.getCurrentDocId() || window.data?.id || 'default'; try { const status = await window.SemanticVectorSearch.getIndexStatus(docId); if (status.indexed) { statusDiv.innerHTML = `
✓ 已建立索引
意群数: ${status.count} | 维度: ${status.dimensions} | 大小: ${status.size}
`; } else { statusDiv.textContent = '未建立索引'; } } catch (error) { statusDiv.textContent = '无法获取索引状态'; } } function open() { const modal = createModal(); const overlay = document.getElementById('embedding-config-overlay'); const $ = (id) => document.getElementById(id); // 加载向量搜索配置 const config = window.EmbeddingClient?.config || {}; $('emb-enabled').checked = config.enabled || false; $('emb-provider').value = config.provider || 'openai'; $('emb-api-key').value = config.apiKey || ''; $('emb-model').value = config.model || 'text-embedding-3-small'; $('emb-dimensions').value = config.dimensions || ''; updateProviderUI(config.provider || 'openai'); updateIndexStatus(); // 加载重排配置 const rerankConfig = window.RerankClient?.config || {}; $('rerank-enabled').checked = rerankConfig.enabled || false; $('rerank-provider').value = rerankConfig.provider || 'jina'; $('rerank-api-key').value = rerankConfig.apiKey || ''; $('rerank-endpoint').value = rerankConfig.endpoint || ''; $('rerank-model').value = rerankConfig.model || 'jina-reranker-v2-base-multilingual'; $('rerank-top-n').value = rerankConfig.topN || 10; // 设置scope单选按钮 const scope = rerankConfig.scope || 'vector-only'; const scopeRadios = document.getElementsByName('rerank-scope'); for (const radio of scopeRadios) { if (radio.value === scope) { radio.checked = true; break; } } overlay.style.display = 'block'; modal.style.display = 'block'; } function close() { const modal = document.getElementById('embedding-config-modal'); const overlay = document.getElementById('embedding-config-overlay'); if (modal) modal.style.display = 'none'; if (overlay) overlay.style.display = 'none'; } // 导出 window.EmbeddingConfigUI = { open, close }; window.EmbeddingConfigUILoaded = true; console.log('[EmbeddingConfigUI] 配置面板已加载'); })(window, document);