From 826ef78f18d32789f439f3932a03c9c7b51f6ee6 Mon Sep 17 00:00:00 2001 From: Youzini-afk <13153778771cx@gmail.com> Date: Tue, 24 Mar 2026 22:58:17 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=B8=BA=E7=8B=AC=E7=AB=8B=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E8=A1=A5=E5=85=85=E8=B6=85=E6=97=B6=E4=B8=8E=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- embedding.js | 44 ++++++++++++++++++++- index.js | 26 ++++++++++-- llm.js | 109 +++++++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 162 insertions(+), 17 deletions(-) diff --git a/embedding.js b/embedding.js index 2a9c854..644744e 100644 --- a/embedding.js +++ b/embedding.js @@ -6,6 +6,8 @@ * 调用外部 API 获取文本向量,并提供暴力搜索 cosine 相似度 */ +const EMBEDDING_REQUEST_TIMEOUT_MS = 45000; + function normalizeOpenAICompatibleBaseUrl(value) { return String(value || '') .trim() @@ -13,6 +15,44 @@ function normalizeOpenAICompatibleBaseUrl(value) { .replace(/\/+$/, ''); } +function createCombinedAbortSignal(...signals) { + const validSignals = signals.filter(Boolean); + if (validSignals.length <= 1) { + return validSignals[0] || undefined; + } + + if (typeof AbortSignal !== 'undefined' && typeof AbortSignal.any === 'function') { + return AbortSignal.any(validSignals); + } + + const controller = new AbortController(); + for (const signal of validSignals) { + if (signal.aborted) { + controller.abort(); + return controller.signal; + } + signal.addEventListener('abort', () => controller.abort(), { once: true }); + } + return controller.signal; +} + +async function fetchWithTimeout(url, options = {}, timeoutMs = EMBEDDING_REQUEST_TIMEOUT_MS) { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), timeoutMs); + const signal = options.signal + ? createCombinedAbortSignal(options.signal, controller.signal) + : controller.signal; + + try { + return await fetch(url, { + ...options, + signal, + }); + } finally { + clearTimeout(timeout); + } +} + /** * 调用外部 Embedding API * @@ -31,7 +71,7 @@ export async function embedText(text, config) { } try { - const response = await fetch(`${apiUrl}/embeddings`, { + const response = await fetchWithTimeout(`${apiUrl}/embeddings`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -78,7 +118,7 @@ export async function embedBatch(texts, config) { } try { - const response = await fetch(`${apiUrl}/embeddings`, { + const response = await fetchWithTimeout(`${apiUrl}/embeddings`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/index.js b/index.js index 4d05d17..f9709e9 100644 --- a/index.js +++ b/index.js @@ -177,6 +177,7 @@ let serverSettingsSaveTimer = null; let isRecoveringHistory = false; let lastHistoryWarningAt = 0; let lastRecallFallbackNoticeAt = 0; +let lastExtractionWarningAt = 0; function getNodeDisplayName(node) { return ( @@ -320,6 +321,13 @@ function setLastRecallStatus(text, meta, level = "info") { refreshPanelLiveState(); } +function notifyExtractionIssue(message, title = "ST-BME 提取提示") { + const now = Date.now(); + if (now - lastExtractionWarningAt < 5000) return; + lastExtractionWarningAt = now; + toastr.warning(message, title, { timeOut: 4500 }); +} + function snapshotRuntimeUiState() { return { extractionCount, @@ -1237,7 +1245,10 @@ async function runExtraction() { const settings = getSettings(); if (!settings.enabled) return; if (!(await recoverHistoryIfNeeded("auto-extract"))) return; - await ensureVectorReadyIfNeeded("pre-extract"); + const vectorPrep = await ensureVectorReadyIfNeeded("pre-extract"); + if (vectorPrep?.error) { + notifyExtractionIssue(`提取前向量修复失败: ${vectorPrep.error}`); + } const context = getContext(); const chat = context.chat; @@ -1281,10 +1292,16 @@ async function runExtraction() { }); if (!batchResult.success) { - console.warn("[ST-BME] 提取批次未返回有效结果"); + const message = + batchResult.error || + batchResult?.result?.error || + "提取批次未返回有效结果"; + console.warn("[ST-BME] 提取批次未返回有效结果:", message); + notifyExtractionIssue(message); } } catch (e) { console.error("[ST-BME] 提取失败:", e); + notifyExtractionIssue(e?.message || String(e) || "自动提取失败"); } finally { isExtracting = false; } @@ -1707,7 +1724,10 @@ async function onFetchEmbeddingModels(mode = null) { } async function onManualExtract() { - if (isExtracting) return; + if (isExtracting) { + toastr.info("记忆提取正在进行中,请稍候"); + return; + } if (!(await recoverHistoryIfNeeded("manual-extract"))) return; const vectorPrep = await ensureVectorReadyIfNeeded("manual-extract"); if (!currentGraph) currentGraph = normalizeGraphRuntimeState(createEmptyGraph(), getCurrentChatId()); diff --git a/llm.js b/llm.js index 20c1ee6..3113d2e 100644 --- a/llm.js +++ b/llm.js @@ -6,6 +6,7 @@ import { chat_completion_sources, sendOpenAIRequest } from "../../../openai.js"; import { getRequestHeaders } from "../../../../script.js"; const MODULE_NAME = "st_bme"; +const LLM_REQUEST_TIMEOUT_MS = 60000; function getMemoryLLMConfig() { const settings = extension_settings[MODULE_NAME] || {}; @@ -53,13 +54,104 @@ function normalizeModelList(items = []) { return models; } +function extractContentFromResponsePayload(payload) { + if (typeof payload === 'string') { + return payload; + } + + if (Array.isArray(payload)) { + return payload + .map((item) => item?.text || item?.content || '') + .join('') + .trim(); + } + + if (!payload || typeof payload !== 'object') { + return ''; + } + + const messageContent = payload?.choices?.[0]?.message?.content; + if (typeof messageContent === 'string') { + return messageContent; + } + + if (Array.isArray(messageContent)) { + return messageContent + .map((item) => item?.text || item?.content || '') + .join('') + .trim(); + } + + const textContent = + payload?.choices?.[0]?.text ?? + payload?.text ?? + payload?.message?.content ?? + payload?.content; + + if (typeof textContent === 'string') { + return textContent; + } + + if (Array.isArray(textContent)) { + return textContent + .map((item) => item?.text || item?.content || '') + .join('') + .trim(); + } + + return ''; +} + +async function fetchWithTimeout(url, options = {}, timeoutMs = LLM_REQUEST_TIMEOUT_MS) { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), timeoutMs); + const signal = options.signal + ? createCombinedAbortSignal(options.signal, controller.signal) + : controller.signal; + + try { + return await fetch(url, { + ...options, + signal, + }); + } finally { + clearTimeout(timeout); + } +} + +function createCombinedAbortSignal(...signals) { + const validSignals = signals.filter(Boolean); + if (validSignals.length <= 1) { + return validSignals[0] || undefined; + } + + if (typeof AbortSignal !== 'undefined' && typeof AbortSignal.any === 'function') { + return AbortSignal.any(validSignals); + } + + const controller = new AbortController(); + for (const signal of validSignals) { + if (signal.aborted) { + controller.abort(); + return controller.signal; + } + signal.addEventListener('abort', () => controller.abort(), { once: true }); + } + return controller.signal; +} + // 自动检测:如果 API 不支持 response_format,记住并跳过 let _jsonModeSupported = true; async function callDedicatedOpenAICompatible(messages, { signal, jsonMode = false } = {}) { const config = getMemoryLLMConfig(); if (!hasDedicatedLLMConfig(config)) { - return await sendOpenAIRequest('quiet', messages, signal); + const payload = await sendOpenAIRequest('quiet', messages, signal); + const content = extractContentFromResponsePayload(payload); + if (typeof content === 'string' && content.trim().length > 0) { + return content.trim(); + } + throw new Error('SillyTavern current model returned an unexpected response format'); } const body = { @@ -79,7 +171,7 @@ async function callDedicatedOpenAICompatible(messages, { signal, jsonMode = fals body.response_format = { type: 'json_object' }; } - const response = await fetch('/api/backends/chat-completions/generate', { + const response = await fetchWithTimeout('/api/backends/chat-completions/generate', { method: 'POST', headers: getRequestHeaders(), body: JSON.stringify(body), @@ -92,7 +184,7 @@ async function callDedicatedOpenAICompatible(messages, { signal, jsonMode = fals _jsonModeSupported = false; // 去掉 response_format 重试 delete body.response_format; - const retryResponse = await fetch('/api/backends/chat-completions/generate', { + const retryResponse = await fetchWithTimeout('/api/backends/chat-completions/generate', { method: 'POST', headers: getRequestHeaders(), body: JSON.stringify(body), @@ -123,18 +215,11 @@ async function _parseResponse(response) { if (data?.error?.message) { throw new Error(`Memory LLM proxy error: ${data.error.message}`); } - const content = data?.choices?.[0]?.message?.content; - if (typeof content === 'string') { + const content = extractContentFromResponsePayload(data); + if (typeof content === 'string' && content.length > 0) { return content; } - if (Array.isArray(content)) { - return content - .map((item) => item?.text || item?.content || '') - .join('') - .trim(); - } - throw new Error('Memory LLM API returned an unexpected response format'); }