mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
fix: 为独立请求补充超时与错误提示
This commit is contained in:
44
embedding.js
44
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',
|
||||
|
||||
26
index.js
26
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());
|
||||
|
||||
109
llm.js
109
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');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user