AI Chat

ai助手

  1. // ==UserScript==
  2. // @name AI Chat
  3. // @namespace Auntilz
  4. // @version 1.0
  5. // @description ai助手
  6. // @author Auntilz
  7. // @license MIT
  8. // @match *://*/*
  9. // @grant GM_getValue
  10. // @grant GM_setValue
  11. // @grant GM_xmlhttpRequest
  12. // @grant GM_registerMenuCommand
  13. // @connect api.siliconflow.cn
  14. // ==/UserScript==
  15.  
  16. (function() {
  17. 'use strict';
  18. window.addEventListener('error', function(e) {
  19. alert('脚本错误: ' + e.message);
  20. console.error('脚本错误:', e)
  21. });
  22.  
  23. function debugLog(...args) {
  24. if (!debugMode) return;
  25. console.log('[SiliconFlow Debug]', ...args);
  26. let debugEl = document.getElementById('silicon-flow-debug');
  27. if (!debugEl) {
  28. debugEl = document.createElement('div');
  29. debugEl.id = 'silicon-flow-debug';
  30. debugEl.style.position = 'fixed';
  31. debugEl.style.bottom = '10px';
  32. debugEl.style.left = '10px';
  33. debugEl.style.background = 'rgba(0,0,0,0.8)';
  34. debugEl.style.color = 'white';
  35. debugEl.style.padding = '10px';
  36. debugEl.style.borderRadius = '5px';
  37. debugEl.style.maxHeight = '200px';
  38. debugEl.style.overflowY = 'auto';
  39. debugEl.style.maxWidth = '500px';
  40. debugEl.style.zIndex = '10000';
  41. debugEl.style.fontSize = '12px';
  42. debugEl.style.fontFamily = 'monospace';
  43. debugEl.style.display = 'none';
  44. document.body.appendChild(debugEl)
  45. }
  46. if (debugMode) {
  47. const line = document.createElement('div');
  48. line.textContent = args.map(a => typeof a === 'object' ? JSON.stringify(a) : a).join(' ');
  49. debugEl.appendChild(line);
  50. debugEl.scrollTop = debugEl.scrollHeight;
  51. while (debugEl.children.length > 50) {
  52. debugEl.removeChild(debugEl.firstChild)
  53. }
  54. }
  55. debugEl.style.display = debugMode ? 'block' : 'none'
  56. }
  57. let apiKey = GM_getValue('apiKey', '');
  58. let model = GM_getValue('model', 'deepseek-ai/DeepSeek-V3');
  59. let currentStream = null;
  60. let chatHistory = GM_getValue('chatHistory', []);
  61. let isDarkMode = GM_getValue('isDarkMode', false);
  62. let fontSize = GM_getValue('fontSize', 14);
  63. let debugMode = GM_getValue('debugMode', true);
  64. let useStreamMode = GM_getValue('useStreamMode', true);
  65. debugLog('脚本初始化', '模型:', model, '深色模式:', isDarkMode, '调试模式:', debugMode);
  66. const colors = {
  67. light: {
  68. background: '#ffffff',
  69. secondaryBg: '#f8f9fa',
  70. border: '#e1e4e8',
  71. text: '#24292e',
  72. primaryText: '#0366d6',
  73. secondaryText: '#586069',
  74. iconBg: '#0366d6',
  75. iconHover: '#0257c5',
  76. inputBg: '#ffffff',
  77. aiResponse: '#f1f8ff'
  78. },
  79. dark: {
  80. background: '#1e1e1e',
  81. secondaryBg: '#252525',
  82. border: '#333333',
  83. text: '#e1e4e8',
  84. primaryText: '#58a6ff',
  85. secondaryText: '#8b949e',
  86. iconBg: '#1f6feb',
  87. iconHover: '#388bfd',
  88. inputBg: '#2d2d2d',
  89. aiResponse: '#161b22'
  90. }
  91. };
  92. const getColor = (key) => isDarkMode ? colors.dark[key] : colors.light[key];
  93. const style = document.createElement('style');
  94. document.head.appendChild(style);
  95.  
  96. function updateStyles() {
  97. style.textContent = `.ai-chat-icon{position:fixed;width:56px;height:56px;background-color:${getColor('iconBg')};border-radius:50%;display:flex;align-items:center;justify-content:center;color:white;font-weight:bold;box-shadow:0 4px 14px rgba(0,0,0,0.16);cursor:grab;user-select:none;touch-action:none;z-index:2147483647;font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif}.ai-chat-icon svg{filter:drop-shadow(0 2px 4px rgba(0,0,0,0.2));transition:transform 0.2s ease}.ai-chat-icon:hover{transform:scale(1.08);box-shadow:0 6px 20px rgba(0,0,0,0.2)}.ai-chat-icon:hover svg{transform:scale(1.1)}.ai-chat-icon:hover{transform:scale(1.05);background-color:${getColor('iconHover')}}.ai-chat-window{position:fixed;bottom:85px;right:20px;width:380px;height:550px;background-color:${getColor('background')};border-radius:16px;box-shadow:0 8px 30px rgba(0,0,0,0.12);display:flex;flex-direction:column;overflow:hidden;z-index:9998;font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;border:1px solid ${getColor('border')};transition:all 0.3s ease}.ai-chat-header{display:flex;align-items:center;justify-content:space-between;padding:14px 16px;background-color:${getColor('secondaryBg')};border-bottom:1px solid ${getColor('border')};border-top-left-radius:16px;border-top-right-radius:16px}.ai-chat-title{font-weight:600;font-size:16px;color:${getColor('text')};margin:0}.ai-chat-actions{display:flex;align-items:center;gap:14px}.ai-chat-action{background:none;border:none;color:${getColor('secondaryText')};cursor:pointer;font-size:18px;padding:0;display:flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:6px;transition:all 0.2s ease}.ai-chat-action:hover{background-color:${getColor('border')};color:${getColor('primaryText')}}.ai-chat-content{flex:1;padding:16px;overflow-y:auto;overflow-x:hidden;scroll-behavior:smooth}.ai-chat-content::-webkit-scrollbar{width:6px}.ai-chat-content::-webkit-scrollbar-thumb{background-color:${getColor('border')};border-radius:10px}.ai-chat-content::-webkit-scrollbar-track{background-color:transparent}.ai-message{margin-bottom:18px;font-size:${fontSize}px;line-height:1.5;word-wrap:break-word}.ai-user-message{color:${getColor('text')}}.ai-response{padding:12px 16px;background-color:${getColor('aiResponse')};border-radius:10px;margin-top:6px;color:${getColor('text')};word-break:break-word}.ai-sender{font-weight:600;margin-bottom:4px;color:${getColor('primaryText')};display:flex;align-items:center;gap:6px}.ai-chat-input-container{padding:12px 16px;border-top:1px solid ${getColor('border')};background-color:${getColor('secondaryBg')}}.ai-chat-input-wrapper{position:relative;border-radius:10px;background-color:${getColor('inputBg')};border:1px solid ${getColor('border')};padding:8px 40px 8px 12px;transition:border-color 0.2s ease}.ai-chat-input-wrapper:focus-within{border-color:${getColor('primaryText')};box-shadow:0 0 0 2px rgba(3,102,214,0.2)}.ai-chat-input{width:100%;min-height:24px;max-height:120px;border:none;outline:none;background:transparent;resize:none;font-family:inherit;font-size:${fontSize}px;line-height:1.5;color:${getColor('text')};overflow-y:auto}.ai-chat-input::placeholder{color:${getColor('secondaryText')}}.ai-chat-send{position:absolute;right:8px;bottom:8px;width:28px;height:28px;border-radius:50%;background-color:${getColor('iconBg')};color:white;border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all 0.2s ease}.ai-chat-send:hover{background-color:${getColor('iconHover')}}.ai-chat-send svg{width:16px;height:16px}.ai-settings-panel{position:absolute;top:60px;right:16px;background-color:${getColor('background')};border-radius:10px;border:1px solid ${getColor('border')};box-shadow:0 8px 24px rgba(0,0,0,0.12);padding:16px;width:270px;z-index:10000;display:none;flex-direction:column;gap:14px}.ai-settings-group{display:flex;flex-direction:column;gap:8px}.ai-settings-label{font-size:14px;font-weight:600;color:${getColor('text')}}.ai-settings-input{padding:8px 12px;border-radius:6px;border:1px solid ${getColor('border')};font-size:14px;background-color:${getColor('inputBg')};color:${getColor('text')};width:100%;outline:none}.ai-settings-input:focus{border-color:${getColor('primaryText')};box-shadow:0 0 0 2px rgba(3,102,214,0.2)}.ai-toggle-wrapper{display:flex;justify-content:space-between;align-items:center}.ai-toggle{position:relative;display:inline-block;width:44px;height:22px}.ai-toggle input{opacity:0;width:0;height:0}.ai-toggle-slider{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background-color:${getColor('border')};transition:0.4s;border-radius:22px}.ai-toggle-slider:before{position:absolute;content:"";height:18px;width:18px;left:2px;bottom:2px;background-color:white;transition:0.4s;border-radius:50%}input:checked+.ai-toggle-slider{background-color:${getColor('iconBg')}}input:checked+.ai-toggle-slider:before{transform:translateX(22px)}.ai-settings-footer{display:flex;justify-content:flex-end;gap:10px;margin-top:6px}.ai-settings-button{padding:8px 14px;border-radius:6px;font-size:14px;font-weight:500;cursor:pointer;transition:all 0.2s ease}.ai-settings-cancel{background-color:transparent;border:1px solid ${getColor('border')};color:${getColor('text')}}.ai-settings-cancel:hover{background-color:${getColor('border')}}.ai-settings-save{background-color:${getColor('iconBg')};border:1px solid ${getColor('iconBg')};color:white}.ai-settings-save:hover{background-color:${getColor('iconHover')}}.ai-typing{display:inline-block;font-weight:bold;animation:ai-cursor-blink 0.8s infinite;white-space:nowrap}@keyframes ai-cursor-blink{0%{opacity:1}50%{opacity:0}100%{opacity:1}}.ai-code-block{background-color:${isDarkMode?'#1a1a1a':'#f6f8fa'};border-radius:6px;padding:12px;margin:10px 0;overflow-x:auto;font-family:monospace}.ai-markdown p{margin:4px 0}.ai-markdown br{display:block;margin-bottom:2px;content:""}.ai-markdown ul,.ai-markdown ol{padding-left:24px;margin:10px 0}.ai-markdown code{background-color:${isDarkMode?'#1a1a1a':'#f6f8fa'};padding:2px 4px;border-radius:4px;font-family:monospace}.ai-font-size-control{display:flex;align-items:center;gap:10px}.ai-font-button{width:28px;height:28px;display:flex;align-items:center;justify-content:center;border-radius:6px;background-color:${getColor('secondaryBg')};border:1px solid ${getColor('border')};color:${getColor('text')};cursor:pointer}.ai-font-button:hover{background-color:${getColor('border')}}.ai-font-size-display{width:30px;text-align:center;font-size:14px;color:${getColor('text')}}.ai-settings-note{font-size:12px;color:${getColor('secondaryText')};margin-top:4px}.ai-clear-chat{color:#d73a49;cursor:pointer;font-size:14px;display:flex;align-items:center;gap:6px;margin-top:10px}.ai-clear-chat:hover{text-decoration:underline}.ai-model-selector{position:relative}.ai-model-selector select{width:100%;padding:8px 12px;border-radius:6px;border:1px solid ${getColor('border')};background-color:${getColor('inputBg')};color:${getColor('text')};appearance:none;font-size:14px;cursor:pointer}.ai-model-selector::after{content:"▼";font-size:12px;color:${getColor('secondaryText')};position:absolute;right:12px;top:50%;transform:translateY(-50%);pointer-events:none}.ai-debug-toggle{margin-top:5px}.ai-debug-panel{display:${debugMode?'block':'none'};position:fixed;bottom:10px;left:10px;background-color:rgba(0,0,0,0.8);color:white;padding:10px;border-radius:5px;max-height:200px;overflow-y:auto;max-width:500px;z-index:10000;font-size:12px;font-family:monospace}.ai-status-badge{display:inline-block;padding:2px 6px;border-radius:10px;font-size:10px;margin-left:8px;font-weight:normal}.ai-status-online{background-color:#28a745;color:white}.ai-status-error{background-color:#dc3545;color:white}.ai-error-message{color:#dc3545;font-weight:bold;margin-top:4px}.ai-code-container{position:relative;margin:12px 0;border-radius:6px;overflow:hidden;background-color:${isDarkMode?'#1a1a1a':'#f6f8fa'}}.ai-code-header{display:flex;justify-content:space-between;align-items:center;padding:8px 12px;background-color:${isDarkMode?'#2d2d2d':'#e8e8e8'};font-family:monospace;font-size:14px}.ai-copy-button{background:none;border:1px solid ${getColor('border')};color:${getColor('text')};padding:4px 8px;border-radius:4px;cursor:pointer;transition:all 0.2s ease}.ai-copy-button:hover{background-color:${getColor('border')}}.ai-code-block{padding:12px;margin:0;overflow-x:auto;tab-size:4}.ai-inline-code{background-color:${isDarkMode?'#2d2d2d':'#e8e8e8'};padding:2px 4px;border-radius:4px}.ai-markdown-h1{font-size:2em;margin:0.67em 0}.ai-markdown-h2{font-size:1.5em;margin:0.75em 0}.ai-markdown-h3{font-size:1.17em;margin:0.83em 0}.ai-markdown-h4{margin:1.12em 0}.ai-markdown-h5{font-size:0.83em;margin:1.5em 0}.ai-markdown-h6{font-size:0.75em;margin:1.67em 0}`
  98. }
  99.  
  100. function createUI() {
  101. updateStyles();
  102. const icon = document.createElement('div');
  103. icon.className = 'ai-chat-icon';
  104. icon.innerHTML = `<svg viewBox="0 0 24 24"width="24"height="24"style="fill: white;"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z"/><path d="M17 11h-4V7h-2v4H7v2h4v4h2v-4h4z"style="fill: white;"/></svg>`;
  105. let isDragging = false;
  106. let startX, startY;
  107. let startLeft, startTop;
  108. icon.addEventListener('mousedown', function(e) {
  109. isDragging = true;
  110. icon.style.cursor = 'grabbing';
  111. startX = e.clientX;
  112. startY = e.clientY;
  113. const rect = icon.getBoundingClientRect();
  114. startLeft = rect.left;
  115. startTop = rect.top;
  116. e.preventDefault();
  117. e.stopPropagation()
  118. });
  119. document.addEventListener('mousemove', function(e) {
  120. if (!isDragging) return;
  121. const deltaX = e.clientX - startX;
  122. const deltaY = e.clientY - startY;
  123. let newLeft = startLeft + deltaX;
  124. let newTop = startTop + deltaY;
  125. const windowWidth = window.innerWidth;
  126. const windowHeight = window.innerHeight;
  127. const iconWidth = icon.offsetWidth;
  128. const iconHeight = icon.offsetHeight;
  129. newLeft = Math.max(0, Math.min(windowWidth - iconWidth, newLeft));
  130. newTop = Math.max(0, Math.min(windowHeight - iconHeight, newTop));
  131. const threshold = 20;
  132. if (newLeft < threshold) {
  133. newLeft = 0
  134. } else if (newLeft > windowWidth - iconWidth - threshold) {
  135. newLeft = windowWidth - iconWidth
  136. }
  137. if (newTop < threshold) {
  138. newTop = 0
  139. } else if (newTop > windowHeight - iconHeight - threshold) {
  140. newTop = windowHeight - iconHeight
  141. }
  142. icon.style.left = `${newLeft}px`;
  143. icon.style.top = `${newTop}px`
  144. });
  145. document.addEventListener('mouseup', function() {
  146. if (!isDragging) return;
  147. isDragging = false;
  148. icon.style.cursor = 'grab';
  149. const rect = icon.getBoundingClientRect();
  150. GM_setValue('iconPosition', {
  151. x: rect.left,
  152. y: rect.top
  153. })
  154. });
  155. const savedPos = GM_getValue('iconPosition');
  156. let initialLeft, initialTop;
  157. if (savedPos) {
  158. initialLeft = savedPos.x;
  159. initialTop = savedPos.y
  160. } else {
  161. initialLeft = window.innerWidth - 76;
  162. initialTop = window.innerHeight - 76
  163. }
  164. const windowWidth = window.innerWidth;
  165. const windowHeight = window.innerHeight;
  166. const iconWidth = 56;
  167. const iconHeight = 56;
  168. initialLeft = Math.max(0, Math.min(windowWidth - iconWidth, initialLeft));
  169. initialTop = Math.max(0, Math.min(windowHeight - iconHeight, initialTop));
  170. icon.style.left = `${initialLeft}px`;
  171. icon.style.top = `${initialTop}px`;
  172. document.body.appendChild(icon);
  173. const chatWindow = document.createElement('div');
  174. chatWindow.className = 'ai-chat-window';
  175. chatWindow.style.display = 'none';
  176. chatWindow.style.opacity = '0';
  177. chatWindow.style.transform = 'translateY(20px)';
  178. const header = document.createElement('div');
  179. header.className = 'ai-chat-header';
  180. const title = document.createElement('h3');
  181. title.className = 'ai-chat-title';
  182. title.innerHTML = 'AI 助手 <span class="ai-status-badge ai-status-online">V1.0</span>';
  183. const actions = document.createElement('div');
  184. actions.className = 'ai-chat-actions';
  185. const settingsButton = document.createElement('button');
  186. settingsButton.className = 'ai-chat-action ai-settings-button';
  187. settingsButton.innerHTML = '⚙️';
  188. settingsButton.title = '设置';
  189. const themeButton = document.createElement('button');
  190. themeButton.className = 'ai-chat-action ai-theme-button';
  191. themeButton.innerHTML = isDarkMode ? '☀️' : '🌙';
  192. themeButton.title = isDarkMode ? '切换到亮色模式' : '切换到暗色模式';
  193. const minimizeButton = document.createElement('button');
  194. minimizeButton.className = 'ai-chat-action ai-minimize-button';
  195. minimizeButton.innerHTML = '—';
  196. minimizeButton.title = '最小化';
  197. actions.appendChild(settingsButton);
  198. actions.appendChild(themeButton);
  199. actions.appendChild(minimizeButton);
  200. header.appendChild(title);
  201. header.appendChild(actions);
  202. const content = document.createElement('div');
  203. content.className = 'ai-chat-content';
  204. const inputContainer = document.createElement('div');
  205. inputContainer.className = 'ai-chat-input-container';
  206. const inputWrapper = document.createElement('div');
  207. inputWrapper.className = 'ai-chat-input-wrapper';
  208. const textarea = document.createElement('textarea');
  209. textarea.className = 'ai-chat-input';
  210. textarea.placeholder = '输入消息...';
  211. textarea.rows = 1;
  212. const sendButton = document.createElement('button');
  213. sendButton.className = 'ai-chat-send';
  214. sendButton.innerHTML = `<svg viewBox="0 0 24 24"fill="none"xmlns="http://www.w3.org/2000/svg"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"fill="currentColor"></path></svg>`;
  215. inputWrapper.appendChild(textarea);
  216. inputWrapper.appendChild(sendButton);
  217. inputContainer.appendChild(inputWrapper);
  218. chatWindow.appendChild(header);
  219. chatWindow.appendChild(content);
  220. chatWindow.appendChild(inputContainer);
  221. const settingsPanel = document.createElement('div');
  222. settingsPanel.className = 'ai-settings-panel';
  223. settingsPanel.innerHTML = `<div class="ai-settings-group"><label class="ai-settings-label">API密钥</label><input type="password"class="ai-settings-input ai-api-key"value="${apiKey}"placeholder="请输入硅基流动API密钥"><div class="ai-settings-note">请保管好您的API密钥,不要泄露给他人</div></div><div class="ai-settings-group"><label class="ai-settings-label">AI模型</label><div class="ai-model-selector"><select class="ai-settings-input ai-model-select"><option value="deepseek-ai/DeepSeek-V3"${model==='deepseek-ai/DeepSeek-V3'?'selected':''}>DeepSeek-V3</option><option value="deepseek-ai/DeepSeek-R1"${model==='deepseek-ai/DeepSeek-R1'?'selected':''}>DeepSeek-R1</option><option value="deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B"${model==='deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B'?'selected':''}>DeepSeek-R1-Distill-Qwen-1.5B</option><option value="deepseek-ai/DeepSeek-R1-Distill-Qwen-7B"${model==='deepseek-ai/DeepSeek-R1-Distill-Qwen-7B'?'selected':''}>DeepSeek-R1-Distill-Qwen-7B</option><option value="deepseek-ai/DeepSeek-R1-Distill-Qwen-14B"${model==='deepseek-ai/DeepSeek-R1-Distill-Qwen-14B'?'selected':''}>DeepSeek-R1-Distill-Qwen-14B</option><option value="deepseek-ai/DeepSeek-R1-Distill-Qwen-32B"${model==='deepseek-ai/DeepSeek-R1-Distill-Qwen-32B'?'selected':''}>DeepSeek-R1-Distill-Qwen-32B</option><option value="custom"${!['deepseek-ai/DeepSeek-V3','deepseek-ai/DeepSeek-R1','deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B','deepseek-ai/DeepSeek-R1-Distill-Qwen-7B','deepseek-ai/DeepSeek-R1-Distill-Qwen-14B','deepseek-ai/DeepSeek-R1-Distill-Qwen-32B'].includes(model)?'selected':''}>自定义</option></select></div><input type="text"class="ai-settings-input ai-custom-model"placeholder="输入自定义模型名称"style="display: ${!['deepseek-ai/DeepSeek-V3', 'deepseek-ai/deepseek-coder', 'deepseek-ai/DeepSeek-R1-Distill-Qwen-7B', 'mistralai/mistral-small-latest'].includes(model) ? 'block' : 'none'}"value="${!['deepseek-ai/DeepSeek-V3', 'deepseek-ai/deepseek-coder', 'deepseek-ai/DeepSeek-R1-Distill-Qwen-7B', 'mistralai/mistral-small-latest'].includes(model) ? model : ''}"></div><div class="ai-settings-group"><label class="ai-settings-label">字体大小</label><div class="ai-font-size-control"><button class="ai-font-button ai-font-decrease">-</button><span class="ai-font-size-display">${fontSize}</span><button class="ai-font-button ai-font-increase">+</button></div></div><div class="ai-settings-group"><div class="ai-toggle-wrapper"><label class="ai-settings-label">暗黑模式</label><label class="ai-toggle"><input type="checkbox"class="ai-dark-mode-toggle"${isDarkMode?'checked':''}><span class="ai-toggle-slider"></span></label></div></div><div class="ai-settings-group"><div class="ai-toggle-wrapper"><label class="ai-settings-label">调试模式</label><label class="ai-toggle"><input type="checkbox"class="ai-debug-mode-toggle"${debugMode?'checked':''}><span class="ai-toggle-slider"></span></label></div></div><div class="ai-settings-group"><div class="ai-toggle-wrapper"><label class="ai-settings-label">使用流式API</label><label class="ai-toggle"><input type="checkbox"class="ai-stream-mode-toggle"${useStreamMode?'checked':''}><span class="ai-toggle-slider"></span></label></div><div class="ai-settings-note">关闭此选项可使用非流式API进行调试</div></div><div class="ai-clear-chat"><span>🗑️</span>清除聊天记录</div><div class="ai-settings-footer"><button class="ai-settings-button ai-settings-cancel">取消</button><button class="ai-settings-button ai-settings-save">保存</button></div>`;
  224. document.body.appendChild(chatWindow);
  225. document.body.appendChild(settingsPanel);
  226. return {
  227. icon,
  228. chatWindow,
  229. content,
  230. textarea,
  231. sendButton,
  232. settingsButton,
  233. settingsPanel,
  234. themeButton,
  235. minimizeButton
  236. }
  237. }
  238. const ui = createUI();
  239.  
  240. function loadChatHistory() {
  241. ui.content.innerHTML = '';
  242. chatHistory.forEach(msg => {
  243. const messageDiv = document.createElement('div');
  244. messageDiv.className = 'ai-message';
  245. if (msg.role === 'user') {
  246. messageDiv.innerHTML = `<div class="ai-sender"><span>👤</span>你</div><div class="ai-user-message">${msg.content}</div>`
  247. } else {
  248. messageDiv.innerHTML = `<div class="ai-sender"><span>🤖</span>AI</div><div class="ai-response ai-markdown">${formatAIResponse(msg.content)}</div>`
  249. }
  250. ui.content.appendChild(messageDiv)
  251. });
  252. ui.content.scrollTop = ui.content.scrollHeight
  253. }
  254.  
  255. function formatAIResponse(text) {
  256. if (!text) return '';
  257. const blockIdPrefix = 'code_block_' + Math.random().toString(36).substr(2, 9) + '_';
  258. const codeBlocks = [];
  259. text = text.replace(/```([\w]*)\n([\s\S]*?)```/g, function(match, lang, code) {
  260. const id = blockIdPrefix + codeBlocks.length;
  261. codeBlocks.push({
  262. id: id,
  263. lang: lang || 'code',
  264. code: code.endsWith('\n') ? code.slice(0, -1) : code
  265. });
  266. return `<div id="placeholder_${id}"></div>`
  267. });
  268. text = text.replace(/^#{1,6}\s+(.*)$/gm, function(match, content) {
  269. const level = match.match(/^#+/)[0].length;
  270. return `<h${level}class="ai-markdown-h${level}">${content.trim()}</h${level}>`
  271. });
  272. text = text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
  273. text = text.replace(/\*(.*?)\*/g, '<em>$1</em>');
  274. text = text.replace(/`([^`]+)`/g, '<code class="ai-inline-code">$1</code>');
  275. text = text.replace(/\n{2,}/g, '<br><br>');
  276. text = text.replace(/([^>\n])\n([^<\n])/g, '$1<br>$2');
  277. setTimeout(() => {
  278. codeBlocks.forEach(block => {
  279. const placeholder = document.getElementById(`placeholder_${block.id}`);
  280. if (placeholder) {
  281. const container = document.createElement('div');
  282. container.className = 'ai-code-container';
  283. const header = document.createElement('div');
  284. header.className = 'ai-code-header';
  285. const langSpan = document.createElement('span');
  286. langSpan.textContent = block.lang;
  287. header.appendChild(langSpan);
  288. const copyButton = document.createElement('button');
  289. copyButton.className = 'ai-copy-button';
  290. copyButton.textContent = '复制';
  291. copyButton.setAttribute('data-raw-code', block.code);
  292. copyButton.onclick = function() {
  293. const rawCode = this.getAttribute('data-raw-code');
  294. const textArea = document.createElement('textarea');
  295. textArea.value = rawCode;
  296. document.body.appendChild(textArea);
  297. textArea.select();
  298. document.execCommand('copy');
  299. document.body.removeChild(textArea);
  300. this.textContent = '已复制';
  301. setTimeout(() => {
  302. this.textContent = '复制'
  303. }, 2000)
  304. };
  305. header.appendChild(copyButton);
  306. container.appendChild(header);
  307. const pre = document.createElement('pre');
  308. pre.className = 'ai-code-block';
  309. const code = document.createElement('code');
  310. code.textContent = block.code;
  311. pre.appendChild(code);
  312. container.appendChild(pre);
  313. placeholder.parentNode.replaceChild(container, placeholder)
  314. }
  315. })
  316. }, 100);
  317. return text
  318. }
  319. ui.icon.addEventListener('click', () => {
  320. if (ui.chatWindow.style.display === 'none') {
  321. ui.chatWindow.style.display = 'flex';
  322. setTimeout(() => {
  323. ui.chatWindow.style.opacity = '1';
  324. ui.chatWindow.style.transform = 'translateY(0)';
  325. ui.textarea.focus()
  326. }, 10)
  327. } else {
  328. ui.chatWindow.style.opacity = '0';
  329. ui.chatWindow.style.transform = 'translateY(20px)';
  330. setTimeout(() => {
  331. ui.chatWindow.style.display = 'none'
  332. }, 300)
  333. }
  334. });
  335. ui.minimizeButton.addEventListener('click', () => {
  336. ui.chatWindow.style.opacity = '0';
  337. ui.chatWindow.style.transform = 'translateY(20px)';
  338. setTimeout(() => {
  339. ui.chatWindow.style.display = 'none'
  340. }, 300)
  341. });
  342. ui.themeButton.addEventListener('click', () => {
  343. isDarkMode = !isDarkMode;
  344. GM_setValue('isDarkMode', isDarkMode);
  345. ui.themeButton.innerHTML = isDarkMode ? '☀️' : '🌙';
  346. ui.themeButton.title = isDarkMode ? '切换到亮色模式' : '切换到暗色模式';
  347. updateStyles()
  348. });
  349. ui.settingsButton.addEventListener('click', () => {
  350. const rect = ui.settingsButton.getBoundingClientRect();
  351. ui.settingsPanel.style.display = ui.settingsPanel.style.display === 'flex' ? 'none' : 'flex'
  352. });
  353. document.addEventListener('click', (e) => {
  354. if (!ui.settingsPanel.contains(e.target) && e.target !== ui.settingsButton) {
  355. ui.settingsPanel.style.display = 'none'
  356. }
  357. });
  358. const fontDecreaseBtn = ui.settingsPanel.querySelector('.ai-font-decrease');
  359. const fontIncreaseBtn = ui.settingsPanel.querySelector('.ai-font-increase');
  360. const fontSizeDisplay = ui.settingsPanel.querySelector('.ai-font-size-display');
  361. fontDecreaseBtn.addEventListener('click', () => {
  362. if (fontSize > 12) {
  363. fontSize--;
  364. fontSizeDisplay.textContent = fontSize;
  365. updateStyles()
  366. }
  367. });
  368. fontIncreaseBtn.addEventListener('click', () => {
  369. if (fontSize < 20) {
  370. fontSize++;
  371. fontSizeDisplay.textContent = fontSize;
  372. updateStyles()
  373. }
  374. });
  375. const modelSelect = ui.settingsPanel.querySelector('.ai-model-select');
  376. const customModelInput = ui.settingsPanel.querySelector('.ai-custom-model');
  377. modelSelect.addEventListener('change', () => {
  378. if (modelSelect.value === 'custom') {
  379. customModelInput.style.display = 'block'
  380. } else {
  381. customModelInput.style.display = 'none'
  382. }
  383. });
  384. const clearChatBtn = ui.settingsPanel.querySelector('.ai-clear-chat');
  385. clearChatBtn.addEventListener('click', () => {
  386. if (confirm('确定要清除所有聊天记录吗?')) {
  387. chatHistory = [];
  388. GM_setValue('chatHistory', chatHistory);
  389. loadChatHistory()
  390. }
  391. });
  392. const saveBtn = ui.settingsPanel.querySelector('.ai-settings-save');
  393. saveBtn.addEventListener('click', () => {
  394. const newApiKey = ui.settingsPanel.querySelector('.ai-api-key').value;
  395. let newModel = modelSelect.value;
  396. if (newModel === 'custom') {
  397. newModel = customModelInput.value.trim()
  398. }
  399. isDarkMode = ui.settingsPanel.querySelector('.ai-dark-mode-toggle').checked;
  400. debugMode = ui.settingsPanel.querySelector('.ai-debug-mode-toggle').checked;
  401. useStreamMode = ui.settingsPanel.querySelector('.ai-stream-mode-toggle').checked;
  402. apiKey = newApiKey;
  403. model = newModel;
  404. GM_setValue('apiKey', apiKey);
  405. GM_setValue('model', model);
  406. GM_setValue('isDarkMode', isDarkMode);
  407. GM_setValue('fontSize', fontSize);
  408. GM_setValue('debugMode', debugMode);
  409. GM_setValue('useStreamMode', useStreamMode);
  410. ui.themeButton.innerHTML = isDarkMode ? '☀️' : '🌙';
  411. updateStyles();
  412. ui.settingsPanel.style.display = 'none';
  413. if (!apiKey) {
  414. alert('请设置API密钥以使用聊天功能!')
  415. }
  416. debugLog('设置已保存', {
  417. model,
  418. isDarkMode,
  419. debugMode,
  420. useStreamMode
  421. })
  422. });
  423. const cancelBtn = ui.settingsPanel.querySelector('.ai-settings-cancel');
  424. cancelBtn.addEventListener('click', () => {
  425. ui.settingsPanel.style.display = 'none';
  426. ui.settingsPanel.querySelector('.ai-api-key').value = apiKey;
  427. modelSelect.value = ['deepseek-ai/DeepSeek-V3', 'deepseek-ai/deepseek-coder', 'deepseek-ai/DeepSeek-R1-Distill-Qwen-7B', 'mistralai/mistral-small-latest'].includes(model) ? model : 'custom';
  428. if (modelSelect.value === 'custom') {
  429. customModelInput.style.display = 'block';
  430. customModelInput.value = model
  431. } else {
  432. customModelInput.style.display = 'none'
  433. }
  434. ui.settingsPanel.querySelector('.ai-dark-mode-toggle').checked = isDarkMode;
  435. ui.settingsPanel.querySelector('.ai-debug-mode-toggle').checked = debugMode;
  436. ui.settingsPanel.querySelector('.ai-stream-mode-toggle').checked = useStreamMode;
  437. fontSizeDisplay.textContent = fontSize
  438. });
  439. ui.textarea.addEventListener('input', function() {
  440. this.style.height = 'auto';
  441. this.style.height = (this.scrollHeight) + 'px'
  442. });
  443. ui.textarea.addEventListener('keydown', (e) => {
  444. if (e.key === 'Enter' && !e.shiftKey) {
  445. e.preventDefault();
  446. sendMessage()
  447. }
  448. });
  449. ui.sendButton.addEventListener('click', sendMessage);
  450.  
  451. function sendNonStreamRequest(message, aiResponseElement) {
  452. debugLog('发送非流式API请求', {
  453. model,
  454. message: message.substring(0, 30) + '...'
  455. });
  456. return GM_xmlhttpRequest({
  457. method: 'POST',
  458. url: 'https://api.siliconflow.cn/v1/chat/completions',
  459. headers: {
  460. 'Content-Type': 'application/json',
  461. 'Authorization': `Bearer ${apiKey}`
  462. },
  463. data: JSON.stringify({
  464. model: model,
  465. messages: [{
  466. role: 'user',
  467. content: message
  468. }],
  469. stream: false,
  470. temperature: 0.7,
  471. max_tokens: 2048
  472. }),
  473. onload: function(response) {
  474. debugLog('收到完整响应:', response.status, response.statusText);
  475. try {
  476. const data = JSON.parse(response.responseText);
  477. debugLog('解析后数据:', JSON.stringify(data).substring(0, 200) + '...');
  478. if (data.error) {
  479. aiResponseElement.innerHTML = `<span style="color:#ff3860">API错误:${data.error.message||JSON.stringify(data.error)}</span>`;
  480. debugLog('API返回错误:', data.error)
  481. } else {
  482. let content = '';
  483. if (data.choices && data.choices[0]) {
  484. if (data.choices[0].message) {
  485. content = data.choices[0].message.content || ''
  486. } else if (data.choices[0].text) {
  487. content = data.choices[0].text || ''
  488. }
  489. }
  490. debugLog('提取的内容:', content ? (content.substring(0, 50) + '...') : '无内容');
  491. if (content) {
  492. aiResponseElement.innerHTML = formatAIResponse(content);
  493. chatHistory.push({
  494. role: 'assistant',
  495. content: content
  496. });
  497. GM_setValue('chatHistory', chatHistory)
  498. } else {
  499. aiResponseElement.innerHTML = '<span style="color:#ff3860">无法从响应中提取内容</span>';
  500. debugLog('响应中没有内容')
  501. }
  502. }
  503. } catch (e) {
  504. aiResponseElement.innerHTML = `<span style="color:#ff3860">解析响应失败:${e.message}</span>`;
  505. debugLog('解析错误:', e, '原始响应:', response.responseText.substring(0, 200))
  506. }
  507. if (aiResponseElement.querySelector('.ai-typing')) {
  508. aiResponseElement.querySelector('.ai-typing').remove()
  509. }
  510. },
  511. onerror: function(error) {
  512. debugLog('请求错误:', error);
  513. aiResponseElement.innerHTML = `<span style="color:#ff3860">请求失败(${error.status||'网络错误'})</span>`;
  514. if (aiResponseElement.querySelector('.ai-typing')) {
  515. aiResponseElement.querySelector('.ai-typing').remove()
  516. }
  517. },
  518. ontimeout: function() {
  519. debugLog('请求超时');
  520. aiResponseElement.innerHTML = '<span style="color:#ff3860">请求超时</span>';
  521. if (aiResponseElement.querySelector('.ai-typing')) {
  522. aiResponseElement.querySelector('.ai-typing').remove()
  523. }
  524. }
  525. })
  526. }
  527.  
  528. function sendStreamRequest(message, aiResponseElement) {
  529. debugLog('开始流式请求,使用HTML示例的方法');
  530. aiResponseElement.innerHTML = '<span class="ai-typing">连接中...</span>';
  531. let fullText = '';
  532. let aborted = false;
  533. const controller = new AbortController();
  534. (async function() {
  535. try {
  536. debugLog('发送fetch请求');
  537. const response = await fetch('https://api.siliconflow.cn/v1/chat/completions', {
  538. method: 'POST',
  539. headers: {
  540. 'Authorization': `Bearer ${apiKey}`,
  541. 'Content-Type': 'application/json'
  542. },
  543. body: JSON.stringify({
  544. model: model,
  545. stream: true,
  546. messages: [{
  547. role: "user",
  548. content: message
  549. }],
  550. temperature: 0.7,
  551. max_tokens: 2048
  552. }),
  553. signal: controller.signal
  554. });
  555. debugLog('收到响应:', response.status, response.statusText);
  556. if (!response.ok) {
  557. throw new Error(`服务器返回错误:${response.status}${response.statusText}`)
  558. }
  559. if (!response.body) {
  560. throw new Error('没有可读的响应体');
  561. }
  562. const reader = response.body.getReader();
  563. const decoder = new TextDecoder();
  564. let buffer = '';
  565. if (aiResponseElement.querySelector('.ai-typing')) {
  566. aiResponseElement.querySelector('.ai-typing').textContent = '接收数据中...'
  567. }
  568. while (!aborted) {
  569. const {
  570. done,
  571. value
  572. } = await reader.read();
  573. if (done) break;
  574. buffer += decoder.decode(value, {
  575. stream: true
  576. });
  577. debugLog(`接收到数据块:${value.length}字节`);
  578. const chunks = buffer.split('\n');
  579. buffer = chunks.pop() || '';
  580. for (const chunk of chunks) {
  581. const trimmed = chunk.trim();
  582. if (!trimmed) continue;
  583. if (trimmed.startsWith('data: ')) {
  584. try {
  585. const dataContent = trimmed.replace(/^data: /, '');
  586. if (dataContent === '[DONE]') continue;
  587. const json = JSON.parse(dataContent);
  588. const content = json.choices[0].delta.content;
  589. if (content) {
  590. debugLog('提取到内容:', content);
  591. fullText += content;
  592. if (aiResponseElement.querySelector('.ai-typing')) {
  593. aiResponseElement.querySelector('.ai-typing').remove()
  594. }
  595. aiResponseElement.innerHTML = formatAIResponse(fullText);
  596. ui.content.scrollTop = ui.content.scrollHeight
  597. }
  598. } catch (e) {
  599. debugLog('解析JSON失败:', e.message, '数据:', trimmed.substring(0, 50))
  600. }
  601. }
  602. }
  603. }
  604. if (buffer && !aborted) {
  605. try {
  606. const dataContent = buffer.replace(/^data: /, '');
  607. if (dataContent !== '[DONE]') {
  608. const json = JSON.parse(dataContent);
  609. if (json.choices[0].delta.content) {
  610. fullText += json.choices[0].delta.content;
  611. aiResponseElement.innerHTML = formatAIResponse(fullText)
  612. }
  613. }
  614. } catch (e) {
  615. debugLog('最终解析失败:', e.message)
  616. }
  617. }
  618. if (aiResponseElement.querySelector('.ai-typing')) {
  619. aiResponseElement.querySelector('.ai-typing').remove()
  620. }
  621. if (fullText) {
  622. chatHistory.push({
  623. role: 'assistant',
  624. content: fullText
  625. });
  626. GM_setValue('chatHistory', chatHistory);
  627. debugLog('聊天历史已保存')
  628. }
  629. } catch (error) {
  630. debugLog('流处理失败:', error);
  631. if (!aborted) {
  632. if (error.message.includes('401') && !apiKey.startsWith('sk-')) {
  633. aiResponseElement.innerHTML = `<span style="color:#ff3860">API密钥格式可能不正确。硅基流动API密钥通常以sk-开头。${apiKey}</span>`;
  634. setTimeout(() => {
  635. ui.settingsButton.click()
  636. }, 1000)
  637. } else {
  638. debugLog('尝试使用非流式API作为备选');
  639. aiResponseElement.innerHTML = '<span class="ai-typing">使用备选方法请求中...</span>';
  640. sendNonStreamRequest(message, aiResponseElement)
  641. }
  642. } else {
  643. aiResponseElement.innerHTML = '<span style="color:#ff3860">请求已中断</span>'
  644. }
  645. }
  646. })();
  647. return {
  648. abort: () => {
  649. debugLog('中断流式请求');
  650. aborted = true;
  651. controller.abort()
  652. }
  653. }
  654. }
  655.  
  656. function sendNonStreamRequest(message, aiResponseElement) {
  657. debugLog('发送非流式API请求');
  658. const requestData = {
  659. model: model,
  660. messages: [{
  661. role: "user",
  662. content: message
  663. }],
  664. stream: false,
  665. max_tokens: 2048,
  666. temperature: 0.7,
  667. top_p: 0.7,
  668. top_k: 50,
  669. frequency_penalty: 0.5,
  670. n: 1
  671. };
  672. return GM_xmlhttpRequest({
  673. method: 'POST',
  674. url: 'https://api.siliconflow.cn/v1/chat/completions',
  675. headers: {
  676. 'Authorization': `Bearer ${apiKey}`,
  677. 'Content-Type': 'application/json'
  678. },
  679. data: JSON.stringify(requestData),
  680. onload: function(response) {
  681. debugLog('收到非流式响应:', response.status);
  682. try {
  683. const data = JSON.parse(response.responseText);
  684. if (data.error) {
  685. aiResponseElement.innerHTML = `<span style="color:#ff3860">API错误:${data.error.message||JSON.stringify(data.error)}</span>`;
  686. debugLog('API返回错误:', data.error)
  687. } else {
  688. let content = '';
  689. if (data.choices && data.choices[0]) {
  690. if (data.choices[0].message) {
  691. content = data.choices[0].message.content || ''
  692. } else if (data.choices[0].text) {
  693. content = data.choices[0].text || ''
  694. }
  695. }
  696. debugLog('提取的内容:', content ? (content.substring(0, 50) + '...') : '无内容');
  697. if (content) {
  698. aiResponseElement.innerHTML = formatAIResponse(content);
  699. chatHistory.push({
  700. role: 'assistant',
  701. content: content
  702. });
  703. GM_setValue('chatHistory', chatHistory)
  704. } else {
  705. aiResponseElement.innerHTML = '<span style="color:#ff3860">无法从响应中提取内容</span>';
  706. debugLog('响应中没有内容')
  707. }
  708. }
  709. } catch (e) {
  710. aiResponseElement.innerHTML = `<span style="color:#ff3860">解析响应失败:${e.message}</span>`;
  711. debugLog('解析错误:', e, '原始响应:', response.responseText.substring(0, 200))
  712. }
  713. if (aiResponseElement.querySelector('.ai-typing')) {
  714. aiResponseElement.querySelector('.ai-typing').remove()
  715. }
  716. },
  717. onerror: function(error) {
  718. debugLog('请求错误:', error);
  719. aiResponseElement.innerHTML = `<span style="color:#ff3860">请求失败(${error.status||'网络错误'})</span>`;
  720. if (aiResponseElement.querySelector('.ai-typing')) {
  721. aiResponseElement.querySelector('.ai-typing').remove()
  722. }
  723. }
  724. })
  725. }
  726.  
  727. function sendMessage() {
  728. const message = ui.textarea.value.trim();
  729. if (!message) return;
  730. if (!apiKey) {
  731. alert('请先设置API密钥!');
  732. ui.settingsPanel.style.display = 'flex';
  733. return
  734. }
  735. if (currentStream) {
  736. try {
  737. currentStream.abort()
  738. } catch (e) {
  739. debugLog('中断请求失败:', e)
  740. }
  741. currentStream = null
  742. }
  743. debugLog('开始发送消息', '模式:', useStreamMode ? '流式' : '非流式');
  744. const userMessageDiv = document.createElement('div');
  745. userMessageDiv.className = 'ai-message';
  746. userMessageDiv.innerHTML = `<div class="ai-sender"><span>👤</span>你</div><div class="ai-user-message">${message}</div>`;
  747. ui.content.appendChild(userMessageDiv);
  748. const aiMessageDiv = document.createElement('div');
  749. aiMessageDiv.className = 'ai-message';
  750. aiMessageDiv.innerHTML = `<div class="ai-sender"><span>🤖</span>AI</div><div class="ai-response"><span class="ai-typing">▌</span></div>`;
  751. ui.content.appendChild(aiMessageDiv);
  752. ui.content.scrollTop = ui.content.scrollHeight;
  753. ui.textarea.value = '';
  754. ui.textarea.style.height = 'auto';
  755. chatHistory.push({
  756. role: 'user',
  757. content: message
  758. });
  759. const aiResponseElement = aiMessageDiv.querySelector('.ai-response');
  760. try {
  761. if (useStreamMode) {
  762. currentStream = sendStreamRequest(message, aiResponseElement)
  763. } else {
  764. currentStream = sendNonStreamRequest(message, aiResponseElement)
  765. }
  766. } catch (e) {
  767. debugLog('发送请求失败:', e.message);
  768. aiResponseElement.innerHTML = `<span style="color:#ff3860">发送请求失败:${e.message}</span>`;
  769. if (aiResponseElement.querySelector('.ai-typing')) {
  770. aiResponseElement.querySelector('.ai-typing').remove()
  771. }
  772. }
  773. }
  774. loadChatHistory();
  775. GM_registerMenuCommand('打开AI助手', () => {
  776. ui.icon.click()
  777. });
  778. GM_registerMenuCommand('设置', () => {
  779. ui.icon.click();
  780. setTimeout(() => {
  781. ui.settingsButton.click()
  782. }, 300)
  783. });
  784. GM_registerMenuCommand('切换调试模式', () => {
  785. debugMode = !debugMode;
  786. GM_setValue('debugMode', debugMode);
  787. alert('调试模式已' + (debugMode ? '开启' : '关闭'));
  788. updateStyles()
  789. });
  790. debugLog('初始化完成', '版本: 2.2', '模型:', model)
  791. })();

QingJ © 2025

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