GlobalChatgpt Pro Cartoon Cat Background + Voice (修正版)

多密钥支持、拖动、主题切换、卡通猫背景、最小化/隐藏、语音输入+朗读、真实 Assistants 聊天,连接状态显示

当前为 2025-07-07 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         GlobalChatgpt Pro Cartoon Cat Background + Voice (修正版)
// @namespace    http://tampermonkey.net/
// @version      1.14
// @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'),
        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, voiceBtn: 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;
    }

    async function initThread() {
        const apiKey = getCurrentApiKey();
        if (!apiKey || !CONFIG.assistantId.startsWith('asst_')) {
            updateStatusIcon('red', '无效的 API Key 或 Assistant ID');
            return false;
        }
        const headers = {
            'Authorization': `Bearer ${apiKey}`,
            'Content-Type': 'application/json',
            'OpenAI-Beta': 'assistants=v2'
        };
        try {
            const threadRes = await fetch('https://api.openai.com/v1/threads', {
                method: 'POST',
                headers,
                body: JSON.stringify({ json: {} })
            });
            const threadData = await threadRes.json();
            if (threadData.error || !threadData.id) {
                console.error('线程创建失败:', threadData.error || threadData);
                updateStatusIcon('red', '线程创建失败: ' + (threadData.error?.message || '未知错误'));
                return false;
            }
            assistantState.threadId = threadData.id;
            updateStatusIcon('green', '连接正常');
            return true;
        } catch (e) {
            console.error('线程创建异常:', e);
            updateStatusIcon('red', '线程创建异常,检查网络');
            return false;
        }
    }

    function updateStatusIcon(color, title) {
        if (state.statusIcon) {
            state.statusIcon.style.backgroundColor = color;
            state.statusIcon.title = title;
        }
    }

    async function sendMessage() {
        const inputText = state.input.value.trim();
        if (!inputText) return;

        // 确保线程有效,线程不存在则初始化
        if (!assistantState.threadId) {
            const ok = await initThread();
            if (!ok) {
                GM_notification({ text: '初始化线程失败,无法发送消息', timeout: 3000 });
                return;
            }
        }

        showMessage('我', inputText);
        state.input.value = '';
        state.input.focus();

        const apiKey = getCurrentApiKey();
        if (!apiKey || !CONFIG.assistantId.startsWith('asst_')) {
            GM_notification({ text: '请设置有效的 API Key 和 Assistant ID', timeout: 2000 });
            updateStatusIcon('red', '无效的 API Key 或 Assistant ID');
            return;
        }

        const headers = {
            'Authorization': `Bearer ${apiKey}`,
            'Content-Type': 'application/json',
            'OpenAI-Beta': 'assistants=v2'
        };

        // 发送消息
        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 (messageData.error) {
                console.error('发送消息失败:', messageData.error);
                GM_notification({ text: '发送消息失败: ' + messageData.error.message, timeout: 3000 });
                updateStatusIcon('red', '发送消息失败');
                return;
            }
            GM_notification({ text: '消息已发送', timeout: 1500 });
        } catch (e) {
            console.error('发送消息异常:', e);
            GM_notification({ text: '发送消息异常,检查网络', timeout: 3000 });
            updateStatusIcon('red', '发送消息异常');
            return;
        }

        // 触发执行
        let runId = '';
        try {
            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();
            if (runData.error || !runData.id) {
                console.error('触发执行失败:', runData.error || runData);
                GM_notification({ text: '触发执行失败: ' + (runData.error?.message || '未知错误'), timeout: 3000 });
                updateStatusIcon('red', '触发执行失败');
                return;
            }
            runId = runData.id;
        } catch (e) {
            console.error('触发执行异常:', e);
            GM_notification({ text: '触发执行异常,检查网络', timeout: 3000 });
            updateStatusIcon('red', '触发执行异常');
            return;
        }

        // 轮询获取回复
        let status = 'queued';
        while (status !== 'completed' && status !== 'failed') {
            await new Promise(r => setTimeout(r, 1500));
            try {
                const statusRes = await fetch(`https://api.openai.com/v1/threads/${assistantState.threadId}/runs/${runId}`, { headers });
                const statusData = await statusRes.json();
                if (statusData.error) {
                    console.error('查询状态失败:', statusData.error);
                    GM_notification({ text: '查询状态失败: ' + statusData.error.message, timeout: 3000 });
                    updateStatusIcon('red', '查询状态失败');
                    return;
                }
                status = statusData.status;
            } catch (e) {
                console.error('查询状态异常:', e);
                GM_notification({ text: '查询状态异常,检查网络', timeout: 3000 });
                updateStatusIcon('red', '查询状态异常');
                return;
            }
        }

        // 获取回复消息
        try {
            const msgRes = await fetch(`https://api.openai.com/v1/threads/${assistantState.threadId}/messages`, { headers });
            const msgData = await msgRes.json();
            if (msgData.error) {
                console.error('获取回复失败:', msgData.error);
                GM_notification({ text: '获取回复失败: ' + msgData.error.message, timeout: 3000 });
                updateStatusIcon('red', '获取回复失败');
                return;
            }
            const assistantMsgs = msgData.data.filter(m => m.role === 'assistant');
            const reply = assistantMsgs.length > 0 ? assistantMsgs[assistantMsgs.length - 1].content[0].text.value : '⚠️ 无有效回复';
            showMessage('AI', reply);
            updateStatusIcon('green', '连接正常');
        } catch (e) {
            console.error('获取回复异常:', e);
            GM_notification({ text: '获取回复异常,检查网络', timeout: 3000 });
            updateStatusIcon('red', '获取回复异常');
        }
    }

    function speakText(text) {
        if (!window.speechSynthesis) return;
        const msg = new SpeechSynthesisUtterance(text);
        msg.lang = 'zh-CN';
        window.speechSynthesis.speak(msg);
    }

    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 statusIcon = document.createElement('div');
        statusIcon.style.width = '12px';
        statusIcon.style.height = '12px';
        statusIcon.style.borderRadius = '50%';
        statusIcon.style.backgroundColor = 'red';
        statusIcon.title = '连接状态未知';
        controls.appendChild(statusIcon);
        state.statusIcon = statusIcon;

        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 });
            // 重新初始化线程及状态
            assistantState.threadId = '';
            initThread();
        };
        controls.appendChild(keyBtn);

        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';
        });

        // 页面加载完成后立即初始化线程检测连接状态
        initThread();
    }

    buildUI();
})();