commit cc111683c902aff47ac51f7c606965ce32276625 Author: Wx-2025 <351320169@qq.com> Date: Tue Jul 1 17:49:41 2025 +0800 Add files via upload diff --git a/index.js b/index.js new file mode 100644 index 0000000..1769a52 --- /dev/null +++ b/index.js @@ -0,0 +1,1490 @@ +import { extension_settings, getContext } from '/scripts/extensions.js'; +import { + saveSettingsDebounced, + eventSource, + event_types, + saveChatConditional, + reloadCurrentChat +} from '/script.js'; +import { SlashCommand } from '/scripts/slash-commands/SlashCommand.js'; + +let availableModels = []; +let isFetchingModels = false; +// 插件名称 +const extensionName = 'ST-Amily2-Chat-Optimisation'; +const extensionFolderPath = `scripts/extensions/third-party/ST-Amily2-Chat-Optimisation`; + +// === 动态密码生成器 === +function generateDynamicPassword(date = new Date()) { + // 种子值 + const seed = { + a: 1103515245, + c: 12345, + m: 2147483647, + }; + + // 核心哈希算法 + function customHash(input) { + let hash = 0; + for(let i = 0; i < input.length; i++) { + hash = ((hash << 5) - hash) + input.charCodeAt(i); + hash |= 0; // 转为32位整型 + } + return hash >>> 0; // 确保为正整数 + } + + // 使用传入的日期作为基准 + const month = date.getMonth() + 1; + const day = date.getDate(); + const year = date.getFullYear(); + const baseInput = `${month}-${day}-AMILY_${year}`; + + // 生成伪随机种子 + const str1 = `SD${customHash(baseInput)}`; + const str2 = `V${customHash(str1)}`; + + // 使用线性同余算法生成密码 + function lcgRandom(params) { + return function() { + params.seed = (params.a * params.seed + params.c) % params.m; + return params.seed; + }; + } + + const combinedSeed = customHash(str2) % seed.m; + const randFunc = lcgRandom({ + a: seed.a, + c: seed.c, + m: seed.m, + seed: combinedSeed + }); + + // 密码字符集(移除易混淆字符) + const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; + + // 生成密码段 + const segments = []; + for (let segIdx = 0; segIdx < 3; segIdx++) { + let segment = ''; + for (let i = 0; i < 4; i++) { + const randValue = Math.abs(randFunc()); + segment += chars.charAt(randValue % chars.length); + } + segments.push(segment); + } + + return segments.join('-'); +} + +// === 开发者使用的密码工具 === +function getPasswordForDate(date = new Date()) { + return generateDynamicPassword(date); +} + +// 密码有效期设置(默认为7天) +const PASSWORD_VALIDITY_DAYS = 7; + +// 开发者提示 - 在控制台显示今日密码 +console.warn("[Amily2号] 开发者提示:今日密码 - ", getPasswordForDate()); +console.log(`[Amily2号] 密码有效期为: ${PASSWORD_VALIDITY_DAYS}天`); + +// ================ 实际使用的授权配置 ================ +const AUTH_CONFIG = { + expiryDate: new Date('2024-12-31'), + validityDays: PASSWORD_VALIDITY_DAYS +}; + +// 默认设置 +const defaultSettings = { + enabled: true, + activated: false, + apiUrl: 'http://localhost:5001/v1', + apiKey: '', + model: 'deepseek-r1-250528', + maxTokens: 12000, + temperature: 1.2, + contextMessages: 2, + systemPrompt: '', + mainPrompt: '', + showOptimizationToast: true, + suppressToast: false, +}; + +// 授权状态变量 +window.pluginAuthStatus = { + authorized: false, + expired: false +}; + +// ============= 新增函数: 获取模型列表 ============= +async function fetchSupportedModels() { + const settings = extension_settings[extensionName]; + + if (!settings.apiUrl) { + toastr.error('请先配置API URL', '获取模型失败'); + return []; + } + + if (isFetchingModels) { + toastr.info('正在获取模型列表,请稍候...', '获取模型'); + return; + } + + isFetchingModels = true; + try { + const originalButtonText = $('#amily2_refresh_models').html(); + + $('#amily2_refresh_models') + .prop('disabled', true) + .html(' 加载中'); + + let modelListUrl = settings.apiUrl; + if (!modelListUrl.endsWith('/v1/models')) { + if (modelListUrl.endsWith('/')) { + modelListUrl += 'v1/models'; + } else { + modelListUrl += '/v1/models'; + } + } + + const headers = { + 'Content-Type': 'application/json' + }; + + if (settings.apiKey) { + headers['Authorization'] = `Bearer ${settings.apiKey}`; + } + + console.log('发送模型列表请求到:', modelListUrl); + + const response = await fetch(modelListUrl, { + method: 'GET', + headers: headers + }); + + if (!response.ok) { + throw new Error(`API返回错误: ${response.status} ${response.statusText}`); + } + + const data = await response.json(); + let models = []; + + if (Array.isArray(data)) { + models = data.map(m => m.id || m); + } else if (data.data && Array.isArray(data.data)) { + models = data.data.map(m => m.id); + } else if (data.models && Array.isArray(data.models)) { + models = data.models; + } else { + throw new Error('未知的模型列表格式'); + } + + availableModels = models.filter(m => + !m.includes('embed') && + !m.includes('search') && + !m.includes('similarity') && + !m.includes('audio') + ); + + availableModels.sort(); + + console.log(`获取模型列表成功 (${availableModels.length}个):`, availableModels); + toastr.success(`成功获取 ${availableModels.length} 个可用模型`, '模型加载完成'); + + return availableModels; + } catch (error) { + console.error('获取模型列表失败:', error); + toastr.error(`获取模型失败: ${error.message}`, '错误'); + return []; + } finally { + isFetchingModels = false; + + $('#amily2_refresh_models') + .prop('disabled', false) + .html(' 刷新模型'); + } +} + +// ============= 新增函数: 填充模型下拉菜单 ============= +function populateModelDropdown() { + const modelSelect = $('#amily2_model'); + const modelNotes = $('#amily2_model_notes'); + + modelSelect.empty(); + + const currentModel = extension_settings[extensionName].model || ''; + + if (availableModels.length === 0) { + modelSelect.append(''); + modelNotes.html('请检查API配置后点击"刷新模型"按钮'); + return; + } + + const defaultOption = $('') + .val('') + .text('-- 选择模型 --'); + modelSelect.append(defaultOption); + + availableModels.forEach(model => { + const option = $('') + .val(model) + .text(model); + + if (model === currentModel) { + option.attr('selected', 'selected'); + } + + modelSelect.append(option); + }); + + if (currentModel && modelSelect.val() === currentModel) { + modelNotes.html(`已选择: ${currentModel}`); + } else { + modelNotes.html(`已加载 ${availableModels.length} 个可用模型`); + } +} + +// 加载设置 +async function loadSettings() { + if (!extension_settings[extensionName]) { + extension_settings[extensionName] = {}; + } + + // ===== 授权过期检查 ===== + const now = new Date(); + window.pluginAuthStatus.expired = now > AUTH_CONFIG.expiryDate; + + if (window.pluginAuthStatus.expired) { + localStorage.removeItem('plugin_activated'); + localStorage.removeItem('plugin_auth_code'); + localStorage.removeItem('plugin_valid_until'); + console.log('[Amily2号] 检测到授权过期,已清理本地存储'); + } + + // 合并默认设置 + extension_settings[extensionName] = { + ...defaultSettings, + ...extension_settings[extensionName] + }; + + // 检查授权状态 + window.pluginAuthStatus.authorized = await checkAuthorization(); + + // 更新UI + updateUI(); + + // 自动加载模型 + if (window.pluginAuthStatus.authorized && extension_settings[extensionName].apiUrl) { + const cachedModels = localStorage.getItem('amily2_cached_models'); + if (cachedModels) { + availableModels = JSON.parse(cachedModels); + console.log('从缓存加载模型列表:', availableModels.length); + populateModelDropdown(); + } + + setTimeout(() => { + if (availableModels.length === 0) { + toastr.info('正在自动加载模型列表...', '模型初始化'); + $('#amily2_refresh_models').click(); + } + }, 1500); + } + + $('#amily2_api_url').on('input', function() { + localStorage.removeItem('amily2_cached_models'); + availableModels = []; + populateModelDropdown(); + }); +} + +// 检查授权状态(无UI更新) +function checkAuthorization() { + const now = new Date(); + window.pluginAuthStatus.expired = now > AUTH_CONFIG.expiryDate; + + const activated = localStorage.getItem('plugin_activated') === 'true'; + const savedAuthCode = localStorage.getItem('plugin_auth_code'); + const validUntil = localStorage.getItem('plugin_valid_until'); + + let withinValidityPeriod = false; + + if (validUntil) { + const validUntilDate = new Date(validUntil); + withinValidityPeriod = now <= validUntilDate; + console.log(`[Amily2号] 授权有效期检查: + 当前时间: ${now.toISOString()} + 授权有效期至: ${validUntilDate.toISOString()} + 是否在有效期内: ${withinValidityPeriod}`); + } + + let passwordMatches = false; + if (savedAuthCode) { + const today = new Date(); + for (let i = 0; i < AUTH_CONFIG.validityDays; i++) { + const checkDate = new Date(); + checkDate.setDate(today.getDate() - i); + const passwordForDay = getPasswordForDate(checkDate); + + if (savedAuthCode === passwordForDay) { + passwordMatches = true; + console.log(`[Amily2号] 密码匹配: ${savedAuthCode} 对应第${i+1}天前`); + break; + } + } + } + + window.pluginAuthStatus.authorized = activated && + !window.pluginAuthStatus.expired && + passwordMatches && + withinValidityPeriod; + + return window.pluginAuthStatus.authorized; +} + +// 激活授权 +async function activatePluginAuthorization(authCode) { + let isValidCode = false; + const today = new Date(); + + for (let i = 0; i < AUTH_CONFIG.validityDays; i++) { + const checkDate = new Date(); + checkDate.setDate(today.getDate() - i); + const passwordForDay = getPasswordForDate(checkDate); + + if (authCode === passwordForDay) { + isValidCode = true; + console.log(`[Amily2号] 输入的密码匹配第${i+1}天前的有效密码`); + break; + } + } + + if (!isValidCode) { + toastr.error('授权码无效', '激活失败'); + return false; + } + + const now = new Date(); + if (now > AUTH_CONFIG.expiryDate) { + toastr.error('授权已过期', '激活失败'); + return false; + } + + const validUntil = new Date(); + validUntil.setDate(now.getDate() + AUTH_CONFIG.validityDays); + + localStorage.setItem('plugin_valid_until', validUntil.toISOString()); + localStorage.setItem('plugin_auth_code', authCode); + localStorage.setItem('plugin_activated', 'true'); + + toastr.success(`授权激活成功,有效期至 ${validUntil.toLocaleDateString()}`, 'Amily2号启用'); + window.pluginAuthStatus.authorized = true; + + $('#auth_panel').hide(); + $('.plugin-features').show(); + + extension_settings[extensionName].enabled = true; + saveSettings(); + + return true; +} + +// 显示过期信息 +function displayExpiryInfo() { + const now = new Date(); + const daysLeft = Math.ceil((AUTH_CONFIG.expiryDate - now) / (1000 * 60 * 60 * 24)); + const validUntil = localStorage.getItem('plugin_valid_until'); + + if (window.pluginAuthStatus.expired) { + return '
授权已过期
'; + } else { + let validUntilHtml = ''; + if (validUntil) { + const validUntilDate = new Date(validUntil); + validUntilHtml = `当前授权有效期至: ${validUntilDate.toLocaleDateString()}`; + } + + return ` +
+ 授权有效期: ${daysLeft}天 + 有效期至: ${AUTH_CONFIG.expiryDate.toLocaleDateString()} + ${validUntilHtml} +
+ `; + } +} + +// ============= 配置验证函数 ============= +function validateSettings() { + const settings = extension_settings[extensionName] || {}; + const errors = []; + + if (!settings.apiUrl) { + errors.push('API URL未配置'); + } else if (!/^https?:\/\//.test(settings.apiUrl)) { + errors.push('API URL必须以http://或https://开头'); + } + + if (settings.apiKey) { + if (settings.apiKey.length < 8) { + errors.push('API密钥太短(至少8位)'); + } + if (/(key|secret|password)/i.test(settings.apiKey)) { + toastr.warning('请注意:API Key包含敏感关键词("key", "secret", "password")', '安全提醒', { timeOut: 5000 }); + } + } + + if (!settings.model) { + errors.push('未选择模型'); + } + + if (settings.maxTokens < 100 || settings.maxTokens > 20000) { + errors.push(`Token数超限 (${settings.maxTokens}) - 必须在100-20000之间`); + } + + return errors.length ? errors : null; +} + +// ============= 统一的设置项事件处理 ============= +function saveSettings() { + if (!window.pluginAuthStatus.authorized) return false; // 确保在未授权时不保存 + + const validationErrors = validateSettings(); + + if (validationErrors) { + const errorHtml = validationErrors.map(err => `
❌ ${err}
`).join(''); + toastr.error(`配置存在错误:${errorHtml}`, '设置未保存', { + timeOut: 8000, + extendedTimeOut: 0, + preventDuplicates: true + }); + return false; + } + + saveSettingsDebounced(); + return true; +} + +// 统一处理所有设置项变更 +$('[id^="amily2_"]').on('change', function() { + if (!window.pluginAuthStatus.authorized) return; + + // 获取设置名称(从ID转换) + const settingName = this.id.replace('amily2_', ''); + + // 根据控件类型获取值 + let value; + if ($(this).is(':checkbox')) { + value = $(this).prop('checked'); + } + else if (settingName === 'max_tokens' || settingName === 'context_messages') { + value = parseInt($(this).val()); + } + else if (settingName === 'temperature') { + value = parseFloat($(this).val()); + } + else { + value = $(this).val(); + } + + // 更新设置 + extension_settings[extensionName][settingName] = value; + + // 更新显示值(如果适用) + if (settingName === 'enabled') { + extension_settings[extensionName].enabled = value; + } + else if (settingName === 'max_tokens') { + $('#amily2_max_tokens_value').text(value); + } + else if (settingName === 'temperature') { + $('#amily2_temperature_value').text(value); + } + else if (settingName === 'context_messages') { + $('#amily2_context_messages_value').text(value); + } + else if (settingName === 'show_toast') { + extension_settings[extensionName].suppressToast = false; + } + + console.log(`[Amily2设置] ${settingName} 更新为:`, value); + + // 尝试保存,保存失败则恢复原值 + if (!saveSettings()) { + // 恢复原始值 + const originalValue = defaultSettings[settingName]; + + if ($(this).is(':checkbox')) { + $(this).prop('checked', originalValue); + } + else { + $(this).val(originalValue); + } + + // 特殊控件恢复 + if (settingName === 'max_tokens') { + $('#amily2_max_tokens_value').text(originalValue); + } + else if (settingName === 'temperature') { + $('#amily2_temperature_value').text(originalValue); + } + else if (settingName === 'context_messages') { + $('#amily2_context_messages_value').text(originalValue); + } + } +}); + +// ============= 更新UI ============= +function updateUI() { + const authStatus = window.pluginAuthStatus; + + if (!authStatus.authorized) { + $('#amily2_enabled').prop('checked', false); + $('#amily2_enabled').prop('disabled', true); + $('[id^="amily2_"]').not('#auth_input, #auth_submit').prop('disabled', true); + toastr.warning('插件未授权,功能已禁用', 'Amily2号'); + } else { + const settings = extension_settings[extensionName]; + $('#amily2_enabled').prop('disabled', false); + $('[id^="amily2_"]').prop('disabled', false); + + $('#amily2_enabled').prop('checked', settings.enabled); + $('#amily2_api_url').val(settings.apiUrl); + $('#amily2_api_key').val(settings.apiKey); + + populateModelDropdown(); + + $('#amily2_max_tokens').val(settings.maxTokens); + $('#amily2_max_tokens_value').text(settings.maxTokens); + $('#amily2_temperature').val(settings.temperature); + $('#amily2_temperature_value').text(settings.temperature); + $('#amily2_context_messages').val(settings.contextMessages); + $('#amily2_context_messages_value').text(settings.contextMessages); + $('#amily2_main_prompt').val(settings.mainPrompt); + $('#amily2_system_prompt').val(settings.systemPrompt); + + if ($('#amily2_show_toast').length) { + $('#amily2_show_toast').prop('checked', settings.showOptimizationToast); + extension_settings[extensionName].suppressToast = settings.suppressToast; + } + } +} + +// 检查最新消息 +async function checkLatestMessage() { + const context = getContext(); + const chat = context.chat || []; + + if (!chat || chat.length === 0) { + console.log('[聊天回复检查器] 没有聊天记录'); + return { message: null, previousMessages: [] }; + } + + const latestMessage = chat[chat.length - 1]; + + console.log('[聊天回复检查器] 检查消息:', { + isUser: latestMessage.is_user, + messageLength: latestMessage.mes?.length, + messagePreview: latestMessage.mes?.substring(0, 50) + '...' + }); + + if (latestMessage.is_user) { + console.log('[聊天回复检查器] 跳过用户消息'); + return { message: latestMessage, previousMessages: [] }; + } + + // 获取上下文消息(根据用户设置) + const settings = extension_settings[extensionName]; + const contextCount = settings.contextMessages || 2; + const startIndex = Math.max(0, chat.length - contextCount - 1); + const previousMessages = chat.slice(startIndex, chat.length - 1); + + console.log('[聊天回复检查器] 上下文设置:', { + contextMessages: settings.contextMessages, + contextCount: contextCount, + chatLength: chat.length, + startIndex: startIndex, + previousMessagesCount: previousMessages.length + }); + + console.log('[聊天回复检查器] 获取上下文消息:', { + previousMessages: previousMessages.length, + startIndex: startIndex + }); + + return { message: latestMessage, previousMessages }; +} + +// 使用API检查和修复消息(添加防止无限重试) +async function checkAndFixWithAPI(latestMessage, previousMessages, isRetry = false, retryCount = 0) { + const settings = extension_settings[extensionName]; + + if (!settings.apiUrl) { + console.error('[聊天回复检查器] 未配置API URL'); + return null; + } + + // 优先使用主要提示词,如果为空则使用系统提示词 + const usePrompt = settings.mainPrompt || settings.systemPrompt; + + if (!usePrompt) { + console.error('[聊天回复检查器] 未配置主要或系统提示词'); + toastr.error('请配置主要提示词或系统提示词', '聊天回复检查器'); + return null; + } + + // 构建检查内容 + let checkContent = `请检查并优化以下文本:\n\n"${latestMessage.mes}"\n\n`; + + // 始终提供上下文参考(让AI自主判断是否需要考虑) + if (previousMessages.length > 0) { + checkContent += '上下文参考:\n'; + const recentMessages = previousMessages.slice(-2); + recentMessages.forEach((msg, index) => { + const speaker = msg.is_user ? '用户' : 'AI'; + checkContent += `${speaker}: "${msg.mes}"\n`; + }); + checkContent += '\n'; + } + + checkContent += '请按照系统提示的格式分析并回复。'; + + // 构建请求消息(使用优先的提示词) + const messages = [ + { + role: 'system', + content: usePrompt + }, + { + role: 'user', + content: checkContent + } + ]; + + try { + // 确保URL格式正确 + let apiUrl = settings.apiUrl; + // 针对不同API提供商处理URL + if (apiUrl.includes('ark.cn-beijing.volces.com')) { + // 火山引擎 ARK API + if (!apiUrl.endsWith('/completion')) { + apiUrl = apiUrl.replace(/\/completion$/, ''); + if (apiUrl.endsWith('/')) { + apiUrl += 'completion'; + } else { + apiUrl += '/completion'; + } + } + } else if (!apiUrl.endsWith('/chat/completions')) { + // 标准 OpenAI 格式 + if (apiUrl.endsWith('/v1')) { + apiUrl = apiUrl + '/chat/completions'; + } else if (apiUrl.endsWith('/')) { + apiUrl = apiUrl + 'v1/chat/completions'; + } else { + apiUrl = apiUrl + '/v1/chat/completions'; + } + } + + const requestBody = { + model: settings.model, + messages: messages, + max_tokens: settings.maxTokens, + temperature: settings.temperature, + stream: false + }; + + console.log('[聊天回复检查器] API请求:', { + url: apiUrl, + model: settings.model, + messagesCount: messages.length, + isRetry: isRetry, + retryCount: retryCount + }); + + const headers = { + 'Content-Type': 'application/json' + }; + + // 只有在有API Key时才添加Authorization头 + if (settings.apiKey) { + headers['Authorization'] = `Bearer ${settings.apiKey}`; + } + + const response = await fetch(apiUrl, { + method: 'POST', + headers: headers, + body: JSON.stringify(requestBody) + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error('[聊天回复检查器] API请求失败详情:', { + status: response.status, + statusText: response.statusText, + headers: Object.fromEntries(response.headers.entries()), + errorBody: errorText + }); + throw new Error(`API请求失败: ${response.status} ${response.statusText} - ${errorText}`); + } + + const data = await response.json(); + const apiResponse = data.choices?.[0]?.message?.content; + + if (!apiResponse) { + console.error('[聊天回复检查器] API响应格式错误:', data); + throw new Error('API返回的消息为空'); + } + + console.log('[聊天回复检查器] API返回内容:', apiResponse); + + // 检查API是否返回错误信息 + if (apiResponse.includes('无法生成回复') || + apiResponse.includes('请尝试修改') || + apiResponse.includes('内容过滤') || + apiResponse.includes('违反政策')) { + console.log('[聊天回复检查器] API返回错误信息', apiResponse); + + // 添加重试限制:最多4次 + if (!isRetry && retryCount < 4) { + const nextRetryCount = retryCount + 1; + console.log(`[聊天回复检查器] API错误,开始第${nextRetryCount}次重试...`); + toastr.info(`API优化失败,正在重试 (${nextRetryCount}/4)`, '聊天回复检查器'); + // 增加延迟(随重试次数增加) + await new Promise(resolve => setTimeout(resolve, 1000 * nextRetryCount)); + return await checkAndFixWithAPI(latestMessage, previousMessages, true, nextRetryCount); + } else { + console.log('[聊天回复检查器] API重试次数已达上限,放弃优化'); + toastr.warning('API优化失败已达到最大重试次数', '聊天回复检查器'); + return null; + } + } + + const hasThinkTag = apiResponse.includes('think'); + const hasContentTag = apiResponse.includes('content'); + + if (has888Tag && has666Tag) { + // 匹配think标签 + const thinkMatch = apiResponse.match(/([\s\S]*?)<\/think>/); + // 匹配content标签 + const contentMatch = apiResponse.match(/([\s\S]*?)<\/content>/); + + if (!thinkMatch) { + console.log('[聊天回复检查器] API响应格式错误,未找到888标签'); + return null; + } + + const thinkContent = thinkMatch[1].trim(); + const fixedContent = contentMatch ? contentMatch[1].trim() : ''; + + console.log('[聊天回复检查器] 分析结果:', thinkContent); + console.log('[聊天回复检查器] 修复内容:', fixedContent); + + // 在界面显示分析结果(带"不再显示"选项) + const settings = extension_settings[extensionName]; + if (thinkContent && settings.showOptimizationToast && !settings.suppressToast) { + // 构建弹窗内容 + const toastContent = ` +
${thinkContent.substring(0, 100)}${thinkContent.length > 100 ? "..." : ""}
+
+ +
+ `; + + // 显示带选项的弹窗 + const toast = toastr.info(toastContent, 'AI优化分析', { + timeOut: 0, // 不会自动关闭 + extendedTimeOut: 0, + preventDuplicates: true, + closeButton: true, + tapToDismiss: false, + onclick: null, + onShown: function() { + // 绑定"不再显示"复选框的事件 + $('#amily2_dont_show_again').on('change', function() { + if (this.checked) { + // 更新设置 + extension_settings[extensionName].suppressToast = true; + saveSettings(); + toastr.remove(toast); + toastr.success('已隐藏优化通知', '设置更新'); + } + }); + } + }); + } + + // 如果修复内容为空,则不需要修复 + if (!fixedContent) { + console.log('[聊天回复检查器] API判定:不需要优化'); + return null; + } + + console.log('[聊天回复检查器] API判定:需要优化'); + // 返回完整的API响应,包含标签 + return apiResponse; + } else { + // 如果没有标签格式 + console.log('[聊天回复检查器] API返回普通文本格式'); + + // 如果返回"无需改进"类似内容,则不修复 + if (apiResponse.includes('无需改进') || + apiResponse.includes('不需要改进') || + apiResponse.includes('质量良好') || + apiResponse.includes('没有问题')) { + console.log('[聊天回复检查器] API判定:不需要优化'); + return null; + } + + console.log('[聊天回复检查器] API判定:需要优化'); + return apiResponse; + } + } catch (error) { + console.error('[聊天回复检查器] API调用出错:', error); + toastr.error(`API调用失败: ${error.message}`, '聊天回复检查器', {timeOut: 8000}); + + return null; + } +} + +// 存储已处理的消息,防止循环修复 +const processedMessages = new Set(); + +// 处理消息接收事件(在渲染前拦截) +async function onMessageReceived(data) { + console.log('[聊天回复检查器] 消息接收事件触发:', { data, eventType: 'onMessageReceived' }); + + const settings = extension_settings[extensionName]; + + console.log('[聊天回复检查器] 当前设置:', { + enabled: settings.enabled, + hasApiUrl: !!settings.apiUrl, + apiUrl: settings.apiUrl + }); + + if (!settings.enabled) { + console.log('[聊天回复检查器] 插件未启用,跳过检查'); + return; + } + + if (!settings.apiUrl) { + console.log('[聊天回复检查器] 未配置API URL,跳过检查'); + return; + } + + const context = getContext(); + const chat = context.chat; + + if (!chat || chat.length === 0) { + console.log('[聊天回复检查器] 没有聊天记录'); + return; + } + + const latestMessage = chat[chat.length - 1]; + + // 只处理AI的回复 + if (latestMessage.is_user) { + console.log('[聊天回复检查器] 跳过用户消息'); + return; + } + + // 跳过第一条消息(通常是系统消息或角色介绍) + if (chat.length <= 1) { + console.log('[聊天回复检查器] 跳过第一条消息'); + return; + } + + // 跳过过短的消息(可能是系统消息) + if (latestMessage.mes.length < 10) { + console.log('[聊天回复检查器] 跳过过短的消息'); + return; + } + + // 防循环检查:为消息生成唯一标识 + const messageKey = `${chat.length}-${latestMessage.mes.substring(0, 50)}`; + + if (processedMessages.has(messageKey)) { + console.log('[聊天回复检查器] 消息已处理过,跳过检查避免循环'); + return; + } + + // 标记消息为已处理 + processedMessages.add(messageKey); + + // 清理过期的标记(保留最近10条) + if (processedMessages.size > 50) { + const entries = Array.from(processedMessages); + processedMessages.clear(); + entries.slice(-50).forEach(id => processedMessages.add(id)); +} + + // 获取上下文消息 + const contextCount = settings.contextMessages || 2; + const startIndex = Math.max(0, chat.length - 1 - contextCount); + const previousMessages = chat.slice(startIndex, chat.length - 1); + + console.log('[聊天回复检查器] 开始检查生成的回复...'); + + // 使用API检查和修复(添加初始重试次数0) + const fixedMessage = await checkAndFixWithAPI(latestMessage, previousMessages, false, 0); + + if (fixedMessage && fixedMessage !== latestMessage.mes) { + console.log('[聊天回复检查器] 内容已优化,显示优化版本'); + + // 直接修改消息内容,不需要重新加载 + latestMessage.mes = fixedMessage; + + console.log('[聊天回复检查器] 回复已在显示前优化'); + } else { + console.log('[聊天回复检查器] 内容无需优化,正常显示'); + } +} + +// 手动检查命令(使用API检查) +async function checkCommand() { + const settings = extension_settings[extensionName]; + if (!settings.apiUrl) { + toastr.error('请先配置API URL', '聊天回复检查器'); + return ''; + } + + const checkResult = await checkLatestMessage(); + + if (!checkResult.message) { + toastr.info('没有可检查的消息', '聊天回复检查器'); + return ''; + } + + if (checkResult.message.is_user) { + toastr.info('最新消息是用户消息,无需检查', '聊天回复检查器'); + return ''; + } + + toastr.info('正在使用API检查回复...', '聊天回复检查器'); + + // 添加重试初始参数 + const fixedMessage = await checkAndFixWithAPI(checkResult.message, checkResult.previousMessages, false, 0); + if (fixedMessage && fixedMessage !== checkResult.message.mes) { + toastr.warning('检测到问题,建议使用修复功能', '聊天回复检查器'); + } else { + toastr.success('未检测到问题', '聊天回复检查器'); + } + + return ''; +} + +// 手动修复命令 +async function fixCommand() { + const settings = extension_settings[extensionName]; + if (!settings.apiUrl) { + toastr.error('请先配置API URL', '聊天回复检查器'); + return ''; + } + + const context = getContext(); + const chat = context.chat; + + if (!chat || chat.length === 0) { + toastr.info('没有可修复的消息', '聊天回复检查器'); + return ''; + } + + const latestMessage = chat[chat.length - 1]; + + if (latestMessage.is_user) { + toastr.info('最新消息是用户消息,无需修复', '聊天回复检查器'); + return ''; + } + + // 获取上下文消息 + const contextCount = settings.contextMessages || 2; + const startIndex = Math.max(0, chat.length - 1 - contextCount); + const previousMessages = chat.slice(startIndex, chat.length - 1); + + toastr.info('正在检查并修复回复...', '聊天回复检查器'); + + // 添加重试初始参数 + const fixedMessage = await checkAndFixWithAPI(latestMessage, previousMessages, false, 0); + + if (fixedMessage && fixedMessage !== latestMessage.mes) { + latestMessage.mes = fixedMessage; + await saveChatConditional(); + await reloadCurrentChat(); + toastr.success('回复已修复', '聊天回复检查器'); + } else { + toastr.info('未检测到需要修复的问题', '聊天回复检查器'); + } + + return ''; +} + +// 测试命令(使用API测试) +async function testReplyChecker() { + const settings = extension_settings[extensionName]; + if (!settings.apiUrl) { + toastr.error('请先配置API URL', '聊天回复检查器'); + return ''; + } + + const context = getContext(); + const chat = context.chat; + + if (!chat || chat.length < 2) { + toastr.warning('需要至少2条消息才能测试', '聊天回复检查器'); + return ''; + } + // 获取倒数第二条AI消息 + let testMessage = null; + for (let i = chat.length - 2; i >= 0; i--) { + if (!chat[i].is_user) { + testMessage = chat[i].mes; + break; + } + } + + if (!testMessage) { + toastr.warning('没有找到可用于测试的AI消息', '聊天回复检查器'); + return ''; + } + + const lastMessage = chat[chat.length - 1]; + + if (lastMessage.is_user) { + toastr.warning('最后一条消息是用户消息,无法测试', '聊天回复检查器'); + return ''; + } + + // 临时修改最后一条消息来模拟重复 + const originalMessage = lastMessage.mes; + lastMessage.mes = testMessage + '\n\n' + testMessage; + + toastr.info('正在使用API测试检测功能...', '聊天回复检查器'); + + // 获取上下文消息 + const contextCount = settings.contextMessages || 2; + const startIndex = Math.max(0, chat.length - contextCount - 1); + const previousMessages = chat.slice(startIndex, chat.length - 1); + + // 使用API检查(添加重试初始参数) + const fixedMessage = await checkAndFixWithAPI(lastMessage, previousMessages, false, 0); + + // 恢复原始消息 + lastMessage.mes = originalMessage; + if (fixedMessage && fixedMessage !== (testMessage + '\n\n' + testMessage)) { + toastr.success('测试成功!API检测到重复内容并提供了修复建议', '聊天回复检查器'); + } else { + toastr.warning('测试结果:API未检测到问题,请检查API配置或提示词', '聊天回复检查器'); + } + + return ''; +} + +jQuery(async () => { + // 加载设置 + await loadSettings(); + + // 添加设置面板HTML + const settingsHtml = await $.get(`${extensionFolderPath}/settings.html`); + $('#extensions_settings2').append(settingsHtml); + $('#expiry_info').html(displayExpiryInfo()); + + // 添加华丽的CSS样式 + addAuthStyles(); + + // 注册激活按钮事件 + $('#auth_submit').on('click', async function() { + const authCode = $('#auth_input').val().trim(); + if (!authCode) { + toastr.error('请输入授权码', '验证失败'); + return; + } + + const success = await activatePluginAuthorization(authCode); + if (success) { + // 隐藏授权面板,显示功能面板 + $('#auth_panel').slideUp(400); + setTimeout(() => { + $('.plugin-features').slideDown(400); + }, 400); + } + }); + + // ============= 模型下拉菜单事件 ============= + // 在API URL输入框的change事件中添加 + $('#amily2_api_url').on('change', function() { + const url = $(this).val(); + if (url && !/^https?:\/\//.test(url)) { + $(this).css('border', '2px solid #ff5252'); + toastr.error('API URL必须以http://或https://开头'); + } else { + $(this).css('border', ''); + } + }); + // 在Token输入框的change事件中添加 + $('#amily2_max_tokens').on('change', function() { + const tokens = parseInt($(this).val()); + if (tokens < 100 || tokens > 20000) { + $(this).siblings('label').css('color', '#ff5252'); + } else { + $(this).siblings('label').css('color', ''); + } + }); + $('#amily2_model').on('change', function() { + const selectedModel = $(this).val(); + extension_settings[extensionName].model = selectedModel; + saveSettings(); + + // 更新状态信息 + if (selectedModel && selectedModel.length > 0) { + $('#amily2_model_notes').html(`已选择模型: ${selectedModel}`); + } else { + $('#amily2_model_notes').html('请选择一个模型'); + } + }); + + // ============= 刷新模型按钮事件 ============= + $('#amily2_refresh_models').on('click', async function() { + // 添加视觉反馈 - 按钮动画 + $(this).addClass('pulse'); + setTimeout(() => $(this).removeClass('pulse'), 500); + + // 获取模型列表 + await fetchSupportedModels(); + + // 填充下拉菜单 + populateModelDropdown(); + + // 缓存模型列表 + if (availableModels.length > 0) { + localStorage.setItem('amily2_cached_models', JSON.stringify(availableModels)); + } + }); + + // 绑定设置事件(所有其他设置项) + $('#amily2_enabled').on('change', function() { + if (!window.pluginAuthStatus.authorized) return; + + extension_settings[extensionName].enabled = $(this).prop('checked'); + saveSettings(); + }); + + $('#amily2_api_url').on('input', function() { + extension_settings[extensionName].apiUrl = String($(this).val()); + saveSettings(); + }); + + $('#amily2_api_key').on('input', function() { + extension_settings[extensionName].apiKey = String($(this).val()); + saveSettings(); + }); + + $('#amily2_max_tokens').on('input', function() { + extension_settings[extensionName].maxTokens = parseInt(String($(this).val())); + $('#amily2_max_tokens_value').text(extension_settings[extensionName].maxTokens); + saveSettings(); + }); + + $('#amily2_temperature').on('input', function() { + extension_settings[extensionName].temperature = parseFloat(String($(this).val())); + $('#amily2_temperature_value').text(extension_settings[extensionName].temperature); + saveSettings(); + }); + + $('#amily2_context_messages').on('input', function() { + const newValue = parseInt(String($(this).val()), 10); + extension_settings[extensionName].contextMessages = newValue; + $('#amily2_context_messages_value').text(newValue); + console.log('[聊天回复检查器] 上下文消息数量已更新为:', newValue); + saveSettings(); + }); + + // 新增主要提示词事件绑定 + $('#amily2_main_prompt').on('input', function() { + extension_settings[extensionName].mainPrompt = $(this).val(); + saveSettings(); + }); + + // 系统提示词事件绑定 + $('#amily2_system_prompt').on('input', function() { + extension_settings[extensionName].systemPrompt = $(this).val(); + saveSettings(); + }); + + // 新增开关状态更新 + $('#amily2_show_toast').on('change', function() { + extension_settings[extensionName].showOptimizationToast = $(this).prop('checked'); + saveSettings(); + + // 如果重新开启通知,清除抑制状态 + if ($(this).prop('checked')) { + extension_settings[extensionName].suppressToast = false; + saveSettings(); + } + }); + + // 新增重置开关 + $('#amily2_reset_toast').on('click', function() { + extension_settings[extensionName].showOptimizationToast = true; + extension_settings[extensionName].suppressToast = false; + saveSettings(); + toastr.success('通知设置已重置', 'Amily2号'); + $('#amily2_show_toast').prop('checked', true); + }); + + // 绑定测试按钮 + $('#amily2_test').on('click', checkCommand); + $('#amily2_fix_now').on('click', fixCommand); + + // 监听消息生成完成但未渲染的事件 + if (!window.amily2EventsRegistered) { + eventSource.on(event_types.MESSAGE_RECEIVED, onMessageReceived); + eventSource.on(event_types.IMPERSONATE_READY, onMessageReceived); + window.amily2EventsRegistered = true; + console.log('Amily2消息事件监听已注册'); + } + + // 注册斜杆命令 + SlashCommand.registerCommand(SlashCommand.fromProps({ + name: 'check-reply', + callback: checkCommand, + helpString: '检查最新的AI回复是否有问题', + })); + + SlashCommand.registerCommand(SlashCommand.fromProps({ + name: 'fix-reply', + callback: fixCommand, + helpString: '修复最新的AI回复中的问题', + })); + + SlashCommand.registerCommand(SlashCommand.fromProps({ + name: 'test-reply-checker', + callback: testReplyChecker, + helpString: '测试聊天回复检查器功能', + })); + + // 更新UI + updateUI(); + console.log('Amily2号优化助手已加载'); +}); + +// ============= 添加华丽CSS样式 ============= +function addAuthStyles() { + const style = document.createElement('style'); + style.textContent = ` + .flex-container { + display: flex; + gap: 10px; + margin-bottom: 10px; + align-items: center; + } + + #amily2_model { + flex: 1; + height: 42px; + padding: 0 15px; + background: rgba(50, 50, 75, 0.5); + border: 1px solid rgba(255,255,255,0.15); + color: white; + border-radius: 8px; + font-size: 0.95rem; + appearance: auto; + outline: none; + transition: all 0.3s; + } + + #amily2_model:focus { + border-color: #4CAF50; + box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.3); + } + + #amily2_refresh_models { + height: 42px; + padding: 0 15px; + display: flex; + align-items: center; + background: linear-gradient(to right, #4CAF50, #8BC34A); + color: white; + border: none; + border-radius: 8px; + cursor: pointer; + font-weight: 500; + font-size: 14px; + transition: all 0.3s; + justify-content: center; + } + + #amily2_refresh_models:hover { + background: linear-gradient(to right, #43A047, #7CB342); + } + + #amily2_refresh_models:disabled { + background: #9E9E9E; + cursor: not-allowed; + opacity: 0.7; + } + + /* 按钮脉冲动画 */ + .pulse { + animation: pulseAnimation 0.6s ease; + } + + @keyframes pulseAnimation { + 0% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.7); } + 70% { box-shadow: 0 0 0 10px rgba(76, 175, 80, 0); } + 100% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0); } + } + + /* 旋转动画 */ + .fa-spinner { + animation: spin 1.5s linear infinite; + } + + @keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } + } + + #amily2_model_notes { + margin-top: 8px; + font-size: 0.85em; + color: #aaa; + min-height: 1.2em; + } + + #auth_panel { + background: linear-gradient(135deg, #1a237e, #4a148c); + padding: 20px; + border-radius: 12px; + margin-bottom: 20px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); + color: #ffffff; + position: relative; + overflow: hidden; + transform: perspective(1000px) rotateX(5deg); + transition: all 0.5s ease; + } + + #auth_panel:before { + content: ''; + position: absolute; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: radial-gradient(circle at center, rgba(255,255,255,0.1) 0%, transparent 70%); + animation: rotate 20s linear infinite; + } + + .auth-header { + position: relative; + z-index: 2; + text-align: center; + margin-bottom: 20px; + } + .auth-title { + font-size: 1.8rem; + font-weight: 700; + margin-bottom: 5px; + background: linear-gradient(to right, #ff9800, #ff5722); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + text-shadow: 0 2px 5px rgba(0,0,0,0.2); + } + .auth-subtitle { + font-size: 1rem; + color: #e0e0e0; + margin-bottom: 15px; + } + .auth-code-input { + position: relative; + z-index: 2; + display: flex; + gap: 10px; + margin-bottom: 15px; + } + #auth_input { + flex: 1; + padding: 15px; + border-radius: 8px; + background: rgba(255, 255, 255, 0.15); + border: 1px solid rgba(255, 255, 255, 0.2); + color: white; + font-size: 1.1rem; + backdrop-filter: blur(5px); + box-shadow: 0 4px 10px rgba(0,0,0,0.1); + transition: all 0.3s ease; + } + + #auth_input:focus { + background: rgba(255, 255, 255, 0.25); + border-color: #ff9800; + outline: none; + box-shadow: 0 4px 15px rgba(255,152,0,0.3); + } + #auth_submit { + background: linear-gradient(to right, #ff9800, #ff5722); + color: white; + border: none; + border-radius: 8px; + padding: 0 25px; + cursor: pointer; + font-weight: 600; + box-shadow: 0 4px 10px rgba(255,152,0,0.4); + transition: all 0.3s ease; + } + + #auth_submit:hover { + transform: translateY(-2px); + box-shadow: 0 6px 15px rgba(255,152,0,0.5); + } + + .auth-footer { + position: relative; + z-index: 2; + text-align: center; + margin-top: 15px; + font-size: 0.9rem; + color: #bdbdbd; + } + + .auth-status { + background: rgba(0, 0, 0, 0.3); + padding: 8px 15px; + border-radius: 6px; + font-weight: 500; + margin-bottom: 15px; + display: flex; + align-items: center; + gap: 10px; + } + + .valid { + color: #76ff03; + border-left: 3px solid #76ff03; + } + + .expired { + color: #ff5252; + border-left: 3px solid #ff5252; + } + + .plugin-features { + display: none; + background: rgba(30,30,46,0.8); + backdrop-filter: blur(10px); + border-radius: 12px; + padding: 20px; + box-shadow: 0 10px 25px rgba(0,0,0,0.3); + margin-bottom: 20px; + } + + @keyframes rotate { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } + } + `; + document.head.appendChild(style); +} diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..50e200a --- /dev/null +++ b/manifest.json @@ -0,0 +1,13 @@ +{ + "display_name": "Amily2号聊天优化助手", + "loading_order": 100, + "requires": [], + "optional": [], + "js": "index.js", + "css": "style.css", + "author": "Wx-2025", + "version": "2.0.1", + "homePage": "https://github.com/Wx-2025/sillytavern.git", + "description": "自动检查和修复AI聊天回复中的重复内容和抢话问题", + "minSillyTavernVersion": "1.10.0" +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..34ba4ab --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "ST-Amily2-Chat-Optimisation", + "version": "2.0.1", + "description": "Amily2's advanced chat optimization engine", + "main": "index.js", + "scripts": { + "zip": "zip -r amily2-chat-optimiser.zip . -x '*.git*' -x 'node_modules/*' -x '*.zip' -x 'package*.json'" + }, + "keywords": [ + "sillytavern", + "extension", + "chat", + "ai", + "optimization" + ], + "author": "Wx-2025", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/Wx-2025/sillytavern-chat-optimiser.git" + } +} \ No newline at end of file diff --git a/srttings.html b/srttings.html new file mode 100644 index 0000000..b848024 --- /dev/null +++ b/srttings.html @@ -0,0 +1,135 @@ +
+ +
+
+
+ Amily2号优化助手 - 授权验证 +
+
+ 解锁完整功能 享受智能优化体验 +
+
+
+ +
+ + +
+ + +
+ + +
+
+
+ Amily2号优化助手 +
+
+
+
+ + +
+ +
+ + +
+
+
+ + +
+ +
+

控制优化完成后的通知显示

+
+ +
+ +

API 配置

+ +
+ + + OpenAI兼容接口地址 +
+ +
+ + +
+ +
+
+ + +
+
+
+ +
+ + +
+ +
+ + +
+ +
+ + + 提供更多上下文提升优化准确度 +
+ +
+ +

核心提示词配置

+ +
+ + + 使用think标签包裹思考过程,content标签包裹优化结果 +
+ +
+ + + 默认系统提示词 +
+ +
+ +

操作面板

+ +
+
+ + +
+
+
+
+
+
diff --git a/style.css b/style.css new file mode 100644 index 0000000..c787b87 --- /dev/null +++ b/style.css @@ -0,0 +1,87 @@ +/* Amily2号优化助手华丽样式 */ +#extensions_settings2 #amily2_chat_optimiser { + padding: 20px; + border-radius: 16px; + background: linear-gradient(145deg, #1e1e2e, #2d2b42); +} +.amily2_settings_block { + margin: 15px 0; + padding: 15px; + border-radius: 10px; + background: rgba(255, 255, 255, 0.08); + transition: all 0.3s ease; +} +.amily2_settings_block:hover { + background: rgba(255, 255, 255, 0.12); + transform: translateY(-2px); +} + +.amily2_settings_block label { + display: block; + margin-bottom: 8px; + color: #e0e0e0; + font-weight: 500; +} + +.text_pole { + width: 100%; + padding: 12px 15px; + border-radius: 8px; + border: 1px solid rgba(255,255,255,0.15); + background: rgba(50, 50, 75, 0.5); + color: #ffffff; + font-size: 0.95rem; + transition: all 0.3s ease; +} +.text_pole:focus { + border-color: #ff9800; + box-shadow: 0 0 0 2px rgba(255,152,0,0.3); + background: rgba(65, 65, 95, 0.7); +} +.menu_button { + padding: 12px 20px; + border-radius: 8px; + background: rgba(100, 100, 150, 0.3); + color: white; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + border: none; + text-align: center; + font-size: 0.95rem; +} + +#amily2_chat_optimiser h4 { + margin: 20px 0 12px; + font-size: 1.2rem; + color: #ff9800; + display: flex; + align-items: center; + gap: 10px; + border-bottom: 1px solid rgba(255,255,255,0.1); + padding-bottom: 8px; +} +#amily2_chat_optimiser hr { + border: none; + height: 1px; + background: linear-gradient(to right, transparent, rgba(255,152,0,0.3), transparent); + margin: 20px 0; +} +.notes { + font-size: 0.85rem; + color: #a0a0c0; + display: block; + margin-top: 8px; +} + +#extensions_settings2::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: radial-gradient(circle at center, transparent 0%, rgba(0,0,0,0.6) 70%); + pointer-events: none; + z-index: -1; +} \ No newline at end of file