您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
在任意网站上添加 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或关注我们的公众号极客氢云获取最新地址