DeepSeek API聊天

在任意网站上添加 DeepSeek 聊天窗口:窗口默认隐藏,仅通过快捷键 Ctrl+Alt+D 显示;通过官方API 输入,支持聊天、历史记录管理、新对话、模型切换与记忆功能。API 输出支持 Markdown 渲染。

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

// ==UserScript==
// @name         DeepSeek API聊天
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  在任意网站上添加 DeepSeek 聊天窗口:窗口默认隐藏,仅通过快捷键 Ctrl+Alt+D 显示;通过官方API 输入,支持聊天、历史记录管理、新对话、模型切换与记忆功能。API 输出支持 Markdown 渲染。
// @author       AMT
// @match        *://*/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @grant        GM_setClipboard
// @require https://cdn.jsdelivr.net/npm/[email protected]/marked.min.js
// @connect      *.deepseek.com
// @connect      deepseek.com
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';
    // 检查跨域请求权限
    if (typeof GM_xmlhttpRequest === 'undefined') {
        alert('请启用Tampermonkey的"允许访问跨域URL"权限');
        return;
    }

    /***************** 创建Shadow DOM容器,确保UI样式隔离 *****************/
    const host = document.createElement('div');
    host.id = 'deepseek-chat-host';
    host.style.all = 'initial'; // 重置所有样式
    host.style.position = 'fixed';
    host.style.right = '0';
    host.style.top = '50%';
    host.style.transform = 'translateY(-50%)';
    host.style.zIndex = '9999';
    // 默认隐藏整个界面
    host.style.display = 'none';
    document.body.appendChild(host);

    const shadow = host.attachShadow({ mode: 'open' });

    // 添加全局样式到 shadow root(仅影响本UI内)
    const style = document.createElement('style');
    style.textContent = `
    /* 全局字体重置为系统默认 */
    * {
        font-family: inherit !important;
        font-size: 18px;
        line-height: 1.5;
    }
    /* 全局按钮字体设置 */
    button {
        font-size: 0.8em !important;
        font-family: inherit !important;
        font-weight: normal !important;
        line-height: normal !important;
    }
    /* 统一滚动条样式 */
    ::-webkit-scrollbar {
        width: 12px;
        height: 12px;
    }
    /* 输出框滚动条 */
    .conversation-div-style::-webkit-scrollbar-track {
        background: transparent;
        margin: 10px 0;
    }
    /* 输入框滚动条 */
    textarea::-webkit-scrollbar-track {
        background: transparent !important;
        margin: 10px 0;
    }
    textarea {
        font-size: 1em !important;
        font-family: inherit !important;
        line-height: normal !important;
    }
    ::-webkit-scrollbar-corner {
        background: #333;
    }
    ::-webkit-scrollbar-thumb {
        background-color: #555;
        border-radius: 10px;
        border: 2px solid transparent;
        background-clip: content-box;
    }
    textarea:focus, input:focus {
        outline: none !important;
        border: 1px solid #4682B4 !important;
    }
    p {
        margin: 0.3em 0 !important;
    }
    pre {
        white-space: pre !important;
        background-color: #222 !important;
        color: #eee !important;
        padding: 0.8em !important;
        border-radius: 8px !important;
        overflow-x: auto !important;
        font-size: 0.9em !important;
        margin: 0.5em 0 !important;
    }
    code {
        background-color: transparent !important;
        padding: 0 !important;
        font-family: monospace !important;
    }
    .code-block-wrapper:hover button {
        opacity: 1 !important;
    }
    .code-block-wrapper button:hover {
        background-color: #666 !important;
    }
    `;
    shadow.appendChild(style);

    /***************** 全局变量与存储 *****************/
    let apiKey = GM_getValue('deepseek_api', '');
    let currentModel = "deepseek-chat";
    let isStreaming = false;
    let streamCancel = false;
    let autoScrollEnabled = true;
    const modelDisplay = {
        "deepseek-chat": "Chat",
        "deepseek-reasoner": "Reasoner"
    };
    let memoryEnabled = false;
    let chatHistory = JSON.parse(GM_getValue('deepseek_history', '[]'));
    let currentSession = [];
    let currentSessionId = Date.now();
    let isSending = false;

    /***************** 工具函数 *****************/
    function safeCopyToClipboard(text, button) {
    try {
        if (typeof GM_setClipboard !== 'undefined') {
            GM_setClipboard(text);
        } else if (navigator.clipboard?.writeText) {
            navigator.clipboard.writeText(text);
        } else {
            const textarea = document.createElement('textarea');
            textarea.value = text;
            textarea.style.position = 'fixed';
            textarea.style.top = '-9999px';
            document.body.appendChild(textarea);
            textarea.select();
            document.execCommand('copy');
            document.body.removeChild(textarea);
        }

        if (button) {
            button.textContent = '✅';
            button.style.backgroundColor = '#4CAF50';
            // ✅ 强制刷新 Shadow DOM 以更新图标
            requestAnimationFrame(() => {
                setTimeout(() => {
                    button.textContent = '📋';
                    button.style.backgroundColor = '#444';
                }, 1000);
            });
        }
    } catch (e) {
        alert('复制失败,请手动复制');
        console.error('复制失败:', e);
    }
}
    function renderMarkdown(text) {
    const parsed = marked.parse(text.trim());
    const tempDiv = document.createElement('div');
    tempDiv.innerHTML = parsed;

    const codeBlocks = tempDiv.querySelectorAll('pre code');
    codeBlocks.forEach(code => {
        const wrapper = document.createElement('div');
        wrapper.className = 'code-block-wrapper';
        wrapper.style.position = 'relative';

        const pre = code.parentElement;
        if (pre && pre.tagName === 'PRE') {
            pre.style.margin = '0';
            pre.parentElement.replaceChild(wrapper, pre);
            wrapper.appendChild(pre);

            const copyBtn = document.createElement('button');
            copyBtn.textContent = '📋';
            copyBtn.title = '复制代码';
            copyBtn.style.cssText = `
                position: absolute;
                top: 6px;
                right: 6px;
                font-size: 0.8em;
                background: #444;
                color: white;
                border: none;
                border-radius: 5px;
                padding: 2px 6px;
                cursor: pointer;
                opacity: 0.7;
                z-index: 10;
                transition: background 0.3s, opacity 0.3s;
            `;
            copyBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                safeCopyToClipboard(code.textContent, copyBtn);
            });

            wrapper.appendChild(copyBtn);
        }
    });

    return tempDiv;
}

    function saveCurrentSession() {
        if (currentSession.length > 0) {
            let idx = chatHistory.findIndex(s => s.id === currentSessionId);
            const sessionRecord = {
                id: currentSessionId,
                messages: currentSession,
                timestamp: new Date().toLocaleString()
            };
            if (idx === -1) {
                chatHistory.push(sessionRecord);
            } else {
                chatHistory[idx] = sessionRecord;
            }
            GM_setValue('deepseek_history', JSON.stringify(chatHistory));
        }
    }

    function loadSession(sessionRecord) {
        currentSession = sessionRecord.messages;
        currentSessionId = sessionRecord.id;
        renderConversation();
    }

    /***************** 跨域请求函数 *****************/
    function gmFetch(url, options) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: options.method,
                url: url,
                headers: options.headers,
                data: options.body,
                onload: function(response) {
                    if (response.status < 200 || response.status >= 300) {
                        const error = new Error(`HTTP Error ${response.status}`);
                        error.response = response;
                        return reject(error);
                    }
                    response.json = function() {
                        try {
                            return JSON.parse(response.responseText);
                        } catch (e) {
                            console.error('JSON解析失败:', e);
                            return null;
                        }
                    };
                    resolve(response);
                },
                onerror: function(error) {
                    error.message = `网络错误: ${error.error || error.statusText}`;
                    reject(error);
                }
            });
        });
    }

    /***************** 主窗口与UI *****************/
    const chatContainer = document.createElement('div');
    chatContainer.id = 'deepseek-chat-ui';
    chatContainer.style.all = 'initial';
    chatContainer.style.fontFamily = 'Arial, sans-serif';
    chatContainer.style.fontSize = '14px';
    chatContainer.style.isolation = 'isolate';
    chatContainer.style.position = 'fixed';
    chatContainer.style.right = '0';
    chatContainer.style.top = '50%';
    chatContainer.style.transform = 'translateY(-50%)';
    chatContainer.style.width = apiKey ? '35vw' : '15vw';
    chatContainer.style.height = apiKey ? '75vh' : '16.67vh';
    chatContainer.style.backgroundColor = '#333';
    chatContainer.style.borderRadius = '10px';
    chatContainer.style.boxShadow = '0 2px 8px rgba(0,0,0,0.2)';
    chatContainer.style.transition = 'opacity 0.3s, transform 0.3s';
    // 不再使用 opacity 控制显示,统一由 host.style.display 控制
    shadow.appendChild(chatContainer);

    // 阻止内部点击影响外部,同时若点击不在历史记录面板内,则关闭面板
    chatContainer.addEventListener('click', (e) => {
        e.stopPropagation();
        if(historyPanel && !historyPanel.contains(e.target)) { hideHistoryPanel(); }
    });

    const contentDiv = document.createElement('div');
    contentDiv.style.width = '100%';
    contentDiv.style.height = '100%';
    contentDiv.style.display = 'flex';
    contentDiv.style.flexDirection = 'column';
    contentDiv.style.boxSizing = 'border-box';
    contentDiv.style.color = 'white';
    contentDiv.style.padding = '1em';
    chatContainer.appendChild(contentDiv);

    /***************** 历史记录面板 *****************/
    let historyPanel;
    function showHistoryPanel() {
        if(historyPanel) return;
        historyPanel = document.createElement('div');
        historyPanel.id = 'history-panel';
        historyPanel.style.zIndex = '10000';
        historyPanel.style.position = 'absolute';
        historyPanel.style.left = '0';
        historyPanel.style.top = '0';
        historyPanel.style.height = '100%';
        historyPanel.style.width = '40%';
        historyPanel.style.backgroundColor = '#222';
        historyPanel.style.borderTopLeftRadius = '10px';
        historyPanel.style.borderBottomLeftRadius = '10px';
        historyPanel.style.overflowY = 'auto';
        historyPanel.style.padding = '0.5em';
        historyPanel.style.boxSizing = 'border-box';
        const header = document.createElement('div');
        header.style.display = 'flex';
        header.style.justifyContent = 'space-between';
        header.style.alignItems = 'center';
        header.style.marginBottom = '0.5em';
        const backBtn = document.createElement('button');
        backBtn.innerText = '返回';
        backBtn.style.fontSize = '1em';
        backBtn.style.padding = '0.2em 0.5em';
        backBtn.style.border = '1px solid white';
        backBtn.style.borderRadius = '10px';
        backBtn.style.backgroundColor = '#444';
        backBtn.style.color = 'white';
        backBtn.style.cursor = 'pointer';
        backBtn.addEventListener('click', () => { hideHistoryPanel(); });
        header.appendChild(backBtn);
        const title = document.createElement('span');
        title.style.color = 'white';
        title.innerText = '聊天历史';
        header.appendChild(title);
        const clearBtn = document.createElement('button');
        clearBtn.innerText = '清空所有';
        clearBtn.style.fontSize = '1em';
        clearBtn.style.padding = '0.2em 0.5em';
        clearBtn.style.border = '1px solid white';
        clearBtn.style.borderRadius = '10px';
        clearBtn.style.backgroundColor = '#444';
        clearBtn.style.color = 'white';
        clearBtn.style.cursor = 'pointer';
        clearBtn.addEventListener('click', () => {
            if(confirm("确定清空所有聊天记录吗?")) {
                chatHistory = [];
                GM_setValue('deepseek_history', JSON.stringify(chatHistory));
                renderHistoryPanel();
                currentSession = [];
                currentSessionId = Date.now();
                if(conversationDiv) { conversationDiv.innerHTML = ''; }
            }
        });
        header.appendChild(clearBtn);
        historyPanel.appendChild(header);
        renderHistoryPanel();
        chatContainer.appendChild(historyPanel);
    }
    function hideHistoryPanel() {
        if(historyPanel && historyPanel.parentNode) {
            chatContainer.removeChild(historyPanel);
            historyPanel = null;
        }
    }
    function renderHistoryPanel() {
        if(!historyPanel) return;
        while(historyPanel.childNodes.length > 1) {
            historyPanel.removeChild(historyPanel.lastChild);
        }
        chatHistory.forEach(session => {
            const item = document.createElement('div');
            let summary = session.timestamp;
            if(session.messages.length > 0) {
                const firstMsg = session.messages.find(m => m.role === 'user');
                if(firstMsg) {
                    summary += " - " + firstMsg.content.substring(0, 20) + "...";
                }
            }
            item.innerText = summary;
            item.style.color = 'white';
            item.style.padding = '0.3em';
            item.style.borderBottom = '1px solid #666';
            item.style.cursor = 'pointer';
            item.addEventListener('click', () => {
                loadSession(session);
                renderConversation();
                hideHistoryPanel();
            });
            item.addEventListener('contextmenu', (e) => {
                e.preventDefault();
                if(confirm("删除该聊天记录?")) {
                    if(session.id === currentSessionId) {
                        currentSession = [];
                        currentSessionId = Date.now();
                        if(conversationDiv) { conversationDiv.innerHTML = ''; }
                    }
                    chatHistory = chatHistory.filter(s => s.id !== session.id);
                    GM_setValue('deepseek_history', JSON.stringify(chatHistory));
                    renderHistoryPanel();
                }
            });
            historyPanel.appendChild(item);
        });
    }

    /***************** 对话区与输入区 *****************/
    let conversationDiv;
    let messageInput;
    let sendBtn; // 全局声明

    // 自动滚动监听:当滚动接近底部时自动恢复
    function setupAutoScroll() {
        conversationDiv.addEventListener('scroll', () => {
            if (conversationDiv.scrollTop + conversationDiv.clientHeight >= conversationDiv.scrollHeight - 10) {
                autoScrollEnabled = true;
            } else {
                autoScrollEnabled = false;
            }
        });
    }

    function renderConversation() {
        if(!conversationDiv) return;
        conversationDiv.innerHTML = '';
        currentSession.forEach(msgObj => {
            // 使用自定义Markdown渲染(不高亮)
            addChatBubble(msgObj.role === 'user', msgObj.content, false);
        });
    }

    // 流式输出结束后调用,恢复输入框和发送按钮
    function finishStreaming() {
        isStreaming = false;
        streamCancel = false;
        messageInput.disabled = false;
        sendBtn.disabled = false;
    }

    function addChatBubble(isUser, text, isStream = false) {
        // 保留原始换行和空格,避免破坏代码缩进
        const cleanedText = text.trim();

        if (isUser) {
            const bubble = document.createElement('div');
            bubble.style.cssText = `
                padding: 0.5em;
                margin: 0.5em 0;
                border-radius: 12px;
                max-width: 80%;
                word-wrap: break-word;
                white-space: pre-wrap;
                background-color: #6699CC;
                color: white;
                align-self: flex-end;
                box-shadow: 0 2px 4px rgba(0,0,0,0.1);
            `;
            bubble.textContent = cleanedText;
            conversationDiv.appendChild(bubble);
        } else {
            const streamContainer = document.createElement('div');
            streamContainer.style.cssText = `
                padding: 0.5em;
                color: #EEEEEE;
                font-size: 0.95em;
                line-height: 1.6;
                white-space: pre-wrap;
                word-break: break-word;
            `;
            if (isStream) {
                messageInput.disabled = true;
                sendBtn.disabled = true;
                let rawContent = '';
                let index = 0;
                const charArray = cleanedText.split('');
                function streamOutput() {
                    if (index < charArray.length && !streamCancel) {
                        rawContent += charArray[index++];
                        streamContainer.textContent = rawContent; // 逐字符显示,纯文本
                        if (autoScrollEnabled) {
                            conversationDiv.scrollTop = conversationDiv.scrollHeight;
                        }
                        setTimeout(streamOutput, 1000 / 60);
                    } else {
                        const renderedNode = renderMarkdown(rawContent);
                        streamContainer.innerHTML = ''; // 清空旧内容
                        streamContainer.appendChild(renderedNode);
                        finishStreaming();
                    }
                }
                isStreaming = true;
                streamOutput();
            } else {
                const renderedNode = renderMarkdown(cleanedText);
                streamContainer.appendChild(renderedNode);
            }
            conversationDiv.appendChild(streamContainer);
        }
        if (autoScrollEnabled) {
            conversationDiv.scrollTop = conversationDiv.scrollHeight;
        }
    }

    /***************** 渲染整个界面 *****************/
    function renderUI() {
        contentDiv.innerHTML = '';
        if (!apiKey) {
            const promptText = document.createElement('div');
            promptText.innerText = 'DeepSeek';
            promptText.style.textAlign = 'center';
            promptText.style.fontSize = '2em';
            promptText.style.marginBottom = '0.5em';
            contentDiv.appendChild(promptText);

            const apiInput = document.createElement('input');
            apiInput.type = 'text';
            apiInput.placeholder = '请输入api key';
            apiInput.style.width = '100%';
            apiInput.style.fontSize = '1em';
            apiInput.style.padding = '0.5em';
            apiInput.style.boxSizing = 'border-box';
            apiInput.style.borderRadius = '10px';
            apiInput.style.border = '1px solid white';
            apiInput.style.backgroundColor = '#444';
            apiInput.style.color = 'white';
            apiInput.style.marginBottom = '0.5em';
            contentDiv.appendChild(apiInput);

            const confirmBtn = document.createElement('button');
            confirmBtn.innerText = '确认';
            confirmBtn.style.width = '100%';
            confirmBtn.style.fontSize = '1em';
            confirmBtn.style.padding = '0.5em';
            confirmBtn.style.borderRadius = '10px';
            confirmBtn.style.border = '1px solid white';
            confirmBtn.style.backgroundColor = '#444';
            confirmBtn.style.color = 'white';
            confirmBtn.style.cursor = 'pointer';
            contentDiv.appendChild(confirmBtn);

            confirmBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                const value = apiInput.value.trim();
                if (value) {
                    apiKey = value;
                    GM_setValue('deepseek_api', apiKey);
                    chatContainer.style.height = '75vh';
                    chatContainer.style.width = '35vw';
                    currentSession = [];
                    currentSessionId = Date.now();
                    renderUI();
                }
            });
        } else {
            const headerDiv = document.createElement('div');
            headerDiv.style.display = 'flex';
            headerDiv.style.justifyContent = 'space-between';
            headerDiv.style.alignItems = 'center';
            headerDiv.style.marginBottom = '0.5em';
            contentDiv.appendChild(headerDiv);

            const leftHeader = document.createElement('div');
            leftHeader.style.display = 'flex';
            leftHeader.style.gap = '0.5em';
            headerDiv.appendChild(leftHeader);

            const historyBtn = document.createElement('button');
            historyBtn.innerText = '历史记录';
            historyBtn.style.fontSize = '1em';
            historyBtn.style.padding = '0.3em';
            historyBtn.style.borderRadius = '10px';
            historyBtn.style.border = '1px solid white';
            historyBtn.style.backgroundColor = '#444';
            historyBtn.style.color = 'white';
            historyBtn.style.cursor = 'pointer';
            historyBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                if(historyPanel) {
                    hideHistoryPanel();
                } else {
                    showHistoryPanel();
                }
            });
            leftHeader.appendChild(historyBtn);

            const newConvBtn = document.createElement('button');
            newConvBtn.innerText = '开启新对话';
            newConvBtn.style.fontSize = '1em';
            newConvBtn.style.padding = '0.3em';
            newConvBtn.style.borderRadius = '10px';
            newConvBtn.style.border = '1px solid white';
            newConvBtn.style.backgroundColor = '#444';
            newConvBtn.style.color = 'white';
            newConvBtn.style.cursor = 'pointer';
            newConvBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                saveCurrentSession();
                currentSession = [];
                currentSessionId = Date.now();
                if(conversationDiv) { conversationDiv.innerHTML = ''; }
            });
            leftHeader.appendChild(newConvBtn);

            const reenterBtn = document.createElement('button');
            reenterBtn.innerText = '重新输入api';
            reenterBtn.style.fontSize = '1em';
            reenterBtn.style.padding = '0.3em';
            reenterBtn.style.borderRadius = '10px';
            reenterBtn.style.border = '1px solid white';
            reenterBtn.style.backgroundColor = '#444';
            reenterBtn.style.color = 'white';
            reenterBtn.style.cursor = 'pointer';
            reenterBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                hideHistoryPanel();
                saveCurrentSession();
                apiKey = '';
                GM_setValue('deepseek_api', '');
                chatContainer.style.height = '16.67vh';
                chatContainer.style.width = '15vw';
                renderUI();
            });
            headerDiv.appendChild(reenterBtn);

            conversationDiv = document.createElement('div');
            conversationDiv.style.flex = '1';
            conversationDiv.style.overflowY = 'auto';
            conversationDiv.style.marginBottom = '0.5em';
            conversationDiv.style.padding = '0.5em';
            conversationDiv.style.boxSizing = 'border-box';
            conversationDiv.style.backgroundColor = '#333';
            conversationDiv.style.display = 'flex';
            conversationDiv.style.flexDirection = 'column';
            conversationDiv.style.gap = '0.5em';
            contentDiv.appendChild(conversationDiv);
            setupAutoScroll();
            renderConversation();

            const inputContainer = document.createElement('div');
            inputContainer.style.position = 'relative';
            inputContainer.style.width = '100%';
            inputContainer.style.boxSizing = 'border-box';
            inputContainer.style.height = '10vh';
            contentDiv.appendChild(inputContainer);

            messageInput = document.createElement('textarea');
            messageInput.placeholder = '给deepseek发送消息';
            messageInput.style.position = 'absolute';
            messageInput.style.left = '0';
            messageInput.style.right = '0';
            messageInput.style.bottom = '0';
            messageInput.style.height = '10vh';
            messageInput.style.padding = '0.5em 0.5em 3em 0.5em';
            messageInput.style.fontSize = '1.2em';
            messageInput.style.boxSizing = 'border-box';
            messageInput.style.borderRadius = '10px';
            messageInput.style.border = '1px solid white';
            messageInput.style.backgroundColor = '#444';
            messageInput.style.color = 'white';
            messageInput.style.overflowY = 'hidden';
            messageInput.style.resize = 'none';
            inputContainer.appendChild(messageInput);

            const inputOverlay = document.createElement('div');
            inputOverlay.style.position = 'absolute';
            inputOverlay.style.left = '0.5em';
            inputOverlay.style.right = '0.8em';
            inputOverlay.style.bottom = '0.07em';
            inputOverlay.style.height = '2.5em';
            inputOverlay.style.backgroundColor = '#444';
            inputOverlay.style.pointerEvents = 'none';
            inputContainer.appendChild(inputOverlay);

            // 创建模型、记忆和发送按钮
            const modelBtn = document.createElement('button');
            modelBtn.innerText = "深度思考R1";
            modelBtn.style.position = 'absolute';
            modelBtn.style.left = '0.5em';
            modelBtn.style.bottom = '0.5em';
            modelBtn.style.width = '8em';
            modelBtn.style.height = '2em';
            modelBtn.style.fontSize = '1em';
            modelBtn.style.lineHeight = '2em';
            modelBtn.style.textAlign = 'center';
            modelBtn.style.borderRadius = '10px';
            modelBtn.style.border = '1px solid white';
            modelBtn.style.backgroundColor = '#444';
            modelBtn.style.color = 'white';
            modelBtn.style.cursor = 'pointer';
            modelBtn.style.zIndex = '10';
            modelBtn.addEventListener('click', () => {
                currentModel = (currentModel === 'deepseek-chat' ? 'deepseek-reasoner' : 'deepseek-chat');
                modelBtn.style.backgroundColor = currentModel === 'deepseek-reasoner' ? '#87CEFA' : '#444';
            });
            inputContainer.appendChild(modelBtn);

            const memoryBtn = document.createElement('button');
            memoryBtn.innerText = '记忆';
            memoryBtn.style.position = 'absolute';
            memoryBtn.style.left = '9em';
            memoryBtn.style.bottom = '0.5em';
            memoryBtn.style.width = '3em';
            memoryBtn.style.height = '2em';
            memoryBtn.style.fontSize = '1em';
            memoryBtn.style.lineHeight = '2em';
            memoryBtn.style.textAlign = 'center';
            memoryBtn.style.borderRadius = '10px';
            memoryBtn.style.border = '1px solid white';
            memoryBtn.style.backgroundColor = memoryEnabled ? '#87CEFA' : '#444';
            memoryBtn.style.color = 'white';
            memoryBtn.style.cursor = 'pointer';
            memoryBtn.style.zIndex = '10';
            memoryBtn.addEventListener('click', () => {
                memoryEnabled = !memoryEnabled;
                memoryBtn.style.backgroundColor = memoryEnabled ? '#87CEFA' : '#444';
            });
            inputContainer.appendChild(memoryBtn);

            sendBtn = document.createElement('button');
            sendBtn.innerText = '发送';
            sendBtn.style.position = 'absolute';
            sendBtn.style.right = '1.2em';
            sendBtn.style.bottom = '0.5em';
            sendBtn.style.width = '3em';
            sendBtn.style.height = '2em';
            sendBtn.style.fontSize = '1em';
            sendBtn.style.lineHeight = '2em';
            sendBtn.style.textAlign = 'center';
            sendBtn.style.borderRadius = '10px';
            sendBtn.style.border = '1px solid white';
            sendBtn.style.backgroundColor = '#444';
            sendBtn.style.color = 'white';
            sendBtn.style.cursor = 'pointer';
            sendBtn.style.zIndex = '10';
            inputContainer.appendChild(sendBtn);

            // 自动调整输入框高度
            function autoResize() {
                const initialHeight = window.innerHeight * 0.10;
                messageInput.style.height = 'auto';
                let newHeight = messageInput.scrollHeight;
                if (newHeight < initialHeight) newHeight = initialHeight;
                const maxHeight = window.innerHeight * 0.25;
                if (newHeight > maxHeight) {
                    inputContainer.style.height = messageInput.style.height = maxHeight + 'px';
                    messageInput.style.overflowY = 'auto';
                } else {
                    inputContainer.style.height = messageInput.style.height = newHeight + 'px';
                    messageInput.style.overflowY = 'hidden';
                }
                sendBtn.disabled = (messageInput.value.trim() === '');
            }
            messageInput.addEventListener('input', autoResize);
            autoResize();

            // 阻止流式输出期间的输入(包括回车发送)
            messageInput.addEventListener('keydown', (e) => {
                if (isStreaming) {
                    e.preventDefault();
                    return;
                }
                if (e.key === 'Enter' && !e.shiftKey) {
                    e.preventDefault();
                    if(messageInput.value.trim() === '') return;
                    if (!isSending) sendMessage();
                }
            });
            messageInput.addEventListener('focus', () => {
                if(isStreaming) {
                    messageInput.blur();
                }
            });

            // 发送消息函数
            async function sendMessage() {
                if (isSending || isStreaming) {
                    if (isStreaming) {
                        streamCancel = true;
                    }
                    return;
                }
                isSending = true;
                messageInput.disabled = true;
                sendBtn.disabled = true;
                const msg = messageInput.value.trim();
                if (!msg) {
                    isSending = false;
                    messageInput.disabled = false;
                    return;
                }
                addChatBubble(true, msg);
                currentSession.push({ role: "user", content: msg });
                saveCurrentSession();
                messageInput.value = '';
                autoResize();
                const waitingIndicator = document.createElement('div');
                waitingIndicator.textContent = '思考中...';
                waitingIndicator.style.cssText = `
                    color: #888;
                    font-size: 0.9em;
                    font-style: italic;
                    padding: 0.3em 0;
                    align-self: flex-start;
                    animation: blink 1s infinite;
                `;
                conversationDiv.appendChild(waitingIndicator);
                conversationDiv.scrollTop = conversationDiv.scrollHeight;
                try {
                    let messagesPayload = [
                        { role: "system", content: "You are a helpful assistant." }
                    ];
                    if(memoryEnabled && currentSession.length > 0) {
                        messagesPayload = messagesPayload.concat(currentSession);
                    }
                    if(!memoryEnabled) {
                        messagesPayload.push({ role: "user", content: msg });
                    }
                    const response = await gmFetch('https://api.deepseek.com/chat/completions', {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/json',
                            'Authorization': 'Bearer ' + apiKey
                        },
                        body: JSON.stringify({
                            messages: messagesPayload,
                            model: currentModel
                        })
                    });
                    const data = response.json();
                    if (waitingIndicator.parentNode) {
                        conversationDiv.removeChild(waitingIndicator);
                    }
                    if (data.choices?.[0]?.message?.content) {
                        const reply = data.choices[0].message.content.trim();
                        addChatBubble(false, reply, true); // 流式输出
                        currentSession.push({ role: "assistant", content: reply });
                        saveCurrentSession();
                    }
                } catch (err) {
                    let errorMsg = '请求失败: ';
                    if (err.response) {
                        errorMsg += `${err.response.status} - ${err.response.statusText}`;
                        try {
                            const errData = JSON.parse(err.response.responseText);
                            errorMsg += ` (${errData.error?.message || '未知错误'})`;
                        } catch(e) {}
                    } else {
                        errorMsg += err.message || err.toString();
                    }
                    addChatBubble(false, errorMsg);
                } finally {
                    isSending = false;
                    if (waitingIndicator && waitingIndicator.parentNode) {
                        conversationDiv.removeChild(waitingIndicator);
                    }
                    if (!isStreaming) {
                        messageInput.disabled = false;
                        sendBtn.disabled = false;
                    }
                    conversationDiv.scrollTop = conversationDiv.scrollHeight;
                }
            }
            sendBtn.addEventListener('click', sendMessage);
        }
    }

    renderUI();

    /***************** 显示与隐藏窗口 *****************/
    // 使用 host.style.display 控制整个聊天界面的显示/隐藏
    let visible = false;
    document.addEventListener('keydown', (e) => {
        if (e.ctrlKey && e.altKey && e.key.toLowerCase() === 'd') {
            visible = !visible;
            host.style.display = visible ? 'block' : 'none';
            if (visible) {
                setTimeout(() => {
                    const input = shadow.querySelector('textarea');
                    input?.focus();
                }, 100);
            }
            e.preventDefault();
            e.stopPropagation();
        }
    }, true);

    document.addEventListener('click', (e) => {
        if (!e.composedPath().includes(host)) {
            visible = false;
            host.style.display = 'none';
            hideHistoryPanel();
        }
    });
})();