您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Select AI model, recalculates auth headers. UI enhancements + status indicator. Uses unsafeWindow.
当前为
// ==UserScript== // @name Model Selector for AI Uncensored (v2.4 - Status Indicator) // @namespace http://tampermonkey.net/ // @version 2.4 // @description Select AI model, recalculates auth headers. UI enhancements + status indicator. Uses unsafeWindow. // @author saros // @match https://www.aiuncensored.info/* // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant unsafeWindow // @run-at document-start // ==/UserScript== (function() { 'use strict'; console.log('[Model Selector] Script starting (v2.4 - Status Indicator)...'); // --- Configuration --- const AVAILABLE_MODELS = [ "deepseek-ai/DeepSeek-V3-0324", "hermes3-405b", "hermes3-8b", "hermes3-70b", "deepseek-r1-671b", ]; const STORAGE_KEY = 'selectedChatModel_AIUncensored_v2'; const API_ENDPOINT_PATH = '/api/chat'; // --- Get the initially selected model (or default) --- let selectedModel = GM_getValue(STORAGE_KEY, AVAILABLE_MODELS[0]); // --- Replicated Header Generation Function (based on site's tF) --- const generateAuthHeaders = async (requestBodyObject) => { // ... (Header generation logic remains unchanged) ... const timestamp = Math.floor(Date.now() / 1e3).toString(); const bodyString = JSON.stringify(requestBodyObject); const payload = `${timestamp}${bodyString}`; const encoder = new TextEncoder(); const secretKey = encoder.encode("your-super-secret-key-replace-in-production"); const dataToSign = encoder.encode(payload); try { const importedKey = await crypto.subtle.importKey("raw", secretKey, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]); const signatureBuffer = await crypto.subtle.sign("HMAC", importedKey, dataToSign); const signatureHex = Array.from(new Uint8Array(signatureBuffer)).map(b => b.toString(16).padStart(2, "0")).join(""); return { "X-API-Key": "62852b00cb9e44bca86f0ec7e7455dc6", "X-Timestamp": timestamp, "X-Signature": signatureHex, "Content-Type": "application/json", "accept": "*/*", "accept-language": "en-US,en;q=0.9", }; } catch (error) { console.error("[Model Selector] Error generating signature:", error); throw error; } }; // --- Function to Update Status Display (accessible within IIFE) --- function updateStatusDisplay(type, messageContent = '') { const indicator = document.getElementById('modelStatusIndicator'); if (!indicator) return; let messagePrefix = ''; indicator.className = 'status-indicator-base'; // Base class const modelName = messageContent.includes('/') ? messageContent.split('/')[1] : messageContent; switch (type) { case 'success': messagePrefix = 'Using: '; indicator.classList.add('status-success'); indicator.textContent = messagePrefix + modelName; break; case 'no-change': messagePrefix = 'Already: '; indicator.classList.add('status-no-change'); indicator.textContent = messagePrefix + modelName; break; case 'warning': messagePrefix = 'Warning: '; indicator.classList.add('status-warning'); indicator.textContent = messagePrefix + (messageContent || 'Check console.'); break; case 'error': messagePrefix = 'Error: '; indicator.classList.add('status-error'); indicator.textContent = messagePrefix + (messageContent || 'Override failed. Check console.'); break; case 'selected': messagePrefix = 'Selected: '; indicator.classList.add('status-selected'); indicator.textContent = messagePrefix + modelName + ". Awaiting API call."; break; case 'idle': default: indicator.classList.add('status-idle'); indicator.textContent = messageContent || 'Panel active. Status updates after API usage.'; break; } } // --- Intercept Fetch using unsafeWindow --- const originalFetch = unsafeWindow.fetch; unsafeWindow.fetch = async function(input, init) { const url = (input instanceof Request) ? input.url : input; const method = ((init && init.method) || (input instanceof Request && input.method) || 'GET').toUpperCase(); if (url.endsWith(API_ENDPOINT_PATH) && method === 'POST' && init && init.body) { try { let bodyData = JSON.parse(init.body); if (bodyData && typeof bodyData === 'object' && bodyData.hasOwnProperty('model')) { if (bodyData.model !== selectedModel) { console.log(`[Model Selector] Intercepted ${API_ENDPOINT_PATH}. Original model: ${bodyData.model}`); bodyData.model = selectedModel; console.log(`[Model Selector] Modified model to: ${selectedModel}`); console.log('[Model Selector] Regenerating auth headers...'); const newHeaders = await generateAuthHeaders(bodyData); updateStatusDisplay('success', selectedModel); // Update status: SUCCESS const newInit = { ...init, headers: newHeaders, body: JSON.stringify(bodyData) }; console.log('[Model Selector] Sending fetch with new headers and modified body.'); return originalFetch(input, newInit); } else { console.log(`[Model Selector] Intercepted ${API_ENDPOINT_PATH}. Model already matches selection (${selectedModel}). Passing through.`); updateStatusDisplay('no-change', selectedModel); // Update status: NO CHANGE } } else { console.warn('[Model Selector] Fetch body did not contain expected "model" property:', bodyData); updateStatusDisplay('warning', 'Payload issue'); // Update status: WARNING } } catch (e) { console.error('[Model Selector] Error processing fetch interceptor:', e); updateStatusDisplay('error', 'Processing error'); // Update status: ERROR } } return originalFetch(input, init); }; // --- Create Improved UI --- function createEnhancedUI() { console.log('[Model Selector] createEnhancedUI called'); GM_addStyle(` /* ... (all previous styles for toggle button, container, header, body, label, dropdown remain the same) ... */ #modelSelectorToggleBtn { position: fixed; top: 16px; left: 60px; z-index: 9998; background-color: #2a2a2a; color: #e0e0e0; border: 1px solid #c15a17; border-radius: 50%; width: 40px; height: 40px; font-size: 22px; cursor: pointer; box-shadow: 0 2px 8px rgba(0,0,0,0.35); transition: background-color 0.2s, transform 0.2s, border-color 0.3s ease; display: flex; align-items: center; justify-content: center; line-height: 1; } #modelSelectorToggleBtn:hover { background-color: #383838; transform: translateY(-1px) scale(1.05); } #modelSelectorContainer { position: fixed; top: 70px; left: 15px; z-index: 9999; background-color: #1e1e1e; border: 1px solid #c15a17; padding: 0; border-radius: 10px; box-shadow: 0 5px 15px rgba(0,0,0,0.6); font-family: 'Segoe UI', 'Avenir', 'Arial', sans-serif; color: #e0e0e0; min-width: 290px; display: flex; flex-direction: column; opacity: 0; transform: translateY(-15px) scale(0.98); visibility: hidden; transition: opacity 0.25s ease-out, transform 0.25s ease-out, visibility 0s linear 0.25s; } #modelSelectorContainer.visible { opacity: 1; transform: translateY(0) scale(1); visibility: visible; transition-delay: 0s; } #modelSelectorHeader { background-color: #282828; padding: 10px 15px; border-bottom: 1px solid #c15a17; border-radius: 9px 9px 0 0; display: flex; justify-content: space-between; align-items: center; font-weight: 600; font-size: 15px; } #modelSelectorHeader span { color: #c15a17; font-weight: 700; } #modelSelectorCloseBtn { background: none; border: none; color: #b0b0b0; font-size: 26px; font-weight: bold; cursor: pointer; padding: 0 5px; line-height: 1; opacity: 0.7; transition: opacity 0.2s, color 0.2s; } #modelSelectorCloseBtn:hover { opacity: 1; color: #ffffff; } #modelSelectorBody { padding: 18px; display: flex; flex-direction: column; gap: 10px; } #modelSelectorContainer label { margin: 0 0 2px 0; font-weight: 500; color: #cccccc; font-size: 13.5px; } #modelSelectorDropdown { width: 100%; padding: 10px 12px; background-color: #2c2c2c; color: #e0e0e0; border: 1px solid #555; border-radius: 6px; font-size: 14px; box-sizing: border-box; transition: border-color 0.3s ease, background-color 0.3s ease, box-shadow 0.3s ease; } #modelSelectorDropdown:hover { border-color: #777; } #modelSelectorDropdown:focus { border-color: #c15a17; box-shadow: 0 0 0 2px rgba(193, 90, 23, 0.3); outline: none; } #modelSelectorDropdown option { background-color: #2c2c2c; color: #e0e0e0; padding: 8px 10px; } /* --- NEW: Status Indicator Styles --- */ .status-indicator-base { /* Renamed from #modelStatusIndicator for more general class use */ padding: 8px 10px; margin-top: 12px; /* Increased margin a bit */ border-radius: 5px; /* Slightly more rounded */ font-size: 12.5px; /* Slightly larger */ text-align: center; transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease; border: 1px solid transparent; /* Base border */ line-height: 1.4; } .status-indicator-base.status-idle, .status-indicator-base.status-selected { background-color: #303030; color: #b0b0b0; border-color: #404040; } .status-indicator-base.status-success { background-color: #203c20; /* Darker, less saturated green */ color: #90c090; /* Softer green text */ border-color: #305c30; } .status-indicator-base.status-no-change { background-color: #22303f; /* Darker, less saturated blue */ color: #8ab0d0; /* Softer blue text */ border-color: #32506f; } .status-indicator-base.status-warning { background-color: #3f3f22; /* Darker, less saturated yellow */ color: #c0c08a; /* Softer yellow text */ border-color: #5f5f32; } .status-indicator-base.status-error { background-color: #3f2222; /* Darker, less saturated red */ color: #d08a8a; /* Softer red text */ border-color: #5f3232; } `); const toggleBtn = document.createElement('button'); /* ... */ toggleBtn.id = 'modelSelectorToggleBtn'; toggleBtn.innerHTML = '⚙️'; toggleBtn.title = 'Toggle Model Selector'; const container = document.createElement('div'); container.id = 'modelSelectorContainer'; const header = document.createElement('div'); header.id = 'modelSelectorHeader'; const title = document.createElement('span'); title.textContent = 'Model Settings'; const closeBtn = document.createElement('button'); closeBtn.id = 'modelSelectorCloseBtn'; closeBtn.innerHTML = '×'; closeBtn.title = 'Close Settings'; header.appendChild(title); header.appendChild(closeBtn); const bodyEl = document.createElement('div'); bodyEl.id = 'modelSelectorBody'; // Renamed to avoid conflict with global 'body' const label = document.createElement('label'); label.htmlFor = 'modelSelectorDropdown'; label.textContent = 'Active AI Model:'; const select = document.createElement('select'); select.id = 'modelSelectorDropdown'; AVAILABLE_MODELS.forEach(model => { /* ... option population ... */ const option = document.createElement('option'); option.value = model; option.textContent = model.includes('/') ? model.split('/')[1] : model; option.title = model; if (model === selectedModel) { option.selected = true; } select.appendChild(option); }); bodyEl.appendChild(label); bodyEl.appendChild(select); // --- Create Status Indicator Element --- const statusIndicator = document.createElement('div'); statusIndicator.id = 'modelStatusIndicator'; // Used by getElementById // Initial status set after appending, or by calling updateStatusDisplay bodyEl.appendChild(statusIndicator); // Add it to the panel body container.appendChild(header); container.appendChild(bodyEl); // Use bodyEl function appendElements() { /* ... (same appendElements logic) ... */ if (document.body) { document.body.appendChild(toggleBtn); document.body.appendChild(container); updateStatusDisplay('idle'); // Set initial status message once UI is in DOM } else { window.addEventListener('DOMContentLoaded', () => { document.body.appendChild(toggleBtn); document.body.appendChild(container); updateStatusDisplay('idle'); // Set initial status message });}} if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', appendElements); } else { appendElements(); } // --- Event Listeners --- toggleBtn.addEventListener('click', () => { container.classList.toggle('visible'); }); closeBtn.addEventListener('click', () => { container.classList.remove('visible'); }); select.addEventListener('change', (event) => { selectedModel = event.target.value; GM_setValue(STORAGE_KEY, selectedModel); console.log(`[Model Selector] Model selection changed to: ${selectedModel}`); updateStatusDisplay('selected', selectedModel); // Update status for selection toggleBtn.style.borderColor = '#4caf50'; setTimeout(() => { toggleBtn.style.borderColor = '#c15a17'; }, 600); select.style.transition = 'none'; select.style.borderColor = '#4caf50'; select.style.backgroundColor = '#3a4a3a'; setTimeout(() => { select.style.transition = 'border-color 0.3s ease, background-color 0.3s ease, box-shadow 0.3s ease'; select.style.borderColor = '#555'; select.style.backgroundColor = '#2c2c2c'; }, 500); }); } // --- Initialize UI --- if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', createEnhancedUI); } else { createEnhancedUI(); } })();