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

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

目前為 2025-07-07 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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();
})();