DeepSeek API聊天

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

目前为 2025-03-22 提交的版本。查看 最新版本

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

// 加载 marked 库用于 Markdown 渲染(若尚未加载)
(function loadMarked(callback) {
    if (typeof marked !== 'undefined') {
        if(callback) callback();
    } else {
        let script = document.createElement('script');
        script.src = "https://cdn.jsdelivr.net/npm/marked/marked.min.js";
        script.onload = function() {
            if(callback) callback();
        };
        document.head.appendChild(script);
    }
})();

(function() {
    'use strict';

    /***************** 全局变量与存储 *****************/
    let apiKey = GM_getValue('deepseek_api', '');
    // 内部仍使用完整模型名用于 API 调用,但按钮显示简写
    let currentModel = "deepseek-chat";
    const modelDisplay = {
        "deepseek-chat": "Chat",
        "deepseek-reasoner": "Reasoner"
    };
    let memoryEnabled = false;  // 记忆功能状态
    // 聊天历史记录:数组,每个会话记录包含 id、messages(数组)和 timestamp
    let chatHistory = JSON.parse(GM_getValue('deepseek_history', '[]'));
    // 当前会话记录
    let currentSession = [];
    let currentSessionId = Date.now();

    // 保存当前会话到历史记录
    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();
    }

    /***************** 主窗口与UI *****************/
    // 创建主聊天窗口容器(背景深灰色)
    const chatContainer = document.createElement('div');
    chatContainer.id = 'deepseek-chat-ui';
    chatContainer.style.position = 'fixed';
    chatContainer.style.right = '0';
    chatContainer.style.top = '50%';
    chatContainer.style.transform = 'translateY(-50%)';
    chatContainer.style.width = apiKey ? '35vw':'15vw';
    // API 未输入时高度约16.67vh,输入后为75vh
    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.zIndex = '9999';
    chatContainer.style.transition = 'opacity 0.3s, transform 0.3s';
    // 默认隐藏(仅通过快捷键显示)
    chatContainer.style.opacity = '0';
    document.body.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';  // 最高图层
        // 与主窗口同高,宽度为主窗口的 40%
        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.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.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;    // 文本输入框引用

    // 渲染对话区(加载当前会话记录)
    function renderConversation() {
        if(!conversationDiv) return;
        conversationDiv.innerHTML = '';
        currentSession.forEach(msgObj => {
            // 仅显示消息内容,不显示角色前缀
            addChatBubble(msgObj.role === 'user', msgObj.content);
        });
    }

    // 添加聊天气泡,支持 Markdown 渲染,并强制字体为黑色
    function addChatBubble(isUser, text) {
        const bubble = document.createElement('div');
        bubble.style.padding = '0.5em';
        bubble.style.borderRadius = '10px';
        bubble.style.maxWidth = '70%';
        bubble.style.wordWrap = 'break-word';
        bubble.style.fontSize = '1.2em';
        if(isUser) {
            bubble.style.backgroundColor = '#E6E6FA';
            bubble.style.color = 'black';
            bubble.style.alignSelf = 'flex-end';
            bubble.innerText = text;
        } else {
            bubble.style.backgroundColor = '#D3D3D3';
            bubble.style.color = 'black';
            bubble.style.alignSelf = 'flex-start';
            if(typeof marked !== 'undefined') {
                bubble.innerHTML = marked.parse(text);
                // 强制所有内部元素字体颜色为黑色
                const elems = bubble.querySelectorAll("*");
                elems.forEach(el => { el.style.color = 'black'; });
            } else {
                bubble.innerText = text;
            }
        }
        conversationDiv.appendChild(bubble);
        conversationDiv.scrollTop = conversationDiv.scrollHeight;
    }

    /***************** 渲染整个界面 *****************/
    function renderUI() {
        contentDiv.innerHTML = '';

        if (!apiKey) {
            // API 输入界面
            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 = '1.5em';
            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 = '1.5em';
            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 {
            // 聊天界面
            // 头部区域:左侧放【历史记录】和【开启新对话】按钮,右侧【重新输入api】按钮
            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);
            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);

            // 文本输入框(固定底部,向上扩展,最大20vh)
            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';
            // 内边距:上0.5em,右预留6em(两个按钮),下预留3em(按钮区域),左0.5em
            messageInput.style.padding = '0.5em 6em 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 = '1.2em';
            inputOverlay.style.bottom = '0.1em';
            inputOverlay.style.height = '3em';
            inputOverlay.style.backgroundColor = '#444';
            inputOverlay.style.pointerEvents = 'none';
            inputContainer.appendChild(inputOverlay);

            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';
                }
            }
            messageInput.addEventListener('input', autoResize);

            // 输入区域内按钮——均固定在底部,与文本框底部间隔约1px
            // 模型切换按钮(左下角)——显示简写
            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');
                // 修改为普通蓝色 (#87CEFA)
                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);

            // 发送按钮(右下角)
            const 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);

            // 发送消息函数
            async function sendMessage() {
                const msg = messageInput.value.trim();
                if (!msg) return;
                addChatBubble(true, msg);
                currentSession.push({ role: "user", content: msg });
                saveCurrentSession();
                messageInput.value = '';
                autoResize();
                const waitingBubble = document.createElement('div');
                waitingBubble.innerText = '思考中...';
                waitingBubble.style.padding = '0.5em';
                waitingBubble.style.borderRadius = '10px';
                waitingBubble.style.maxWidth = '70%';
                waitingBubble.style.backgroundColor = '#D3D3D3';
                waitingBubble.style.color = 'black';
                waitingBubble.style.alignSelf = 'flex-start';
                conversationDiv.appendChild(waitingBubble);
                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 fetch('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 = await response.json();
                    conversationDiv.removeChild(waitingBubble);
                    if (data && data.choices && data.choices[0] && data.choices[0].message) {
                        const reply = data.choices[0].message.content;
                        addChatBubble(false, reply);
                        currentSession.push({ role: "assistant", content: reply });
                        saveCurrentSession();
                    } else {
                        addChatBubble(false, '无回复或返回格式错误');
                    }
                } catch (err) {
                    conversationDiv.removeChild(waitingBubble);
                    addChatBubble(false, '调用API错误: ' + err);
                }
                conversationDiv.scrollTop = conversationDiv.scrollHeight;
            }
            sendBtn.addEventListener('click', sendMessage);
            messageInput.addEventListener('keydown', (e) => {
                if (e.key === 'Enter' && !e.shiftKey) {
                    e.preventDefault();
                    sendMessage();
                }
            });
        }
    }

    renderUI();

    /***************** 显示与隐藏窗口 *****************/
    // 使用快捷键 Ctrl+Alt+D 显示窗口
    document.addEventListener('keydown', (e) => {
        if (e.ctrlKey && e.altKey && e.key.toLowerCase() === 'd') {
            chatContainer.style.opacity = '1';
            e.stopPropagation();
        }
    });
    // 点击窗口外部时隐藏窗口及历史记录面板
    document.addEventListener('click', () => {
        chatContainer.style.opacity = '0';
        hideHistoryPanel();
    });
})();