Files
ST-Amily2-Chat-Optimisation/core/api.js
Jenkins CI 08e1dbde85 release: v2.2.3 [2026-05-27 23:06:48]
### 新功能
- Function Call 填表开关下方新增公益站风险提示横幅:部分公益站会屏蔽 tools 参数,请确认支持情况避免被意外封禁
### 修复
- **Function Call 填表**:
  - 修复 ST 代理以 HTTP 200 + error body 形式返回错误、导致降级重试机制从未触发的问题
  - 修复思考模式模型(如 DeepSeek v4-flash)因 tool_choice 不兼容返回 Bad Request 后正确降级并重试
  - 重试时自动追加强制调用指令,防止思考模型绕过工具直接输出文本造成无效二次开销
- **超级记忆 / 翰林院**:
  - 修复 `getRagSettings()` 读写顶层路径而非嵌套路径,导致打开超级记忆面板后向量化、归档等开关在重载时被全默认值覆盖的问题
  - 修复自动归档失效问题
  - 修复归档管理器在同一事件中被三次触发的回归问题
  - 修复翰林院设置旧版迁移逻辑异常
2026-05-27 23:06:48 +08:00

1059 lines
38 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { extension_settings, getContext } from "/scripts/extensions.js";
import { characters } from "/script.js";
import { getSlotProfile, providerToApiMode } from './api/api-resolver.js';
import { configManager } from '../utils/config/ConfigManager.js';
import { world_names } from "/scripts/world-info.js";
import { extensionName } from "../utils/settings.js";
import { extractContentByTag, replaceContentByTag, extractFullTagBlock } from '../utils/tagProcessor.js';
import {
getCombinedWorldbookContent,
findLatestSummaryLore,
DEDICATED_LOREBOOK_NAME,
getChatIdentifier,
} from "./lore.js";
import { compatibleTriggerSlash } from "./tavernhelper-compatibility.js";
import {
isGoogleEndpoint,
convertToGoogleRequest,
parseGoogleResponse,
buildGoogleApiUrl
} from '../core/utils/googleAdapter.js';
import {
intelligentPoll,
createGooglePollingTask,
progressTracker
} from '../core/utils/pollingManager.js';
import {
buildGoogleEmbeddingRequest,
parseGoogleEmbeddingResponse,
buildGoogleEmbeddingApiUrl
} from './utils/googleAdapter.js';
import { getRequestHeaders } from '/script.js';
let ChatCompletionService = undefined;
try {
const module = await import('/scripts/custom-request.js');
ChatCompletionService = module.ChatCompletionService;
console.log('[Amily2号-外交部] 已成功召唤“皇家信使”(ChatCompletionService)。');
} catch (e) {
console.warn("[Amily2号-外交部] 未能召唤“皇家信使”部分高级功能如Claw代理将受限。请考虑更新SillyTavern版本。", e);
}
const UPDATE_CHECK_URL =
"https://raw.githubusercontent.com/Wx-2025/ST-Amily2-Chat-Optimisation/refs/heads/main/amily2_update_info.json";
const MESSAGE_BOARD_URL =
"https://amilyservice.amily49.cc/amily2_message_board.json";
const PROXIES = [
"https://corsproxy.io/?",
"https://api.allorigins.win/raw?url=",
"https://api.codetabs.com/v1/proxy?quest="
];
let lastMessageId = null;
export async function fetchMessageBoardContent() {
if (!MESSAGE_BOARD_URL) {
console.log('[Amily2号-内务府] 任务取消陛下尚未配置留言板URL。');
return null;
}
const processResponse = async (response) => {
if (response.status === 304) {
console.log('[Amily2号-内务府] 留言板内容未变更 (304)。');
return null;
}
if (!response.ok) {
throw new Error(`服务器响应异常: ${response.status}`);
}
const data = await response.json();
if (data && data.id) {
lastMessageId = data.id;
}
return data;
};
// 1. 尝试直连
try {
let url = MESSAGE_BOARD_URL;
if (lastMessageId) {
const separator = url.includes('?') ? '&' : '?';
url += `${separator}nowId=${encodeURIComponent(lastMessageId)}`;
}
const response = await fetch(url, { cache: 'no-store' });
return await processResponse(response);
} catch (error) {
console.warn('[Amily2号-内务府] 直连失败,开始尝试代理链...', error);
}
// 2. 尝试代理链
for (const proxyPrefix of PROXIES) {
try {
let targetUrl = MESSAGE_BOARD_URL;
if (lastMessageId) {
const separator = targetUrl.includes('?') ? '&' : '?';
targetUrl += `${separator}nowId=${encodeURIComponent(lastMessageId)}`;
}
let proxyUrl;
// corsproxy.io 支持直接拼接,其他通常需要编码
if (proxyPrefix.includes('corsproxy.io')) {
proxyUrl = proxyPrefix + targetUrl;
} else {
proxyUrl = proxyPrefix + encodeURIComponent(targetUrl);
}
console.log(`[Amily2号-内务府] 尝试代理: ${proxyPrefix}`);
const response = await fetch(proxyUrl, { cache: 'no-store' });
const data = await processResponse(response);
console.log(`[Amily2号-内务府] 代理成功: ${proxyPrefix}`);
return data;
} catch (e) {
console.warn(`[Amily2号-内务府] 代理失败: ${proxyPrefix}`, e);
}
}
console.error('[Amily2号-内务府] 所有通道均已失效,无法获取留言板内容。');
return null;
}
export async function checkForUpdates() {
if (!UPDATE_CHECK_URL || UPDATE_CHECK_URL.includes('YourUsername')) {
console.log('[Amily2号-外交部] 任务取消陛下尚未配置情报来源URL。');
return null;
}
try {
console.log('[Amily2号-外交部] 已派遣使者前往云端获取最新情报...');
const response = await fetch(UPDATE_CHECK_URL, {
method: 'GET',
cache: 'no-store',
mode: 'cors'
});
if (!response.ok) {
throw new Error(`远方服务器响应异常,状态: ${response.status}`);
}
const data = await response.json();
console.log('[Amily2号-外交部] 情报已成功获取并解析。');
return data;
} catch (error) {
console.error('[Amily2号-外交部] 紧急军情:外交任务失败!', error);
return null;
}
}
function normalizeApiResponse(responseData) {
let data = responseData;
if (typeof data === 'string') {
try {
data = JSON.parse(data);
} catch (e) {
console.error(`[${extensionName}] API响应JSON解析失败:`, e);
return { error: { message: 'Invalid JSON response' } };
}
}
if (data && typeof data.data === 'object' && data.data !== null && !Array.isArray(data.data)) {
if (Object.hasOwn(data.data, 'data')) {
data = data.data;
}
}
if (data && data.choices && data.choices[0]) {
return { content: data.choices[0].message?.content?.trim() };
}
if (data && data.content) {
return { content: data.content.trim() };
}
if (data && data.data) {
return { data: data.data };
}
if (data && data.error) {
return { error: data.error };
}
return data;
}
export async function fetchModels() {
if (window.AMILY2_LOCK_MODEL_FETCHING) {
console.warn("[Amily2号-使节团] 上次任务尚未完成,本次任务取消。");
toastr.info("上次任务尚未完成,请稍后再试。", "任务排队中");
return [];
}
window.AMILY2_LOCK_MODEL_FETCHING = true;
try {
const apiSettings = await getApiSettings('main');
const apiProvider = apiSettings.apiProvider || 'openai';
const apiUrl = apiSettings.apiUrl;
const apiKey = apiSettings.apiKey;
const $button = $("#amily2_refresh_models");
const $selector = $("#amily2_model");
console.log(`[Amily2号-使节团] 使用 API 提供商: ${apiProvider}`);
$button.prop("disabled", true).html('<i class="fas fa-spinner fa-spin"></i> 加载中');
$selector.empty().append($('<option>', { value: '', text: '正在获取模型列表...' }));
let result = [];
switch (apiProvider) {
case 'openai':
result = await fetchOpenAICompatibleModels(apiUrl, apiKey);
break;
case 'openai_test':
result = await fetchOpenAITestModels(apiUrl, apiKey);
break;
case 'google':
result = await fetchGoogleDirectModels(apiUrl, apiKey);
break;
case 'sillytavern_backend':
result = await fetchSillyTavernBackendModels(apiUrl, apiKey);
break;
case 'sillytavern_preset':
result = await fetchSillyTavernPresetModels();
break;
default:
throw new Error(`未支持的API提供商: ${apiProvider}`);
}
if (result.length > 0) {
toastr.success(`成功获取 ${result.length} 个模型`, "任务成功");
return result;
} else {
toastr.warning("未找到可用模型", "注意");
return [];
}
} catch (error) {
console.error("[Amily2号-使节团] 获取模型列表失败:", error);
toastr.error(`获取模型列表失败: ${error.message}`, "任务失败");
return [];
} finally {
window.AMILY2_LOCK_MODEL_FETCHING = false;
const $button = $("#amily2_refresh_models");
$button.prop("disabled", false).html('<i class="fas fa-sync-alt"></i> 刷新模型');
}
}
async function fetchOpenAICompatibleModels(apiUrl, apiKey) {
if (!apiUrl || !apiKey) {
throw new Error("OpenAI兼容模式需要API URL和API Key");
}
const baseUrl = apiUrl.replace(/\/$/, '').replace(/\/v1$/, '');
const modelsUrl = `${baseUrl}/v1/models`;
console.log(`[Amily2号-使节团] OpenAI兼容模式: ${modelsUrl}`);
const response = await fetch(modelsUrl, {
method: 'GET',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP ${response.status}: ${errorText}`);
}
const data = await response.json();
const models = data.data || data.models || [];
return models
.map(m => m.id || m.model)
.filter(Boolean)
.filter(m => !m.toLowerCase().includes('embed'))
.sort();
}
async function fetchOpenAITestModels(apiUrl, apiKey) {
const response = await fetch('/api/backends/chat-completions/status', {
method: 'POST',
headers: { ...getRequestHeaders(), 'Content-Type': 'application/json' },
body: JSON.stringify({
reverse_proxy: apiUrl,
proxy_password: apiKey,
chat_completion_source: 'openai'
})
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP ${response.status}: ${errorText}`);
}
const rawData = await response.json();
const models = Array.isArray(rawData) ? rawData : (rawData.data || rawData.models || []);
if (!Array.isArray(models)) {
const errorMessage = 'API未返回有效的模型列表数组';
throw new Error(errorMessage);
}
const formattedModels = models
.map(m => {
const modelName = m.name ? m.name.replace('models/', '') : (m.id || m.model || m);
return {
id: m.name || m.id || m.model || m,
name: modelName
};
})
.filter(m => m.id)
.sort((a, b) => String(a.name).localeCompare(String(b.name)));
console.log('[Amily2号-使节团] 全兼容(测试)模式获取到模型:', formattedModels);
return formattedModels.map(m => m.name);
}
async function fetchGoogleDirectModels(apiUrl, apiKey) {
if (!apiKey) {
throw new Error("Google直连模式需要API Key");
}
const GOOGLE_API_BASE_URL = 'https://generativelanguage.googleapis.com';
const fetchGoogleModels = async (version) => {
const url = `${GOOGLE_API_BASE_URL}/${version}/models`;
console.log(`[Amily2号-使节团] 正在从 Google API (${version}) 获取模型列表: ${url}`);
const response = await fetch(url, {
headers: { 'x-goog-api-key': apiKey },
});
if (!response.ok) {
console.warn(`获取 Google API (${version}) 模型列表失败: ${response.status}`);
return [];
}
const json = await response.json();
if (!json.models || !Array.isArray(json.models)) {
return [];
}
return json.models
.filter(model =>
model.supportedGenerationMethods?.includes('generateContent') ||
model.supportedGenerationMethods?.includes('streamGenerateContent')
)
.map(model => model.name.replace('models/', ''));
};
const [v1Models, v1betaModels] = await Promise.all([
fetchGoogleModels('v1'),
fetchGoogleModels('v1beta')
]);
const allModels = [...new Set([...v1Models, ...v1betaModels])].sort();
return allModels;
}
async function fetchSillyTavernBackendModels(apiUrl, apiKey) {
if (!apiUrl) {
throw new Error("SillyTavern后端模式需要API URL");
}
console.log('[Amily2号-使节团] 通过SillyTavern后端获取模型列表');
const rawResponse = await $.ajax({
url: '/api/backends/chat-completions/status',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({
chat_completion_source: 'custom',
custom_url: apiUrl,
api_key: apiKey
})
});
const result = normalizeApiResponse(rawResponse);
const models = result.data || [];
if (result.error || !Array.isArray(models)) {
const errorMessage = result.error?.message || 'API未返回有效的模型列表数组';
throw new Error(errorMessage);
}
return models
.map(model => model.id || model.model)
.filter(Boolean)
.sort();
}
async function fetchSillyTavernPresetModels() {
console.log('[Amily2号-使节团] 使用SillyTavern预设模式');
try {
const context = getContext();
if (!context) {
throw new Error("无法获取SillyTavern上下文");
}
const currentModel = context.chat_completion_source;
const models = [];
if (currentModel) {
models.push(currentModel);
}
const defaultModels = [
'gpt-3.5-turbo',
'gpt-4',
'claude-3-sonnet',
'claude-3-haiku',
'gemini-pro'
];
const allModels = [...new Set([...models, ...defaultModels])].sort();
return allModels;
} catch (error) {
console.warn('[Amily2号-使节团] 获取SillyTavern预设失败返回默认模型列表:', error);
return [
'gpt-3.5-turbo',
'gpt-4',
'claude-3-sonnet',
'claude-3-haiku',
'gemini-pro'
];
}
}
export async function getApiSettings(slot = 'main') {
const s = extension_settings[extensionName] || {};
// 优先读取槽位分配的 Profileprofile 一旦分配即为权威,不再被主面板/模块独立设置压制)
const profile = await getSlotProfile(slot);
if (profile) {
const resolvedProvider = profile.provider === 'sillytavern_backend'
? 'sillytavern_backend'
: providerToApiMode(profile.provider);
return {
apiProvider: resolvedProvider,
apiUrl: profile.apiUrl,
apiKey: profile.apiKey ?? '',
model: profile.model,
maxTokens: profile.maxTokens ?? 65500,
temperature: profile.temperature ?? 1.0,
fakeStream: profile.fakeStream ?? false,
customParams: profile.customParams ?? {},
tavernProfile: '',
};
}
// 降级:按槽位读取各自的独立配置
const settings = extension_settings[extensionName] || {};
// plotOpt 槽有独立 API 面板(剧情优化),优先读其专属设置
if (slot === 'plotOpt') {
const apiMode = settings.plotOpt_apiMode || 'openai_test';
if (apiMode === 'sillytavern_preset') {
const context = getContext();
const profileId = settings.plotOpt_tavernProfile || '';
const stProfile = context.extensionSettings?.connectionManager?.profiles?.find(p => p.id === profileId);
return {
apiProvider: 'sillytavern_preset',
apiUrl: '',
apiKey: '',
model: stProfile?.openai_model || 'Preset Model',
maxTokens: settings.plotOpt_max_tokens ?? 65500,
temperature: settings.plotOpt_temperature ?? 1.0,
tavernProfile: profileId,
};
}
return {
apiProvider: apiMode,
apiUrl: settings.plotOpt_apiUrl?.trim() || '',
apiKey: configManager.get('plotOpt_apiKey') || '',
model: settings.plotOpt_model || '',
maxTokens: settings.plotOpt_max_tokens ?? 65500,
temperature: settings.plotOpt_temperature ?? 1.0,
tavernProfile: '',
};
}
// main 槽(及其余未明确处理的槽):读主面板 DOM 配置
const apiProvider = document.getElementById('amily2_api_provider')?.value || 'openai';
let model;
if (apiProvider === 'sillytavern_preset') {
const context = getContext();
const profileId = document.getElementById('amily2_preset_selector')?.value;
const stProfile = context.extensionSettings?.connectionManager?.profiles?.find(p => p.id === profileId);
model = stProfile?.openai_model || 'Preset Model';
} else {
model = document.getElementById('amily2_model')?.value;
}
return {
apiProvider,
apiUrl: document.getElementById('amily2_api_url')?.value.trim() || '',
apiKey: document.getElementById('amily2_api_key')?.value.trim() || '',
model,
maxTokens: settings.maxTokens || 4000,
temperature: settings.temperature || 0.7,
tavernProfile: document.getElementById('amily2_preset_selector')?.value || '',
};
}
export async function testApiConnection() {
console.log('[Amily2号-外交部] 开始API连接测试');
const $button = $("#amily2_test_api_connection");
if (!$button.length) return;
const originalHtml = $button.html();
$button.prop("disabled", true).html('<i class="fas fa-spinner fa-spin"></i> 测试中');
try {
const apiSettings = await getApiSettings();
const apiProvider = apiSettings.apiProvider || 'openai';
const requiresApiKey = !['sillytavern_backend', 'sillytavern_preset'].includes(apiProvider);
if (apiProvider === 'sillytavern_preset') {
if (!apiSettings.tavernProfile) {
throw new Error("请先在下方选择一个SillyTavern预设");
}
} else {
if (!apiSettings.apiUrl || !apiSettings.model) {
throw new Error("API配置不完整请检查URL、Key和模型选择");
}
}
toastr.info('正在发送测试消息"你好!"...', 'API连接测试');
const userName = getContext()?.name1 || '用户';
const systemPrompt = `接下来用户会使用测试按钮测试api连接是否正常请回复ta连接是正常的称呼用户为${userName}大人”并复述群号1060183271告知ta本扩展开发者“诗与酒.”正静待ta的入驻。`;
const testMessages = [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: '你好!' }
];
const response = await callAI(testMessages, {
maxTokens: 8192,
temperature: 0.5
});
if (response && response.trim()) {
console.log('[Amily2号-外交部] 测试消息响应:', response);
toastr.success(`连接测试成功AI回复: "${response}"`, 'API连接测试成功');
return true;
} else {
throw new Error('API未返回有效响应请检查您的代理、API URL和密钥是否正确。这通常发生在网络问题或认证失败时。');
}
} catch (error) {
console.error('[Amily2号-使节团] API连接测试失败:', error);
toastr.error(`连接测试失败: ${error.message}`, 'API连接测试失败');
return false;
} finally {
$button.prop("disabled", false).html(originalHtml);
}
}
export async function callAI(messages, options = {}) {
if (window.AMILY2_SYSTEM_PARALYZED === true) {
console.error("[Amily2-制裁] 系统完整性已受损,所有外交活动被无限期中止。");
return null;
}
const apiSettings = await getApiSettings(options.slot || 'main');
const finalOptions = {
maxTokens: apiSettings.maxTokens,
temperature: apiSettings.temperature,
model: apiSettings.model,
apiUrl: apiSettings.apiUrl,
apiKey: apiSettings.apiKey,
apiProvider: apiSettings.apiProvider,
customParams: apiSettings.customParams ?? {},
...options,
// options 可显式覆盖 customParams体现"代码内显式 > profile 配置"
customParams: { ...(apiSettings.customParams ?? {}), ...(options.customParams ?? {}) },
};
if (finalOptions.apiProvider !== 'sillytavern_preset') {
if (!finalOptions.apiUrl || !finalOptions.model) {
console.warn("[Amily2-外交部] API URL或模型未配置无法调用AI");
toastr.error("API URL或模型未配置无法调用AI。", "Amily2-外交部");
return null;
}
}
console.groupCollapsed(`[Amily2号-统一API调用] ${new Date().toLocaleTimeString()}`);
console.log("【请求参数】:", {
provider: finalOptions.apiProvider,
model: finalOptions.model,
maxTokens: finalOptions.maxTokens,
temperature: finalOptions.temperature,
messagesCount: messages.length
});
console.log("【消息内容】:", messages);
console.groupEnd();
try {
let responseContent;
switch (finalOptions.apiProvider) {
case 'openai':
responseContent = await callOpenAICompatible(messages, finalOptions);
break;
case 'openai_test':
responseContent = await callOpenAITest(messages, finalOptions);
break;
case 'google':
responseContent = await callGoogleDirect(messages, finalOptions);
break;
case 'sillytavern_backend':
responseContent = await callSillyTavernBackend(messages, finalOptions);
break;
case 'sillytavern_preset':
responseContent = await callSillyTavernPreset(messages, finalOptions);
break;
default:
console.error(`[Amily2-外交部] 未支持的API提供商: ${finalOptions.apiProvider}`);
return null;
}
if (!responseContent) {
console.warn('[Amily2-外交部] 未能获取AI响应内容但不视为错误');
return null;
}
console.groupCollapsed("[Amily2号-AI回复]");
console.log(responseContent);
console.groupEnd();
return responseContent;
} catch (error) {
console.error(`[Amily2-外交部] API调用发生错误:`, error);
if (error.message.includes('400')) {
toastr.error(`API请求格式错误 (400): 请检查消息格式和模型配置`, "API调用失败");
} else if (error.message.includes('401')) {
toastr.error(`API认证失败 (401): 请检查API Key配置`, "API调用失败");
} else if (error.message.includes('403')) {
toastr.error(`API访问被拒绝 (403): 请检查权限设置`, "API调用失败");
} else if (error.message.includes('429')) {
toastr.error(`API调用频率超限 (429): 请稍后重试`, "API调用失败");
} else if (error.message.includes('500')) {
toastr.error(`API服务器错误 (500): 请稍后重试`, "API调用失败");
} else {
toastr.error(`API调用失败: ${error.message}`, "API调用失败");
}
return null;
}
}
async function callOpenAICompatible(messages, options) {
const baseUrl = options.apiUrl.replace(/\/$/, '').replace(/\/v1$/, '');
const apiUrl = `${baseUrl}/v1/chat/completions`;
console.log(`[Amily2号-OpenAI兼容] API地址: ${apiUrl}`);
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Authorization': `Bearer ${options.apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
// 用户自定义参数profile.customParams + 显式 options.customParams 已在 callAI 合并)
...(options.customParams || {}),
// 表单托管的核心字段总是覆盖 customParams
model: options.model,
messages: messages,
max_tokens: options.maxTokens,
temperature: options.temperature,
stream: false,
})
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`OpenAI兼容API请求失败: ${response.status} - ${errorText}`);
}
const responseData = await response.json();
return responseData?.choices?.[0]?.message?.content;
}
async function callOpenAITest(messages, options) {
const body = {
// 1. 可调默认值(用户 customParams 可覆盖)
top_p: options.top_p || 1,
frequency_penalty: 0,
presence_penalty: 0.12,
include_reasoning: false,
reasoning_effort: 'medium',
enable_web_search: false,
request_images: false,
custom_prompt_post_processing: 'strict',
group_names: [],
// 2. 用户 customParams 覆盖上层默认值
...(options.customParams || {}),
// 3. 表单托管的核心字段总是 win
chat_completion_source: 'openai',
messages: messages,
model: options.model,
reverse_proxy: options.apiUrl,
proxy_password: options.apiKey,
stream: false,
max_tokens: options.maxTokens || 30000,
temperature: options.temperature || 1,
};
const response = await fetch('/api/backends/chat-completions/generate', {
method: 'POST',
headers: { ...getRequestHeaders(), 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`OpenAI兼容(测试)API请求失败: ${response.status} - ${errorText}`);
}
const responseData = await response.json();
if (!responseData || !responseData.choices || responseData.choices.length === 0) {
console.error('[Amily2号-OpenAI兼容(测试)] API返回了空的choices数组或错误:', responseData);
if (responseData.error) {
throw new Error(`API返回错误: ${responseData.error.message || JSON.stringify(responseData.error)}`);
}
return null;
}
return responseData?.choices?.[0]?.message?.content;
}
async function callGoogleDirect(messages, options) {
const GOOGLE_API_BASE_URL = 'https://generativelanguage.googleapis.com';
const apiVersion = options.model.includes('gemini-1.5') ? 'v1beta' : 'v1';
const finalApiUrl = `${GOOGLE_API_BASE_URL}/${apiVersion}/models/${options.model}:generateContent`;
console.log(`[Amily2号-Google直连] API地址: ${finalApiUrl}`);
const headers = {
"Content-Type": "application/json",
"x-goog-api-key": options.apiKey,
};
const requestBody = JSON.stringify(convertToGoogleRequest({
model: options.model,
messages,
max_tokens: options.maxTokens,
temperature: options.temperature
}));
const response = await fetch(finalApiUrl, {
method: "POST",
headers: headers,
body: requestBody
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Google API请求失败: ${response.status} - ${errorText}`);
}
let responseData = await response.json();
if (responseData.name && responseData.metadata) {
console.log("[Amily2号-Google] 收到异步操作ID启用轮询机制...");
const operationId = responseData.name;
const tracker = progressTracker(operationId, 6);
tracker.start();
try {
const pollingTask = createGooglePollingTask(operationId, GOOGLE_API_BASE_URL, { "Content-Type": "application/json" });
const pollingOptions = {
maxAttempts: 6,
baseDelay: 3000,
shouldStop: res => res.done,
onAttempt: (attempt, delay) => { tracker.onAttempt(attempt, delay); },
onError: (error, attempt) => { tracker.error(error.message); }
};
const pollingResult = await intelligentPoll(pollingTask, pollingOptions);
tracker.complete();
if (!pollingResult.response) {
throw new Error("轮询完成但未获得有效响应");
}
responseData = pollingResult.response;
} catch (pollingError) {
console.error('[Google轮询错误]', pollingError);
tracker.error(`轮询失败: ${pollingError.message}`);
throw new Error("Google轮询任务失败: " + pollingError.message);
}
}
return parseGoogleResponse(responseData)?.choices?.[0]?.message?.content;
}
async function callSillyTavernBackend(messages, options) {
console.log('[Amily2号-ST后端] 通过SillyTavern后端调用API');
const rawResponse = await $.ajax({
url: '/api/backends/chat-completions/generate',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({
// 用户 customParams可被核心字段覆盖
...(options.customParams || {}),
// 表单托管字段总是 win
chat_completion_source: 'custom',
custom_url: options.apiUrl,
api_key: options.apiKey,
model: options.model,
messages: messages,
max_tokens: options.maxTokens,
temperature: options.temperature,
stream: false,
})
});
const result = normalizeApiResponse(rawResponse);
if (result.error) {
throw new Error(result.error.message || 'SillyTavern后端API调用失败');
}
return result.content;
}
async function callSillyTavernPreset(messages, options) {
console.log('[Amily2号-ST预设] 使用SillyTavern预设调用');
const context = getContext();
if (!context) {
throw new Error('无法获取SillyTavern上下文');
}
const profileId = options.tavernProfile || extension_settings[extensionName]?.tavernProfile;
if (!profileId) {
throw new Error('未配置SillyTavern预设ID');
}
let originalProfile = '';
let responsePromise;
try {
originalProfile = await compatibleTriggerSlash('/profile');
console.log(`[Amily2号-ST预设] 当前配置文件: ${originalProfile}`);
const targetProfile = context.extensionSettings?.connectionManager?.profiles?.find(p => p.id === profileId);
if (!targetProfile) {
throw new Error(`未找到配置文件ID: ${profileId}`);
}
const targetProfileName = targetProfile.name;
console.log(`[Amily2号-ST预设] 目标配置文件: ${targetProfileName}`);
const currentProfile = await compatibleTriggerSlash('/profile');
if (currentProfile !== targetProfileName) {
console.log(`[Amily2号-ST预设] 切换配置文件: ${currentProfile} -> ${targetProfileName}`);
const escapedProfileName = targetProfileName.replace(/"/g, '\\"');
await compatibleTriggerSlash(`/profile await=true "${escapedProfileName}"`);
}
if (!context.ConnectionManagerRequestService) {
throw new Error('ConnectionManagerRequestService不可用');
}
console.log(`[Amily2号-ST预设] 通过配置文件 ${targetProfileName} 发送请求`);
responsePromise = context.ConnectionManagerRequestService.sendRequest(
targetProfile.id,
messages,
options.maxTokens || 4000
);
} finally {
try {
const currentProfileAfterCall = await compatibleTriggerSlash('/profile');
if (originalProfile && originalProfile !== currentProfileAfterCall) {
console.log(`[Amily2号-ST预设] 恢复原始配置文件: ${currentProfileAfterCall} -> ${originalProfile}`);
const escapedOriginalProfile = originalProfile.replace(/"/g, '\\"');
await compatibleTriggerSlash(`/profile await=true "${escapedOriginalProfile}"`);
}
} catch (restoreError) {
console.error('[Amily2号-ST预设] 恢复配置文件失败:', restoreError);
}
}
const result = await responsePromise;
if (!result) {
throw new Error('未收到API响应');
}
const normalizedResult = normalizeApiResponse(result);
if (normalizedResult.error) {
throw new Error(normalizedResult.error.message || 'SillyTavern预设API调用失败');
}
return normalizedResult.content;
}
export function generateRandomSeed() {
const letters = 'abcdefghijklmnopqrstuvwxyz';
const randomLetter = () => letters[Math.floor(Math.random() * letters.length)];
const randomRoll = (max) => Math.floor(Math.random() * max) + 1;
let seed = '';
seed += randomLetter();
seed += randomRoll(1919819);
seed += randomLetter();
seed += randomLetter();
seed += randomRoll(114514);
seed += randomLetter();
seed += randomLetter();
seed += randomRoll(9999);
seed += randomRoll(9999);
seed += randomLetter();
return seed;
}
export async function checkAndFixWithAPI(latestMessage, previousMessages) {
const { processOptimization } = await import('./summarizer.js');
return await processOptimization(latestMessage, previousMessages);
}
/**
* 使用 OpenAI Function Call 调用 AI返回 tool_calls[0].function.arguments 字符串。
* 仅支持 openai / openai_test 接口Google / ST preset / backend 不在标准 tool_calls 格式下工作)。
*
* @param {Array} messages
* @param {Object} tool - OpenAI tools 定义对象(单个,含 type/function 字段)
* @param {Object} options - 同 callAI 的 options支持 slot / customParams 等
* @returns {Promise<string|null>} arguments JSON 字符串,失败返回 null
*/
export async function callAIForTools(messages, tool, options = {}) {
const apiSettings = await getApiSettings(options.slot || 'main');
const finalOptions = {
maxTokens: apiSettings.maxTokens,
temperature: apiSettings.temperature,
model: apiSettings.model,
apiUrl: apiSettings.apiUrl,
apiKey: apiSettings.apiKey,
apiProvider: apiSettings.apiProvider,
customParams: { ...(apiSettings.customParams ?? {}), ...(options.customParams ?? {}) },
...options,
};
const FC_SUPPORTED_PROVIDERS = new Set(['openai', 'openai_test', 'custom_oai', 'openrouter', 'deepseek', 'xai']);
if (!FC_SUPPORTED_PROVIDERS.has(finalOptions.apiProvider)) {
console.warn(`[Amily2-外交部] Function Call 不支持当前接口类型: ${finalOptions.apiProvider}`);
toastr.warning(`当前 API 接口类型(${finalOptions.apiProvider})不支持 Function Call。`, 'Function Call');
return null;
}
if (!finalOptions.apiUrl || !finalOptions.model) {
console.warn('[Amily2-外交部] API URL 或模型未配置,无法调用 Function Call AI');
toastr.error('API URL 或模型未配置。', 'Amily2-外交部');
return null;
}
const buildFCBody = (withToolChoice, overrideMessages) => ({
chat_completion_source: 'openai',
reverse_proxy: finalOptions.apiUrl,
proxy_password: finalOptions.apiKey,
model: finalOptions.model,
messages: overrideMessages ?? messages,
max_tokens: finalOptions.maxTokens || 30000,
temperature: finalOptions.temperature ?? 1,
stream: false,
...(finalOptions.customParams || {}),
tools: [tool],
...(withToolChoice ? { tool_choice: { type: 'function', function: { name: tool.function.name } } } : {}),
});
const doFCRequest = async (withToolChoice, overrideMessages) => {
const response = await fetch('/api/backends/chat-completions/generate', {
method: 'POST',
headers: { ...getRequestHeaders(), 'Content-Type': 'application/json' },
body: JSON.stringify(buildFCBody(withToolChoice, overrideMessages)),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Function Call 请求失败: ${response.status} - ${errorText}`);
}
const data = await response.json();
// ST 代理在上游报错时仍返回 HTTP 200错误信息在 body 里
if (data?.error) {
throw new Error(`Function Call 请求失败: ${JSON.stringify(data.error)}`);
}
return data;
};
try {
console.groupCollapsed(`[Amily2号-Function Call] ${new Date().toLocaleTimeString()}`);
console.log('【工具】:', tool.function?.name, '【模型】:', finalOptions.model);
console.log('【消息】:', messages);
console.groupEnd();
let data;
try {
// 走 ST 后端代理,避免浏览器 CSP 拦截直连外部 URL
data = await doFCRequest(true);
} catch (firstError) {
// 首次失败(含 ST 代理吞掉错误码场景)无条件去掉 tool_choice 重试一次
// 思考模式模型支持 tools 但不支持强制 tool_choice追加强制指令防止模型直接输出文本
console.warn('[Amily2-外交部] 首次 FC 请求失败,去掉 tool_choice 重试…', firstError.message);
const retryMessages = [
...messages,
{ role: 'user', content: `你必须通过调用 \`${tool.function.name}\` 函数来返回结果,禁止直接输出文本内容。` },
];
data = await doFCRequest(false, retryMessages);
}
const toolCalls = data?.choices?.[0]?.message?.tool_calls;
if (!Array.isArray(toolCalls) || toolCalls.length === 0) {
console.warn('[Amily2-外交部] Function Call 响应中无 tool_callsfinish_reason:', data?.choices?.[0]?.finish_reason);
return null;
}
const argsString = toolCalls[0]?.function?.arguments;
console.groupCollapsed('[Amily2号-Function Call 响应]');
console.log(argsString);
console.groupEnd();
return argsString ?? null;
} catch (error) {
console.error('[Amily2-外交部] Function Call 调用失败:', error);
toastr.error(`Function Call 调用失败: ${error.message}`, 'Amily2-外交部');
return null;
}
}