From 40c3d6c735b6b5c5095a85319f65a6fb3b97c283 Mon Sep 17 00:00:00 2001 From: Silence_Lurker Date: Tue, 20 Jan 2026 11:21:14 +0800 Subject: [PATCH] fix --- core/api/NccsApi.js | 249 ++++++++++++++++++-------------------------- 1 file changed, 100 insertions(+), 149 deletions(-) diff --git a/core/api/NccsApi.js b/core/api/NccsApi.js index d51f50c..8ed78dc 100644 --- a/core/api/NccsApi.js +++ b/core/api/NccsApi.js @@ -51,7 +51,7 @@ export function getNccsApiSettings() { } // ================================================================================================= -// 核心调用入口 (Hybrid Mode: Bus First -> Fallback Legacy) +// 核心调用入口 (Legacy Mode with Stream Support) // ================================================================================================= export async function callNccsAI(messages, options = {}) { @@ -61,160 +61,97 @@ export async function callNccsAI(messages, options = {}) { } const settings = getNccsApiSettings(); + const finalOptions = { + ...settings, + ...options, + // 显式合并流式开关 + stream: options.useFakeStream ?? settings.useFakeStream ?? false + }; - // 0. 全局开关检查 - if (settings.nccsEnabled === false) { - // 暂不阻断,仅作为配置读取,保持兼容性 - } - - // 1. 基础配置确定 (options 覆盖 settings) - const activeMode = options.apiMode || settings.apiMode; - const activeUrl = options.apiUrl || settings.apiUrl; - const activeKey = options.apiKey || settings.apiKey; - const activeModel = options.model || settings.model; - const activeProfile = options.tavernProfile || settings.tavernProfile; - const activeMaxTokens = options.maxTokens ?? settings.maxTokens; - const activeTemperature = options.temperature ?? settings.temperature; - const activeFakeStream = options.useFakeStream ?? settings.useFakeStream; - - if (activeMode !== 'sillytavern_preset') { - if (!activeUrl || !activeModel || !activeKey) { + if (finalOptions.apiMode !== 'sillytavern_preset') { + if (!finalOptions.apiUrl || !finalOptions.model || !finalOptions.apiKey) { console.warn("[Amily2-Nccs外交部] API配置不完整,无法调用AI"); toastr.error("API配置不完整,请检查URL、Key和模型配置。", "Nccs-外交部"); return null; } } - // [兼容性修复] 自动收集 options 中的额外参数到 params,防止 ModelCaller 丢失 top_p 等参数 - const standardKeys = [ - 'apiMode', 'apiUrl', 'apiKey', 'model', - 'maxTokens', 'temperature', 'tavernProfile', 'useFakeStream', - 'params' - ]; - const extraParams = {}; - Object.keys(options).forEach(key => { - if (!standardKeys.includes(key)) { - extraParams[key] = options[key]; - } - }); - // 合并显式的 options.params 和 收集到的 extraParams - const finalParams = { ...extraParams, ...(options.params || {}) }; - - - // ============================================================ - // 尝试路径 A: 新版 Amily2Bus ModelCaller (支持 FakeStream) - // ============================================================ - if (nccsCtx && nccsCtx.model) { - try { - nccsCtx.log('Main', 'info', `[v2] 尝试通过 ModelCaller 调用 (${activeFakeStream ? 'FakeStream' : 'Standard'})...`); - - const builder = nccsCtx.model.Options.builder() - .setFakeStream(activeFakeStream) - .setMaxTokens(activeMaxTokens) - .setTemperature(activeTemperature) - .setParams(finalParams); - - if (activeMode === 'sillytavern_preset') { - builder.setMode('preset') - .setPresetId(activeProfile) - .setModel(activeModel); - } else { - builder.setMode('direct') - .setApiUrl(activeUrl) - .setApiKey(activeKey) - .setModel(activeModel); - } - - // 发起请求 - const response = await nccsCtx.model.call(messages, builder.build()); - - // 校验结果 - if (response) { - nccsCtx.log('Main', 'info', `[v2] ModelCaller 调用成功。`); - return response; - } else { - throw new Error("ModelCaller 返回了空响应"); - } - - } catch (busError) { - const errorMsg = `[v2] ModelCaller 调用失败,准备回退到旧版逻辑。原因: ${busError.message}`; - // 记录错误但阻断抛出,以便执行下方代码 - if (nccsCtx) nccsCtx.log('Main', 'warn', errorMsg); - else console.warn(errorMsg); - } - } else { - console.warn("[Amily2-Nccs] Bus 未连接,直接使用旧版逻辑。"); - } - - // ============================================================ - // 尝试路径 B: 旧版 Legacy 方法 (Fallback) - // ============================================================ - // 构建 Legacy 兼容对象 - const legacyOptions = { - apiMode: activeMode, - apiUrl: activeUrl, - apiKey: activeKey, - model: activeModel, - tavernProfile: activeProfile, - maxTokens: activeMaxTokens, - temperature: activeTemperature, - useFakeStream: activeFakeStream, - ...finalParams // 将额外参数直接展平回 legacyOptions 根目录 - }; - try { - console.groupCollapsed(`[Amily2-Nccs] 降级使用 Legacy API 调用`); - console.log("Fallback Mode Active"); - let responseContent; - - switch (activeMode) { + switch (finalOptions.apiMode) { case 'openai_test': - responseContent = await callNccsOpenAITest(messages, legacyOptions); + responseContent = await callNccsOpenAITest(messages, finalOptions); break; case 'sillytavern_preset': - responseContent = await callNccsSillyTavernPreset(messages, legacyOptions); + responseContent = await callNccsSillyTavernPreset(messages, finalOptions); break; default: - console.error(`未支持的 API 模式: ${activeMode}`); + console.error(`未支持的 API 模式: ${finalOptions.apiMode}`); return null; } - - console.log("Legacy Response:", responseContent); - console.groupEnd(); - return responseContent; - - } catch (legacyError) { - console.groupEnd(); - console.error(`[Amily2-Nccs] Legacy API 调用也失败了:`, legacyError); - - // 统一错误提示 - const msg = legacyError.message; - if (msg.includes('401')) toastr.error("API认证失败 (401)", "Nccs API Error"); - else if (msg.includes('403')) toastr.error("权限拒绝 (403)", "Nccs API Error"); - else if (msg.includes('500')) toastr.error("服务器错误 (500)", "Nccs API Error"); - else toastr.error(`调用失败: ${msg}`, "Nccs API Error"); - + } catch (error) { + console.error(`[Amily2-Nccs] API 调用失败:`, error); + toastr.error(`调用失败: ${error.message}`, "Nccs API Error"); return null; } } +async function fetchFakeStream(url, opts) { + const res = await fetch(url, opts); + if (!res.ok) throw new Error(`Stream HTTP ${res.status}: ${await res.text()}`); + + const reader = res.body.getReader(); + const decoder = new TextDecoder(); + let fullContent = ""; + let buffer = ""; + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split('\n'); + buffer = lines.pop(); + + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed || trimmed === 'data: [DONE]') continue; + if (trimmed.startsWith('data: ')) { + try { + const json = JSON.parse(trimmed.substring(6)); + const delta = json.choices?.[0]?.delta?.content; + if (delta) fullContent += delta; + } catch (e) { + console.warn('[NccsApi] SSE Parse Error:', e); + } + } + } + } + } finally { + reader.releaseLock(); + } + + if (!fullContent && buffer) { + try { return JSON.parse(buffer).choices?.[0]?.message?.content || buffer; } + catch { return buffer; } + } + return fullContent; +} + // ================================================================================================= -// Legacy Implementations (保留旧代码以供降级使用) +// Legacy Implementations (直接增加流式适配) // ================================================================================================= function normalizeApiResponse(responseData) { let data = responseData; if (typeof data === 'string') { - try { data = JSON.parse(data); } catch (e) { return { error: { message: 'Invalid JSON' } }; } + try { data = JSON.parse(data); } catch (e) { return data; } } - if (data?.data?.data) data = data.data; // Unpack nested data - if (data?.choices?.[0]?.message?.content) return { content: data.choices[0].message.content.trim() }; - if (data?.content) return { content: data.content.trim() }; - if (data?.data) return { data: data.data }; - if (data?.error) return { error: data.error }; - return data; + if (data?.choices?.[0]?.message?.content) return data.choices[0].message.content.trim(); + if (data?.content) return data.content.trim(); + return typeof data === 'object' ? JSON.stringify(data) : data; } async function callNccsOpenAITest(messages, options) { @@ -225,7 +162,7 @@ async function callNccsOpenAITest(messages, options) { model: options.model, reverse_proxy: options.apiUrl, proxy_password: options.apiKey, - stream: false, // 旧版不支持 FakeStream + stream: !!options.stream, max_tokens: options.maxTokens || 4000, temperature: options.temperature || 1, top_p: options.top_p || 1, @@ -234,25 +171,23 @@ async function callNccsOpenAITest(messages, options) { if (!isGoogleApi) { Object.assign(body, { custom_prompt_post_processing: 'strict', - enable_web_search: false, - frequency_penalty: 0, presence_penalty: 0.12, - request_images: false, }); } - const response = await fetch('/api/backends/chat-completions/generate', { + const fetchOpts = { method: 'POST', headers: { ...getRequestHeaders(), 'Content-Type': 'application/json' }, body: JSON.stringify(body) - }); + }; - if (!response.ok) { - throw new Error(`Legacy HTTP ${response.status}: ${await response.text()}`); + if (options.stream) { + return await fetchFakeStream('/api/backends/chat-completions/generate', fetchOpts); } - const responseData = await response.json(); - return responseData?.choices?.[0]?.message?.content; + const response = await fetch('/api/backends/chat-completions/generate', fetchOpts); + if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`); + return normalizeApiResponse(await response.json()); } async function callNccsSillyTavernPreset(messages, options) { @@ -269,24 +204,40 @@ async function callNccsSillyTavernPreset(messages, options) { try { if (originalProfile !== targetProfile.name) { - console.log(`[Legacy Switching profile: ${originalProfile} -> ${targetProfile.name}`); await amilyHelper.triggerSlash(`/profile await=true "${targetProfile.name.replace(/"/g, '\\"')}"`); } + if (options.stream) { + // [关键复刻] 完全继承 Profile 属性并注入运行时元数据 + const body = { + ...targetProfile, + messages: messages, + stream: true, + max_tokens: options.maxTokens || targetProfile.max_tokens || 2000, + user_name: context.name1 || 'User', + char_name: context.name2 || 'AI', + }; + + // 补全 URL 映射 (ST 后端对不同 source 字段名要求不同) + const rawUrl = body['api-url'] || body['api_url'] || body.custom_url || body.url; + if (rawUrl) { + if (body.chat_completion_source === 'custom') body.custom_url = rawUrl; + else body.reverse_proxy = rawUrl; + } + + const fetchOpts = { + method: 'POST', + headers: { ...getRequestHeaders(), 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }; + return await fetchFakeStream('/api/backends/chat-completions/generate', fetchOpts); + } + if (!context.ConnectionManagerRequestService) throw new Error('ConnectionManagerRequestService unavailable'); - - const result = await context.ConnectionManagerRequestService.sendRequest( - targetProfile.id, - messages, - options.maxTokens || 4000 - ); - - const normalized = normalizeApiResponse(result); - if (normalized.error) throw new Error(normalized.error.message); - return normalized.content; + const result = await context.ConnectionManagerRequestService.sendRequest(targetProfile.id, messages, options.maxTokens || 4000); + return normalizeApiResponse(result); } finally { - // Restore profile const current = await amilyHelper.triggerSlash('/profile'); if (originalProfile && originalProfile !== current) { await amilyHelper.triggerSlash(`/profile await=true "${originalProfile.replace(/"/g, '\\"')}"`);