Chatgpt Pro

多密钥支持、拖动、主题切换、卡通猫背景、最小化/隐藏、真实 Assistants 聊天,使用很方便。

// ==UserScript==
// @name         Chatgpt Pro 
// @namespace    http://tampermonkey.net/
// @version      3.2
// @description  多密钥支持、拖动、主题切换、卡通猫背景、最小化/隐藏、真实 Assistants 聊天,使用很方便。
// @author       maken
// @match        *://*/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_notification
// @grant        GM_xmlhttpRequest
// @connect      api.openai.com
// @license      MIT
// ==/UserScript==
(function () {
    'use strict';
    function safeGetValue(key, def = '') {
        if (typeof GM_getValue === 'function') return GM_getValue(key, def);
        try { return localStorage.getItem(key) ?? def; } catch { return def; }
    }
    function safeSetValue(key, val) {
        if (typeof GM_setValue === 'function') return GM_setValue(key, val);
        try { localStorage.setItem(key, val); } catch { }
    }
    const CONFIG = {
        customKey: safeGetValue('gcui_apiKey', ''),
        assistantId: safeGetValue('gcui_assistantId', 'asst_你的ID'),
        theme: safeGetValue('gcui_theme', 'pink'),
        expanded: true,
        posX: Number(safeGetValue('gcui_posX', 100)),
        posY: Number(safeGetValue('gcui_posY', 100))
    };
    const assistantState = { threadId: '' };
    const state = {
        container: null, header: null, body: null, inputArea: null,
        input: null, sendBtn: null, themeBtn: null, keyBtn: null,
        toggleBtn: null, hideBtn: null, statusIcon: null
    };
    function getCurrentApiKey() {
        return CONFIG.customKey || '';
    }
    function savePosition(x, y) {
        safeSetValue('gcui_posX', x);
        safeSetValue('gcui_posY', y);
    }
    function showMessage(sender, text) {
        const msg = document.createElement('div');
        Object.assign(msg.style, {
            marginBottom: '8px', wordBreak: 'break-word',
            backgroundColor: sender === '我' ? 'rgba(255,255,255,0.7)' : 'rgba(255,255,255,0.3)',
            padding: '6px 10px', borderRadius: '6px',
            alignSelf: sender === '我' ? 'flex-end' : 'flex-start',
            maxWidth: '80%'
        });
        msg.innerHTML = `<strong style="color:${sender === '我' ? '#d6006e' : '#333'}">${sender}:</strong> ${text}`;
        state.body.appendChild(msg);
        state.body.scrollTop = state.body.scrollHeight;
    }
    function updateStatusIcon(color, status) {
        state.statusIcon.style.backgroundColor = color;
        state.statusIcon.title = status;
    }
    async function initThread() {
        try {
            const apiKey = getCurrentApiKey();
            if (!apiKey || !CONFIG.assistantId.startsWith('asst_')) {
                GM_notification({ text: 'API Key 或 Assistant ID 无效,请检查设置。', timeout: 3000 });
                updateStatusIcon('red', '无效的 API Key 或 Assistant ID');
                return;
            }
            const headers = {
                'Authorization': `Bearer ${apiKey}`,
                'Content-Type': 'application/json',
                'OpenAI-Beta': 'assistants=v2'
            };
            // 创建新线程
const threadRes = await fetch('https://api.openai.com/v1/threads', {
    method: 'POST',
    headers,
    body: JSON.stringify({})
});
const threadData = await threadRes.json();
if (threadRes.ok) {
    assistantState.threadId = threadData.id;
    updateStatusIcon('green', '连接正常');
    console.log('成功创建线程:', assistantState.threadId);
} else {
    throw new Error(threadData.error.message);
}
        } catch (error) {
            console.error('初始化线程失败:', error);
            GM_notification({ text: `初始化线程失败: ${error.message}`, timeout: 3000 });
            updateStatusIcon('red', '初始化线程失败');
        }
    }
    async function sendMessage() {
        const inputText = state.input.value.trim();
        if (!inputText) return;

        const apiKey = getCurrentApiKey();
        if (!apiKey || !CONFIG.assistantId.startsWith('asst_')) {
            GM_notification({ text: '请设置有效的 API Key 和 Assistant ID', timeout: 2000 });
            return;
        }
        showMessage('我', inputText);
        state.input.value = '';
        state.input.focus();
        const headers = {
            'Authorization': `Bearer ${apiKey}`,
            'Content-Type': 'application/json',
            'OpenAI-Beta': 'assistants=v2'
        };
        if (!assistantState.threadId) {
            await initThread();
            if (!assistantState.threadId) return;
        }
        try {
            const messageRes = await fetch(`https://api.openai.com/v1/threads/${assistantState.threadId}/messages`, {
                method: 'POST',
                headers,
                body: JSON.stringify({ role: 'user', content: inputText })
            });
            const messageData = await messageRes.json();
            if (messageRes.ok) {
                console.log('成功发送消息: ', messageData);
            } else {
                throw new Error(messageData.error.message);
            }
            const runRes = await fetch(`https://api.openai.com/v1/threads/${assistantState.threadId}/runs`, {
                method: 'POST', headers,
                body: JSON.stringify({ assistant_id: CONFIG.assistantId })
            });
            const runData = await runRes.json();
            const runId = runData.id;

            let status = 'queued';
            while (status !== 'completed' && status !== 'failed') {
                await new Promise(r => setTimeout(r, 1500)); // 等待
                const statusRes = await fetch(`https://api.openai.com/v1/threads/${assistantState.threadId}/runs/${runId}`, { headers });
                const statusData = await statusRes.json();
                status = statusData.status;
            }
            const msgRes = await fetch(`https://api.openai.com/v1/threads/${assistantState.threadId}/messages`, { headers });
            const msgData = await msgRes.json();
            const assistantMsgs = msgData.data.filter(m => m.role === 'assistant');
            const reply = assistantMsgs[0]?.content?.[0]?.text?.value || '⚠️ 无有效回复';
            showMessage('AI', reply);
        } catch (e) {
            console.error('发送消息失败:', e);
            GM_notification({ text: `发送消息失败: ${e.message}`, timeout: 3000 });
            updateStatusIcon('red', '发送消息失败');
        }
    }
    function updateTheme() {
        const t = CONFIG.theme;
        const { container, body, inputArea, sendBtn, themeBtn } = state;
        container.style.backgroundColor = t === 'pink' ? '#ffc0d9' : '#2c2c2c';
        state.header.style.backgroundColor = t === 'pink' ? '#ff66b2' : '#444';
        body.style.color = t === 'pink' ? '#333' : '#ddd';
        if (t === 'pink') {
            body.style.backgroundImage = 'url("https://img.redocn.com/sheji/20240607/keaikatongmaosucaituAItu_13339783.jpg")';
            body.style.backgroundRepeat = 'no-repeat';
            body.style.backgroundPosition = 'bottom right';
            body.style.backgroundSize = '150px';
            body.style.backgroundColor = '#fff0f7';
            themeBtn.textContent = '🌙';
        } else {
            body.style.backgroundImage = 'none';
            body.style.backgroundColor = '#222';
            themeBtn.textContent = '☀️';
        }
        inputArea.style.backgroundColor = t === 'pink' ? '#ffd6e8' : '#333';
        sendBtn.style.backgroundColor = t === 'pink' ? '#ff66b2' : '#666';
    }
    async function buildUI() {
        const container = document.createElement('div');
        Object.assign(container.style, {
            position: 'fixed', top: CONFIG.posY + 'px', left: CONFIG.posX + 'px',
            width: '360px', height: '500px', borderRadius: '10px', zIndex: 99999,
            boxShadow: '0 0 10px rgba(0,0,0,0.3)', display: 'flex', flexDirection: 'column'
        });
        const header = document.createElement('div');
        Object.assign(header.style, {
            padding: '8px 10px', fontSize: '16px', fontWeight: 'bold', display: 'flex',
            justifyContent: 'space-between', alignItems: 'center', cursor: 'grab'
        });
        const title = document.createElement('span');
        title.textContent = 'GlobalChat Pro';
        header.appendChild(title);
        const controls = document.createElement('div');
        controls.style.display = 'flex'; controls.style.gap = '5px';
        const themeBtn = document.createElement('button');
        themeBtn.textContent = '🌙';
        themeBtn.title = '切换主题';
        themeBtn.style.cssText = 'background:#fff;border:none;padding:2px 6px;border-radius:4px;cursor:pointer;';
        themeBtn.onclick = () => {
            CONFIG.theme = CONFIG.theme === 'pink' ? 'dark' : 'pink';
            safeSetValue('gcui_theme', CONFIG.theme);
            updateTheme();
        };
        controls.appendChild(themeBtn);
        state.themeBtn = themeBtn;
        const keyBtn = document.createElement('button');
        keyBtn.textContent = '🔑';
        keyBtn.title = '设置 API Key 与 Assistant ID';
        keyBtn.style.cssText = themeBtn.style.cssText;
        keyBtn.onclick = () => {
            const key = prompt('请输入 API Key (sk-开头)', CONFIG.customKey);
            const aid = prompt('请输入 Assistant ID (asst_ 开头)', CONFIG.assistantId);
            if (key) safeSetValue('gcui_apiKey', key);
            if (aid) safeSetValue('gcui_assistantId', aid);
            CONFIG.customKey = key;
            CONFIG.assistantId = aid;
            GM_notification({ text: '密钥设置已保存', timeout: 1500 });
        };
        controls.appendChild(keyBtn);
        const statusIcon = document.createElement('div');
        statusIcon.style.width = '12px';
        statusIcon.style.height = '12px';
        statusIcon.style.borderRadius = '50%';
        statusIcon.style.backgroundColor = 'red'; // 初始为红色,表示未连接
        statusIcon.title = '未连接';
        header.appendChild(statusIcon);
        state.statusIcon = statusIcon;
        const toggleBtn = document.createElement('button');
        toggleBtn.textContent = '🗕';
        toggleBtn.title = '最小化';
        toggleBtn.style.cssText = themeBtn.style.cssText;
        toggleBtn.onclick = () => {
            const show = state.body.style.display !== 'none';
            state.body.style.display = show ? 'none' : 'flex';
            state.inputArea.style.display = show ? 'none' : 'flex';
        };
        controls.appendChild(toggleBtn);
        const hideBtn = document.createElement('button');
        hideBtn.textContent = '👁️';
        hideBtn.title = '隐藏窗口';
        hideBtn.style.cssText = themeBtn.style.cssText;
        hideBtn.onclick = () => {
            state.container.style.display = 'none';
            setTimeout(() => {
                const btn = document.createElement('button');
                btn.textContent = '📢 显示聊天';
                btn.style.cssText = 'position:fixed;bottom:10px;left:10px;z-index:999999;padding:5px 10px;border-radius:6px;background:#ff66b2;color:#fff;border:none;cursor:pointer;';
                document.body.appendChild(btn);
                btn.onclick = () => { state.container.style.display = 'flex'; btn.remove(); };
            }, 100);
        };
        controls.appendChild(hideBtn);
        header.appendChild(controls);
        container.appendChild(header);
        state.header = header;
        const body = document.createElement('div');
        Object.assign(body.style, {
            flex: '1', overflowY: 'auto', padding: '10px',
            fontSize: '14px', display: 'flex', flexDirection: 'column'
        });
        container.appendChild(body);
        state.body = body;
        const inputArea = document.createElement('div');
        Object.assign(inputArea.style, {
            display: 'flex', gap: '5px', alignItems: 'center', padding: '10px',
            borderTop: '1px solid #ccc'
        });
        const input = document.createElement('textarea');
        input.rows = 2;
        input.placeholder = '请输入消息...';
        Object.assign(input.style, {
            flex: '1', resize: 'none', padding: '5px', borderRadius: '5px', border: '1px solid #ccc'
        });
        const sendBtn = document.createElement('button');
        sendBtn.textContent = '发送';
        sendBtn.style.cssText = 'padding:6px 15px;border:none;border-radius:5px;color:#fff;background:#ff66b2;cursor:pointer;';
        inputArea.appendChild(input);
        inputArea.appendChild(sendBtn);
        container.appendChild(inputArea);
        Object.assign(state, { container, input, sendBtn, inputArea });
        sendBtn.onclick = sendMessage;
        input.addEventListener('keydown', e => {
            if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendBtn.click(); }
        });
        document.body.appendChild(container);
        updateTheme();
        // 拖动
        let dragging = false, offsetX = 0, offsetY = 0;
        header.addEventListener('mousedown', e => {
            dragging = true;
            offsetX = e.clientX - container.offsetLeft;
            offsetY = e.clientY - container.offsetTop;
            header.style.cursor = 'grabbing';
        });
        document.addEventListener('mouseup', () => {
            if (dragging) {
                dragging = false;
                header.style.cursor = 'grab';
                savePosition(container.offsetLeft, container.offsetTop);
            }
        });
        document.addEventListener('mousemove', e => {
            if (!dragging) return;
            const x = e.clientX - offsetX, y = e.clientY - offsetY;
            container.style.left = Math.max(0, Math.min(x, window.innerWidth - container.offsetWidth)) + 'px';
            container.style.top = Math.max(0, Math.min(y, window.innerHeight - container.offsetHeight)) + 'px';
        });
    }
    buildUI();
})();