Greasy Fork is available in English.
AI对话侧边栏,支持多AI提供商配置
// ==UserScript== // @name AI Chat Sidebar // @namespace http://tampermonkey.net/ // @version 1.0 // @description AI对话侧边栏,支持多AI提供商配置 // @author You // @match *://*/* // @grant GM_setValue // @grant GM_getValue // @grant GM_xmlhttpRequest // @license GPL-3.0 // @connect * // @require https://cdn.jsdelivr.net/npm/[email protected]/marked.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js // ==/UserScript== (function() { 'use strict'; // 配置管理 // Trusted Types 策略支持 let ttPolicy; if (window.trustedTypes && window.trustedTypes.createPolicy) { try { ttPolicy = window.trustedTypes.createPolicy('ai-chat-sidebar-policy', { createHTML: (string) => string }); } catch (e) { console.warn('Failed to create Trusted Types policy:', e); } } const safeInnerHTML = (element, html, fallbackText) => { if (!element) return; try { if (ttPolicy) { element.innerHTML = ttPolicy.createHTML(html); } else { element.innerHTML = html; } } catch (e) { console.warn('innerHTML assignment failed:', e); element.textContent = fallbackText !== undefined ? fallbackText : html; } }; const ConfigManager = { get: (key, defaultValue) => GM_getValue(key, defaultValue), set: (key, value) => GM_setValue(key, value), getProviders: () => GM_getValue('ai_providers', []), saveProviders: (providers) => GM_setValue('ai_providers', providers), getPrompts: () => GM_getValue('prompts', []), savePrompts: (prompts) => GM_setValue('prompts', prompts), getModels: (providerIndex) => GM_getValue(`models_${providerIndex}`, []), saveModels: (providerIndex, models) => GM_setValue(`models_${providerIndex}`, models), getAvailableModels: (providerIndex) => GM_getValue(`available_models_${providerIndex}`, []), saveAvailableModels: (providerIndex, models) => GM_setValue(`available_models_${providerIndex}`, models), getConversations: () => GM_getValue('conversations', []), saveConversations: (conversations) => GM_setValue('conversations', conversations), getTheme: () => GM_getValue('theme', 'default'), saveTheme: (theme) => GM_setValue('theme', theme), getSystemConfig: () => GM_getValue('system_config', {defaultModel: null, defaultPrompt: null}), saveSystemConfig: (config) => GM_setValue('system_config', config) }; const THEMES = { default: { name: '默认极光', colors: { primary: '#667eea', primaryGradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', bg: '#ffffff', bgSecondary: '#f8f9fa', text: '#333333', textSecondary: '#666666', border: '#e1e4e8', hover: '#f1f3f5', userMsgBg: '#667eea', userMsgText: '#ffffff', aiMsgBg: '#f1f3f5', aiMsgText: '#333333', shadow: 'rgba(0,0,0,0.1)' }, styles: { borderRadius: '4px', btnRadius: '4px', fontFamily: 'inherit', borderWidth: '1px', shadowLg: '-2px 0 8px rgba(0,0,0,0.1)', spacing: '15px', fontSize: '14px', fontWeight: '400', headerHeight: 'auto', transition: 'all 0.2s ease' } }, notion: { name: 'Notion风格', colors: { primary: '#333333', primaryGradient: '#ffffff', bg: '#ffffff', bgSecondary: '#f7f7f5', text: '#37352f', textSecondary: 'rgba(55, 53, 47, 0.65)', border: '#e9e9e8', hover: 'rgba(55, 53, 47, 0.08)', userMsgBg: 'transparent', // Notion style: minimal background userMsgText: '#37352f', aiMsgBg: 'transparent', aiMsgText: '#37352f', shadow: 'rgba(15, 15, 15, 0.05)' }, styles: { borderRadius: '3px', btnRadius: '3px', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, "Apple Color Emoji", Arial, sans-serif', borderWidth: '1px', shadowLg: '0 0 0 1px rgba(15,15,15,0.02), 0 3px 6px rgba(15,15,15,0.04)', spacing: '12px', fontSize: '14px', fontWeight: '400', headerHeight: '45px', transition: 'background 0.1s ease' } }, youtube: { name: 'YouTube风格', colors: { primary: '#ff0000', primaryGradient: '#ffffff', bg: '#ffffff', bgSecondary: '#f2f2f2', text: '#0f0f0f', textSecondary: '#606060', border: 'transparent', // Flat design hover: '#e5e5e5', userMsgBg: '#f2f2f2', userMsgText: '#0f0f0f', aiMsgBg: '#ffffff', aiMsgText: '#0f0f0f', shadow: 'rgba(0,0,0,0.1)' }, styles: { borderRadius: '12px', btnRadius: '18px', // Pill shape fontFamily: 'Roboto, Arial, sans-serif', borderWidth: '0px', shadowLg: '0 4px 12px rgba(0,0,0,0.08)', spacing: '16px', fontSize: '14px', fontWeight: '400', headerHeight: '56px', transition: 'background 0.2s cubic-bezier(0.2, 0, 0, 1)' } }, github: { name: 'GitHub风格', colors: { primary: '#2da44e', primaryGradient: '#24292f', bg: '#ffffff', bgSecondary: '#f6f8fa', text: '#24292f', textSecondary: '#57606a', border: '#d0d7de', hover: '#f3f4f6', userMsgBg: '#ddf4ff', userMsgText: '#24292f', aiMsgBg: '#f6f8fa', aiMsgText: '#24292f', shadow: 'rgba(140,149,159,0.2)' }, styles: { borderRadius: '6px', btnRadius: '6px', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif', borderWidth: '1px', shadowLg: '0 8px 24px rgba(140,149,159,0.2)', spacing: '16px', fontSize: '14px', fontWeight: '400', headerHeight: '60px', transition: 'all 0.2s cubic-bezier(0.3, 0, 0.5, 1)' } }, discord: { name: 'Discord风格', colors: { primary: '#5865F2', primaryGradient: '#313338', bg: '#313338', bgSecondary: '#2b2d31', text: '#dbdee1', textSecondary: '#949ba4', border: '#1e1f22', hover: '#3f4147', userMsgBg: '#5865F2', userMsgText: '#ffffff', aiMsgBg: '#2b2d31', aiMsgText: '#dbdee1', shadow: 'rgba(0,0,0,0.2)' }, styles: { borderRadius: '8px', btnRadius: '4px', fontFamily: '"gg sans", "Noto Sans", "Helvetica Neue", Helvetica, Arial, sans-serif', borderWidth: '0px', shadowLg: '0 0 10px rgba(0,0,0,0.5)', spacing: '16px', fontSize: '15px', fontWeight: '500', headerHeight: '48px', transition: 'background 0.15s ease-out' } }, apple: { name: 'Apple风格', colors: { primary: '#0071e3', primaryGradient: 'rgba(255, 255, 255, 0.72)', // Glassmorphism bg: 'rgba(255, 255, 255, 0.72)', bgSecondary: 'rgba(245, 245, 247, 0.5)', text: '#1d1d1f', textSecondary: '#86868b', border: 'rgba(0,0,0,0.05)', hover: 'rgba(0,0,0,0.03)', userMsgBg: '#0071e3', userMsgText: '#ffffff', aiMsgBg: 'rgba(255,255,255,0.5)', aiMsgText: '#1d1d1f', shadow: 'rgba(0,0,0,0.1)' }, styles: { borderRadius: '12px', btnRadius: '980px', // Fully rounded fontFamily: '-apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", sans-serif', borderWidth: '1px', shadowLg: '0 20px 40px rgba(0,0,0,0.1)', backdropFilter: 'saturate(180%) blur(20px)', spacing: '18px', fontSize: '15px', fontWeight: '400', headerHeight: '52px', transition: 'all 0.3s cubic-bezier(0.25, 0.1, 0.25, 1)' } } }; const applyTheme = (themeName) => { const theme = THEMES[themeName] || THEMES.default; const sidebar = document.getElementById('ai-chat-sidebar'); if (!sidebar) return; const setVar = (name, value) => { sidebar.style.setProperty(`--ai-${name}`, value); }; Object.entries(theme.colors).forEach(([key, value]) => { setVar(key, value); }); // Apply default styles if not present in theme const defaultStyles = THEMES.default.styles; const styles = { ...defaultStyles, ...(theme.styles || {}) }; Object.entries(styles).forEach(([key, value]) => { setVar(key, value); }); // Special handling for backdrop filter (Apple style) if (styles.backdropFilter) { sidebar.style.backdropFilter = styles.backdropFilter; sidebar.style.webkitBackdropFilter = styles.backdropFilter; } else { sidebar.style.backdropFilter = 'none'; sidebar.style.webkitBackdropFilter = 'none'; } // Special handling for header text color based on gradient const header = sidebar.querySelector('.sidebar-header'); if (header) { if (themeName === 'notion' || themeName === 'youtube' || themeName === 'apple') { header.style.color = '#333'; header.style.borderBottom = `1px solid ${theme.colors.border}`; header.querySelectorAll('button').forEach(btn => { if (!btn.classList.contains('close-btn')) { btn.style.color = '#333'; btn.style.background = 'rgba(0,0,0,0.05)'; } }); const closeBtn = header.querySelector('.close-btn'); if (closeBtn) closeBtn.style.color = '#333'; } else { header.style.color = 'white'; header.style.borderBottom = 'none'; header.querySelectorAll('button').forEach(btn => { if (!btn.classList.contains('close-btn')) { btn.style.color = 'white'; btn.style.background = 'rgba(255,255,255,0.2)'; } }); const closeBtn = header.querySelector('.close-btn'); if (closeBtn) closeBtn.style.color = 'white'; } } ConfigManager.saveTheme(themeName); }; // 创建侧边栏HTML const createSidebar = () => { const sidebar = document.createElement('div'); sidebar.id = 'ai-chat-sidebar'; // 创建调整大小的手柄 ['left', 'right', 'top', 'bottom'].forEach(pos => { const handle = document.createElement('div'); handle.className = `resize-handle-${pos}`; sidebar.appendChild(handle); }); ['tl', 'tr', 'bl', 'br'].forEach(pos => { const handle = document.createElement('div'); handle.className = `resize-handle-corner-${pos}`; sidebar.appendChild(handle); }); // 创建头部 const header = document.createElement('div'); header.className = 'sidebar-header'; const tabs = document.createElement('div'); tabs.className = 'tabs'; tabs.id = 'tabs-container'; ['chat', 'providers', 'prompts', 'system'].forEach((tab, i) => { const btn = document.createElement('button'); btn.className = i === 0 ? 'tab active' : 'tab'; btn.dataset.tab = tab; btn.textContent = tab === 'chat' ? '对话' : tab === 'providers' ? 'AI提供商' : tab === 'prompts' ? '提示词库' : '系统配置'; tabs.appendChild(btn); }); const controls = document.createElement('div'); controls.className = 'header-controls'; const themeBtn = document.createElement('button'); themeBtn.className = 'theme-btn'; themeBtn.textContent = '🎨'; themeBtn.title = '切换主题'; const themeDropdown = document.createElement('div'); themeDropdown.className = 'theme-dropdown'; Object.entries(THEMES).forEach(([key, theme]) => { const item = document.createElement('div'); item.className = 'theme-item'; item.dataset.theme = key; item.innerHTML = ` <span class="theme-preview" style="background: ${theme.colors.primaryGradient}"></span> <span>${theme.name}</span> `; themeDropdown.appendChild(item); }); const closeBtn = document.createElement('button'); closeBtn.className = 'close-btn'; closeBtn.textContent = '×'; controls.appendChild(themeBtn); controls.appendChild(themeDropdown); controls.appendChild(closeBtn); header.appendChild(tabs); header.appendChild(controls); sidebar.appendChild(header); // 创建内容区 const content = document.createElement('div'); content.className = 'sidebar-content'; // 对话标签页 const chatTab = document.createElement('div'); chatTab.className = 'tab-content active'; chatTab.id = 'chat-tab'; const chatContainer = document.createElement('div'); chatContainer.className = 'chat-container'; const conversationsSidebar = document.createElement('div'); conversationsSidebar.className = 'conversations-sidebar'; conversationsSidebar.id = 'conversations-sidebar'; const chatMain = document.createElement('div'); chatMain.className = 'chat-main'; const modelSelector = document.createElement('div'); modelSelector.className = 'model-selector'; const modelBtn = document.createElement('button'); modelBtn.id = 'model-display-btn'; const modelName = document.createElement('span'); modelName.id = 'model-name'; modelName.textContent = '选择模型'; const arrow = document.createElement('span'); arrow.className = 'arrow'; arrow.textContent = '▼'; modelBtn.appendChild(modelName); modelBtn.appendChild(arrow); const modelDropdown = document.createElement('div'); modelDropdown.id = 'model-dropdown'; modelDropdown.className = 'model-dropdown'; modelDropdown.style.display = 'none'; modelSelector.appendChild(modelBtn); modelSelector.appendChild(modelDropdown); const messages = document.createElement('div'); messages.className = 'messages'; messages.id = 'messages'; const inputArea = document.createElement('div'); inputArea.className = 'input-area'; const inputWrapper = document.createElement('div'); inputWrapper.className = 'input-wrapper'; const btnContainer = document.createElement('div'); btnContainer.style.display = 'flex'; btnContainer.style.gap = '5px'; const newChatBtn = document.createElement('button'); newChatBtn.id = 'new-chat-btn'; newChatBtn.className = 'prompt-icon-top'; newChatBtn.title = '新建对话'; newChatBtn.textContent = '➕'; const promptBtn = document.createElement('button'); promptBtn.id = 'prompt-selector-btn'; promptBtn.className = 'prompt-icon-top'; promptBtn.title = '选择提示词'; promptBtn.textContent = '💡'; const paramsBtn = document.createElement('button'); paramsBtn.id = 'params-selector-btn'; paramsBtn.className = 'prompt-icon-top'; paramsBtn.title = '模型参数'; paramsBtn.textContent = '⚙️'; const clearBtn = document.createElement('button'); clearBtn.id = 'clear-chat-btn'; clearBtn.className = 'prompt-icon-top'; clearBtn.title = '清除对话'; clearBtn.textContent = '🗑️'; const summarizeBtn = document.createElement('button'); summarizeBtn.id = 'summarize-page-btn'; summarizeBtn.className = 'prompt-icon-top'; summarizeBtn.title = '总结网页'; summarizeBtn.textContent = '📄'; btnContainer.appendChild(newChatBtn); btnContainer.appendChild(promptBtn); btnContainer.appendChild(paramsBtn); btnContainer.appendChild(clearBtn); btnContainer.appendChild(summarizeBtn); const textarea = document.createElement('textarea'); textarea.id = 'user-input'; textarea.placeholder = '输入消息...'; inputWrapper.appendChild(btnContainer); inputWrapper.appendChild(textarea); const sendBtn = document.createElement('button'); sendBtn.id = 'send-btn'; sendBtn.textContent = '发送'; const promptDropdown = document.createElement('div'); promptDropdown.id = 'prompt-dropdown'; promptDropdown.className = 'prompt-dropdown'; promptDropdown.style.display = 'none'; const paramsPanel = document.createElement('div'); paramsPanel.id = 'params-panel'; paramsPanel.className = 'params-panel'; paramsPanel.style.display = 'none'; const tempItem = document.createElement('div'); tempItem.className = 'params-item'; const tempLabel = document.createElement('label'); tempLabel.textContent = '温度 (Temperature):'; const tempInput = document.createElement('input'); tempInput.type = 'number'; tempInput.id = 'param-temperature'; tempInput.min = '0'; tempInput.max = '2'; tempInput.step = '0.1'; tempInput.value = '0.7'; tempItem.appendChild(tempLabel); tempItem.appendChild(tempInput); const tokensItem = document.createElement('div'); tokensItem.className = 'params-item'; const tokensLabel = document.createElement('label'); tokensLabel.textContent = '最大上下文 (Max Tokens):'; const tokensInput = document.createElement('input'); tokensInput.type = 'number'; tokensInput.id = 'param-max-tokens'; tokensInput.min = '1'; tokensInput.step = '1'; tokensInput.value = '2048'; tokensItem.appendChild(tokensLabel); tokensItem.appendChild(tokensInput); const memoryItem = document.createElement('div'); memoryItem.className = 'params-item'; const memoryLabel = document.createElement('label'); memoryLabel.textContent = '记忆轮数:'; const memoryInput = document.createElement('input'); memoryInput.type = 'number'; memoryInput.id = 'param-memory-rounds'; memoryInput.min = '0'; memoryInput.step = '1'; memoryInput.value = '15'; memoryInput.title = '设置为0表示不限制'; memoryItem.appendChild(memoryLabel); memoryItem.appendChild(memoryInput); paramsPanel.appendChild(tempItem); paramsPanel.appendChild(tokensItem); paramsPanel.appendChild(memoryItem); inputArea.appendChild(inputWrapper); inputArea.appendChild(sendBtn); inputArea.appendChild(promptDropdown); inputArea.appendChild(paramsPanel); chatMain.appendChild(modelSelector); chatMain.appendChild(messages); chatMain.appendChild(inputArea); chatContainer.appendChild(conversationsSidebar); chatContainer.appendChild(chatMain); chatTab.appendChild(chatContainer); // 提供商标签页 const providersTab = document.createElement('div'); providersTab.className = 'tab-content'; providersTab.id = 'providers-tab'; const providersContainer = document.createElement('div'); providersContainer.className = 'providers-container'; const providersSidebar = document.createElement('div'); providersSidebar.className = 'providers-sidebar'; const addProviderBtn = document.createElement('button'); addProviderBtn.id = 'add-provider-btn'; addProviderBtn.textContent = '+ 添加供应商'; const providersList = document.createElement('div'); providersList.className = 'providers-list'; providersList.id = 'providers-sidebar-list'; providersSidebar.appendChild(addProviderBtn); providersSidebar.appendChild(providersList); const providerDetail = document.createElement('div'); providerDetail.className = 'provider-detail'; providerDetail.id = 'provider-detail'; const emptyState = document.createElement('div'); emptyState.className = 'empty-state'; emptyState.textContent = '请选择或添加一个供应商'; providerDetail.appendChild(emptyState); providersContainer.appendChild(providersSidebar); providersContainer.appendChild(providerDetail); providersTab.appendChild(providersContainer); // 提示词标签页 const promptsTab = document.createElement('div'); promptsTab.className = 'tab-content'; promptsTab.id = 'prompts-tab'; const promptsToolbar = document.createElement('div'); promptsToolbar.className = 'prompts-toolbar'; const addPromptBtn = document.createElement('button'); addPromptBtn.id = 'add-prompt'; addPromptBtn.textContent = '+ 新增'; const batchDeleteBtn = document.createElement('button'); batchDeleteBtn.id = 'batch-delete-prompt'; batchDeleteBtn.textContent = '批量删除'; promptsToolbar.appendChild(addPromptBtn); promptsToolbar.appendChild(batchDeleteBtn); const promptsList = document.createElement('div'); promptsList.className = 'prompts-list'; promptsList.id = 'prompts-list'; promptsTab.appendChild(promptsToolbar); promptsTab.appendChild(promptsList); // 系统配置标签页 const systemTab = document.createElement('div'); systemTab.className = 'tab-content'; systemTab.id = 'system-tab'; const systemContainer = document.createElement('div'); systemContainer.className = 'system-config-container'; systemContainer.innerHTML = ` <h3>系统配置</h3> <div class="config-section"> <h4>新建对话默认设置</h4> <div class="form-group"> <label>默认模型</label> <select id="default-model-select" class="config-select"> <option value="">未设置</option> </select> </div> <div class="form-group"> <label>默认提示词</label> <select id="default-prompt-select" class="config-select"> <option value="">未设置</option> </select> </div> <button id="save-system-config" class="save-btn">保存配置</button> </div> `; systemTab.appendChild(systemContainer); content.appendChild(chatTab); content.appendChild(providersTab); content.appendChild(promptsTab); content.appendChild(systemTab); sidebar.appendChild(content); document.body.appendChild(sidebar); return sidebar; }; // 创建触发按钮 const createTriggerButton = () => { const btn = document.createElement('button'); btn.id = 'ai-chat-trigger'; btn.textContent = '💬'; btn.title = 'AI对话'; document.body.appendChild(btn); return btn; }; // 添加样式 const addStyles = () => { // 添加highlight.js样式 const highlightStyle = document.createElement('link'); highlightStyle.rel = 'stylesheet'; highlightStyle.href = 'https://cdn.jsdelivr.net/gh/highlightjs/[email protected]/build/styles/github-dark.min.css'; document.head.appendChild(highlightStyle); const style = document.createElement('style'); style.textContent = ` #ai-chat-trigger { position: fixed; right: 20px; bottom: 20px; width: 60px; height: 60px; border-radius: 50%; background: var(--ai-primaryGradient, linear-gradient(135deg, #667eea 0%, #764ba2 100%)); border: none; color: white; font-size: 28px; cursor: pointer; box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 9998; transition: transform 0.2s; } #ai-chat-trigger:hover { transform: scale(1.1); } #ai-chat-sidebar { position: fixed; top: 0; right: 0; width: 400px; height: 100vh; background: var(--ai-bg, white); color: var(--ai-text, #333); box-shadow: var(--ai-shadowLg, -2px 0 8px rgba(0,0,0,0.1)); z-index: 9999; display: none; flex-direction: column; border-radius: 0; font-family: var(--ai-fontFamily, inherit); } #ai-chat-sidebar.open { display: flex; } #ai-chat-sidebar.resizing, #ai-chat-sidebar.dragging { transition: none; } .resize-handle-left, .resize-handle-right { position: absolute; top: 0; width: 5px; height: 100%; cursor: ew-resize; z-index: 10; } .resize-handle-left { left: 0; } .resize-handle-right { right: 0; } .resize-handle-top, .resize-handle-bottom { position: absolute; left: 0; width: 100%; height: 5px; cursor: ns-resize; z-index: 10; } .resize-handle-top { top: 0; } .resize-handle-bottom { bottom: 0; } .resize-handle-corner-tl, .resize-handle-corner-tr, .resize-handle-corner-bl, .resize-handle-corner-br { position: absolute; width: 10px; height: 10px; z-index: 11; } .resize-handle-corner-tl { top: 0; left: 0; cursor: nwse-resize; } .resize-handle-corner-tr { top: 0; right: 0; cursor: nesw-resize; } .resize-handle-corner-bl { bottom: 0; left: 0; cursor: nesw-resize; } .resize-handle-corner-br { bottom: 0; right: 0; cursor: nwse-resize; } .sidebar-header { padding: 0 var(--ai-spacing, 15px); height: var(--ai-headerHeight, auto); min-height: 50px; background: var(--ai-primaryGradient, linear-gradient(135deg, #667eea 0%, #764ba2 100%)); color: white; display: flex; gap: 10px; align-items: center; cursor: move; user-select: none; flex-wrap: wrap; font-weight: 600; transition: var(--ai-transition, all 0.2s ease); } .header-controls { display: flex; align-items: center; gap: 8px; margin-left: auto; position: relative; } .theme-btn { background: rgba(255,255,255,0.2); border: none; color: white; width: 28px; height: 28px; border-radius: var(--ai-btnRadius, 4px); cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 14px; transition: background 0.2s; } .theme-btn:hover { background: rgba(255,255,255,0.3); } .theme-dropdown { position: absolute; top: 100%; right: 0; margin-top: 8px; background: var(--ai-bg, white); border: var(--ai-borderWidth, 1px) solid var(--ai-border, #ddd); border-radius: var(--ai-borderRadius, 4px); box-shadow: var(--ai-shadowLg, 0 2px 8px rgba(0,0,0,0.1)); padding: 5px; display: none; z-index: 1000; min-width: 150px; } .theme-dropdown.show { display: block; } .theme-item { padding: 8px 12px; cursor: pointer; display: flex; align-items: center; gap: 8px; border-radius: var(--ai-borderRadius, 4px); color: var(--ai-text, #333); font-size: 13px; } .theme-item:hover { background: var(--ai-hover, #f5f5f5); } .theme-preview { width: 16px; height: 16px; border-radius: 50%; border: 1px solid rgba(0,0,0,0.1); } .tabs { display: flex; gap: 10px; flex-wrap: wrap; flex: 1; } .tab { background: transparent; border: none; color: inherit; padding: 8px 12px; border-radius: var(--ai-btnRadius, 4px); cursor: pointer; transition: var(--ai-transition, all 0.2s ease); font-family: inherit; font-size: var(--ai-fontSize, 14px); font-weight: 500; opacity: 0.8; } .tab:hover { opacity: 1; background: rgba(255,255,255,0.1); } .tab.active { opacity: 1; background: rgba(255,255,255,0.2); font-weight: 600; } .close-btn { background: none; border: none; color: white; font-size: 28px; cursor: pointer; line-height: 1; padding: 0 5px; } .sidebar-content { flex: 1; overflow: hidden; display: flex; flex-direction: column; background: var(--ai-bg, white); } .tab-content { display: none; flex: 1; flex-direction: column; overflow: hidden; } .tab-content.active { display: flex; } .messages { flex: 1; overflow-y: auto; padding: 15px; display: flex; flex-direction: column; background: var(--ai-bg, white); } .message { margin-bottom: 25px; padding: 10px; border-radius: var(--ai-borderRadius, 8px); max-width: 80%; width: fit-content; text-align: left; position: relative; color: var(--ai-text, #333); border: var(--ai-borderWidth, 1px) solid transparent; } .message.user { background: var(--ai-userMsgBg, #667eea); color: var(--ai-userMsgText, white); align-self: flex-end; border-color: transparent; } .message.ai { background: var(--ai-aiMsgBg, #f0f0f0); color: var(--ai-aiMsgText, #333); align-self: flex-start; border-color: var(--ai-border, #ddd); } .message:hover .message-actions { opacity: 1; } .message.user { background: var(--ai-userMsgBg, #667eea); color: var(--ai-userMsgText, white); align-self: flex-end; } .message.ai { background: var(--ai-aiMsgBg, #f0f0f0); color: var(--ai-aiMsgText, #333); align-self: flex-start; } .message-actions { opacity: 0; position: absolute; bottom: -20px; display: flex; gap: 5px; transition: opacity 0.2s; } .message.user .message-actions { right: 0; } .message.ai .message-actions { left: 0; } .message-action-btn { background: var(--ai-bg, rgba(255,255,255,0.95)); border: 1px solid var(--ai-border, #ddd); cursor: pointer; font-size: 12px; padding: 4px 8px; border-radius: var(--ai-btnRadius, 3px); transition: var(--ai-transition, all 0.2s ease); color: var(--ai-textSecondary, #666); display: flex; align-items: center; justify-content: center; } .message-action-btn:hover { background: var(--ai-hover, #f5f5f5); color: var(--ai-primary, #667eea); transform: scale(1.05); } #ai-chat-sidebar .message pre { background: #282c34; padding: 12px; border-radius: var(--ai-borderRadius, 6px); overflow-x: auto; margin: 8px 0; } #ai-chat-sidebar .message code { font-family: 'Consolas', 'Monaco', 'Courier New', monospace; font-size: 13px; } #ai-chat-sidebar .message pre code { background: none; padding: 0; } #ai-chat-sidebar .message :not(pre) > code { background: var(--ai-hover, rgba(0,0,0,0.1)); padding: 2px 6px; border-radius: var(--ai-btnRadius, 3px); } #ai-chat-sidebar .message p { margin: 8px 0; } #ai-chat-sidebar .message p:first-child { margin-top: 0; } #ai-chat-sidebar .message p:last-child { margin-bottom: 0; } #ai-chat-sidebar .message ul, #ai-chat-sidebar .message ol { margin: 8px 0; padding-left: 24px; } #ai-chat-sidebar .message blockquote { border-left: 3px solid var(--ai-primary, #667eea); padding-left: 12px; margin: 8px 0; color: var(--ai-textSecondary, #666); } #ai-chat-sidebar .message table { border-collapse: collapse; margin: 8px 0; width: 100%; } #ai-chat-sidebar .message th, #ai-chat-sidebar .message td { border: 1px solid var(--ai-border, #ddd); padding: 8px; text-align: left; } #ai-chat-sidebar .message th { background: var(--ai-bgSecondary, #f5f5f5); font-weight: bold; } .thinking-section { margin: 8px 0; border: 1px solid var(--ai-border, #ddd); border-radius: var(--ai-borderRadius, 4px); overflow: hidden; } .thinking-header { background: var(--ai-bgSecondary, #f5f5f5); padding: 8px 12px; cursor: pointer; display: flex; align-items: center; gap: 8px; user-select: none; color: var(--ai-text, #333); } .thinking-header:hover { background: var(--ai-hover, #e8e8e8); } .thinking-toggle { font-size: 12px; transition: transform 0.2s; } .thinking-toggle.collapsed { transform: rotate(-90deg); } .thinking-content { padding: 12px; background: var(--ai-bgSecondary, #fafafa); border-top: 1px solid var(--ai-border, #ddd); max-height: 300px; overflow-y: auto; color: var(--ai-text, #333); } .thinking-content.collapsed { display: none; } .model-selector { padding: 10px var(--ai-spacing, 15px); border-bottom: var(--ai-borderWidth, 1px) solid var(--ai-border, #eee); position: relative; background: var(--ai-bg, white); transition: var(--ai-transition, all 0.2s ease); } #model-display-btn { background: none; border: none; color: var(--ai-primary, #667eea); font-size: var(--ai-fontSize, 14px); cursor: pointer; display: flex; align-items: center; gap: 5px; padding: 5px 0; font-weight: 600; font-family: inherit; } #model-display-btn:hover { opacity: 0.8; } #model-display-btn .arrow { font-size: 10px; transition: transform 0.2s; } #model-display-btn.open .arrow { transform: rotate(180deg); } .model-dropdown { position: absolute; top: 100%; left: var(--ai-spacing, 15px); right: var(--ai-spacing, 15px); background: var(--ai-bg, white); border: var(--ai-borderWidth, 1px) solid var(--ai-border, #ddd); border-radius: var(--ai-borderRadius, 4px); box-shadow: var(--ai-shadowLg, 0 2px 8px rgba(0,0,0,0.1)); max-height: 200px; overflow-y: auto; z-index: 100; margin-top: 5px; } .model-dropdown-item { padding: 10px var(--ai-spacing, 15px); cursor: pointer; border-bottom: var(--ai-borderWidth, 1px) solid var(--ai-border, #f0f0f0); color: var(--ai-text, #333); font-size: var(--ai-fontSize, 14px); transition: var(--ai-transition, all 0.2s ease); } .model-dropdown-item:last-child { border-bottom: none; } .model-dropdown-item:hover { background: var(--ai-hover, #f5f5f5); } .model-dropdown-item.selected { background: var(--ai-primary, #667eea); color: white; } .input-area { padding: var(--ai-spacing, 15px); border-top: var(--ai-borderWidth, 1px) solid var(--ai-border, #eee); position: relative; background: var(--ai-bg, white); transition: var(--ai-transition, all 0.2s ease); } .input-wrapper { position: relative; border: var(--ai-borderWidth, 1px) solid var(--ai-border, #ddd); border-radius: var(--ai-borderRadius, 4px); background: var(--ai-bg, white); padding: 12px; display: flex; flex-direction: column; gap: 8px; resize: vertical; overflow: hidden; min-height: 80px; transition: var(--ai-transition, all 0.2s ease); box-shadow: 0 2px 6px rgba(0,0,0,0.02); } .input-wrapper:focus-within { border-color: var(--ai-primary, #667eea); box-shadow: 0 0 0 2px var(--ai-shadow, rgba(102, 126, 234, 0.1)); } .prompt-icon-top { background: none; border: none; font-size: 18px; cursor: pointer; padding: 4px; align-self: flex-start; line-height: 1; border-radius: 4px; transition: background 0.2s; flex-shrink: 0; } .prompt-icon-top:hover { background: rgba(102, 126, 234, 0.1); } .prompt-icon-top.selected { background: rgba(102, 126, 234, 0.2); } #user-input { flex: 1; border: none; outline: none; resize: none; font-family: inherit; min-height: 40px; padding: 0; overflow-y: auto; background: var(--ai-bg, white); color: var(--ai-text, #333); } #send-btn { margin-top: 10px; } .prompt-dropdown { position: absolute; bottom: 100%; left: 0; right: 0; background: var(--ai-bg, white); border: var(--ai-borderWidth, 1px) solid var(--ai-border, #ddd); border-radius: var(--ai-borderRadius, 4px); box-shadow: var(--ai-shadowLg, 0 -2px 8px rgba(0,0,0,0.1)); max-height: 300px; overflow-y: auto; z-index: 100; margin-bottom: 8px; } .prompt-dropdown-item { padding: 10px var(--ai-spacing, 15px); cursor: pointer; border-bottom: var(--ai-borderWidth, 1px) solid var(--ai-border, #f0f0f0); color: var(--ai-text, #333); transition: var(--ai-transition, all 0.2s ease); } .prompt-dropdown-item:last-child { border-bottom: none; } .prompt-dropdown-item:hover { background: var(--ai-hover, #f5f5f5); } .prompt-dropdown-item .prompt-title { font-weight: 600; margin-bottom: 4px; font-size: var(--ai-fontSize, 14px); } .prompt-dropdown-item .prompt-preview { font-size: 12px; color: var(--ai-textSecondary, #999); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .params-panel { position: absolute; bottom: 100%; left: 0; right: 0; background: var(--ai-bg, white); border: var(--ai-borderWidth, 1px) solid var(--ai-border, #ddd); border-radius: var(--ai-borderRadius, 4px); box-shadow: var(--ai-shadowLg, 0 -2px 8px rgba(0,0,0,0.1)); padding: var(--ai-spacing, 15px); z-index: 100; margin-bottom: 8px; } .params-item { margin-bottom: 10px; } .params-item:last-child { margin-bottom: 0; } .params-item label { display: block; margin-bottom: 5px; font-size: 12px; font-weight: 500; color: var(--ai-text, #333); } .params-item input { width: 100%; padding: 6px; border: 1px solid var(--ai-border, #ddd); border-radius: 4px; font-size: 13px; background: var(--ai-bg, white); color: var(--ai-text, #333); } #send-btn { padding: 10px 24px; background: var(--ai-primary, #667eea); color: white; border: none; border-radius: var(--ai-btnRadius, 4px); cursor: pointer; font-weight: 600; font-size: 14px; transition: var(--ai-transition, all 0.2s ease); box-shadow: 0 2px 4px rgba(0,0,0,0.1); } #send-btn:hover { opacity: 0.9; transform: translateY(-1px); box-shadow: 0 4px 8px rgba(0,0,0,0.15); } #send-btn:active { transform: translateY(0); } .chat-container { display: flex; height: 100%; } .conversations-sidebar { width: 200px; border-right: var(--ai-borderWidth, 1px) solid var(--ai-border, #eee); display: flex; flex-direction: column; background: var(--ai-bgSecondary, #fafafa); transition: var(--ai-transition, all 0.2s ease); } .conversations-toolbar { padding: 10px; border-bottom: var(--ai-borderWidth, 1px) solid var(--ai-border, #eee); display: flex; gap: 5px; } .conversations-toolbar button { flex: 1; padding: 6px; border: none; border-radius: var(--ai-btnRadius, 4px); cursor: pointer; font-size: 12px; color: white; font-family: inherit; transition: var(--ai-transition, all 0.2s ease); } .batch-delete-conv-btn { background: #e74c3c; } .batch-delete-conv-btn:hover { opacity: 0.9; } .conversations-list { flex: 1; overflow-y: auto; } .conversation-item { padding: 10px 12px; cursor: pointer; border-bottom: var(--ai-borderWidth, 1px) solid var(--ai-border, #eee); transition: background 0.2s; font-size: 13px; display: flex; align-items: center; gap: 8px; position: relative; color: var(--ai-text, #333); } .conversation-item input[type="checkbox"] { width: 16px; height: 16px; cursor: pointer; flex-shrink: 0; } .conversation-item .conv-title { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .conversation-item .conv-actions { display: none; gap: 4px; flex-shrink: 0; } .conversation-item:hover .conv-actions { display: flex; } .conversation-item .conv-action-btn { padding: 2px 6px; background: rgba(0,0,0,0.1); border: none; border-radius: 3px; cursor: pointer; font-size: 12px; } .conversation-item .conv-action-btn:hover { background: rgba(0,0,0,0.2); } .conversation-item:hover { background: var(--ai-hover, #f0f0f0); } .conversation-item.active { background: var(--ai-primary, #667eea); color: white; } .conversation-item.active .conv-action-btn { background: rgba(255,255,255,0.2); } .conversation-item.active .conv-action-btn:hover { background: rgba(255,255,255,0.3); } .conversation-item.editing .conv-title { display: none; } .conversation-item .conv-rename-input { display: none; flex: 1; padding: 4px; border: 1px solid var(--ai-border, #ddd); border-radius: 3px; font-size: 12px; background: var(--ai-bg, white); color: var(--ai-text, #333); } .conversation-item.editing .conv-rename-input { display: block; } .chat-main { flex: 1; display: flex; flex-direction: column; overflow: hidden; background: var(--ai-bg, white); } .providers-container { display: flex; height: 100%; background: var(--ai-bg, white); } .providers-sidebar { width: 200px; border-right: var(--ai-borderWidth, 1px) solid var(--ai-border, #eee); display: flex; flex-direction: column; background: var(--ai-bgSecondary, #fafafa); } #add-provider-btn { margin: 15px; padding: 10px; background: var(--ai-primary, #667eea); color: white; border: none; border-radius: var(--ai-btnRadius, 4px); cursor: pointer; font-weight: 600; transition: var(--ai-transition, all 0.2s ease); } #add-provider-btn:hover { opacity: 0.9; transform: translateY(-1px); } .providers-list { flex: 1; overflow-y: auto; } .provider-sidebar-item { padding: 12px 15px; cursor: pointer; border-bottom: var(--ai-borderWidth, 1px) solid var(--ai-border, #eee); transition: background 0.2s; display: flex; align-items: center; justify-content: space-between; color: var(--ai-text, #333); } .provider-sidebar-item:hover { background: var(--ai-hover, #f5f5f5); } .provider-sidebar-item.active { background: var(--ai-primary, #667eea); color: white; } .provider-sidebar-item .delete-icon { opacity: 0; cursor: pointer; font-size: 16px; padding: 2px 6px; border-radius: 3px; transition: opacity 0.2s; } .provider-sidebar-item:hover .delete-icon { opacity: 1; } .provider-sidebar-item .delete-icon:hover { background: rgba(231, 76, 60, 0.2); } .provider-sidebar-item.active .delete-icon:hover { background: rgba(255, 255, 255, 0.3); } .provider-detail { flex: 1; overflow-y: auto; padding: 20px; color: var(--ai-text, #333); } .empty-state { text-align: center; color: var(--ai-textSecondary, #999); padding: 50px 20px; } .provider-form { max-width: 600px; } .provider-form h3 { margin: 0 0 20px 0; } .form-group { margin-bottom: 15px; display: flex; align-items: center; gap: 15px; } .form-group label { min-width: 100px; font-weight: bold; } .form-group input { flex: 1; padding: 8px; border: var(--ai-borderWidth, 1px) solid var(--ai-border, #ddd); border-radius: var(--ai-borderRadius, 4px); background: var(--ai-bg, white); color: var(--ai-text, #333); } .final-url-display { margin-left: 115px; padding: 8px; background: var(--ai-bgSecondary, #f0f0f0); border-radius: 4px; font-size: 12px; color: var(--ai-textSecondary, #666); word-break: break-all; } .final-url-display strong { color: var(--ai-text, #333); } .form-group.password-group { position: relative; } .form-group.password-group input { padding-right: 40px; } .form-group .toggle-password { position: absolute; right: 10px; cursor: pointer; font-size: 18px; user-select: none; } .form-actions { display: flex; gap: 10px; margin-top: 20px; margin-left: 115px; } .form-actions button { padding: 10px 20px; border: none; border-radius: var(--ai-btnRadius, 4px); cursor: pointer; color: white; } .save-provider-btn { background: var(--ai-primary, #667eea); } .models-section { margin-top: 30px; padding-top: 20px; border-top: calc(var(--ai-borderWidth, 1px) * 2) solid var(--ai-border, #eee); } .models-section h3 { margin: 0 0 15px 0; display: flex; align-items: center; gap: 10px; color: var(--ai-text, #333); } .fetch-models-btn, .refresh-models-btn { padding: 6px 12px; background: #3498db; color: white; border: none; border-radius: var(--ai-btnRadius, 4px); cursor: pointer; font-size: 12px; transition: var(--ai-transition, all 0.2s ease); } .fetch-models-btn:hover, .refresh-models-btn:hover { opacity: 0.9; } .refresh-models-btn { background: #2ecc71; } .available-models-section { margin-top: 20px; padding: 15px; background: var(--ai-bgSecondary, #f9f9f9); border-radius: var(--ai-borderRadius, 8px); border: var(--ai-borderWidth, 1px) solid var(--ai-border, #eee); } .available-models-section h4 { margin: 0 0 10px 0; font-size: 14px; } .model-search { width: 100%; padding: 8px; margin-bottom: 10px; border: var(--ai-borderWidth, 1px) solid var(--ai-border, #ddd); border-radius: var(--ai-borderRadius, 4px); background: var(--ai-bg, white); color: var(--ai-text, #333); } .available-models-list { max-height: 300px; overflow-y: auto; display: flex; flex-direction: column; gap: 5px; } .available-model-item { display: flex; align-items: center; justify-content: space-between; padding: 8px; background: var(--ai-bg, white); border-radius: var(--ai-borderRadius, 4px); border: var(--ai-borderWidth, 1px) solid var(--ai-border, #eee); } .available-model-item .model-name { flex: 1; font-size: 13px; } .available-model-item .add-model-icon { cursor: pointer; color: var(--ai-primary, #667eea); font-size: 18px; padding: 2px 6px; border-radius: 3px; transition: background 0.2s; } .available-model-item .add-model-icon:hover { background: rgba(102, 126, 234, 0.1); } .loading-models { text-align: center; padding: 20px; color: var(--ai-textSecondary, #999); } .models-list { display: flex; flex-direction: column; gap: 10px; } .model-item { background: var(--ai-bgSecondary, #f9f9f9); padding: 10px; border-radius: var(--ai-borderRadius, 4px); display: flex; align-items: center; gap: 10px; } .model-item input { flex: 1; padding: 8px; border: var(--ai-borderWidth, 1px) solid var(--ai-border, #ddd); border-radius: var(--ai-borderRadius, 4px); background: var(--ai-bg, white); color: var(--ai-text, #333); } .model-item button { padding: 6px 12px; border: none; border-radius: var(--ai-btnRadius, 4px); cursor: pointer; font-size: 12px; color: white; } .save-model-btn { background: var(--ai-primary, #667eea); } .delete-model-btn { background: #e74c3c; } .add-model-btn { margin-top: 10px; padding: 8px 16px; background: var(--ai-primary, #667eea); color: white; border: none; border-radius: 4px; cursor: pointer; } .prompts-toolbar { padding: var(--ai-spacing, 15px); display: flex; gap: 10px; border-bottom: var(--ai-borderWidth, 1px) solid var(--ai-border, #eee); background: var(--ai-bg, white); } .prompts-toolbar button { padding: 8px 16px; border: none; border-radius: var(--ai-btnRadius, 4px); cursor: pointer; color: white; font-weight: 600; transition: var(--ai-transition, all 0.2s ease); } .prompts-toolbar button:hover { opacity: 0.9; transform: translateY(-1px); } #add-prompt { background: var(--ai-primary, #667eea); } #batch-delete-prompt { background: #e74c3c; } .prompts-list { flex: 1; overflow-y: auto; padding: 15px; background: var(--ai-bg, white); } .prompt-item { background: var(--ai-bgSecondary, #f9f9f9); padding: 15px; margin-bottom: 10px; border-radius: var(--ai-borderRadius, 8px); position: relative; color: var(--ai-text, #333); } .prompt-item.editing { background: var(--ai-bg, #fff); border: 2px solid var(--ai-primary, #667eea); } .prompt-item input[type="checkbox"] { position: absolute; top: 15px; left: 15px; width: 18px; height: 18px; cursor: pointer; } .prompt-item .prompt-header { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; padding-left: 30px; } .prompt-item .prompt-title { flex: 1; font-weight: bold; font-size: 16px; } .prompt-item .prompt-actions { display: flex; gap: 5px; } .prompt-item .prompt-actions button { padding: 4px 10px; border: none; border-radius: var(--ai-btnRadius, 4px); cursor: pointer; font-size: 12px; } .prompt-item .view-btn { background: #3498db; color: white; } .prompt-item .edit-btn { background: #f39c12; color: white; } .prompt-item .delete-btn { background: #e74c3c; color: white; } .prompt-item .prompt-content { padding-left: 30px; color: var(--ai-textSecondary, #666); white-space: pre-wrap; word-break: break-word; } .prompt-item .prompt-form { padding-left: 30px; } .prompt-item .prompt-form input, .prompt-item .prompt-form textarea { width: 100%; padding: 8px; margin: 5px 0; border: var(--ai-borderWidth, 1px) solid var(--ai-border, #ddd); border-radius: var(--ai-borderRadius, 4px); font-family: inherit; background: var(--ai-bg, white); color: var(--ai-text, #333); } .prompt-item .prompt-form textarea { min-height: 100px; resize: vertical; } .prompt-item .prompt-form .form-actions { margin-top: 10px; display: flex; gap: 10px; } .prompt-item .prompt-form .form-actions button { padding: 6px 16px; border: none; border-radius: var(--ai-btnRadius, 4px); cursor: pointer; color: white; } .prompt-item .save-prompt-btn { background: var(--ai-primary, #667eea); } .prompt-item .cancel-btn { background: #95a5a6; } .system-config-container { padding: 20px; color: var(--ai-text, #333); } .system-config-container h3 { margin: 0 0 20px 0; } .config-section { background: var(--ai-bgSecondary, #f9f9f9); padding: 20px; border-radius: var(--ai-borderRadius, 8px); margin-bottom: 20px; } .config-section h4 { margin: 0 0 15px 0; font-size: 16px; } .config-select { width: 100%; padding: 8px; border: var(--ai-borderWidth, 1px) solid var(--ai-border, #ddd); border-radius: var(--ai-borderRadius, 4px); background: var(--ai-bg, white); color: var(--ai-text, #333); font-size: 14px; } .save-btn { margin-top: 15px; padding: 10px 20px; background: var(--ai-primary, #667eea); color: white; border: none; border-radius: var(--ai-btnRadius, 4px); cursor: pointer; font-weight: 600; } .save-btn:hover { opacity: 0.9; } /* Scrollbar Styling */ #ai-chat-sidebar ::-webkit-scrollbar { width: 6px; height: 6px; } #ai-chat-sidebar ::-webkit-scrollbar-track { background: transparent; } #ai-chat-sidebar ::-webkit-scrollbar-thumb { background: var(--ai-border, rgba(0, 0, 0, 0.1)); border-radius: 3px; } #ai-chat-sidebar ::-webkit-scrollbar-thumb:hover { background: var(--ai-textSecondary, rgba(0, 0, 0, 0.2)); } `; document.head.appendChild(style); }; // 初始化 const init = () => { addStyles(); const triggerBtn = createTriggerButton(); const sidebar = createSidebar(); // 切换侧边栏 triggerBtn.addEventListener('click', () => { sidebar.classList.toggle('open'); }); sidebar.querySelector('.close-btn').addEventListener('click', () => { sidebar.classList.remove('open'); }); // 主题切换 const themeBtn = sidebar.querySelector('.theme-btn'); const themeDropdown = sidebar.querySelector('.theme-dropdown'); themeBtn.addEventListener('click', (e) => { e.stopPropagation(); themeDropdown.classList.toggle('show'); }); themeDropdown.addEventListener('click', (e) => { const item = e.target.closest('.theme-item'); if (!item) return; const themeName = item.dataset.theme; applyTheme(themeName); themeDropdown.classList.remove('show'); }); document.addEventListener('click', (e) => { if (!e.target.closest('.header-controls')) { themeDropdown.classList.remove('show'); } }); // 初始化主题 applyTheme(ConfigManager.getTheme()); // 拖拽调整大小 let isResizing = false; let resizeType = ''; let startX = 0; let startY = 0; let startWidth = 0; let startHeight = 0; let startLeft = 0; let startTop = 0; const startResize = (e, type) => { isResizing = true; resizeType = type; startX = e.clientX; startY = e.clientY; const rect = sidebar.getBoundingClientRect(); startWidth = rect.width; startHeight = rect.height; startLeft = rect.left; startTop = rect.top; sidebar.classList.add('resizing'); e.preventDefault(); e.stopPropagation(); }; sidebar.querySelector('.resize-handle-left').addEventListener('mousedown', (e) => startResize(e, 'left')); sidebar.querySelector('.resize-handle-right').addEventListener('mousedown', (e) => startResize(e, 'right')); sidebar.querySelector('.resize-handle-top').addEventListener('mousedown', (e) => startResize(e, 'top')); sidebar.querySelector('.resize-handle-bottom').addEventListener('mousedown', (e) => startResize(e, 'bottom')); sidebar.querySelector('.resize-handle-corner-tl').addEventListener('mousedown', (e) => startResize(e, 'top-left')); sidebar.querySelector('.resize-handle-corner-tr').addEventListener('mousedown', (e) => startResize(e, 'top-right')); sidebar.querySelector('.resize-handle-corner-bl').addEventListener('mousedown', (e) => startResize(e, 'bottom-left')); sidebar.querySelector('.resize-handle-corner-br').addEventListener('mousedown', (e) => startResize(e, 'bottom-right')); document.addEventListener('mousemove', (e) => { if (!isResizing) return; const deltaX = e.clientX - startX; const deltaY = e.clientY - startY; if (resizeType.includes('left')) { const newWidth = startWidth - deltaX; if (newWidth >= 200) { sidebar.style.width = newWidth + 'px'; } } if (resizeType.includes('right')) { const newWidth = startWidth + deltaX; if (newWidth >= 200) { sidebar.style.width = newWidth + 'px'; } } if (resizeType.includes('top')) { const newHeight = startHeight - deltaY; if (newHeight >= 200) { sidebar.style.height = newHeight + 'px'; sidebar.style.top = (startTop + deltaY) + 'px'; } } if (resizeType.includes('bottom')) { const newHeight = startHeight + deltaY; if (newHeight >= 200) { sidebar.style.height = newHeight + 'px'; } } }); document.addEventListener('mouseup', () => { if (isResizing) { isResizing = false; resizeType = ''; sidebar.classList.remove('resizing'); } }); // 拖拽移动窗口 const header = sidebar.querySelector('.sidebar-header'); let isDragging = false; let dragStartX = 0; let dragStartY = 0; let sidebarLeft = 0; let sidebarTop = 0; header.addEventListener('mousedown', (e) => { // 如果点击的是按钮,不触发拖动 if (e.target.tagName === 'BUTTON') return; isDragging = true; dragStartX = e.clientX; dragStartY = e.clientY; const rect = sidebar.getBoundingClientRect(); sidebarLeft = rect.left; sidebarTop = rect.top; sidebar.classList.add('dragging'); e.preventDefault(); }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; const deltaX = e.clientX - dragStartX; const deltaY = e.clientY - dragStartY; const newLeft = sidebarLeft + deltaX; const newTop = sidebarTop + deltaY; sidebar.style.left = newLeft + 'px'; sidebar.style.top = newTop + 'px'; }); document.addEventListener('mouseup', () => { if (isDragging) { isDragging = false; sidebar.classList.remove('dragging'); } }); // 选项卡切换 sidebar.querySelectorAll('.tab').forEach(tab => { tab.addEventListener('click', () => { const tabName = tab.dataset.tab; sidebar.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); sidebar.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); tab.classList.add('active'); sidebar.querySelector(`#${tabName}-tab`).classList.add('active'); }); }); // AI提供商管理 let currentProviderIndex = null; const renderProvidersSidebar = () => { const providers = ConfigManager.getProviders(); const list = sidebar.querySelector('#providers-sidebar-list'); list.textContent = ''; providers.forEach((provider, index) => { const item = document.createElement('div'); item.className = 'provider-sidebar-item'; if (index === currentProviderIndex) { item.classList.add('active'); } safeInnerHTML(item, ` <span class="provider-name">${provider.name || '未命名供应商'}</span> <span class="delete-icon" data-index="${index}">×</span> `); item.dataset.index = index; list.appendChild(item); }); updateModelSelect(); }; const normalizeApiUrl = (url) => { if (!url) return ''; url = url.trim(); // 如果已经包含 /chat/completions,直接返回 if (url.includes('/chat/completions')) { return url; } // 移除末尾的斜杠 url = url.replace(/\/+$/, ''); // 如果包含版本号(v1, v2, v3等),在其后添加 /chat/completions if (/\/v\d+$/i.test(url)) { return url + '/chat/completions'; } // 默认添加 /v1/chat/completions return url + '/v1/chat/completions'; }; const getModelsUrl = (url) => { if (!url) return ''; url = url.trim(); // 如果已经包含 /models,直接返回 if (url.includes('/models')) { return url; } // 移除末尾的斜杠 url = url.replace(/\/+$/, ''); // 如果包含版本号(v1, v2, v3等),在其后添加 /models if (/\/v\d+$/i.test(url)) { return url + '/models'; } // 默认添加 /v1/models return url + '/v1/models'; }; const updateFinalUrl = (index) => { const urlInput = sidebar.querySelector(`#provider-url-${index}`); const finalUrlDisplay = sidebar.querySelector(`#final-url-${index}`); if (urlInput && finalUrlDisplay) { const finalUrl = normalizeApiUrl(urlInput.value); safeInnerHTML(finalUrlDisplay, `<strong>最终调用地址:</strong>${finalUrl || '请输入API URL'}`); } }; const renderProviderDetail = (index) => { const providers = ConfigManager.getProviders(); const provider = providers[index]; const detail = sidebar.querySelector('#provider-detail'); const models = ConfigManager.getModels(index); safeInnerHTML(detail, ` <div class="provider-form"> <h3>供应商信息</h3> <div class="form-group"> <label>供应商名称</label> <input type="text" value="${provider.name || ''}" id="provider-name-${index}"> </div> <div class="form-group"> <label>API URL</label> <input type="text" value="${provider.url || ''}" id="provider-url-${index}" placeholder="例如: https://api.openai.com"> </div> <div class="final-url-display" id="final-url-${index}"></div> <div class="form-group password-group"> <label>API Key</label> <input type="password" value="${provider.key || ''}" id="provider-key-${index}"> <span class="toggle-password" data-target="provider-key-${index}">👁️</span> </div> <div class="form-actions"> <button class="save-provider-btn" data-index="${index}">保存</button> </div> <div class="models-section"> <h3> 已添加模型 <button class="fetch-models-btn" data-index="${index}">获取模型列表</button> <button class="refresh-models-btn" data-index="${index}" style="display:none;">刷新</button> </h3> <div class="models-list" id="models-list-${index}"></div> <button class="add-model-btn" data-index="${index}">+ 手动添加模型</button> <div class="available-models-section" id="available-models-${index}" style="display:none;"> <h4>可用模型列表</h4> <input type="text" class="model-search" placeholder="搜索模型..." id="model-search-${index}"> <div class="available-models-list" id="available-models-list-${index}"></div> </div> </div> </div> `); const modelsList = detail.querySelector(`#models-list-${index}`); models.forEach((model, modelIndex) => { const item = document.createElement('div'); item.className = 'model-item'; safeInnerHTML(item, ` <input type="text" value="${model}" data-model="${modelIndex}"> <button class="save-model-btn" data-provider="${index}" data-model="${modelIndex}">保存</button> <button class="delete-model-btn" data-provider="${index}" data-model="${modelIndex}">删除</button> `); modelsList.appendChild(item); }); // 初始化显示最终URL updateFinalUrl(index); // 监听URL输入框变化 const urlInput = sidebar.querySelector(`#provider-url-${index}`); if (urlInput) { urlInput.addEventListener('input', () => updateFinalUrl(index)); } // 监听模型搜索 const searchInput = detail.querySelector(`#model-search-${index}`); if (searchInput) { searchInput.addEventListener('input', (e) => { filterAvailableModels(index, e.target.value); }); } // 加载已保存的可用模型列表 loadAvailableModels(index); }; const fetchAvailableModels = async (index) => { const providers = ConfigManager.getProviders(); const provider = providers[index]; if (!provider.url || !provider.key) { alert('请先配置API URL和API Key'); return; } const availableSection = sidebar.querySelector(`#available-models-${index}`); const availableList = sidebar.querySelector(`#available-models-list-${index}`); const fetchBtn = sidebar.querySelector(`.fetch-models-btn[data-index="${index}"]`); const refreshBtn = sidebar.querySelector(`.refresh-models-btn[data-index="${index}"]`); safeInnerHTML(availableList, '<div class="loading-models">正在获取模型列表...</div>'); availableSection.style.display = 'block'; const modelsUrl = getModelsUrl(provider.url); GM_xmlhttpRequest({ method: 'GET', url: modelsUrl, headers: { 'Authorization': `Bearer ${provider.key}` }, onload: (response) => { try { const data = JSON.parse(response.responseText); const models = data.data || data.models || []; const modelNames = models.map(m => m.id || m.name || m).filter(Boolean); ConfigManager.saveAvailableModels(index, modelNames); renderAvailableModels(index, modelNames); fetchBtn.style.display = 'none'; refreshBtn.style.display = 'inline-block'; } catch (e) { safeInnerHTML(availableList, '<div class="loading-models">获取失败: ' + e.message + '</div>'); } }, onerror: () => { safeInnerHTML(availableList, '<div class="loading-models">请求失败,请检查URL和API Key</div>'); } }); }; const renderAvailableModels = (index, models) => { const availableList = sidebar.querySelector(`#available-models-list-${index}`); const existingModels = ConfigManager.getModels(index); availableList.textContent = ''; models.forEach(modelName => { const item = document.createElement('div'); item.className = 'available-model-item'; item.dataset.modelName = modelName; safeInnerHTML(item, ` <span class="model-name">${modelName}</span> <span class="add-model-icon" data-provider="${index}" data-model-name="${modelName}">+</span> `); availableList.appendChild(item); }); }; const filterAvailableModels = (index, keyword) => { const models = ConfigManager.getAvailableModels(index); const filtered = keyword ? models.filter(m => m.toLowerCase().includes(keyword.toLowerCase())) : models; renderAvailableModels(index, filtered); }; const updateModelSelect = () => { // 保持兼容性,但不再使用 }; sidebar.querySelector('#add-provider-btn').addEventListener('click', () => { const providers = ConfigManager.getProviders(); providers.push({name: '新供应商', url: '', key: ''}); ConfigManager.saveProviders(providers); currentProviderIndex = providers.length - 1; renderProvidersSidebar(); renderProviderDetail(currentProviderIndex); }); sidebar.querySelector('#providers-sidebar-list').addEventListener('click', (e) => { if (e.target.classList.contains('delete-icon')) { e.stopPropagation(); const index = parseInt(e.target.dataset.index); if (!confirm('确定删除此供应商吗?')) return; const providers = ConfigManager.getProviders(); providers.splice(index, 1); ConfigManager.saveProviders(providers); if (currentProviderIndex === index) { currentProviderIndex = null; safeInnerHTML(sidebar.querySelector('#provider-detail'), '<div class="empty-state">请选择或添加一个供应商</div>'); } else if (currentProviderIndex > index) { currentProviderIndex--; } renderProvidersSidebar(); updateModelSelect(); return; } const item = e.target.closest('.provider-sidebar-item'); if (!item) return; currentProviderIndex = parseInt(item.dataset.index); renderProvidersSidebar(); renderProviderDetail(currentProviderIndex); }); sidebar.querySelector('#provider-detail').addEventListener('click', (e) => { if (e.target.classList.contains('toggle-password')) { const targetId = e.target.dataset.target; const input = sidebar.querySelector(`#${targetId}`); if (input.type === 'password') { input.type = 'text'; e.target.textContent = '🙈'; } else { input.type = 'password'; e.target.textContent = '👁️'; } } else if (e.target.classList.contains('save-provider-btn')) { const index = parseInt(e.target.dataset.index); const providers = ConfigManager.getProviders(); providers[index] = { name: sidebar.querySelector(`#provider-name-${index}`).value, url: sidebar.querySelector(`#provider-url-${index}`).value, key: sidebar.querySelector(`#provider-key-${index}`).value }; ConfigManager.saveProviders(providers); renderProvidersSidebar(); updateModelSelect(); alert('保存成功'); } else if (e.target.classList.contains('fetch-models-btn')) { const providerIndex = parseInt(e.target.dataset.index); fetchAvailableModels(providerIndex); } else if (e.target.classList.contains('refresh-models-btn')) { const providerIndex = parseInt(e.target.dataset.index); fetchAvailableModels(providerIndex); } else if (e.target.classList.contains('add-model-icon')) { const providerIndex = parseInt(e.target.dataset.provider); const modelName = e.target.dataset.modelName; const models = ConfigManager.getModels(providerIndex); if (!models.includes(modelName)) { models.push(modelName); ConfigManager.saveModels(providerIndex, models); renderProviderDetail(providerIndex); updateModelSelect(); // 恢复可用模型列表显示 const availableSection = sidebar.querySelector(`#available-models-${providerIndex}`); if (availableSection) { availableSection.style.display = 'block'; const searchInput = sidebar.querySelector(`#model-search-${providerIndex}`); if (searchInput) { filterAvailableModels(providerIndex, searchInput.value); } } } else { alert('该模型已存在'); } } else if (e.target.classList.contains('add-model-btn')) { const providerIndex = parseInt(e.target.dataset.index); const models = ConfigManager.getModels(providerIndex); models.push(''); ConfigManager.saveModels(providerIndex, models); renderProviderDetail(providerIndex); } else if (e.target.classList.contains('save-model-btn')) { const providerIndex = parseInt(e.target.dataset.provider); const modelIndex = parseInt(e.target.dataset.model); const input = e.target.closest('.model-item').querySelector('input'); const models = ConfigManager.getModels(providerIndex); models[modelIndex] = input.value.trim(); ConfigManager.saveModels(providerIndex, models); updateModelSelect(); alert('模型保存成功'); } else if (e.target.classList.contains('delete-model-btn')) { const providerIndex = parseInt(e.target.dataset.provider); const modelIndex = parseInt(e.target.dataset.model); if (!confirm('确定删除此模型吗?')) return; const models = ConfigManager.getModels(providerIndex); models.splice(modelIndex, 1); ConfigManager.saveModels(providerIndex, models); renderProviderDetail(providerIndex); updateModelSelect(); } }); renderProvidersSidebar(); // 模型选择下拉菜单 let currentSelectedModel = null; const modelDisplayBtn = sidebar.querySelector('#model-display-btn'); const modelDropdown = sidebar.querySelector('#model-dropdown'); const modelNameSpan = sidebar.querySelector('#model-name'); const renderModelDropdown = () => { const providers = ConfigManager.getProviders(); modelDropdown.textContent = ''; providers.forEach((provider, providerIndex) => { const models = ConfigManager.getModels(providerIndex); models.forEach(model => { const item = document.createElement('div'); item.className = 'model-dropdown-item'; const modelValue = JSON.stringify({provider: providerIndex, model: model}); if (currentSelectedModel === modelValue) { item.classList.add('selected'); } item.textContent = `${provider.name} - ${model}`; item.dataset.value = modelValue; modelDropdown.appendChild(item); }); }); }; modelDisplayBtn.addEventListener('click', (e) => { e.stopPropagation(); const isOpen = modelDropdown.style.display === 'block'; modelDropdown.style.display = isOpen ? 'none' : 'block'; modelDisplayBtn.classList.toggle('open', !isOpen); if (!isOpen) renderModelDropdown(); }); modelDropdown.addEventListener('click', (e) => { const item = e.target.closest('.model-dropdown-item'); if (!item) return; currentSelectedModel = item.dataset.value; const config = JSON.parse(currentSelectedModel); const providers = ConfigManager.getProviders(); const provider = providers[config.provider]; modelNameSpan.textContent = `${provider.name} - ${config.model}`; modelDropdown.style.display = 'none'; modelDisplayBtn.classList.remove('open'); }); document.addEventListener('click', () => { modelDropdown.style.display = 'none'; modelDisplayBtn.classList.remove('open'); }); // 历史对话管理 let currentConversationId = null; let conversationMessages = []; const renderConversations = () => { const conversations = ConfigManager.getConversations(); const list = document.querySelector('.conversations-list'); if (!list) { const sidebar = document.querySelector('#conversations-sidebar'); sidebar.innerHTML = ` <div class="conversations-toolbar"> <button class="batch-delete-conv-btn">批量删除</button> </div> <div class="conversations-list"></div> `; sidebar.querySelector('.batch-delete-conv-btn').addEventListener('click', () => { const checkboxes = sidebar.querySelectorAll('.conv-checkbox:checked'); if (checkboxes.length === 0) { alert('请选择要删除的对话'); return; } if (!confirm(`确定删除选中的 ${checkboxes.length} 个对话吗?`)) return; const conversations = ConfigManager.getConversations(); const idsToDelete = Array.from(checkboxes).map(cb => cb.dataset.id); const filtered = conversations.filter(c => !idsToDelete.includes(c.id)); ConfigManager.saveConversations(filtered); if (idsToDelete.includes(currentConversationId)) { createNewConversation(); } else { renderConversations(); } }); return renderConversations(); } list.textContent = ''; conversations.forEach((conv) => { const item = document.createElement('div'); item.className = 'conversation-item'; if (conv.id === currentConversationId) { item.classList.add('active'); } item.dataset.id = conv.id; safeInnerHTML(item, ` <input type="checkbox" class="conv-checkbox" data-id="${conv.id}"> <span class="conv-title">${conv.title}</span> <input type="text" class="conv-rename-input" value="${conv.title}"> <div class="conv-actions"> <button class="conv-action-btn rename-conv-btn" title="重命名">✏️</button> <button class="conv-action-btn delete-conv-btn" title="删除">🗑️</button> </div> `); list.appendChild(item); }); }; const saveCurrentConversation = () => { if (!currentConversationId || conversationMessages.length === 0) return; const conversations = ConfigManager.getConversations(); const index = conversations.findIndex(c => c.id === currentConversationId); if (index !== -1) { conversations[index].messages = conversationMessages; conversations[index].updatedAt = Date.now(); ConfigManager.saveConversations(conversations); } }; const loadConversation = (id) => { saveCurrentConversation(); const conversations = ConfigManager.getConversations(); const conv = conversations.find(c => c.id === id); if (!conv) return; currentConversationId = id; conversationMessages = conv.messages || []; const messagesContainer = document.querySelector('#messages'); messagesContainer.textContent = ''; conversationMessages.forEach((msg, index) => { const msgDiv = document.createElement('div'); msgDiv.className = `message ${msg.role}`; msgDiv.dataset.index = index; if (msg.role === 'user') { // 如果是总结类型,显示简洁文本 msgDiv.textContent = msg.isSummary ? msg.displayText : msg.content; } else { safeInnerHTML(msgDiv, msg.html || msg.content, msg.content); } // 添加操作按钮 const actions = document.createElement('div'); actions.className = 'message-actions'; if (msg.role === 'ai') { safeInnerHTML(actions, ` <button class="message-action-btn copy-msg-btn" title="复制">📋</button> <button class="message-action-btn delete-msg-btn" title="删除">🗑️</button> <button class="message-action-btn regenerate-msg-btn" title="重新生成">🔄</button> `); } else { safeInnerHTML(actions, ` <button class="message-action-btn copy-msg-btn" title="复制">📋</button> <button class="message-action-btn delete-msg-btn" title="删除">🗑️</button> `); } msgDiv.appendChild(actions); messagesContainer.appendChild(msgDiv); }); renderConversations(); }; const createNewConversation = () => { saveCurrentConversation(); const conversations = ConfigManager.getConversations(); const newConv = { id: Date.now().toString(), title: '新对话', messages: [], createdAt: Date.now(), updatedAt: Date.now() }; conversations.unshift(newConv); ConfigManager.saveConversations(conversations); currentConversationId = newConv.id; conversationMessages = []; document.querySelector('#messages').textContent = ''; renderConversations(); // 应用系统配置的默认设置 const systemConfig = ConfigManager.getSystemConfig(); if (systemConfig.defaultModel) { currentSelectedModel = systemConfig.defaultModel; const config = JSON.parse(currentSelectedModel); const providers = ConfigManager.getProviders(); const provider = providers[config.provider]; if (provider) { modelNameSpan.textContent = `${provider.name} - ${config.model}`; } } if (systemConfig.defaultPrompt !== null) { const prompts = ConfigManager.getPrompts(); const prompt = prompts[systemConfig.defaultPrompt]; if (prompt) { currentSystemPrompt = prompt.content; promptSelectorBtn.classList.add('selected'); promptSelectorBtn.title = `已选择: ${prompt.title}`; } } }; document.querySelector('#conversations-sidebar').addEventListener('click', (e) => { if (e.target.classList.contains('conv-checkbox')) { e.stopPropagation(); return; } if (e.target.classList.contains('delete-conv-btn')) { e.stopPropagation(); const item = e.target.closest('.conversation-item'); const id = item.dataset.id; if (!confirm('确定删除此对话吗?')) return; const conversations = ConfigManager.getConversations(); const filtered = conversations.filter(c => c.id !== id); ConfigManager.saveConversations(filtered); if (id === currentConversationId) { createNewConversation(); } else { renderConversations(); } return; } if (e.target.classList.contains('rename-conv-btn')) { e.stopPropagation(); const item = e.target.closest('.conversation-item'); const input = item.querySelector('.conv-rename-input'); item.classList.add('editing'); input.focus(); input.select(); return; } const item = e.target.closest('.conversation-item'); if (!item || item.classList.contains('editing')) return; loadConversation(item.dataset.id); }); document.querySelector('#conversations-sidebar').addEventListener('keydown', (e) => { if (e.key === 'Enter' && e.target.classList.contains('conv-rename-input')) { const item = e.target.closest('.conversation-item'); const id = item.dataset.id; const newTitle = e.target.value.trim(); if (newTitle) { const conversations = ConfigManager.getConversations(); const conv = conversations.find(c => c.id === id); if (conv) { conv.title = newTitle; ConfigManager.saveConversations(conversations); } } item.classList.remove('editing'); renderConversations(); } else if (e.key === 'Escape' && e.target.classList.contains('conv-rename-input')) { const item = e.target.closest('.conversation-item'); item.classList.remove('editing'); } }); document.querySelector('#conversations-sidebar').addEventListener('blur', (e) => { if (e.target.classList.contains('conv-rename-input')) { const item = e.target.closest('.conversation-item'); setTimeout(() => { if (item.classList.contains('editing')) { item.classList.remove('editing'); } }, 200); } }, true); document.querySelector('#new-chat-btn').addEventListener('click', createNewConversation); // 清除对话功能 document.querySelector('#clear-chat-btn').addEventListener('click', () => { if (confirm('确定要清除当前对话记录吗?')) { conversationMessages = []; document.querySelector('#messages').textContent = ''; saveCurrentConversation(); } }); // 总结网页功能 document.querySelector('#summarize-page-btn').addEventListener('click', async () => { if (!currentSelectedModel) { alert('请先选择模型'); return; } const input = sidebar.querySelector('#user-input'); const messages = sidebar.querySelector('#messages'); // 提取页面内容 const pageTitle = document.title; const pageUrl = window.location.href; // 获取页面主要文本内容 let pageContent = ''; // 尝试获取主要内容区域 const mainContent = document.querySelector('main, article, .content, .main, #content, #main'); if (mainContent) { pageContent = mainContent.innerText; } else { pageContent = document.body.innerText; } // 限制内容长度,避免超出token限制 const maxLength = 8000; if (pageContent.length > maxLength) { pageContent = pageContent.substring(0, maxLength) + '...(内容过长已截断)'; } // 构建实际发送给AI的完整提示词 const actualPrompt = `请详细总结以下网页的内容。要求: 1. 准确概括网页的主题和核心内容 2. 列出关键信息点和重要细节 3. 保持逻辑清晰,结构分明 4. 不要遗漏重要信息 5. 如果是文章,请总结主要观点;如果是产品页面,请总结产品特点;如果是新闻,请总结事件要点 网页标题:${pageTitle} 网页地址:${pageUrl} 网页内容: ${pageContent} 请开始总结:`; // 显示给用户的简洁文本 const displayText = `总结当前页面:${pageTitle}`; // 如果是新对话的第一条消息,自动命名 if (conversationMessages.length === 0) { const conversations = ConfigManager.getConversations(); const index = conversations.findIndex(c => c.id === currentConversationId); if (index !== -1) { conversations[index].title = displayText.slice(0, 20); ConfigManager.saveConversations(conversations); renderConversations(); } } // 显示用户消息(简洁版本) const userMsg = document.createElement('div'); userMsg.className = 'message user'; userMsg.dataset.index = conversationMessages.length; userMsg.textContent = displayText; const userActions = document.createElement('div'); userActions.className = 'message-actions'; safeInnerHTML(userActions, ` <button class="message-action-btn copy-msg-btn" title="复制">📋</button> <button class="message-action-btn delete-msg-btn" title="删除">🗑️</button> `); userMsg.appendChild(userActions); messages.appendChild(userMsg); // 保存到历史(保存完整提示词用于API调用,但标记为总结类型) conversationMessages.push({role: 'user', content: actualPrompt, displayText: displayText, isSummary: true}); input.value = ''; messages.scrollTop = messages.scrollHeight; const config = JSON.parse(currentSelectedModel); const providers = ConfigManager.getProviders(); const provider = providers[config.provider]; const aiMsg = document.createElement('div'); aiMsg.className = 'message ai'; aiMsg.textContent = '正在总结...'; messages.appendChild(aiMsg); try { const finalUrl = normalizeApiUrl(provider.url); let fullContent = ''; let buffer = ''; let lastIndex = 0; let updateScheduled = false; let isStreamComplete = false; const updateUI = () => { if (updateScheduled) return; updateScheduled = true; requestAnimationFrame(() => { const thinkMatch = fullContent.match(/<think>([\s\S]*?)<\/think>/); let thinkingContent = ''; let mainContent = fullContent; if (thinkMatch) { thinkingContent = thinkMatch[1].trim(); mainContent = fullContent.replace(/<think>[\s\S]*?<\/think>/, '').trim(); } let html = ''; if (thinkingContent) { const collapsed = isStreamComplete ? 'collapsed' : ''; html += `<div class="thinking-section"> <div class="thinking-header"> <span class="thinking-toggle ${collapsed}">▼</span> <span>思考过程</span> </div> <div class="thinking-content ${collapsed}">${marked.parse(thinkingContent)}</div> </div>`; } if (mainContent) { html += marked.parse(mainContent); } safeInnerHTML(aiMsg, html, fullContent); aiMsg.querySelectorAll('pre code').forEach((block) => { hljs.highlightElement(block); }); const thinkingHeader = aiMsg.querySelector('.thinking-header'); if (thinkingHeader && !thinkingHeader.dataset.listenerAdded) { thinkingHeader.dataset.listenerAdded = 'true'; thinkingHeader.addEventListener('click', () => { const toggle = thinkingHeader.querySelector('.thinking-toggle'); const content = thinkingHeader.nextElementSibling; toggle.classList.toggle('collapsed'); content.classList.toggle('collapsed'); }); } messages.scrollTop = messages.scrollHeight; updateScheduled = false; }); }; const processStreamLine = (line) => { const trimmedLine = line.trim(); if (!trimmedLine.startsWith('data:')) return; const data = trimmedLine.slice(5).trim(); if (data === '[DONE]') return; try { const parsed = JSON.parse(data); let delta = ''; if (parsed.choices && parsed.choices.length > 0) { const choice = parsed.choices[0]; if (choice.delta && choice.delta.content) { delta = choice.delta.content; } else if (choice.message && choice.message.content) { delta = choice.message.content; } else if (choice.text) { delta = choice.text; } } else if (parsed.content) { delta = parsed.content; } if (delta) { fullContent += delta; updateUI(); } } catch (e) { // 忽略解析错误 } }; const buildMessages = () => { const messages = []; if (currentSystemPrompt) { messages.push({role: 'system', content: currentSystemPrompt}); } const memoryRounds = modelParams.memory_rounds; if (memoryRounds > 0 && conversationMessages.length > 0) { const maxMessages = memoryRounds * 2; const startIndex = Math.max(0, conversationMessages.length - maxMessages); const historyMessages = conversationMessages.slice(startIndex); historyMessages.forEach(msg => { messages.push({ role: msg.role === 'ai' ? 'assistant' : msg.role, content: msg.content }); }); } messages.push({role: 'user', content: actualPrompt}); return messages; }; const requestData = { model: config.model, messages: buildMessages(), temperature: modelParams.temperature, max_tokens: modelParams.max_tokens, stream: true }; try { aiMsg.textContent = ''; const response = await fetch(finalUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${provider.key}` }, body: JSON.stringify(requestData) }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const reader = response.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); buffer += chunk; const lines = buffer.split('\n'); buffer = lines.pop(); lines.forEach(processStreamLine); } if (buffer) processStreamLine(buffer); isStreamComplete = true; // 等待最后一次 UI 更新完成 requestAnimationFrame(() => { // 流式完成后添加操作按钮 const aiActions = document.createElement('div'); aiActions.className = 'message-actions'; safeInnerHTML(aiActions, ` <button class="message-action-btn copy-msg-btn" title="复制">📋</button> <button class="message-action-btn delete-msg-btn" title="删除">🗑️</button> <button class="message-action-btn regenerate-msg-btn" title="重新生成">🔄</button> `); aiMsg.appendChild(aiActions); aiMsg.dataset.index = conversationMessages.length; conversationMessages.push({role: 'ai', content: fullContent, html: aiMsg.innerHTML}); saveCurrentConversation(); }); } catch (fetchError) { console.warn('Fetch failed, falling back to GM_xmlhttpRequest:', fetchError); // GM_xmlhttpRequest 回退逻辑保持不变... aiMsg.textContent = '请求失败: ' + fetchError.message; } } catch (e) { aiMsg.textContent = '发送失败: ' + e.message; } }); // 初始化对话 - 检查是否已有对话,避免每次刷新都创建新对话 const conversations = ConfigManager.getConversations(); if (conversations.length > 0) { // 加载最近的对话 loadConversation(conversations[0].id); } else { // 只有在没有任何对话时才创建新对话 createNewConversation(); } // 提示词选择下拉菜单 let currentSystemPrompt = ''; const promptSelectorBtn = sidebar.querySelector('#prompt-selector-btn'); const promptDropdown = sidebar.querySelector('#prompt-dropdown'); const renderPromptDropdown = () => { const prompts = ConfigManager.getPrompts(); promptDropdown.textContent = ''; if (prompts.length === 0) { safeInnerHTML(promptDropdown, '<div class="prompt-dropdown-item" style="text-align:center;color:#999;">暂无提示词</div>'); return; } prompts.forEach((prompt, index) => { const item = document.createElement('div'); item.className = 'prompt-dropdown-item'; safeInnerHTML(item, ` <div class="prompt-title">${prompt.title || '未命名'}</div> <div class="prompt-preview">${prompt.content || ''}</div> `); item.dataset.index = index; promptDropdown.appendChild(item); }); }; promptSelectorBtn.addEventListener('click', (e) => { e.stopPropagation(); const isOpen = promptDropdown.style.display === 'block'; promptDropdown.style.display = isOpen ? 'none' : 'block'; sidebar.querySelector('#params-panel').style.display = 'none'; sidebar.querySelector('#params-selector-btn').classList.remove('selected'); if (!isOpen) renderPromptDropdown(); }); // 模型参数设置 let modelParams = {temperature: 0.7, max_tokens: 2048, memory_rounds: 15}; const paramsSelectorBtn = sidebar.querySelector('#params-selector-btn'); const paramsPanel = sidebar.querySelector('#params-panel'); const tempInput = sidebar.querySelector('#param-temperature'); const maxTokensInput = sidebar.querySelector('#param-max-tokens'); const memoryInput = sidebar.querySelector('#param-memory-rounds'); paramsSelectorBtn.addEventListener('click', (e) => { e.stopPropagation(); const isOpen = paramsPanel.style.display === 'block'; paramsPanel.style.display = isOpen ? 'none' : 'block'; promptDropdown.style.display = 'none'; paramsSelectorBtn.classList.toggle('selected', !isOpen); }); tempInput.addEventListener('input', (e) => { modelParams.temperature = parseFloat(e.target.value) || 0.7; }); maxTokensInput.addEventListener('input', (e) => { modelParams.max_tokens = parseInt(e.target.value) || 2048; }); memoryInput.addEventListener('input', (e) => { modelParams.memory_rounds = parseInt(e.target.value) || 15; }); promptDropdown.addEventListener('click', (e) => { const item = e.target.closest('.prompt-dropdown-item'); if (!item || !item.dataset.index) return; const prompts = ConfigManager.getPrompts(); const prompt = prompts[parseInt(item.dataset.index)]; currentSystemPrompt = prompt.content; promptDropdown.style.display = 'none'; promptSelectorBtn.classList.add('selected'); promptSelectorBtn.title = `已选择: ${prompt.title}`; }); // 初始化加载模型和提示词 renderModelDropdown(); renderPromptDropdown(); document.addEventListener('click', (e) => { if (!e.target.closest('.input-area')) { promptDropdown.style.display = 'none'; paramsPanel.style.display = 'none'; paramsSelectorBtn.classList.remove('selected'); } }); // 对话功能 const sendMessage = async () => { const input = sidebar.querySelector('#user-input'); const messages = sidebar.querySelector('#messages'); const select = sidebar.querySelector('#model-select'); const text = input.value.trim(); if (!text) return; if (!currentSelectedModel) { alert('请先选择模型'); return; } // 如果是新对话的第一条消息,自动命名 if (conversationMessages.length === 0) { const conversations = ConfigManager.getConversations(); const index = conversations.findIndex(c => c.id === currentConversationId); if (index !== -1) { conversations[index].title = text.slice(0, 20); ConfigManager.saveConversations(conversations); renderConversations(); } } const userMsg = document.createElement('div'); userMsg.className = 'message user'; userMsg.dataset.index = conversationMessages.length; userMsg.textContent = text; const userActions = document.createElement('div'); userActions.className = 'message-actions'; safeInnerHTML(userActions, ` <button class="message-action-btn copy-msg-btn" title="复制">📋</button> <button class="message-action-btn delete-msg-btn" title="删除">🗑️</button> `); userMsg.appendChild(userActions); messages.appendChild(userMsg); conversationMessages.push({role: 'user', content: text}); input.value = ''; messages.scrollTop = messages.scrollHeight; const config = JSON.parse(currentSelectedModel); const providers = ConfigManager.getProviders(); const provider = providers[config.provider]; const aiMsg = document.createElement('div'); aiMsg.className = 'message ai'; aiMsg.textContent = '思考中...'; messages.appendChild(aiMsg); try { const finalUrl = normalizeApiUrl(provider.url); let fullContent = ''; let buffer = ''; let lastIndex = 0; // UI 更新节流 let updateScheduled = false; let isStreamComplete = false; const updateUI = () => { if (updateScheduled) return; updateScheduled = true; requestAnimationFrame(() => { // 分离思考过程和正文 const thinkMatch = fullContent.match(/<think>([\s\S]*?)<\/think>/); let thinkingContent = ''; let mainContent = fullContent; if (thinkMatch) { thinkingContent = thinkMatch[1].trim(); mainContent = fullContent.replace(/<think>[\s\S]*?<\/think>/, '').trim(); } let html = ''; if (thinkingContent) { const collapsed = isStreamComplete ? 'collapsed' : ''; html += `<div class="thinking-section"> <div class="thinking-header"> <span class="thinking-toggle ${collapsed}">▼</span> <span>思考过程</span> </div> <div class="thinking-content ${collapsed}">${marked.parse(thinkingContent)}</div> </div>`; } if (mainContent) { html += marked.parse(mainContent); } safeInnerHTML(aiMsg, html, fullContent); aiMsg.querySelectorAll('pre code').forEach((block) => { hljs.highlightElement(block); }); // 添加折叠功能 const thinkingHeader = aiMsg.querySelector('.thinking-header'); if (thinkingHeader && !thinkingHeader.dataset.listenerAdded) { thinkingHeader.dataset.listenerAdded = 'true'; thinkingHeader.addEventListener('click', () => { const toggle = thinkingHeader.querySelector('.thinking-toggle'); const content = thinkingHeader.nextElementSibling; toggle.classList.toggle('collapsed'); content.classList.toggle('collapsed'); }); } messages.scrollTop = messages.scrollHeight; updateScheduled = false; }); }; const processStreamLine = (line) => { const trimmedLine = line.trim(); if (!trimmedLine.startsWith('data:')) return; const data = trimmedLine.slice(5).trim(); if (data === '[DONE]') return; try { const parsed = JSON.parse(data); let delta = ''; if (parsed.choices && parsed.choices.length > 0) { const choice = parsed.choices[0]; if (choice.delta && choice.delta.content) { delta = choice.delta.content; } else if (choice.message && choice.message.content) { delta = choice.message.content; } else if (choice.text) { delta = choice.text; } } else if (parsed.content) { delta = parsed.content; } if (delta) { fullContent += delta; updateUI(); } } catch (e) { // 忽略解析错误 } }; // 构建消息历史 const buildMessages = () => { const messages = []; if (currentSystemPrompt) { messages.push({role: 'system', content: currentSystemPrompt}); } // 获取历史消息 const memoryRounds = modelParams.memory_rounds; if (memoryRounds > 0 && conversationMessages.length > 0) { // 计算需要包含的消息数量(每轮包含用户和AI两条消息) const maxMessages = memoryRounds * 2; const startIndex = Math.max(0, conversationMessages.length - maxMessages); const historyMessages = conversationMessages.slice(startIndex); historyMessages.forEach(msg => { messages.push({ role: msg.role === 'ai' ? 'assistant' : msg.role, content: msg.content }); }); } // 添加当前用户消息 messages.push({role: 'user', content: text}); return messages; }; const requestData = { model: config.model, messages: buildMessages(), temperature: modelParams.temperature, max_tokens: modelParams.max_tokens, stream: true }; // 优先尝试使用 fetch (支持流式) try { aiMsg.textContent = ''; const response = await fetch(finalUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${provider.key}` }, body: JSON.stringify(requestData) }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const reader = response.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); buffer += chunk; const lines = buffer.split('\n'); buffer = lines.pop(); lines.forEach(processStreamLine); } // 处理剩余 buffer if (buffer) processStreamLine(buffer); isStreamComplete = true; // 等待最后一次 UI 更新完成后再添加按钮 requestAnimationFrame(() => { updateUI(); requestAnimationFrame(() => { // 流式完成后添加操作按钮 const aiActions = document.createElement('div'); aiActions.className = 'message-actions'; safeInnerHTML(aiActions, ` <button class="message-action-btn copy-msg-btn" title="复制">📋</button> <button class="message-action-btn delete-msg-btn" title="删除">🗑️</button> <button class="message-action-btn regenerate-msg-btn" title="重新生成">🔄</button> `); aiMsg.appendChild(aiActions); aiMsg.dataset.index = conversationMessages.length; // 保存AI回复 conversationMessages.push({role: 'ai', content: fullContent, html: aiMsg.innerHTML}); saveCurrentConversation(); }); }); } catch (fetchError) { console.warn('Fetch failed, falling back to GM_xmlhttpRequest:', fetchError); // 如果 fetch 失败 (可能是 CORS),回退到 GM_xmlhttpRequest // 注意:GM_xmlhttpRequest 在某些环境下可能不支持流式,会退化为一次性输出 GM_xmlhttpRequest({ method: 'POST', url: finalUrl, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${provider.key}` }, data: JSON.stringify(requestData), onloadstart: () => { if (!fullContent) aiMsg.textContent = '正在重试...'; }, onprogress: (response) => { const responseText = response.responseText; if (!responseText) return; const newText = responseText.slice(lastIndex); if (newText.length === 0) return; lastIndex = responseText.length; buffer += newText; const lines = buffer.split('\n'); buffer = lines.pop(); lines.forEach(processStreamLine); }, onload: (response) => { if (buffer) processStreamLine(buffer); isStreamComplete = true; updateUI(); // 保存AI回复 if (fullContent) { // 添加AI消息操作按钮 const aiActions = document.createElement('div'); aiActions.className = 'message-actions'; safeInnerHTML(aiActions, ` <button class="message-action-btn copy-msg-btn" title="复制">📋</button> <button class="message-action-btn delete-msg-btn" title="删除">🗑️</button> <button class="message-action-btn regenerate-msg-btn" title="重新生成">🔄</button> `); aiMsg.appendChild(aiActions); aiMsg.dataset.index = conversationMessages.length; conversationMessages.push({role: 'ai', content: fullContent, html: aiMsg.innerHTML}); saveCurrentConversation(); } if (!fullContent) { // 最后的兜底解析 try { const data = JSON.parse(response.responseText); if (data.choices && data.choices[0]?.message?.content) { fullContent = data.choices[0].message.content; updateUI(); } else if (data.error) { aiMsg.textContent = 'API错误: ' + (data.error.message || JSON.stringify(data.error)); } } catch (e) { aiMsg.textContent = '请求失败: ' + (fetchError.message || '未知错误'); } } }, onerror: (err) => { aiMsg.textContent = '请求失败: ' + (err.statusText || '网络错误'); } }); } } catch (e) { aiMsg.textContent = '发送失败: ' + e.message; } }; // 消息操作事件处理 sidebar.querySelector('#messages').addEventListener('click', async (e) => { const btn = e.target.closest('.message-action-btn'); if (!btn) return; const msgDiv = btn.closest('.message'); const msgIndex = parseInt(msgDiv.dataset.index); if (btn.classList.contains('copy-msg-btn')) { // 复制消息 const msg = conversationMessages[msgIndex]; try { await navigator.clipboard.writeText(msg.content); btn.textContent = '✓'; setTimeout(() => btn.textContent = '📋', 1000); } catch (err) { alert('复制失败'); } } else if (btn.classList.contains('delete-msg-btn')) { // 删除消息 if (!confirm('确定删除此消息吗?')) return; conversationMessages.splice(msgIndex, 1); msgDiv.remove(); // 更新后续消息的索引 const allMsgs = sidebar.querySelectorAll('#messages .message'); allMsgs.forEach((m, i) => { m.dataset.index = i; }); saveCurrentConversation(); } else if (btn.classList.contains('regenerate-msg-btn')) { // 重新生成 if (!confirm('确定重新生成此回复吗?')) return; // 删除当前AI消息 conversationMessages.splice(msgIndex, 1); msgDiv.remove(); // 更新后续消息的索引 const allMsgs = sidebar.querySelectorAll('#messages .message'); allMsgs.forEach((m, i) => { m.dataset.index = i; }); // 获取上一条用户消息并重新发送 const userMsg = conversationMessages[msgIndex - 1]; if (userMsg && userMsg.role === 'user') { const input = sidebar.querySelector('#user-input'); input.value = userMsg.content; // 删除用户消息,因为sendMessage会重新添加 conversationMessages.splice(msgIndex - 1, 1); const userMsgDiv = sidebar.querySelector(`#messages .message[data-index="${msgIndex - 1}"]`); if (userMsgDiv) userMsgDiv.remove(); // 更新索引 const updatedMsgs = sidebar.querySelectorAll('#messages .message'); updatedMsgs.forEach((m, i) => { m.dataset.index = i; }); await sendMessage(); } } }); sidebar.querySelector('#send-btn').addEventListener('click', sendMessage); sidebar.querySelector('#user-input').addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }); // 提示词库功能 const renderPrompts = () => { const prompts = ConfigManager.getPrompts(); const list = sidebar.querySelector('#prompts-list'); list.textContent = ''; prompts.forEach((prompt, index) => { const item = document.createElement('div'); item.className = 'prompt-item'; item.dataset.index = index; safeInnerHTML(item, ` <input type="checkbox" class="prompt-checkbox" data-index="${index}"> <div class="prompt-header"> <div class="prompt-title">${prompt.title || '未命名'}</div> <div class="prompt-actions"> <button class="view-btn" data-index="${index}">查看</button> <button class="edit-btn" data-index="${index}">编辑</button> <button class="delete-btn" data-index="${index}">删除</button> </div> </div> <div class="prompt-content" style="display:none;">${prompt.content || ''}</div> `); list.appendChild(item); }); }; sidebar.querySelector('#add-prompt').addEventListener('click', () => { const prompts = ConfigManager.getPrompts(); prompts.push({title: '', content: ''}); ConfigManager.savePrompts(prompts); renderPrompts(); const items = sidebar.querySelectorAll('.prompt-item'); const lastItem = items[items.length - 1]; if (lastItem) { lastItem.querySelector('.edit-btn').click(); } }); sidebar.querySelector('#batch-delete-prompt').addEventListener('click', () => { const checkboxes = sidebar.querySelectorAll('.prompt-checkbox:checked'); if (checkboxes.length === 0) { alert('请选择要删除的提示词'); return; } if (!confirm(`确定删除选中的 ${checkboxes.length} 个提示词吗?`)) return; const prompts = ConfigManager.getPrompts(); const indices = Array.from(checkboxes).map(cb => parseInt(cb.dataset.index)).sort((a, b) => b - a); indices.forEach(index => prompts.splice(index, 1)); ConfigManager.savePrompts(prompts); renderPrompts(); }); sidebar.querySelector('#prompts-list').addEventListener('click', (e) => { const index = parseInt(e.target.dataset.index); const item = sidebar.querySelector(`.prompt-item[data-index="${index}"]`); if (e.target.classList.contains('view-btn')) { const content = item.querySelector('.prompt-content'); content.style.display = content.style.display === 'none' ? 'block' : 'none'; } else if (e.target.classList.contains('edit-btn')) { const prompts = ConfigManager.getPrompts(); const prompt = prompts[index]; item.classList.add('editing'); safeInnerHTML(item, ` <div class="prompt-form"> <input type="text" placeholder="标题" value="${prompt.title || ''}" class="prompt-title-input"> <textarea placeholder="内容" class="prompt-content-input">${prompt.content || ''}</textarea> <div class="form-actions"> <button class="save-prompt-btn" data-index="${index}">保存</button> <button class="cancel-btn" data-index="${index}">取消</button> </div> </div> `); } else if (e.target.classList.contains('delete-btn')) { if (!confirm('确定删除此提示词吗?')) return; const prompts = ConfigManager.getPrompts(); prompts.splice(index, 1); ConfigManager.savePrompts(prompts); renderPrompts(); } else if (e.target.classList.contains('save-prompt-btn')) { const prompts = ConfigManager.getPrompts(); const titleInput = item.querySelector('.prompt-title-input'); const contentInput = item.querySelector('.prompt-content-input'); prompts[index] = { title: titleInput.value.trim() || '未命名', content: contentInput.value.trim() }; ConfigManager.savePrompts(prompts); renderPrompts(); } else if (e.target.classList.contains('cancel-btn')) { renderPrompts(); } }); renderPrompts(); // 系统配置功能 const renderSystemConfig = () => { const systemConfig = ConfigManager.getSystemConfig(); const modelSelect = sidebar.querySelector('#default-model-select'); const promptSelect = sidebar.querySelector('#default-prompt-select'); // 填充模型选项 modelSelect.innerHTML = '<option value="">未设置</option>'; const providers = ConfigManager.getProviders(); providers.forEach((provider, providerIndex) => { const models = ConfigManager.getModels(providerIndex); models.forEach(model => { const option = document.createElement('option'); const modelValue = JSON.stringify({provider: providerIndex, model: model}); option.value = modelValue; option.textContent = `${provider.name} - ${model}`; if (systemConfig.defaultModel === modelValue) { option.selected = true; } modelSelect.appendChild(option); }); }); // 填充提示词选项 promptSelect.innerHTML = '<option value="">未设置</option>'; const prompts = ConfigManager.getPrompts(); prompts.forEach((prompt, index) => { const option = document.createElement('option'); option.value = index; option.textContent = prompt.title || '未命名'; if (systemConfig.defaultPrompt === index) { option.selected = true; } promptSelect.appendChild(option); }); }; sidebar.querySelector('#save-system-config').addEventListener('click', () => { const modelSelect = sidebar.querySelector('#default-model-select'); const promptSelect = sidebar.querySelector('#default-prompt-select'); const config = { defaultModel: modelSelect.value || null, defaultPrompt: promptSelect.value ? parseInt(promptSelect.value) : null }; ConfigManager.saveSystemConfig(config); alert('系统配置已保存'); }); // 监听标签页切换,当切换到系统配置时刷新选项 sidebar.querySelectorAll('.tab').forEach(tab => { tab.addEventListener('click', () => { if (tab.dataset.tab === 'system') { renderSystemConfig(); } }); }); }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();