Model Selector for AI Uncensored (v2.2 - Header Recalc)

Select AI model, recalculates auth headers after modification. Uses unsafeWindow.

질문, 리뷰하거나, 이 스크립트를 신고하세요.
// ==UserScript==
// @name         Model Selector for AI Uncensored (v2.2 - Header Recalc)
// @namespace    http://tampermonkey.net/
// @version      2.2
// @description  Select AI model, recalculates auth headers after modification. 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.2)...');

    // --- Configuration ---
    const AVAILABLE_MODELS = [
        "deepseek-ai/DeepSeek-V3-0324", // Default from the snippet
        "hermes3-405b",
        "hermes3-8b",
        "hermes3-70b",
        "deepseek-r1-671b", // Beta model from snippet
        // Add more model identifiers as needed
    ];
    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) => {
        const timestamp = Math.floor(Date.now() / 1e3).toString();
        const bodyString = JSON.stringify(requestBodyObject);
        const payload = `${timestamp}${bodyString}`;
        const encoder = new TextEncoder();
        // WARNING: Using potentially hardcoded key found in client-side script
        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("");

            // WARNING: Using potentially hardcoded API key found in client-side script
            return {
                "X-API-Key": "62852b00cb9e44bca86f0ec7e7455dc6",
                "X-Timestamp": timestamp,
                "X-Signature": signatureHex,
                "Content-Type": "application/json",
                // Add other necessary headers from original init if needed (accept, etc.)
                // It's often safer to merge than replace completely if other headers matter
                "accept": "*/*",
                "accept-language": "en-US,en;q=0.9", // Example, might be needed
            };
        } catch (error) {
            console.error("[Model Selector] Error generating signature:", error);
            throw error; // Re-throw to indicate failure
        }
    };

    // --- 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 {
                // 1. Parse the original body
                let bodyData = JSON.parse(init.body);

                // 2. Check if modification is needed
                if (bodyData && typeof bodyData === 'object' && bodyData.hasOwnProperty('model') && bodyData.model !== selectedModel) {
                    console.log(`[Model Selector] Intercepted ${API_ENDPOINT_PATH}. Original model: ${bodyData.model}`);

                    // 3. Modify the model in the JS object
                    bodyData.model = selectedModel;
                    console.log(`[Model Selector] Modified model to: ${selectedModel}`);

                    // 4. Regenerate Auth Headers using the MODIFIED body data
                    console.log('[Model Selector] Regenerating auth headers...');
                    const newHeaders = await generateAuthHeaders(bodyData);

                    // 5. Prepare the new init object for fetch
                    const newInit = {
                        ...init, // Copy original init options (method, credentials, etc.)
                        headers: newHeaders, // Use the NEW headers
                        body: JSON.stringify(bodyData) // Use the MODIFIED body string
                    };

                    console.log('[Model Selector] Sending fetch with new headers and modified body.');
                    // 6. Call the ORIGINAL fetch with the NEW init data
                    return originalFetch(input, newInit);

                } else if (bodyData && bodyData.model === selectedModel) {
                    console.log(`[Model Selector] Intercepted ${API_ENDPOINT_PATH}. Model already matches selection (${selectedModel}). Passing through.`);
                } else {
                     console.warn('[Model Selector] Fetch body did not contain expected "model" property or modification not needed:', bodyData);
                }
            } catch (e) {
                console.error('[Model Selector] Error processing fetch interceptor:', e);
                // Fall through to original fetch if error occurs
            }
        }

        // For non-matching requests or if modification wasn't needed/failed, call the original fetch
        return originalFetch(input, init);
    };

    // --- Create Improved UI (No changes needed here) ---
    function createEnhancedUI() {
        console.log('[Model Selector] createEnhancedUI called');
        // ... (Keep the UI code from version 2.1) ...
        // Inject Styles
        GM_addStyle(`
          /* Toggle Button (Top Left Area - Shifted Right) */
            #modelSelectorToggleBtn {
                position: fixed;
                top: 16px; /* Align with header buttons vertically */
                left: 60px; /* MOVED right - Adjust value if needed */
                z-index: 9998;
                background-color: #2a2a2a;
                color: #ffffff;
                border: 1px solid #c15a17;
                border-radius: 50%;
                width: 38px;
                height: 38px;
                font-size: 20px;
                cursor: pointer;
                box-shadow: 0 2px 5px rgba(0,0,0,0.3);
                transition: background-color 0.2s, transform 0.2s;
                display: flex;
                align-items: center;
                justify-content: center;
                line-height: 1;
            }
            #modelSelectorToggleBtn:hover {
                background-color: #3a3a3a;
                transform: translateY(-1px);
            }

            /* Main Container Panel (Initially Hidden) */
            #modelSelectorContainer {
                position: fixed;
                top: 65px; /* Position below header */
                left: 15px; /* CHANGED FROM right to left */
                z-index: 9999;
                background-color: #1a1a1a;
                border: 1px solid #c15a17;
                padding: 0;
                border-radius: 8px;
                box-shadow: 0 4px 12px rgba(0,0,0,0.5);
                font-family: 'Avenir', 'Arial', sans-serif;
                color: #ffffff;
                min-width: 280px;
                display: none;
                flex-direction: column;
            }
            #modelSelectorContainer.visible {
                display: flex;
            }

            /* Panel Header */
            #modelSelectorHeader {
                background-color: #2a2a2a;
                padding: 8px 12px;
                border-bottom: 1px solid #c15a17;
                border-radius: 8px 8px 0 0;
                display: flex;
                justify-content: space-between;
                align-items: center;
                font-weight: bold;
                font-size: 14px;
            }
            #modelSelectorHeader span {
                 color: #c15a17;
            }

            /* Panel Close Button */
            #modelSelectorCloseBtn {
                background: none;
                border: none;
                color: #ffffff;
                font-size: 22px;
                font-weight: bold;
                cursor: pointer;
                padding: 0 4px;
                line-height: 1;
                opacity: 0.7;
            }
            #modelSelectorCloseBtn:hover {
                opacity: 1;
            }

            /* Panel Body */
            #modelSelectorBody {
                padding: 15px;
                display: flex;
                flex-direction: column;
                gap: 8px;
            }

            /* Label */
            #modelSelectorContainer label {
                margin: 0;
                font-weight: normal;
                color: #dddddd;
                font-size: 13px;
            }

            /* Dropdown */
            #modelSelectorDropdown {
                width: 100%;
                padding: 8px 10px;
                background-color: #333333;
                color: #ffffff;
                border: 1px solid #c15a17;
                border-radius: 5px;
                font-size: 14px;
                box-sizing: border-box;
            }
            #modelSelectorDropdown option {
                background-color: #333333;
                color: #ffffff;
                padding: 5px 8px;
            }
        `);

        // Create Toggle Button
        const toggleBtn = document.createElement('button');
        toggleBtn.id = 'modelSelectorToggleBtn';
        toggleBtn.innerHTML = '⚙️';
        toggleBtn.title = 'Toggle Model Selector';

        // Create Panel Container
        const container = document.createElement('div');
        container.id = 'modelSelectorContainer';

        // Create Panel Header
        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';
        header.appendChild(title);
        header.appendChild(closeBtn);

        // Create Panel Body
        const body = document.createElement('div');
        body.id = 'modelSelectorBody';

        // Create Label
        const label = document.createElement('label');
        label.htmlFor = 'modelSelectorDropdown';
        label.textContent = 'Active Model:';

        // Create Dropdown
        const select = document.createElement('select');
        select.id = 'modelSelectorDropdown';

        // Populate Dropdown
        AVAILABLE_MODELS.forEach(model => {
            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);
        });

        // Assemble Panel Body
        body.appendChild(label);
        body.appendChild(select);

        // Assemble Panel Container
        container.appendChild(header);
        container.appendChild(body);

        // Append elements to the actual page body
        document.body.appendChild(toggleBtn);
        document.body.appendChild(container);

        // --- Add 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}`);
            toggleBtn.style.borderColor = '#4caf50';
            setTimeout(() => { toggleBtn.style.borderColor = '#c15a17'; }, 500);
        });
    }

    // --- Initialize UI ---
     if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', createEnhancedUI);
    } else {
        createEnhancedUI();
    }

})();