From 3043c3d80a4f2e3f9fb837ae71ccb335b5931907 Mon Sep 17 00:00:00 2001 From: Silence_Lurker Date: Tue, 20 Jan 2026 11:32:54 +0800 Subject: [PATCH] rollback --- core/api/NccsApi.js | 249 ++++++++++++++++++++++++++------------------ 1 file changed, 149 insertions(+), 100 deletions(-) diff --git a/core/api/NccsApi.js b/core/api/NccsApi.js index 8ed78dc..ecbd0ed 100644 --- a/core/api/NccsApi.js +++ b/core/api/NccsApi.js @@ -51,7 +51,7 @@ export function getNccsApiSettings() { } // ================================================================================================= -// 核心调用入口 (Legacy Mode with Stream Support) +// 核心调用入口 (Hybrid Mode: Bus First -> Fallback Legacy) // ================================================================================================= export async function callNccsAI(messages, options = {}) { @@ -61,97 +61,160 @@ export async function callNccsAI(messages, options = {}) { } const settings = getNccsApiSettings(); - const finalOptions = { - ...settings, - ...options, - // 显式合并流式开关 - stream: options.useFakeStream ?? settings.useFakeStream ?? false - }; - if (finalOptions.apiMode !== 'sillytavern_preset') { - if (!finalOptions.apiUrl || !finalOptions.model || !finalOptions.apiKey) { + // 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) { 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 (finalOptions.apiMode) { + + switch (activeMode) { case 'openai_test': - responseContent = await callNccsOpenAITest(messages, finalOptions); + responseContent = await callNccsOpenAITest(messages, legacyOptions); break; case 'sillytavern_preset': - responseContent = await callNccsSillyTavernPreset(messages, finalOptions); + responseContent = await callNccsSillyTavernPreset(messages, legacyOptions); break; default: - console.error(`未支持的 API 模式: ${finalOptions.apiMode}`); + console.error(`未支持的 API 模式: ${activeMode}`); return null; } + + console.log("Legacy Response:", responseContent); + console.groupEnd(); + return responseContent; - } catch (error) { - console.error(`[Amily2-Nccs] API 调用失败:`, error); - toastr.error(`调用失败: ${error.message}`, "Nccs API Error"); + + } 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"); + 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 data; } + try { data = JSON.parse(data); } catch (e) { return { error: { message: 'Invalid JSON' } }; } } - 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; + 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; } async function callNccsOpenAITest(messages, options) { @@ -162,7 +225,7 @@ async function callNccsOpenAITest(messages, options) { model: options.model, reverse_proxy: options.apiUrl, proxy_password: options.apiKey, - stream: !!options.stream, + stream: false, // 旧版不支持 FakeStream max_tokens: options.maxTokens || 4000, temperature: options.temperature || 1, top_p: options.top_p || 1, @@ -171,23 +234,25 @@ 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 fetchOpts = { + const response = await fetch('/api/backends/chat-completions/generate', { method: 'POST', headers: { ...getRequestHeaders(), 'Content-Type': 'application/json' }, body: JSON.stringify(body) - }; + }); - if (options.stream) { - return await fetchFakeStream('/api/backends/chat-completions/generate', fetchOpts); + if (!response.ok) { + throw new Error(`Legacy HTTP ${response.status}: ${await response.text()}`); } - 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()); + const responseData = await response.json(); + return responseData?.choices?.[0]?.message?.content; } async function callNccsSillyTavernPreset(messages, options) { @@ -204,40 +269,24 @@ 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); - return normalizeApiResponse(result); + + 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; } finally { + // Restore profile const current = await amilyHelper.triggerSlash('/profile'); if (originalProfile && originalProfile !== current) { await amilyHelper.triggerSlash(`/profile await=true "${originalProfile.replace(/"/g, '\\"')}"`);