diff --git a/modules/story-summary/story-summary-ui.js b/modules/story-summary/story-summary-ui.js index 858f493..cc74289 100644 --- a/modules/story-summary/story-summary-ui.js +++ b/modules/story-summary/story-summary-ui.js @@ -916,16 +916,6 @@ All checks passed. Beginning incremental extraction... $('vector-config-area').classList.toggle('hidden', !e.target.checked); }; - $('btn-test-vector-api').onclick = () => { - $('btn-test-vector-api').disabled = true; - setStatusText($('embedding-api-connect-status'), '测试中...', 'loading'); - saveConfig(); // 先保存新 Key 到 localStorage - postMsg('VECTOR_TEST_ONLINE', { - provider: getVectorConfig().embeddingApi.provider, - config: getVectorConfig().embeddingApi - }); - }; - ['l0', 'embedding', 'rerank'].forEach(prefix => { $(`${prefix}-api-provider`).onchange = e => { saveCurrentVectorApiProfile(prefix); @@ -941,6 +931,18 @@ All checks passed. Beginning incremental extraction... }; $(`${prefix}-btn-connect`).onclick = () => fetchVectorModels(prefix); + $(`${prefix}-btn-test`).onclick = () => { + const btn = $(`${prefix}-btn-test`); + if (btn) btn.disabled = true; + setStatusText($(`${prefix}-api-connect-status`), '测试中...', 'loading'); + saveConfig(); + const cfg = getVectorConfig(); + postMsg('VECTOR_TEST_ONLINE', { + target: prefix, + provider: cfg[`${prefix}Api`].provider, + config: cfg[`${prefix}Api`], + }); + }; }); $('btn-add-filter-rule').onclick = addFilterRule; @@ -987,11 +989,12 @@ All checks passed. Beginning incremental extraction... postMsg('REQUEST_ANCHOR_STATS'); } - function updateVectorOnlineStatus(status, message) { - const btn = $('btn-test-vector-api'); + function updateVectorOnlineStatus(target, status, message) { + const prefix = target || 'embedding'; + const btn = $(`${prefix}-btn-test`); if (btn) btn.disabled = false; setStatusText( - $('embedding-api-connect-status'), + $(`${prefix}-api-connect-status`), message || '', status === 'error' ? 'error' : status === 'success' ? 'success' : 'loading' ); @@ -2099,7 +2102,7 @@ All checks passed. Beginning incremental extraction... break; case 'VECTOR_ONLINE_STATUS': - updateVectorOnlineStatus(d.status, d.message); + updateVectorOnlineStatus(d.target, d.status, d.message); break; case 'VECTOR_STATS': diff --git a/modules/story-summary/story-summary.html b/modules/story-summary/story-summary.html index 6281ece..a111f1f 100644 --- a/modules/story-summary/story-summary.html +++ b/modules/story-summary/story-summary.html @@ -472,6 +472,7 @@
+
@@ -520,7 +521,7 @@
- +
@@ -569,6 +570,7 @@
+
diff --git a/modules/story-summary/story-summary.js b/modules/story-summary/story-summary.js index 245cdd6..f69140e 100644 --- a/modules/story-summary/story-summary.js +++ b/modules/story-summary/story-summary.js @@ -43,6 +43,8 @@ import { runSummaryGeneration } from "./generate/generator.js"; // vector service import { embed, getEngineFingerprint, testOnlineService } from "./vector/utils/embedder.js"; +import { testL0Service } from "./vector/llm/llm-service.js"; +import { testRerankService } from "./vector/llm/reranker.js"; // tokenizer import { preload as preloadTokenizer, injectEntities, isReady as isTokenizerReady } from "./vector/utils/tokenizer.js"; @@ -421,17 +423,23 @@ function handleAnchorCancel() { postToFrame({ type: "ANCHOR_GEN_PROGRESS", current: -1, total: 0 }); } -async function handleTestOnlineService(provider, config) { +async function handleTestOnlineService(provider, config, target = "embedding") { try { - postToFrame({ type: "VECTOR_ONLINE_STATUS", status: "downloading", message: "连接中..." }); - const result = await testOnlineService(provider, config); + postToFrame({ type: "VECTOR_ONLINE_STATUS", target, status: "downloading", message: "连接中..." }); + let result; + if (target === "l0") result = await testL0Service(config); + else if (target === "rerank") result = await testRerankService(config); + else result = await testOnlineService(provider, config); postToFrame({ type: "VECTOR_ONLINE_STATUS", + target, status: "success", - message: `连接成功 (${result.dims}维)`, + message: target === "embedding" + ? `连接成功 (${result.dims}维)` + : (result.message || "连接成功"), }); } catch (e) { - postToFrame({ type: "VECTOR_ONLINE_STATUS", status: "error", message: e.message }); + postToFrame({ type: "VECTOR_ONLINE_STATUS", target, status: "error", message: e.message }); } } @@ -1631,7 +1639,7 @@ async function handleFrameMessage(event) { break; case "VECTOR_TEST_ONLINE": - handleTestOnlineService(data.provider, data.config); + handleTestOnlineService(data.provider, data.config, data.target || "embedding"); break; case "VECTOR_GENERATE": diff --git a/modules/story-summary/vector/llm/llm-service.js b/modules/story-summary/vector/llm/llm-service.js index 3608fbd..d906976 100644 --- a/modules/story-summary/vector/llm/llm-service.js +++ b/modules/story-summary/vector/llm/llm-service.js @@ -39,6 +39,17 @@ function getL0ApiConfig() { }; } +function normalizeL0ApiConfig(apiConfig = null) { + const fallback = getL0ApiConfig(); + const next = apiConfig || {}; + return { + provider: String(next.provider || fallback.provider || 'siliconflow').trim(), + url: String(next.url || fallback.url || DEFAULT_L0_API_URL).trim(), + key: String(next.key || fallback.key || '').trim(), + model: String(next.model || fallback.model || DEFAULT_L0_MODEL).trim(), + }; +} + function getNextKey(rawKey) { const keys = String(rawKey || '') .split(/[,;|\n]+/) @@ -60,12 +71,13 @@ export async function callLLM(messages, options = {}) { temperature = 0.2, max_tokens = 500, timeout = 40000, + apiConfig = null, } = options; const mod = getStreamingModule(); if (!mod) throw new Error('Streaming module not ready'); - const apiCfg = getL0ApiConfig(); + const apiCfg = normalizeL0ApiConfig(apiConfig); const apiKey = getNextKey(apiCfg.key); if (!apiKey) { throw new Error('L0 requires siliconflow API key'); @@ -111,6 +123,24 @@ export async function callLLM(messages, options = {}) { } } +export async function testL0Service(apiConfig = {}) { + if (!apiConfig?.key) { + throw new Error('请配置 L0 API Key'); + } + const result = await callLLM([ + { role: 'system', content: '你是一个测试助手。请只输出 OK。' }, + { role: 'user', content: '只输出 OK' }, + ], { + apiConfig, + temperature: 0, + max_tokens: 16, + timeout: 15000, + }); + const text = String(result || '').trim(); + if (!text) throw new Error('返回为空'); + return { success: true, message: `连接成功:${text.slice(0, 60)}` }; +} + export function cancelAllL0Requests() { const mod = getStreamingModule(); if (!mod?.cancel) return; diff --git a/modules/story-summary/vector/llm/reranker.js b/modules/story-summary/vector/llm/reranker.js index 4fbd37f..5266d65 100644 --- a/modules/story-summary/vector/llm/reranker.js +++ b/modules/story-summary/vector/llm/reranker.js @@ -273,19 +273,51 @@ export async function rerankChunks(query, chunks, options = {}) { /** * 测试 Rerank 服务连接 */ -export async function testRerankService() { - const key = getApiKey(); - if (!key) { - throw new Error('请配置硅基 API Key'); +export async function testRerankService(apiConfig = {}) { + const next = { + provider: String(apiConfig.provider || 'siliconflow').trim(), + url: String(apiConfig.url || DEFAULT_RERANK_URL).trim(), + key: String(apiConfig.key || '').trim(), + model: String(apiConfig.model || RERANK_MODEL).trim(), + }; + if (!next.key) { + throw new Error('请配置 Rerank API Key'); } + const key = getNextRerankKey(next.key); + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 15000); try { - const { results } = await rerank('测试查询', ['测试文档1', '测试文档2'], { topN: 2 }); - return { - success: true, - message: `连接成功,返回 ${results.length} 个结果`, + const baseUrl = String(next.url || DEFAULT_RERANK_URL).replace(/\/+$/, ''); + const response = await fetch(`${baseUrl}/rerank`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${key}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + model: next.model, + query: '测试查询', + documents: ['测试文档1', '测试文档2'], + top_n: 2, + return_documents: false, + }), + signal: controller.signal, + }); + clearTimeout(timeoutId); + if (!response.ok) { + const errorText = await response.text().catch(() => ''); + throw new Error(`Rerank API ${response.status}: ${errorText.slice(0, 200)}`); + } + const data = await response.json(); + const results = Array.isArray(data.results) ? data.results : []; + return { + success: true, + message: `连接成功:返回 ${results.length} 个结果`, }; } catch (e) { throw new Error(`连接失败: ${e.message}`); + } finally { + clearTimeout(timeoutId); } }