DeepSeek API聊天

在任意网站上添加 DeepSeek 聊天窗口,CTRL+ALT+D显示/隐藏界面。支持官方、硅基流动与火山引擎三个平台的API配置;内置API key输入、平台选择、记忆上次平台、聊天、历史记录管理、新对话、模型切换与记忆功能。API 输出支持 Markdown 渲染。

// ==UserScript==
// @name         DeepSeek API聊天
// @namespace    http://tampermonkey.net/
// @version      2.1
// @description  在任意网站上添加 DeepSeek 聊天窗口,CTRL+ALT+D显示/隐藏界面。支持官方、硅基流动与火山引擎三个平台的API配置;内置API key输入、平台选择、记忆上次平台、聊天、历史记录管理、新对话、模型切换与记忆功能。API 输出支持 Markdown 渲染。
// @author       AMT
// @match        *://*/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @grant        GM_setClipboard
// @grant        unsafeWindow
// @require      https://cdn.jsdelivr.net/npm/[email protected]/marked.min.js
// @license      MIT
// ==/UserScript==

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

    // 定义颜色常量
    const inactiveColor = "#444";
    const activeColor = "#4169E1"; // 专属于模型切换和记忆按钮点击后保持的颜色
    const sendDisabledColor = "#888";
    const sendEnabledColor = "#fff";

    // 为普通按钮添加视觉反馈(也适用于历史记录项)
    function addButtonFeedback(el) {
        el.addEventListener('mouseover', () => {
            el.style.opacity = '0.6';
        });
        el.addEventListener('mouseout', () => {
            el.style.opacity = '1';
        });
        el.addEventListener('mousedown', () => {
            el.style.backgroundColor = "#666";
        });
        el.addEventListener('mouseup', () => {
            el.style.backgroundColor = inactiveColor;
        });
    }

    /***************** 创建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;
        transition: background-color 0.3s;
    }
    /* 统一滚动条样式 */
    ::-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: 0em !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);

    /***************** 全局变量与存储 *****************/
    // 使用字典存储各平台配置
    const platforms = {
        official: {
            name: "官方",
            base_url: "https://api.deepseek.com",
            chat_model_id: "deepseek-chat",
            reasoner_model_id: "deepseek-reasoner",
            keyStorageName: "deepseek_api_official"
        },
        siliconflow: {
            name: "硅基流动",
            base_url: "https://api.siliconflow.cn/v1",
            chat_model_id: "deepseek-ai/DeepSeek-V3",
            reasoner_model_id: "deepseek-ai/DeepSeek-R1",
            keyStorageName: "deepseek_api_siliconflow"
        },
        volcengine: {
            name: "火山引擎",
            base_url: "https://ark.cn-beijing.volces.com/api/v3/",
            chat_model_id: "deepseek-v3-241226",
            reasoner_model_id: "deepseek-r1-250120",
            keyStorageName: "deepseek_api_volcengine"
        }
    };

    // 加载各平台已存储的API key
    const storedKeys = {
        official: GM_getValue(platforms.official.keyStorageName, ""),
        siliconflow: GM_getValue(platforms.siliconflow.keyStorageName, ""),
        volcengine: GM_getValue(platforms.volcengine.keyStorageName, "")
    };

    // 记忆上次使用的平台,默认选官方平台
    let currentPlatform = GM_getValue('last_platform', 'official');
    // 若当前平台没有存储api,则进入编辑模式
    let editingAPI = !storedKeys[currentPlatform];

    let currentModel = GM_getValue('currentModel', 'deepseek-chat'); // 读取保存的模型(此处仍为深度聊天/思考切换,仅标识模型类型)
    let memoryEnabled = GM_getValue('memoryEnabled', false); // 读取记忆状态
    let isStreaming = false;
    let autoScrollEnabled = true;
    let chatHistory = JSON.parse(GM_getValue('deepseek_history', '[]'));
    let currentSession = [];
    let currentSessionId = Date.now();
    let isSending = false;
    // 全局存储流控制器,便于中途停止
    let streamAbortController = null;

    /***************** 工具函数 *****************/
    function safeCopyToClipboard(text, button) {
        console.log("Attempting to copy text:", text);
        try {
            if (typeof GM_setClipboard === 'function') {
                GM_setClipboard(text);
            } else if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
                navigator.clipboard.writeText(text).then(() => {
                    console.log("Clipboard write success");
                }).catch(err => {
                    console.error("Clipboard write failed:", err);
                });
            } 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);
            }
            console.log("Copy successful, updating button");
            if (button) {
                button.textContent = '✅';
                button.style.backgroundColor = activeColor;
                setTimeout(() => {
                    button.textContent = '📋';
                    button.style.backgroundColor = inactiveColor;
                }, 1000);
            }
        } catch (e) {
            alert('复制失败,请手动复制');
            console.error('复制失败:', e);
        }
    }
    // 修改后的 renderMarkdown 函数(移除按钮事件绑定,由事件委托处理)
    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') {
                const chatWidth = chatContainer.offsetWidth;
                const fixedWidth = chatWidth * 0.85;
                // 固定预格式化区域的宽度
                pre.style.width = `${fixedWidth}px`;
                pre.style.maxWidth = `${fixedWidth}px`;
                pre.style.margin = '0';
                // 同时让包裹容器固定宽度
                wrapper.style.width = `${fixedWidth}px`;
                pre.parentElement.replaceChild(wrapper, pre);
                wrapper.appendChild(pre);
                // 设置 pre 为相对定位
                pre.style.position = 'relative';
                const copyBtn = document.createElement('button');
                copyBtn.textContent = '📋';
                copyBtn.title = '复制代码';
                copyBtn.style.cssText = `
                    position: absolute;
                    top: 6px;
                    right: 6px;
                    font-size: 0.8em;
                    background: ${inactiveColor};
                    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;
                    pointer-events: all;
                `;
                pre.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.unshift(sessionRecord);
            } else {
                chatHistory[idx] = sessionRecord;
            }
            GM_setValue('deepseek_history', JSON.stringify(chatHistory));
        }
    }

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

    /***************** 主窗口与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%)';
    // 若未配置API则显示较小窗口,否则显示聊天界面
    chatContainer.style.width = (editingAPI ? '15vw' : '35vw');
    chatContainer.style.height = (editingAPI ? '20vh' : '75vh');
    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';
    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 = '#444';
        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 = inactiveColor;
        backBtn.style.color = 'white';
        backBtn.style.cursor = 'pointer';
        backBtn.addEventListener('click', () => { hideHistoryPanel(); });
        addButtonFeedback(backBtn);
        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 = inactiveColor;
        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 = ''; }
            }
        });
        addButtonFeedback(clearBtn);
        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.style.backgroundColor = inactiveColor;
            addButtonFeedback(item);
            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 => {
            if(msgObj.role === 'user') {
                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 = msgObj.content;
                conversationDiv.appendChild(bubble);
            } else {
                const container = document.createElement('div');
                container.style.display = 'flex';
                container.style.flexDirection = 'column';
                container.style.alignSelf = 'flex-start';
                if (msgObj.chain !== undefined) {
                    const headerBubble = document.createElement('div');
                    headerBubble.style.cssText = "padding: 0.5em; margin: 0.5em 0; border-radius: 12px; background-color: #444; color: #EEE; box-shadow: 0 2px 4px rgba(0,0,0,0.1); white-space: nowrap; display: inline-flex; align-items: center;";

                    const headerTextSpan = document.createElement('span');
                    headerTextSpan.innerText = msgObj.headerText || "已深度思考";
                    headerTextSpan.style.display = "inline-block";
                    headerBubble.appendChild(headerTextSpan);

                    const collapseBtn = document.createElement('button');
                    collapseBtn.style.cssText = "font-size: 0.8em; cursor: pointer; background-color: " + inactiveColor + "; color: #EEE; border: none; padding: 0; margin-left: 4px;";
                    let chainCollapsed = GM_getValue('chainCollapsed', false);
                    collapseBtn.textContent = chainCollapsed ? "∨" : "∧";
                    collapseBtn.addEventListener('click', () => {
                        chainCollapsed = !chainCollapsed;
                        GM_setValue('chainCollapsed', chainCollapsed);
                        if (chainDiv) {
                            chainDiv.style.display = chainCollapsed ? "none" : "";
                        }
                        collapseBtn.textContent = chainCollapsed ? "∨" : "∧";
                        setTimeout(() => {
                            const totalWidth = headerTextSpan.offsetWidth + collapseBtn.offsetWidth + 4;
                            headerBubble.style.width = totalWidth + "px";
                        }, 0);
                    });
                    headerBubble.appendChild(collapseBtn);

                    setTimeout(() => {
                        const totalWidth = headerTextSpan.offsetWidth + collapseBtn.offsetWidth + 4;
                        headerBubble.style.width = totalWidth + "px";
                    }, 0);
                    container.appendChild(headerBubble);

                    const chainDiv = document.createElement('div');
                    chainDiv.style.fontSize = "0.9em";
                    chainDiv.style.fontStyle = "italic";
                    chainDiv.style.color = "#aaa";
                    chainDiv.style.whiteSpace = "pre-wrap";
                    chainDiv.innerHTML = renderMarkdown(msgObj.chain).innerHTML;
                    chainDiv.style.display = chainCollapsed ? "none" : "";
                    container.appendChild(chainDiv);
                }
                const finalDiv = document.createElement('div');
                finalDiv.style.whiteSpace = "pre-wrap";
                finalDiv.innerHTML = renderMarkdown(msgObj.content).innerHTML;
                container.appendChild(finalDiv);
                conversationDiv.appendChild(container);
            }
        });
    }

    // 流式输出结束后调用,恢复发送按钮状态
    function finishStreaming() {
        isStreaming = false;
        streamAbortController = null;
        sendBtn.textContent = '发送';
        autoResize();
    }

    /***************** 自动调整输入框及发送按钮状态 *****************/
    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) {
            messageInput.style.height = maxHeight + 'px';
            messageInput.style.overflowY = 'auto';
        } else {
            messageInput.style.height = newHeight + 'px';
            messageInput.style.overflowY = 'hidden';
        }
        if (isStreaming) {
            sendBtn.disabled = false;
            sendBtn.textContent = '停止';
            sendBtn.style.backgroundColor = sendEnabledColor;
            sendBtn.style.color = "#000";
            sendBtn.style.cursor = 'pointer';
        } else {
            sendBtn.disabled = (messageInput.value.trim() === '');
            if (sendBtn.disabled) {
                sendBtn.style.backgroundColor = sendDisabledColor;
                sendBtn.style.color = "#666";
                sendBtn.style.cursor = 'not-allowed';
            } else {
                sendBtn.style.backgroundColor = sendEnabledColor;
                sendBtn.style.color = "#000";
                sendBtn.style.cursor = 'pointer';
            }
            sendBtn.textContent = '发送';
        }
    }

    /***************** 渲染整个界面 *****************/
    function renderUI() {
        contentDiv.innerHTML = '';
        // 若处于API编辑模式或当前平台尚未配置API,则显示API输入界面
        if (editingAPI || !storedKeys[currentPlatform]) {
            // 先清空并把 contentDiv 设置为相对定位
            contentDiv.innerHTML = '';
            contentDiv.style.position = 'relative';
            contentDiv.style.width = '100%';
            contentDiv.style.height = '100%';

            // 1) 标题“DeepSeek”
            const headerText = document.createElement('div');
            headerText.innerText = 'DeepSeek';
            // 绝对定位,使用百分比控制位置与大小
            headerText.style.position = 'absolute';
            headerText.style.top = '5%';
            headerText.style.left = '5%';
            headerText.style.width = '90%';
            headerText.style.fontSize = '1.5em';
            headerText.style.textAlign = 'center';
            headerText.style.color = 'white';
            contentDiv.appendChild(headerText);

            // 2) 平台切换按钮容器
            const platformContainer = document.createElement('div');
            // 绝对定位
            platformContainer.style.position = 'absolute';
            platformContainer.style.top = '30%';       // 相对 contentDiv 顶部 20%
            platformContainer.style.left = '5%';       // 相对 contentDiv 左侧 5%
            platformContainer.style.width = '90%';     // 宽度占 contentDiv 90%
            platformContainer.style.height = '15%';    // 高度可根据需要调整
            // 内部用 flex 排列,让 3 个按钮等宽并占满容器宽度
            platformContainer.style.display = 'flex';
            platformContainer.style.justifyContent = 'space-between';
            platformContainer.style.alignItems = 'center';

            for (const key in platforms) {
                const btn = document.createElement('button');
                btn.innerText = platforms[key].name;
                btn.style.width = '32%';                // 3 个按钮 + 缝隙,大约 32%
                btn.style.height = '100%';             // 填满父容器高度
                btn.style.fontSize = '1em';
                btn.style.borderRadius = '8px';
                btn.style.border = '1px solid white';
                btn.style.backgroundColor = (key === currentPlatform) ? activeColor : inactiveColor;
                btn.style.color = 'white';
                btn.style.cursor = 'pointer';
                btn.addEventListener('click', (e) => {
                    e.stopPropagation();
                    currentPlatform = key;
                    GM_setValue('last_platform', currentPlatform);
                    renderUI();
                });
                addButtonFeedback(btn);
                platformContainer.appendChild(btn);
            }
            contentDiv.appendChild(platformContainer);

            // 3) API 输入框
            const apiInput = document.createElement('input');
            apiInput.type = 'password';
            apiInput.placeholder = '请输入 API key';
            // 绝对定位
            apiInput.style.position = 'absolute';
            apiInput.style.top = '50%';
            apiInput.style.left = '5%';
            apiInput.style.width = '90%';
            apiInput.style.height = '15%';
            apiInput.style.fontSize = '1em';
            apiInput.style.boxSizing = 'border-box';
            apiInput.style.borderRadius = '8px';
            apiInput.style.border = '1px solid white';
            apiInput.style.backgroundColor = inactiveColor;
            apiInput.style.color = 'white';
            apiInput.style.padding = '0.3em';
            if (storedKeys[currentPlatform]) {
                apiInput.value = storedKeys[currentPlatform];
            }
            contentDiv.appendChild(apiInput);

            // 4) “显示api”+“确认”
            const optionsContainer = document.createElement('div');
            optionsContainer.style.position = 'absolute';
            optionsContainer.style.top = '70%';
            optionsContainer.style.left = '5%';
            optionsContainer.style.width = '90%';
            optionsContainer.style.height = '15%';
            optionsContainer.style.display = 'flex';
            optionsContainer.style.justifyContent = 'space-between';
            optionsContainer.style.alignItems = 'center';
            contentDiv.appendChild(optionsContainer);

            // 左侧“显示api”复选框
            const showApiContainer = document.createElement('div');
            showApiContainer.style.display = 'flex';
            showApiContainer.style.alignItems = 'center';
            const showApiLabel = document.createElement('label');
            showApiLabel.innerText = '显示api';
            showApiLabel.style.marginRight = '0.5em';
            const showApiCheckbox = document.createElement('input');
            showApiCheckbox.type = 'checkbox';
            showApiCheckbox.id = 'show-api-checkbox';
            showApiContainer.appendChild(showApiLabel);
            showApiContainer.appendChild(showApiCheckbox);
            optionsContainer.appendChild(showApiContainer);

            // 根据复选框状态切换 API 输入框明/暗文
            showApiCheckbox.addEventListener('change', () => {
                apiInput.type = showApiCheckbox.checked ? 'text' : 'password';
            });

            // 在显示api复选框后,创建一个右侧按钮分组容器
            const buttonGroup = document.createElement('div');
            buttonGroup.style.display = 'flex';
            buttonGroup.style.gap = '0.5em';
            buttonGroup.style.width = '45%';
            buttonGroup.style.height = '100%';
            buttonGroup.style.justifyContent = 'flex-end';
            buttonGroup.style.alignItems = 'center';

            // 1) 新增“删除”按钮
            const deleteBtn = document.createElement('button');
            deleteBtn.innerText = '删除';
            // 与确认按钮保持同样大小
            deleteBtn.style.width = '50%';
            deleteBtn.style.height = '100%';
            deleteBtn.style.fontSize = '1em';
            deleteBtn.style.borderRadius = '8px';
            deleteBtn.style.border = '1px solid white';
            deleteBtn.style.backgroundColor = inactiveColor;
            deleteBtn.style.color = 'white';
            deleteBtn.style.cursor = 'pointer';
            addButtonFeedback(deleteBtn);
            buttonGroup.appendChild(deleteBtn);

            // 删除按钮逻辑:清空当前平台的 API 存储并清空输入框
            deleteBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                // 1) 清空存储的 key
                GM_setValue(platforms[currentPlatform].keyStorageName, "");
                storedKeys[currentPlatform] = "";
                // 2) 清空输入框
                apiInput.value = "";
            });

            // 2) 确认按钮
            const confirmBtn = document.createElement('button');
            confirmBtn.innerText = '确认';
            // 与删除按钮保持同样大小
            confirmBtn.style.width = '50%';
            confirmBtn.style.height = '100%';
            confirmBtn.style.fontSize = '1em';
            confirmBtn.style.borderRadius = '8px';
            confirmBtn.style.border = '1px solid white';
            confirmBtn.style.backgroundColor = inactiveColor;
            confirmBtn.style.color = 'white';
            confirmBtn.style.cursor = 'pointer';
            addButtonFeedback(confirmBtn);
            buttonGroup.appendChild(confirmBtn);

            // 回车触发确认
            apiInput.addEventListener('keydown', (e) => {
                if (e.key === 'Enter') {
                    e.preventDefault();
                    confirmBtn.click();
                }
            });

            // 确认按钮逻辑
            confirmBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                const value = apiInput.value.trim();
                if (value) {
                    storedKeys[currentPlatform] = value;
                    GM_setValue(platforms[currentPlatform].keyStorageName, value);
                    GM_setValue('last_platform', currentPlatform);
                    editingAPI = false;
                    chatContainer.style.height = '75vh';
                    chatContainer.style.width = '35vw';
                    currentSession = [];
                    currentSessionId = Date.now();
                    renderUI();
                }
            });
            // 添加到 optionsContainer 的右侧
            optionsContainer.appendChild(buttonGroup);
        } 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.cursor = 'pointer';
            historyBtn.style.backgroundColor = inactiveColor;
            historyBtn.style.color = 'white';
            historyBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                if(historyPanel) {
                    hideHistoryPanel();
                } else {
                    showHistoryPanel();
                }
            });
            addButtonFeedback(historyBtn);
            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.cursor = 'pointer';
            newConvBtn.style.backgroundColor = inactiveColor;
            newConvBtn.style.color = 'white';
            newConvBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                saveCurrentSession();
                currentSession = [];
                currentSessionId = Date.now();
                if(conversationDiv) { conversationDiv.innerHTML = ''; }
            });
            addButtonFeedback(newConvBtn);
            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.cursor = 'pointer';
            reenterBtn.style.backgroundColor = inactiveColor;
            reenterBtn.style.color = 'white';
            reenterBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                hideHistoryPanel();
                saveCurrentSession();
                // 切换到API输入模式,但不清空已存储的key
                editingAPI = true;
                chatContainer.style.height = '16.67vh';
                chatContainer.style.width = '15vw';
                renderUI();
            });
            addButtonFeedback(reenterBtn);
            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();

            // 事件委托处理代码块复制按钮
            conversationDiv.addEventListener('click', function(e) {
                const button = e.target.closest('.code-block-wrapper button');
                if (button) {
                    e.stopPropagation();
                    const codeBlock = button.closest('.code-block-wrapper').querySelector('code');
                    if (codeBlock) {
                        const text = codeBlock.textContent;
                        safeCopyToClipboard(text, button);
                    }
                }
            });

            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 = inactiveColor;
            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 = inactiveColor;
            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.cursor = 'pointer';
            modelBtn.classList.add('persistent');
            modelBtn.addEventListener('click', () => {
                currentModel = (currentModel === 'deepseek-chat' ? 'deepseek-reasoner' : 'deepseek-chat');
                GM_setValue('currentModel', currentModel);
                modelBtn.style.backgroundColor = currentModel === 'deepseek-reasoner' ? activeColor : inactiveColor;
            });
            modelBtn.style.backgroundColor = currentModel === 'deepseek-reasoner' ? activeColor : inactiveColor;
            modelBtn.style.color = 'white';
            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.cursor = 'pointer';
            memoryBtn.classList.add('persistent');
            memoryBtn.addEventListener('click', () => {
                memoryEnabled = !memoryEnabled;
                GM_setValue('memoryEnabled', memoryEnabled);
                memoryBtn.style.backgroundColor = memoryEnabled ? activeColor : inactiveColor;
            });
            memoryBtn.style.backgroundColor = memoryEnabled ? activeColor : inactiveColor;
            memoryBtn.style.color = 'white';
            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.cursor = 'pointer';
            sendBtn.style.zIndex = '10';
            sendBtn.disabled = (messageInput.value.trim() === '');
            sendBtn.style.backgroundColor = sendBtn.disabled ? sendDisabledColor : sendEnabledColor;
            sendBtn.style.color = sendBtn.disabled ? "#666" : "#000";
            sendBtn.addEventListener('click', () => {
                if(sendBtn.textContent === '停止' && streamAbortController) {
                    streamAbortController.abort();
                } else {
                    sendMessage();
                }
            });
            inputContainer.appendChild(sendBtn);

            messageInput.addEventListener('input', autoResize);
            autoResize();

            messageInput.addEventListener('keydown', (e) => {
                if (e.key === 'Enter' && !e.shiftKey) {
                    e.preventDefault();
                    if(isStreaming) {
                        if(streamAbortController) {
                            streamAbortController.abort();
                        }
                    } else {
                        if(messageInput.value.trim() === '') return;
                        sendMessage();
                    }
                }
            });

            // 发送消息函数(流式处理返回数据)
            async function sendMessage() {
                if (isStreaming) {
                    if (streamAbortController) {
                        streamAbortController.abort();
                    }
                    return;
                }
                const msg = messageInput.value.trim();
                if (!msg) return;
                {
                    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 = msg;
                    conversationDiv.appendChild(bubble);
                }
                currentSession.push({ role: "user", content: msg });
                saveCurrentSession();
                messageInput.value = '';
                isSending = true;
                isStreaming = true;
                sendBtn.textContent = '停止';
                sendBtn.disabled = false;
                autoResize();

                // 构造 assistant 消息显示容器,根据当前模型类型分支
                let finalDiv, headerTextSpan, collapseBtn, chainDiv;
                let assistantContainer;
                if (currentModel === 'deepseek-reasoner') {
                    assistantContainer = document.createElement("div");
                    assistantContainer.style.display = "flex";
                    assistantContainer.style.flexDirection = "column";
                    assistantContainer.style.alignSelf = "flex-start";

                    const headerBubble = document.createElement("div");
                    headerBubble.style.cssText = "padding: 0.5em; margin: 0.5em 0; border-radius: 12px; background-color: #444; color: #EEE; box-shadow: 0 2px 4px rgba(0,0,0,0.1); white-space: nowrap; flex-shrink: 0; display: flex; justify-content: space-between; align-items: center;";
                    headerTextSpan = document.createElement("span");
                    headerTextSpan.innerText = "思考中…";
                    headerBubble.appendChild(headerTextSpan);
                    collapseBtn = document.createElement("button");
                    collapseBtn.style.fontSize = "0.8em";
                    collapseBtn.style.cursor = "pointer";
                    collapseBtn.style.backgroundColor = inactiveColor;
                    collapseBtn.style.color = "#EEE";
                    collapseBtn.style.border = "none";
                    let chainCollapsed = GM_getValue('chainCollapsed', false);
                    collapseBtn.textContent = chainCollapsed ? "∨" : "∧";
                    collapseBtn.addEventListener('click', () => {
                        chainCollapsed = !chainCollapsed;
                        GM_setValue('chainCollapsed', chainCollapsed);
                        if(chainDiv) {
                            chainDiv.style.display = chainCollapsed ? "none" : "";
                        }
                        collapseBtn.textContent = chainCollapsed ? "∨" : "∧";
                        setTimeout(() => {
                            const totalWidth = headerTextSpan.scrollWidth + collapseBtn.offsetWidth;
                            headerTextSpan.parentElement.style.width = totalWidth + "px";
                        }, 0);
                    });
                    headerBubble.appendChild(collapseBtn);
                    setTimeout(() => {
                        const totalWidth = headerTextSpan.scrollWidth + collapseBtn.offsetWidth;
                        headerBubble.style.width = totalWidth + "px";
                    }, 0);
                    assistantContainer.appendChild(headerBubble);

                    const contentContainer = document.createElement("div");
                    contentContainer.style.padding = "0.5em";
                    chainDiv = document.createElement("div");
                    chainDiv.style.fontSize = "0.9em";
                    chainDiv.style.fontStyle = "italic";
                    chainDiv.style.color = "#aaa";
                    chainDiv.style.whiteSpace = "pre-wrap";
                    chainDiv.style.display = GM_getValue('chainCollapsed', false) ? 'none' : '';
                    contentContainer.appendChild(chainDiv);
                    finalDiv = document.createElement("div");
                    finalDiv.style.whiteSpace = "pre-wrap";
                    contentContainer.appendChild(finalDiv);
                    assistantContainer.appendChild(contentContainer);
                    conversationDiv.appendChild(assistantContainer);
                } else {
                    assistantContainer = document.createElement("div");
                    assistantContainer.style.display = "flex";
                    assistantContainer.style.flexDirection = "column";
                    assistantContainer.style.alignSelf = "flex-start";
                    // 添加“…”提示
                    const thinkingPlaceholder = document.createElement("div");
                    thinkingPlaceholder.innerText = "…";
                    thinkingPlaceholder.style.color = "#888";
                    thinkingPlaceholder.style.fontStyle = "italic";
                    thinkingPlaceholder.style.marginBottom = "0.5em";
                    assistantContainer.appendChild(thinkingPlaceholder);

                    finalDiv = document.createElement("div");
                    finalDiv.style.whiteSpace = "pre-wrap";
                    assistantContainer.appendChild(finalDiv);
                    conversationDiv.appendChild(assistantContainer);

                    // 设置 removeThinkingPlaceholder 函数,确保只删一次
                    var hasRemovedPlaceholder = false;
                    var removeThinkingPlaceholder = () => {
                        if (!hasRemovedPlaceholder && thinkingPlaceholder.parentElement) {
                            thinkingPlaceholder.remove();
                            hasRemovedPlaceholder = true;
                        }
                    };
                }
                conversationDiv.scrollTop = conversationDiv.scrollHeight;
                const startTime = Date.now();
                let headerUpdated = false;
                let chainOfThought = "";
                let finalContent = "";

                // 构造请求上下文
                let messagesPayload = [{ role: "system", content: "You are a helpful assistant." }];
                if (memoryEnabled && currentSession.length > 1) {
                    let memoryText = currentSession.map(m => m.content).join("\n");
                    messagesPayload.push({ role: "user", content: memoryText });
                } else {
                    messagesPayload.push({ role: "user", content: msg });
                }

                // 使用当前平台的API key和base_url(注意处理尾部斜杠)
                const apiKey = storedKeys[currentPlatform];
                const base_url = platforms[currentPlatform].base_url.replace(/\/$/, '');
                // 根据当前模型类型使用对应平台模型id
                const modelId = (currentModel === 'deepseek-chat' ? platforms[currentPlatform].chat_model_id : platforms[currentPlatform].reasoner_model_id);

                streamAbortController = new AbortController();
                try {
                    const response = await unsafeWindow.fetch(base_url + '/chat/completions', {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/json',
                            'Authorization': 'Bearer ' + apiKey
                        },
                        body: JSON.stringify({
                            messages: messagesPayload,
                            model: modelId,
                            stream: true
                        }),
                        signal: streamAbortController.signal
                    });
                    if (!response.body) {
                        throw new Error("当前环境不支持流式读取");
                    }
                    const reader = response.body.getReader();
                    const decoder = new TextDecoder();
                    let buffer = "";
                    let done = false;
                    while (!done) {
                        const { value, done: doneReading } = await reader.read();
                        done = doneReading;
                        buffer += decoder.decode(value, { stream: true });
                        const lines = buffer.split("\n");
                        buffer = lines.pop();
                        for (const line of lines) {
                            const trimmed = line.trim();
                            if (!trimmed) continue;
                            if (trimmed.startsWith("data: ")) {
                                const jsonStr = trimmed.slice(6).trim();
                                if (jsonStr === "[DONE]") {
                                    done = true;
                                    break;
                                }
                                try {
                                    const data = JSON.parse(jsonStr);
                                    const delta = data.choices?.[0]?.delta;
                                    if (delta) {
                                        if (currentModel === 'deepseek-reasoner') {
                                            if (delta.reasoning_content) {
                                                chainOfThought += delta.reasoning_content;
                                            }
                                            if (delta.content) {
                                                if (!headerUpdated && delta.content.trim() !== "") {
                                                    const elapsed = Math.round((Date.now() - startTime) / 1000);
                                                    headerTextSpan.innerText = `已深度思考(用时${elapsed}秒)`;
                                                    setTimeout(() => {
                                                        const totalWidth = headerTextSpan.scrollWidth + collapseBtn.offsetWidth;
                                                        headerTextSpan.parentElement.style.width = totalWidth + "px";
                                                    }, 0);
                                                    headerUpdated = true;
                                                }
                                                finalContent += delta.content;
                                            }
                                            chainDiv.innerHTML = renderMarkdown(chainOfThought).innerHTML;
                                            finalDiv.innerHTML = renderMarkdown(finalContent).innerHTML;
                                        } else {
                                            if (delta.content) {
                                                removeThinkingPlaceholder();
                                                finalContent += delta.content;
                                                finalDiv.innerHTML = renderMarkdown(finalContent).innerHTML;
                                            }
                                        }
                                        if (autoScrollEnabled) {
                                            conversationDiv.scrollTop = conversationDiv.scrollHeight;
                                        }
                                    }
                                } catch (err) {
                                    console.error("解析流式数据失败:", err);
                                }
                            }
                        }
                    }
                } catch (err) {
                    if (err.name === "AbortError") {
                        finalDiv.innerHTML += `<div style="color:#faa;">(流式输出已停止)</div>`;
                    } else {
                        finalDiv.innerHTML += `<div style="color:#faa;">请求失败: ${err.message}</div>`;
                    }
                } finally {
                    if (currentModel === 'deepseek-reasoner') {
                        chainDiv.innerHTML = renderMarkdown(chainOfThought).innerHTML;
                        finalDiv.innerHTML = renderMarkdown(finalContent).innerHTML;
                    } else {
                        finalDiv.innerHTML = renderMarkdown(finalContent).innerHTML;
                    }
                    if (currentModel === 'deepseek-reasoner') {
                        const fixedWidth = headerTextSpan.parentElement.offsetWidth;
                        currentSession.push({
                            role: "assistant",
                            content: finalContent,
                            chain: chainOfThought,
                            headerWidth: fixedWidth + "px",
                            headerText: headerTextSpan.innerText
                        });
                    } else {
                        currentSession.push({ role: "assistant", content: finalContent });
                    }
                    saveCurrentSession();
                    isSending = false;
                    finishStreaming();
                    conversationDiv.scrollTop = conversationDiv.scrollHeight;
                }
            }
        }
    }

    renderUI();

    /***************** 显示与隐藏窗口 *****************/
    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);
            } else {
                // 隐藏时调用隐藏历史记录面板的函数
                hideHistoryPanel();
            }
            e.preventDefault();
            e.stopPropagation();
        }
    });
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址