// ==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();
});
})();