From af7e0f689d44b2a0f59af7d4a9547bceb5cf85c2 Mon Sep 17 00:00:00 2001 From: bielie Date: Fri, 3 Apr 2026 15:31:13 +0800 Subject: [PATCH] feat(story-summary): make vector APIs configurable --- modules/story-summary/data/config.js | 76 +++++-- modules/story-summary/story-summary-ui.js | 205 +++++++++++++++--- modules/story-summary/story-summary.html | 140 ++++++++++-- modules/story-summary/story-summary.js | 8 +- .../story-summary/vector/llm/llm-service.js | 36 ++- modules/story-summary/vector/llm/reranker.js | 35 ++- .../story-summary/vector/llm/siliconflow.js | 46 ++-- .../vector/pipeline/state-integration.js | 85 +------- .../story-summary/vector/utils/embedder.js | 24 +- 9 files changed, 468 insertions(+), 187 deletions(-) diff --git a/modules/story-summary/data/config.js b/modules/story-summary/data/config.js index a7944c4..a5b5f52 100644 --- a/modules/story-summary/data/config.js +++ b/modules/story-summary/data/config.js @@ -254,6 +254,12 @@ export const DEFAULT_SUMMARY_USER_CONFIRM_PROMPT = `怎么截断了!重新完 `; export const DEFAULT_SUMMARY_ASSISTANT_PREFILL_PROMPT = '下面重新生成完整JSON。'; +const DEFAULT_VECTOR_PROVIDER = "siliconflow"; +const DEFAULT_L0_URL = "https://api.siliconflow.cn/v1"; +const DEFAULT_OPENROUTER_URL = "https://openrouter.ai/api/v1"; +const DEFAULT_L0_MODEL = "Qwen/Qwen3-8B"; +const DEFAULT_EMBEDDING_MODEL = "BAAI/bge-m3"; +const DEFAULT_RERANK_MODEL = "BAAI/bge-reranker-v2-m3"; export function getSettings() { const ext = (extension_settings[EXT_ID] ||= {}); @@ -261,6 +267,51 @@ export function getSettings() { return ext; } +function normalizeOpenAiCompatApiConfig(src, defaults = {}) { + const provider = String(src?.provider || defaults.provider || DEFAULT_VECTOR_PROVIDER).toLowerCase(); + const defaultUrl = provider === "openrouter" + ? DEFAULT_OPENROUTER_URL + : String(defaults.url || DEFAULT_L0_URL); + return { + provider, + url: String(src?.url || defaultUrl || "").trim(), + key: String(src?.key || defaults.key || "").trim(), + model: String(src?.model || defaults.model || "").trim(), + modelCache: Array.isArray(src?.modelCache) ? src.modelCache.filter(Boolean) : [], + }; +} + +function normalizeVectorConfig(rawVector = null) { + const legacyOnline = rawVector?.online || {}; + const sharedProvider = String(legacyOnline.provider || DEFAULT_VECTOR_PROVIDER).toLowerCase(); + const sharedUrl = String(legacyOnline.url || (sharedProvider === "openrouter" ? DEFAULT_OPENROUTER_URL : DEFAULT_L0_URL)).trim(); + const sharedKey = String(legacyOnline.key || "").trim(); + + return { + enabled: !!rawVector?.enabled, + engine: "online", + l0Concurrency: Math.max(1, Math.min(50, Number(rawVector?.l0Concurrency) || 10)), + l0Api: normalizeOpenAiCompatApiConfig(rawVector?.l0Api, { + provider: sharedProvider, + url: sharedUrl, + key: sharedKey, + model: DEFAULT_L0_MODEL, + }), + embeddingApi: normalizeOpenAiCompatApiConfig(rawVector?.embeddingApi, { + provider: DEFAULT_VECTOR_PROVIDER, + url: DEFAULT_L0_URL, + key: sharedKey, + model: DEFAULT_EMBEDDING_MODEL, + }), + rerankApi: normalizeOpenAiCompatApiConfig(rawVector?.rerankApi, { + provider: DEFAULT_VECTOR_PROVIDER, + url: DEFAULT_L0_URL, + key: sharedKey, + model: DEFAULT_RERANK_MODEL, + }), + }; +} + export function getSummaryPanelConfig() { const clampKeepVisibleCount = (value) => { const n = Number.parseInt(value, 10); @@ -299,7 +350,7 @@ export function getSummaryPanelConfig() { summaryAssistantPrefillPrompt: DEFAULT_SUMMARY_ASSISTANT_PREFILL_PROMPT, memoryTemplate: DEFAULT_MEMORY_PROMPT_TEMPLATE, }, - vector: null, + vector: normalizeVectorConfig(), }; try { @@ -320,7 +371,7 @@ export function getSummaryPanelConfig() { ui: { ...defaults.ui, ...(parsed.ui || {}) }, textFilterRules, prompts: { ...defaults.prompts, ...(parsed.prompts || {}) }, - vector: parsed.vector || null, + vector: normalizeVectorConfig(parsed.vector || null), }; if (result.trigger.timing === "manual") result.trigger.enabled = false; @@ -349,16 +400,7 @@ export function getVectorConfig() { if (!raw) return null; const parsed = JSON.parse(raw); - const cfg = parsed.vector || null; - if (!cfg) return null; - - // Keep vector side normalized to online + siliconflow. - cfg.engine = "online"; - cfg.online = cfg.online || {}; - cfg.online.provider = "siliconflow"; - cfg.online.model = "BAAI/bge-m3"; - - return cfg; + return parsed.vector ? normalizeVectorConfig(parsed.vector) : normalizeVectorConfig(); } catch { return null; } @@ -376,15 +418,7 @@ export function saveVectorConfig(vectorCfg) { const raw = localStorage.getItem("summary_panel_config") || "{}"; const parsed = JSON.parse(raw); - parsed.vector = { - enabled: !!vectorCfg?.enabled, - engine: "online", - online: { - provider: "siliconflow", - key: vectorCfg?.online?.key || "", - model: "BAAI/bge-m3", - }, - }; + parsed.vector = normalizeVectorConfig(vectorCfg || null); localStorage.setItem("summary_panel_config", JSON.stringify(parsed)); CommonSettingStorage.set(SUMMARY_CONFIG_KEY, parsed); diff --git a/modules/story-summary/story-summary-ui.js b/modules/story-summary/story-summary-ui.js index b5490fd..97c49fc 100644 --- a/modules/story-summary/story-summary-ui.js +++ b/modules/story-summary/story-summary-ui.js @@ -297,6 +297,11 @@ All checks passed. Beginning incremental extraction... claude: { url: 'https://api.anthropic.com', needKey: true, canFetch: false }, custom: { url: '', needKey: true, canFetch: true } }; + const VECTOR_PROVIDER_DEFAULTS = { + siliconflow: { url: 'https://api.siliconflow.cn/v1', needKey: true, canFetch: true }, + openrouter: { url: 'https://openrouter.ai/api/v1', needKey: true, canFetch: true }, + custom: { url: '', needKey: true, canFetch: true } + }; const SECTION_META = { keywords: { title: '编辑关键词', hint: '每行一个关键词,格式:关键词|权重(核心/重要/一般)' }, @@ -344,7 +349,14 @@ All checks passed. Beginning incremental extraction... memoryTemplate: '', }, textFilterRules: [...DEFAULT_FILTER_RULES], - vector: { enabled: false, engine: 'online', local: { modelId: 'bge-small-zh' }, online: { provider: 'siliconflow', url: '', key: '', model: '' } } + vector: { + enabled: false, + engine: 'online', + l0Concurrency: 10, + l0Api: { provider: 'siliconflow', url: 'https://api.siliconflow.cn/v1', key: '', model: 'Qwen/Qwen3-8B', modelCache: [] }, + embeddingApi: { provider: 'siliconflow', url: 'https://api.siliconflow.cn/v1', key: '', model: 'BAAI/bge-m3', modelCache: [] }, + rerankApi: { provider: 'siliconflow', url: 'https://api.siliconflow.cn/v1', key: '', model: 'BAAI/bge-reranker-v2-m3', modelCache: [] } + } }; let summaryData = { keywords: [], events: [], characters: { main: [], relationships: [] }, arcs: [], facts: [] }; @@ -369,6 +381,42 @@ All checks passed. Beginning incremental extraction... window.parent.postMessage({ source: 'LittleWhiteBox-StoryFrame', type, ...data }, PARENT_ORIGIN); } + function normalizeVectorConfigUI(raw = null) { + const base = JSON.parse(JSON.stringify(config.vector)); + const legacyOnline = raw?.online || {}; + const sharedKey = String(legacyOnline.key || '').trim(); + const sharedUrl = String(legacyOnline.url || '').trim(); + + if (raw) { + base.enabled = !!raw.enabled; + base.engine = 'online'; + base.l0Concurrency = Math.max(1, Math.min(50, Number(raw.l0Concurrency) || 10)); + Object.assign(base.l0Api, { + provider: raw.l0Api?.provider || legacyOnline.provider || base.l0Api.provider, + url: raw.l0Api?.url || sharedUrl || base.l0Api.url, + key: raw.l0Api?.key || sharedKey || base.l0Api.key, + model: raw.l0Api?.model || base.l0Api.model, + modelCache: Array.isArray(raw.l0Api?.modelCache) ? raw.l0Api.modelCache : [], + }); + Object.assign(base.embeddingApi, { + provider: raw.embeddingApi?.provider || base.embeddingApi.provider, + url: raw.embeddingApi?.url || sharedUrl || base.embeddingApi.url, + key: raw.embeddingApi?.key || sharedKey || base.embeddingApi.key, + model: raw.embeddingApi?.model || legacyOnline.model || base.embeddingApi.model, + modelCache: Array.isArray(raw.embeddingApi?.modelCache) ? raw.embeddingApi.modelCache : [], + }); + Object.assign(base.rerankApi, { + provider: raw.rerankApi?.provider || base.rerankApi.provider, + url: raw.rerankApi?.url || sharedUrl || base.rerankApi.url, + key: raw.rerankApi?.key || sharedKey || base.rerankApi.key, + model: raw.rerankApi?.model || base.rerankApi.model, + modelCache: Array.isArray(raw.rerankApi?.modelCache) ? raw.rerankApi.modelCache : [], + }); + } + + return base; + } + // ═══════════════════════════════════════════════════════════════════════════ // Config Management // ═══════════════════════════════════════════════════════════════════════════ @@ -387,7 +435,7 @@ All checks passed. Beginning incremental extraction... config.textFilterRules = Array.isArray(p.textFilterRules) ? p.textFilterRules : (Array.isArray(p.vector?.textFilterRules) ? p.vector.textFilterRules : [...DEFAULT_FILTER_RULES]); - if (p.vector) config.vector = p.vector; + if (p.vector) config.vector = normalizeVectorConfigUI(p.vector); if (config.trigger.timing === 'manual' && config.trigger.enabled) { config.trigger.enabled = false; saveConfig(); @@ -409,7 +457,7 @@ All checks passed. Beginning incremental extraction... : (Array.isArray(cfg.vector?.textFilterRules) ? cfg.vector.textFilterRules : (Array.isArray(config.textFilterRules) ? config.textFilterRules : [...DEFAULT_FILTER_RULES])); - if (cfg.vector) config.vector = cfg.vector; + if (cfg.vector) config.vector = normalizeVectorConfigUI(cfg.vector); if (config.trigger.timing === 'manual') config.trigger.enabled = false; localStorage.setItem('summary_panel_config', JSON.stringify(config)); } @@ -422,7 +470,14 @@ All checks passed. Beginning incremental extraction... config.textFilterRules = collectFilterRules(); } if (!config.vector) { - config.vector = { enabled: false, engine: 'online', online: { provider: 'siliconflow', key: '', model: 'BAAI/bge-m3' } }; + config.vector = { + enabled: false, + engine: 'online', + l0Concurrency: 10, + l0Api: { provider: 'siliconflow', url: 'https://api.siliconflow.cn/v1', key: '', model: 'Qwen/Qwen3-8B', modelCache: [] }, + embeddingApi: { provider: 'siliconflow', url: 'https://api.siliconflow.cn/v1', key: '', model: 'BAAI/bge-m3', modelCache: [] }, + rerankApi: { provider: 'siliconflow', url: 'https://api.siliconflow.cn/v1', key: '', model: 'BAAI/bge-reranker-v2-m3', modelCache: [] } + }; } localStorage.setItem('summary_panel_config', JSON.stringify(config)); postMsg('SAVE_PANEL_CONFIG', { config }); @@ -435,15 +490,107 @@ All checks passed. Beginning incremental extraction... // Vector Config UI // ═══════════════════════════════════════════════════════════════════════════ + function getVectorApiConfig(prefix) { + return { + provider: $(`${prefix}-api-provider`)?.value || 'siliconflow', + url: $(`${prefix}-api-url`)?.value?.trim() || '', + key: $(`${prefix}-api-key`)?.value?.trim() || '', + model: $(`${prefix}-api-model-text`)?.value?.trim() || '', + modelCache: Array.isArray(config.vector?.[`${prefix}Api`]?.modelCache) + ? [...config.vector[`${prefix}Api`].modelCache] + : [], + }; + } + + function loadVectorApiConfig(prefix, cfg) { + const next = cfg || {}; + $(`${prefix}-api-provider`).value = next.provider || 'siliconflow'; + $(`${prefix}-api-url`).value = next.url || ''; + $(`${prefix}-api-key`).value = next.key || ''; + $(`${prefix}-api-model-text`).value = next.model || ''; + + const cache = Array.isArray(next.modelCache) ? next.modelCache : []; + setSelectOptions($(`${prefix}-api-model-select`), cache, '请选择'); + $(`${prefix}-api-model-select`).value = cache.includes(next.model) ? next.model : ''; + updateVectorProviderUI(prefix, next.provider || 'siliconflow'); + } + + function updateVectorProviderUI(prefix, provider) { + const pv = VECTOR_PROVIDER_DEFAULTS[provider] || VECTOR_PROVIDER_DEFAULTS.custom; + const cache = Array.isArray(config.vector?.[`${prefix}Api`]?.modelCache) + ? config.vector[`${prefix}Api`].modelCache + : []; + const hasModelCache = cache.length > 0; + + $(`${prefix}-api-url-row`).classList.toggle('hidden', false); + $(`${prefix}-api-key-row`).classList.toggle('hidden', !pv.needKey); + $(`${prefix}-api-model-manual-row`).classList.toggle('hidden', false); + $(`${prefix}-api-model-select-row`).classList.toggle('hidden', !hasModelCache); + $(`${prefix}-api-connect-row`).classList.toggle('hidden', !pv.canFetch); + $(`${prefix}-api-connect-status`).classList.toggle('hidden', !pv.canFetch); + + const urlInput = $(`${prefix}-api-url`); + if (urlInput && !urlInput.value && pv.url) urlInput.value = pv.url; + } + + async function fetchVectorModels(prefix) { + const provider = $(`${prefix}-api-provider`).value; + const pv = VECTOR_PROVIDER_DEFAULTS[provider] || VECTOR_PROVIDER_DEFAULTS.custom; + const statusEl = $(`${prefix}-api-connect-status`); + const btn = $(`${prefix}-btn-connect`); + if (!pv.canFetch) { + statusEl.textContent = '当前渠道不支持自动拉取模型'; + return; + } + + let baseUrl = $(`${prefix}-api-url`).value.trim().replace(/\/+$/, ''); + const apiKey = $(`${prefix}-api-key`).value.trim(); + if (!apiKey) { + statusEl.textContent = '请先填写 API KEY'; + return; + } + + btn.disabled = true; + btn.textContent = '连接中...'; + statusEl.textContent = '连接中...'; + + try { + const tryFetch = async url => { + const res = await fetch(url, { + headers: { Authorization: `Bearer ${apiKey}`, Accept: 'application/json' } + }); + return res.ok ? (await res.json())?.data?.map(m => m?.id).filter(Boolean) || null : null; + }; + + if (baseUrl.endsWith('/v1')) baseUrl = baseUrl.slice(0, -3); + let models = await tryFetch(`${baseUrl}/v1/models`); + if (!models) models = await tryFetch(`${baseUrl}/models`); + if (!models?.length) throw new Error('未获取到模型列表'); + + config.vector[`${prefix}Api`].modelCache = [...new Set(models)]; + setSelectOptions($(`${prefix}-api-model-select`), config.vector[`${prefix}Api`].modelCache, '请选择'); + $(`${prefix}-api-model-select-row`).classList.remove('hidden'); + if (!$(`${prefix}-api-model-text`).value.trim()) { + $(`${prefix}-api-model-text`).value = models[0]; + $(`${prefix}-api-model-select`).value = models[0]; + } + statusEl.textContent = `拉取成功:${models.length} 个模型`; + } catch (e) { + statusEl.textContent = '拉取失败:' + (e.message || '请检查 URL 和 KEY'); + } finally { + btn.disabled = false; + btn.textContent = '连接 / 拉取模型列表'; + } + } + function getVectorConfig() { return { enabled: $('vector-enabled')?.checked || false, engine: 'online', - online: { - provider: 'siliconflow', - key: $('vector-api-key')?.value?.trim() || '', - model: 'BAAI/bge-m3', - }, + l0Concurrency: Math.max(1, Math.min(50, Number($('vector-l0-concurrency')?.value) || 10)), + l0Api: getVectorApiConfig('l0'), + embeddingApi: getVectorApiConfig('embedding'), + rerankApi: getVectorApiConfig('rerank'), }; } @@ -451,11 +598,10 @@ All checks passed. Beginning incremental extraction... if (!cfg) return; $('vector-enabled').checked = !!cfg.enabled; $('vector-config-area').classList.toggle('hidden', !cfg.enabled); - - if (cfg.online?.key) { - $('vector-api-key').value = cfg.online.key; - } - + $('vector-l0-concurrency').value = String(Math.max(1, Math.min(50, Number(cfg.l0Concurrency) || 10))); + loadVectorApiConfig('l0', cfg.l0Api || {}); + loadVectorApiConfig('embedding', cfg.embeddingApi || {}); + loadVectorApiConfig('rerank', cfg.rerankApi || {}); } // ═══════════════════════════════════════════════════════════════════════════ @@ -536,13 +682,6 @@ All checks passed. Beginning incremental extraction... el.textContent = count; } - function updateOnlineStatus(status, message) { - const dot = $('online-api-status').querySelector('.status-dot'); - const text = $('online-api-status').querySelector('.status-text'); - dot.className = 'status-dot ' + status; - text.textContent = message; - } - function updateVectorStats(stats) { $('vector-atom-count').textContent = stats.stateVectors || 0; $('vector-chunk-count').textContent = stats.chunkCount || 0; @@ -649,14 +788,28 @@ All checks passed. Beginning incremental extraction... $('btn-test-vector-api').onclick = () => { saveConfig(); // 先保存新 Key 到 localStorage postMsg('VECTOR_TEST_ONLINE', { - provider: 'siliconflow', - config: { - key: $('vector-api-key').value.trim(), - model: 'BAAI/bge-m3', - } + provider: getVectorConfig().embeddingApi.provider, + config: getVectorConfig().embeddingApi }); }; + ['l0', 'embedding', 'rerank'].forEach(prefix => { + $(`${prefix}-api-provider`).onchange = e => { + const pv = VECTOR_PROVIDER_DEFAULTS[e.target.value] || VECTOR_PROVIDER_DEFAULTS.custom; + const target = config.vector[`${prefix}Api`] ||= { modelCache: [] }; + target.provider = e.target.value; + if (!target.url && pv.url) target.url = pv.url; + if (!pv.canFetch) target.modelCache = []; + updateVectorProviderUI(prefix, e.target.value); + }; + + $(`${prefix}-api-model-select`).onchange = e => { + if (e.target.value) $(`${prefix}-api-model-text`).value = e.target.value; + }; + + $(`${prefix}-btn-connect`).onclick = () => fetchVectorModels(prefix); + }); + $('btn-add-filter-rule').onclick = addFilterRule; $('btn-gen-vectors').onclick = () => { diff --git a/modules/story-summary/story-summary.html b/modules/story-summary/story-summary.html index 94ec320..b9f4a41 100644 --- a/modules/story-summary/story-summary.html +++ b/modules/story-summary/story-summary.html @@ -414,31 +414,139 @@