// ==UserScript== // @name 聊天助手大纲 // @namespace http://tampermonkey.net/ // @version 0.0.2 // @description 为多个AI聊天平台生成智能对话大纲,支持实时更新、层级结构、主题切换,提升聊天体验和内容导航效率 // @author xzhao // @match *://chatgpt.com/* // @match *://chat.deepseek.com/* // @match *://grok.com/* // @match *://www.qianwen.com/* // @match *://chat.qwen.ai/* // @match *://*.doubao.com/* // @icon https://cdn.deepseek.com/chat/icon.png // @license MIT // @grant none // @downloadURL https://update.greasyfork.icu/scripts/558751/%E8%81%8A%E5%A4%A9%E5%8A%A9%E6%89%8B%E5%A4%A7%E7%BA%B2.user.js // @updateURL https://update.greasyfork.icu/scripts/558751/%E8%81%8A%E5%A4%A9%E5%8A%A9%E6%89%8B%E5%A4%A7%E7%BA%B2.meta.js // ==/UserScript== (function() { // 全局配置 const GLOBAL_CONFIG = { // 主题配置 theme: { // 高亮配置 highlightColor: '#83daff2a', highlightTime: 2000, // 当前主题模式 currentTheme: 'light', // 'light' 或 'dark' // 颜色配置 colors: { light: { primary: '#28a745', user: '#007acc', background: '#ffffff', border: '#ddd', shadow: 'rgba(0,0,0,0.15)', text: '#333', headerBg: '#ffffff', // 标题级别颜色 headers: { h1: '#dc3545', h2: '#fd7e14', h3: '#ffc107', h4: '#28a745', h5: '#17a2b8', h6: '#6f42c1' } }, dark: { primary: '#4caf50', user: '#64b5f6', background: '#2d2d2d', border: '#555', shadow: 'rgba(0,0,0,0.3)', text: '#e0e0e0', headerBg: '#2d2d2d', // 标题级别颜色 headers: { h1: '#f48fb1', h2: '#ffab91', h3: '#fff176', h4: '#81c784', h5: '#4dd0e1', h6: '#b39ddb' } } }, // 尺寸配置 sizes: { outlineWidth: '300px', borderRadius: '8px', padding: '15px', fontSize: '14px', indentSize: 15 } }, // 功能配置 features: { autoExpand: true, showUserMessages: true, showAIMessages: true, enableAnimation: true, isVisible: true, // 大纲是否可见 textLength: 50, //大纲展示的字符数 debouncedInterval: 500 //大纲更新间隔 }, text:{ title:"对话大纲" } }; function judgePlatform(){ // 根据当前URL判断平台 if(window.location.hostname.includes('deepseek.com')){ return 'deepseek'; } if(window.location.hostname.includes('doubao.com')){ return 'doubao'; } if(window.location.hostname.includes('chatgpt.com')){ return 'chatgpt'; } if(window.location.hostname.includes('grok.com')){ return 'grok'; } if(window.location.hostname.includes('qianwen.com')){ return 'tongyi'; } if(window.location.hostname.includes('qwen.ai')){ return 'qwen'; } return 'unknown'; } //获取解析所需配置 function getParserConfig(platform){ switch(platform){ case 'deepseek': return { //获取对话区域元素,返回一个不会被清除的节点作为监视根节点 selectChatArea:function(){ return document.querySelectorAll('.ds-scroll-area')[2]; }, //根据传入的监视根节点获取其对应的对话历史列表 getMessageList:function(root){ if(!root.firstChild||root.firstChild.tagName=="TEXTAREA"){ return null; } return root.firstChild.children; } , //判断是否为用户消息,传入参数为每一个消息对话框 determineMessageOwner:function(messageEle){ if(messageEle.dataset.umId!=undefined){ return MessageOwner.User; } return MessageOwner.Assistant; } , //将整个大纲元素插入到指定位置中,不要做其它处理,保证出错时会直接抛出异常 insertOutline:function(outlineEle){ let b1=document.querySelectorAll('.ds-scroll-area')[0].parentElement.parentElement.parentElement; b1.appendChild(outlineEle); } }; case "doubao": return { //获取对话区域元素,返回一个不会被清除的节点作为监视根节点 selectChatArea:function(){ return document.querySelector('[data-testid="scroll_view"]').parentElement.parentElement; }, //根据传入的监视根节点获取其对应的对话历史列表 getMessageList:function(root){ if(!root || !root.querySelector){ return null; } const children = root.querySelectorAll('.container-PvPoAn'); if(!children){ return null; } return children; } , //判断是否为用户消息,传入参数为每一个消息对话框 determineMessageOwner:function(messageEle){ // 豆包中用户消息通常包含 send_message 的 data-testid if(messageEle.querySelector('[data-testid*="send_message"]') || messageEle.querySelector('[data-testid="send_message"]')){ return MessageOwner.User; } // AI消息通常包含 receive_message 的 data-testid if(messageEle.querySelector('[data-testid*="receive_message"]') || messageEle.querySelector('[data-testid="receive_message"]')){ return MessageOwner.Assistant; } return MessageOwner.Other; } , //将整个大纲元素插入到指定位置中 insertOutline:function(outlineEle){ // 找到豆包的主布局容器,插入到侧边栏区域 const chatLayout = document.querySelector('[data-testid="scroll_view"]') .parentElement.parentElement.parentElement.parentElement.parentElement; chatLayout.appendChild(outlineEle); } }; case "chatgpt": return { //获取对话区域元素,返回一个不会被清除的节点作为监视根节点 selectChatArea:function(){ return document.querySelector('#main') || document.querySelector('main'); }, //根据传入的监视根节点获取其对应的对话历史列表 getMessageList:function(root){ if(!root || !root.querySelectorAll){ return null; } const messages = root.querySelectorAll('[data-message-author-role]'); if(messages){ return messages; } // 备选方案:查找包含对话的 article 元素 const articles = root.querySelectorAll('article'); if(articles && articles.length > 0){ return articles; } return null; } , //判断是否为用户消息,传入参数为每一个消息对话框 determineMessageOwner:function(messageEle){ // ChatGPT 使用 data-message-author-role 属性来标识消息类型 const authorRole = messageEle.getAttribute('data-message-author-role'); if(authorRole === 'user'){ return MessageOwner.User; } if(authorRole === 'assistant'){ return MessageOwner.Assistant; } // 备选方案:通过查找子元素来判断 if(messageEle.querySelector('[data-message-author-role="user"]')){ return MessageOwner.User; } if(messageEle.querySelector('[data-message-author-role="assistant"]')){ return MessageOwner.Assistant; } return MessageOwner.Other; } , //将整个大纲元素插入到指定位置中 insertOutline:function(outlineEle){ const mainContainer = document.querySelector('#main').parentElement.parentElement; mainContainer.appendChild(outlineEle); } }; case "grok": return { //获取对话区域元素,返回一个不会被清除的节点作为监视根节点 selectChatArea:function(){ return document.querySelector('#last-reply-container').parentElement; }, //根据传入的监视根节点获取其对应的对话历史列表 getMessageList:function(root){ if(!root || !root.querySelectorAll){ return null; } let messages = root.querySelectorAll(':scope > .relative'); return [...messages,...root.querySelectorAll(':scope > #last-reply-container > div > .relative')]; } , //判断是否为用户消息,传入参数为每一个消息对话框 determineMessageOwner:function(messageEle){ //根据对话框下面的按钮数量判断消息发送者 const l=messageEle.children[2].firstChild.children.length; if(l<5){ return MessageOwner.User; } return MessageOwner.Assistant; } , //将整个大纲元素插入到指定位置中 insertOutline:function(outlineEle){ // 找到 Grok 的主容器 const chatContainer = document.querySelector('main'); chatContainer.parentElement.appendChild(outlineEle); } }; case "tongyi": return { //获取对话区域元素,返回一个不会被清除的节点作为监视根节点 selectChatArea:function(){ return document.querySelector('.scrollWrapper-LOelOS'); }, //根据传入的监视根节点获取其对应的对话历史列表 getMessageList:function(root){ if(!root || !root.querySelectorAll){ return null; } // 尝试查找包含对话的元素 let messages = root.querySelectorAll('div[class^="content-"]'); return messages; } , //判断是否为用户消息,传入参数为每一个消息对话框 determineMessageOwner:function(messageEle){ let className=messageEle.parentElement.className; // 通过类名判断 if(className.includes('questionItem')){ return MessageOwner.User; } className=messageEle.parentElement.parentElement.className; if(className.includes('answerItem')){ return MessageOwner.Assistant; } return MessageOwner.Other; } , //将整个大纲元素插入到指定位置中 insertOutline:function(outlineEle){ // 找到通义千问的主容器 const tongyiContainer = document.querySelectorAll('.mainContent-GBAlug')[1] .parentElement.parentElement; tongyiContainer.appendChild(outlineEle); } }; case "qwen": return { //获取对话区域元素,返回一个不会被清除的节点作为监视根节点 selectChatArea:function(){ return document.querySelector('#chat-message-container') }, //根据传入的监视根节点获取其对应的对话历史列表 getMessageList:function(root){ if(!root || !root.querySelectorAll){ return null; } let messages = root.querySelectorAll('.response-message-content, .chat-user-message'); return messages; } , //判断是否为用户消息,传入参数为每一个消息对话框 determineMessageOwner:function(messageEle){ if(messageEle.className.includes('chat-user-message')){ return MessageOwner.User; } return MessageOwner.Assistant; } , //将整个大纲元素插入到指定位置中 insertOutline:function(outlineEle){ // 找到 Qwen 的主容器 const mainContainer = document.querySelector('.desktop-layout'); mainContainer.style.backgroundColor=getCurrentColors().background; mainContainer.appendChild(outlineEle); } }; default: return null; } } const MessageOwner = Object.freeze({ User:'user', Assistant:'assitant', Other:'other' }); // 获取当前主题颜色 function getCurrentColors() { return GLOBAL_CONFIG.theme.colors[GLOBAL_CONFIG.theme.currentTheme]; } // 插入CSS样式 function insertStyles() { const styleId = 'chat-outline-styles'; if (document.getElementById(styleId)) return; // 避免重复插入 const style = document.createElement('style'); style.id = styleId; updateStyleContent(style); document.head.appendChild(style); } // 更新样式内容 function updateStyleContent(style) { const colors = getCurrentColors(); style.textContent = ` /* 大纲容器样式 */ #chat-outline { width: ${GLOBAL_CONFIG.theme.sizes.outlineWidth}; background: ${colors.background}; border-radius: ${GLOBAL_CONFIG.theme.sizes.borderRadius}; padding: 0; box-shadow: 0 4px 12px ${colors.shadow}; font-size: ${GLOBAL_CONFIG.theme.sizes.fontSize}; transition: all 0.3s ease; display: ${GLOBAL_CONFIG.features.isVisible ? 'block' : 'none'}; height: 100dvh; } /* 固定在右侧的大纲样式 */ #chat-outline.outline-fixed-right { position: fixed; top: 0; right: 0; z-index: 10000; height: 100vh; border-radius: ${GLOBAL_CONFIG.theme.sizes.borderRadius} 0 0 ${GLOBAL_CONFIG.theme.sizes.borderRadius}; border-right: none; box-shadow: -4px 0 12px ${colors.shadow}; } /* 大纲头部样式 */ .chat-outline-header { display: flex; justify-content: space-between; align-items: center; padding-left: 15px; border-bottom: 1px solid ${colors.border}; background: ${colors.headerBg}; border-radius: ${GLOBAL_CONFIG.theme.sizes.borderRadius} ${GLOBAL_CONFIG.theme.sizes.borderRadius} 0 0; height: 10%; } /* 大纲标题样式 */ .chat-outline-title { margin: 0; color: ${colors.text}; font-size: 16px; font-weight: bold; } /* 控制按钮容器 */ .outline-controls { display: flex; gap: 8px; } /* 控制按钮样式 */ .outline-btn { background: none; border: 1px solid ${colors.border}; border-radius: 4px; padding: 4px 8px; cursor: pointer; font-size: 12px; color: ${colors.text}; transition: all 0.2s ease; } .outline-btn:hover { background: ${colors.border}; transform: scale(1.05); } #outline-content { overflow-y: auto; height: 90%; } /* 用户消息项样式 */ .outline-user-item { margin: 8px 15px; padding: 8px; background: ${GLOBAL_CONFIG.theme.currentTheme === 'light' ? '#f0f8ff' : '#3a3a3a'}; border-left: 3px solid ${colors.user}; border-radius: 4px; cursor: pointer; transition: background-color 0.2s; color: ${colors.text}; } .outline-user-item:hover { background: ${GLOBAL_CONFIG.theme.currentTheme === 'light' ? '#e6f3ff' : '#4a4a4a'}; } /* AI消息容器样式 */ .outline-ai-container { margin: 8px 15px; border-left: 3px solid ${colors.primary}; border-radius: 4px; background: ${GLOBAL_CONFIG.theme.currentTheme === 'light' ? '#f8f8f8' : '#3a3a3a'}; } /* AI消息头部样式 */ .outline-ai-header { padding: 8px; cursor: pointer; font-weight: bold; display: flex; justify-content: space-between; align-items: center; transition: background-color 0.2s; color: ${colors.text}; } .outline-ai-header:hover { background: ${GLOBAL_CONFIG.theme.currentTheme === 'light' ? '#f0f0f0' : '#4a4a4a'}; } /* AI消息简单项样式 */ .outline-ai-item { margin: 8px 15px; padding: 8px; background: ${GLOBAL_CONFIG.theme.currentTheme === 'light' ? '#f8f8f8' : '#3a3a3a'}; border-left: 3px solid ${colors.primary}; border-radius: 4px; cursor: pointer; transition: background-color 0.2s; color: ${colors.text}; } .outline-ai-item:hover { background: ${GLOBAL_CONFIG.theme.currentTheme === 'light' ? '#f0f0f0' : '#4a4a4a'}; } /* 树形节点样式 */ .tree-node { margin: 4px 0; padding: 4px 8px; border-radius: 3px; cursor: pointer; display: flex; justify-content: space-between; align-items: center; transition: background-color 0.2s; color: ${colors.text}; } .tree-node:hover { background: ${GLOBAL_CONFIG.theme.currentTheme === 'light' ? '#e9ecef' : '#4a4a4a'} !important; } .tree-node-level-0 { background: ${GLOBAL_CONFIG.theme.currentTheme === 'light' ? '#e9ecef' : '#404040'}; font-size: 14px; } .tree-node-level-1 { background: ${GLOBAL_CONFIG.theme.currentTheme === 'light' ? '#f1f3f4' : '#383838'}; font-size: 13px; } .tree-node-level-2 { background: ${GLOBAL_CONFIG.theme.currentTheme === 'light' ? '#f8f9fa' : '#353535'}; font-size: 12px; } .tree-node-level-3 { background: ${GLOBAL_CONFIG.theme.currentTheme === 'light' ? '#f8f9fa' : '#353535'}; font-size: 11px; } /* 切换按钮样式 */ .toggle-btn { font-size: 14px; transition: transform 0.2s; cursor: pointer; margin-left: 8px; user-select: none; color: ${colors.text}; padding: 4px; border-radius: 3px; display: inline-flex; align-items: center; justify-content: center; min-width: 20px; min-height: 20px; line-height: 1; } .toggle-btn:hover { background-color: ${colors.border}; transform: scale(1.1); } .toggle-btn.collapsed { transform: rotate(-90deg); } .toggle-btn.collapsed:hover { background-color: ${colors.border}; transform: rotate(-90deg) scale(1.1); } /* 标题级别边框颜色 */ .header-level-1 { border-left: 2px solid ${colors.headers.h1}; } .header-level-2 { border-left: 2px solid ${colors.headers.h2}; } .header-level-3 { border-left: 2px solid ${colors.headers.h3}; } .header-level-4 { border-left: 2px solid ${colors.headers.h4}; } .header-level-5 { border-left: 2px solid ${colors.headers.h5}; } .header-level-6 { border-left: 2px solid ${colors.headers.h6}; } /* 动画效果 */ .highlight-animation { animation: highlight 0.5s ease-in-out; } @keyframes highlight { 0% { background-color: transparent; } 50% { background-color: ${GLOBAL_CONFIG.theme.highlightColor}; } 100% { background-color: transparent; } } /* 性能优化:使用 transform 和 opacity 进行动画 */ .tree-node, .outline-user-item, .outline-ai-item, .outline-ai-header { will-change: transform; backface-visibility: hidden; } /* 显示按钮样式 */ #show-outline-btn { position: fixed; top: 50%; right: 20px; transform: translateY(-50%); background: ${colors.background}; border: 1px solid ${colors.border}; border-radius: 50%; width: 40px; height: 40px; cursor: pointer; font-size: 16px; color: ${colors.text}; box-shadow: 0 2px 8px ${colors.shadow}; transition: all 0.3s ease; z-index: 9999; display: ${GLOBAL_CONFIG.features.isVisible ? 'none' : 'flex'}; align-items: center; justify-content: center; } #show-outline-btn:hover { background: ${colors.border}; transform: translateY(-50%) scale(1.1); } `; } // 构建标题层级树结构 function buildHeaderTree(headers) { const tree = []; const stack = []; headers.forEach(header => { const level = parseInt(header.tagName.charAt(1)); // h1->1, h2->2, etc. const node = { element: header, level: level, text: header.textContent, children: [] }; // 找到合适的父节点 while (stack.length > 0 && stack[stack.length - 1].level >= level) { stack.pop(); } if (stack.length === 0) { tree.push(node); } else { stack[stack.length - 1].children.push(node); } stack.push(node); }); return tree; } // 创建可展开收起的树形结构DOM元素 function createTreeStructure(nodes, depth) { const container = document.createElement('div'); container.style.marginLeft = `${depth * GLOBAL_CONFIG.theme.sizes.indentSize}px`; nodes.forEach(node => { const nodeWrapper = document.createElement('div'); const nodeElement = document.createElement('div'); nodeElement.className = `tree-node tree-node-level-${Math.min(depth, 3)} header-level-${node.level}`; const textSpan = document.createElement('span'); textSpan.textContent = node.text; nodeElement.appendChild(textSpan); // 如果有子节点,添加展开/收起按钮 if (node.children.length > 0) { const toggleBtn = document.createElement('span'); toggleBtn.textContent = '▼'; toggleBtn.className = 'toggle-btn'; nodeElement.appendChild(toggleBtn); // 创建子节点容器 const childContainer = createTreeStructure(node.children, depth + 1); childContainer.style.display = GLOBAL_CONFIG.features.autoExpand ? 'block' : 'none'; // 添加展开/收起功能 toggleBtn.onclick = (e) => { e.stopPropagation(); const isExpanded = childContainer.style.display !== 'none'; childContainer.style.display = isExpanded ? 'none' : 'block'; toggleBtn.textContent = isExpanded ? '▶' : '▼'; toggleBtn.classList.toggle('collapsed', isExpanded); }; nodeWrapper.appendChild(childContainer); } // 添加点击跳转功能(点击文本部分) nodeElement.onclick = (e) => { e.stopPropagation(); node.element.scrollIntoView({ behavior: 'smooth', block: 'center' }); highlightElement(node.element); }; nodeWrapper.insertBefore(nodeElement, nodeWrapper.firstChild); container.appendChild(nodeWrapper); }); return container; } // 高亮元素 function highlightElement(element) { // 移除可能存在的高亮类 element.classList.remove('highlight-animation'); // 强制重排以确保类被移除 element.offsetHeight; // 添加高亮类 element.classList.add('highlight-animation'); // 在动画结束后移除类 setTimeout(() => { element.classList.remove('highlight-animation'); }, GLOBAL_CONFIG.theme.highlightTime); } // 切换主题 function toggleTheme() { GLOBAL_CONFIG.theme.currentTheme = GLOBAL_CONFIG.theme.currentTheme === 'light' ? 'dark' : 'light'; // 更新样式 const style = document.getElementById('chat-outline-styles'); if (style) { updateStyleContent(style); } // 保存主题设置到localStorage localStorage.setItem('chat-outline-theme', GLOBAL_CONFIG.theme.currentTheme); } // 当大纲插入失败时,将其插入到body并固定在页面右侧 function insertOutlineToBodyFixed(outlineEle) { // 添加固定定位的样式类 outlineEle.classList.add('outline-fixed-right'); // 插入到body document.body.appendChild(outlineEle); console.log('大纲已插入到body并固定在页面右侧'); } // 切换大纲可见性 function toggleOutlineVisibility() { GLOBAL_CONFIG.features.isVisible = !GLOBAL_CONFIG.features.isVisible; const outlineEle = document.getElementById('chat-outline'); const showBtn = document.getElementById('show-outline-btn'); if (outlineEle) { outlineEle.style.display = GLOBAL_CONFIG.features.isVisible ? 'block' : 'none'; } if (showBtn) { showBtn.style.display = GLOBAL_CONFIG.features.isVisible ? 'none' : 'flex'; } // 保存可见性设置到localStorage localStorage.setItem('chat-outline-visible', GLOBAL_CONFIG.features.isVisible); } // 创建显示按钮 function createShowButton() { // 检查是否已存在显示按钮 if (document.getElementById('show-outline-btn')) { return; } const showBtn = document.createElement('button'); showBtn.id = 'show-outline-btn'; showBtn.innerHTML = '📋'; showBtn.title = '显示对话大纲'; showBtn.onclick = toggleOutlineVisibility; document.body.appendChild(showBtn); } // 从localStorage加载设置 function loadSettings() { const savedTheme = localStorage.getItem('chat-outline-theme'); if (savedTheme && (savedTheme === 'light' || savedTheme === 'dark')) { GLOBAL_CONFIG.theme.currentTheme = savedTheme; } const savedVisible = localStorage.getItem('chat-outline-visible'); if (savedVisible !== null) { GLOBAL_CONFIG.features.isVisible = savedVisible === 'true'; } } function initOutlineEle(){ const outlineEle = document.createElement('div'); outlineEle.id = 'chat-outline'; // 创建头部容器 const header = document.createElement('div'); header.className = 'chat-outline-header'; // 添加标题 const title = document.createElement('h3'); title.textContent = GLOBAL_CONFIG.text.title; title.className = 'chat-outline-title'; header.appendChild(title); // 创建控制按钮容器 const controls = document.createElement('div'); controls.className = 'outline-controls'; // 刷新按钮 const refreshBtn = document.createElement('button'); refreshBtn.className = 'outline-btn'; refreshBtn.innerHTML = '🔄'; refreshBtn.title = '强制刷新大纲'; refreshBtn.onclick = () => { // 触发强制刷新 if (GLOBAL_OBJ.forceRefreshOutline) { GLOBAL_OBJ.forceRefreshOutline(); } }; // 展开/收起所有节点按钮 const toggleAllBtn = document.createElement('button'); toggleAllBtn.className = 'outline-btn'; toggleAllBtn.innerHTML = '📂'; toggleAllBtn.title = '展开/收起所有节点'; toggleAllBtn.onclick = () => { if (GLOBAL_OBJ.toggleAllNodes) { GLOBAL_OBJ.toggleAllNodes(); // 更新按钮图标 toggleAllBtn.innerHTML = GLOBAL_OBJ.allExpanded ? '📂' : '📁'; toggleAllBtn.title = GLOBAL_OBJ.allExpanded ? '收起所有节点' : '展开所有节点'; } }; // 主题切换按钮 const themeBtn = document.createElement('button'); themeBtn.className = 'outline-btn'; themeBtn.innerHTML = GLOBAL_CONFIG.theme.currentTheme === 'light' ? '🌙' : '☀️'; themeBtn.title = '切换主题'; themeBtn.onclick = () => { toggleTheme(); themeBtn.innerHTML = GLOBAL_CONFIG.theme.currentTheme === 'light' ? '🌙' : '☀️'; }; // 隐藏按钮 const hideBtn = document.createElement('button'); hideBtn.className = 'outline-btn'; hideBtn.innerHTML = '✕'; hideBtn.title = '隐藏大纲'; hideBtn.onclick = toggleOutlineVisibility; controls.appendChild(refreshBtn); controls.appendChild(toggleAllBtn); controls.appendChild(themeBtn); controls.appendChild(hideBtn); header.appendChild(controls); outlineEle.appendChild(header); // 创建大纲内容容器 const outlineContent = document.createElement('div'); outlineContent.id = 'outline-content'; outlineEle.appendChild(outlineContent); return outlineEle; } // 全局对象,用于存储需要全局访问的变量和函数 const GLOBAL_OBJ = { // 缓存相关变量 messageCache: new Map(), lastMessageCount: 0, MAX_CACHE_SIZE: 100, // 最大缓存条目数 // 运行时对象 currentObserver: null, currentChatArea: null, outlineContent: null, parserConfig: null, debouncedRefresh: null, getCachedChatArea: null, chatArea: null, // 展开/收起状态 allExpanded: true, // 默认展开状态 // 强制刷新函数 forceRefreshOutline: null, toggleAllNodes: null }; // 防抖函数 function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } // 获取消息的唯一标识 function getMessageId(index,messageElement) { let text=messageElement.textContent; // 获取消息唯一标识 return index+text.substring(text.length-10, text.length); } // 检查消息是否已缓存且未变化 function isMessageCached(messageElement, messageId) { if (!GLOBAL_OBJ.messageCache.has(messageId)) { return false; } const cached = GLOBAL_OBJ.messageCache.get(messageId); // 简单检查内容长度是否变化(适用于正在生成的消息) return cached.textLength === messageElement.textContent.length; } // 缓存消息信息 function cacheMessage(messageElement, messageId, outlineElement) { // 如果缓存过大,清理最旧的条目 if (GLOBAL_OBJ.messageCache.size >= GLOBAL_OBJ.MAX_CACHE_SIZE) { const firstKey = GLOBAL_OBJ.messageCache.keys().next().value; GLOBAL_OBJ.messageCache.delete(firstKey); } GLOBAL_OBJ.messageCache.set(messageId, { textLength: messageElement.textContent.length, outlineElement: outlineElement.cloneNode(true), originalElement: messageElement, // 保存原始消息元素的引用 timestamp: Date.now() }); } // 清理过期缓存 function cleanupCache() { const now = Date.now(); const maxAge = 5 * 60 * 1000; // 5分钟 for (const [key, value] of GLOBAL_OBJ.messageCache.entries()) { if (now - value.timestamp > maxAge) { GLOBAL_OBJ.messageCache.delete(key); } } } // 强制清理所有缓存 function clearAllCache() { GLOBAL_OBJ.messageCache.clear(); GLOBAL_OBJ.lastMessageCount = 0; console.log('已清理所有缓存'); } // 重新绑定事件监听器 function rebindEventListeners(clonedElement, originalMessageElement) { // 绑定用户消息项的点击事件 if (clonedElement.classList.contains('outline-user-item')) { clonedElement.onclick = () => { originalMessageElement.scrollIntoView({ behavior: 'smooth', block: 'start' }); highlightElement(originalMessageElement); }; } // 绑定AI消息项的点击事件 if (clonedElement.classList.contains('outline-ai-item')) { clonedElement.onclick = () => { originalMessageElement.scrollIntoView({ behavior: 'smooth', block: 'start' }); highlightElement(originalMessageElement); }; } // 绑定AI消息容器的事件 if (clonedElement.classList.contains('outline-ai-container')) { const aiHeader = clonedElement.querySelector('.outline-ai-header'); if (aiHeader) { // 重新绑定头部点击事件 const headerText = aiHeader.querySelector('span:first-child'); if (headerText) { aiHeader.onclick = (e) => { e.stopPropagation(); originalMessageElement.scrollIntoView({ behavior: 'smooth', block: 'start' }); highlightElement(originalMessageElement); }; } // 重新绑定展开/收起按钮事件 const toggleBtn = aiHeader.querySelector('.toggle-btn'); if (toggleBtn) { const treeContainer = aiHeader.nextElementSibling; if (treeContainer) { toggleBtn.onclick = (e) => { e.stopPropagation(); const isExpanded = treeContainer.style.display !== 'none'; treeContainer.style.display = isExpanded ? 'none' : 'block'; toggleBtn.textContent = isExpanded ? '▶' : '▼'; toggleBtn.classList.toggle('collapsed', isExpanded); }; } } } } // 重新绑定树形节点的事件 const treeNodes = clonedElement.querySelectorAll('.tree-node'); treeNodes.forEach(treeNode => { const toggleBtn = treeNode.querySelector('.toggle-btn'); if (toggleBtn) { const nodeWrapper = treeNode.parentElement; const childContainer = nodeWrapper.querySelector('div:last-child'); if (childContainer && childContainer !== treeNode) { toggleBtn.onclick = (e) => { e.stopPropagation(); const isExpanded = childContainer.style.display !== 'none'; childContainer.style.display = isExpanded ? 'none' : 'block'; toggleBtn.textContent = isExpanded ? '▶' : '▼'; toggleBtn.classList.toggle('collapsed', isExpanded); }; } } // 重新绑定树形节点的点击跳转事件 // 需要从原始消息元素中找到对应的标题元素 const nodeText = treeNode.querySelector('span:first-child'); if (nodeText) { const headerText = nodeText.textContent; const headerElement = findHeaderByText(originalMessageElement, headerText); if (headerElement) { treeNode.onclick = (e) => { e.stopPropagation(); headerElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); highlightElement(headerElement); }; } } }); } // 根据文本内容查找对应的标题元素 function findHeaderByText(messageElement, headerText) { const headers = messageElement.querySelectorAll('h1, h2, h3, h4, h5, h6'); for (const header of headers) { if (header.textContent === headerText) { return header; } } return null; } // 展开或收起所有节点 function toggleAllNodes() { if (!GLOBAL_OBJ.outlineContent) { console.log('大纲内容容器不存在'); return; } const newState = !GLOBAL_OBJ.allExpanded; GLOBAL_OBJ.allExpanded = newState; // 查找所有的切换按钮和对应的容器 const toggleButtons = GLOBAL_OBJ.outlineContent.querySelectorAll('.toggle-btn'); const treeContainers = GLOBAL_OBJ.outlineContent.querySelectorAll('.outline-ai-container > div:last-child'); const childContainers = GLOBAL_OBJ.outlineContent.querySelectorAll('[style*="margin-left"]'); // 处理AI消息容器的展开/收起 toggleButtons.forEach(btn => { const container = btn.parentElement.nextElementSibling; if (container) { container.style.display = newState ? 'block' : 'none'; btn.textContent = newState ? '▼' : '▶'; btn.classList.toggle('collapsed', !newState); } }); // 处理树形结构的展开/收起 const treeNodes = GLOBAL_OBJ.outlineContent.querySelectorAll('.tree-node'); treeNodes.forEach(node => { const toggleBtn = node.querySelector('.toggle-btn'); if (toggleBtn) { const nodeWrapper = node.parentElement; const childContainer = nodeWrapper.querySelector('div:last-child'); if (childContainer && childContainer !== node) { childContainer.style.display = newState ? 'block' : 'none'; toggleBtn.textContent = newState ? '▼' : '▶'; toggleBtn.classList.toggle('collapsed', !newState); } } }); console.log(`已${newState ? '展开' : '收起'}所有节点`); } function refreshOutlineItems(outlineBone, determineMessageOwnerFunc) { const chatArea= GLOBAL_OBJ.getCachedChatArea(); if(!chatArea){ console.log('无法定位到对话区域') return; } const cd = GLOBAL_OBJ.parserConfig.getMessageList(chatArea); if(cd==null){ console.log("对话区域无效,大纲生成失败,chatArea:",chatArea) return; } console.log('刷新大纲,chatArea:',chatArea.firstChild) const currentMessageCount = cd.length; // 如果消息数量没有变化,检查是否需要更新 if (currentMessageCount === GLOBAL_OBJ.lastMessageCount && currentMessageCount > 0) { // 检查最后一条消息是否还在变化(可能是AI正在回复) const lastMessage = cd[cd.length - 1]; const lastMessageId = getMessageId(cd.length-1,lastMessage); if (isMessageCached(lastMessage, lastMessageId)) { return; // 没有变化,跳过更新 } } // 使用文档片段来减少DOM操作 const fragment = document.createDocumentFragment(); let messageIndex = 0; let hasChanges = false; // 遍历对话生成大纲 for (let i = 0; i < cd.length; i++) { const c = cd[i]; const messageId = getMessageId(i,c); const messageType = determineMessageOwnerFunc(c); // 检查是否可以使用缓存 if (isMessageCached(c, messageId)) { const cached = GLOBAL_OBJ.messageCache.get(messageId); const clonedElement = cached.outlineElement.cloneNode(true); // 重新绑定事件监听器 rebindEventListeners(clonedElement, c); fragment.appendChild(clonedElement); if (messageType === MessageOwner.User || messageType === MessageOwner.Assistant) { messageIndex++; } continue; } hasChanges = true; let outlineElement = null; switch (messageType) { case MessageOwner.User: if (!GLOBAL_CONFIG.features.showUserMessages) break; messageIndex++; outlineElement = createUserOutlineItem(c, messageIndex); break; case MessageOwner.Assistant: if (!GLOBAL_CONFIG.features.showAIMessages) break; messageIndex++; outlineElement = createAIOutlineItem(c, messageIndex); break; } if (outlineElement) { fragment.appendChild(outlineElement); // 缓存新创建的元素 cacheMessage(c, messageId, outlineElement); } } // 只有在有变化时才更新DOM if (hasChanges || currentMessageCount !== GLOBAL_OBJ.lastMessageCount) { // 清空并重新填充 if ('replaceChildren' in outlineBone) { outlineBone.replaceChildren(); } else { outlineBone.innerHTML = ''; } outlineBone.appendChild(fragment); } GLOBAL_OBJ.lastMessageCount = currentMessageCount; } // 创建用户消息大纲项 function createUserOutlineItem(messageElement, messageIndex) { const userItem = document.createElement('div'); userItem.className = 'outline-user-item'; const userText = messageElement.textContent.substring(0, GLOBAL_CONFIG.features.textLength) + (messageElement.textContent.length > GLOBAL_CONFIG.features.textLength ? '...' : ''); userItem.textContent = `👤 ${messageIndex}. ${userText}`; // 添加点击跳转功能 userItem.onclick = () => { messageElement.scrollIntoView({ behavior: 'smooth', block: 'start' }); highlightElement(messageElement); }; return userItem; } // 创建AI消息大纲项 function createAIOutlineItem(messageElement, messageIndex) { // 检查是否有标题标签 const headers = messageElement.querySelectorAll('h1, h2, h3, h4, h5, h6'); if (headers.length > 0) { return createAIContainerWithHeaders(messageElement, messageIndex, headers); } else { return createSimpleAIItem(messageElement, messageIndex); } } // 创建带标题的AI消息容器 function createAIContainerWithHeaders(messageElement, messageIndex, headers) { const aiContainer = document.createElement('div'); aiContainer.className = 'outline-ai-container'; // 创建AI消息头部 const aiHeader = document.createElement('div'); aiHeader.className = 'outline-ai-header'; const headerText = document.createElement('span'); const aiText = messageElement.textContent.substring(0, GLOBAL_CONFIG.features.textLength) + (messageElement.textContent.length > GLOBAL_CONFIG.features.textLength ? '...' : ''); headerText.textContent = `🤖 ${messageIndex}. ${aiText}`; const toggleBtn = document.createElement('span'); toggleBtn.textContent = '▼'; toggleBtn.className = 'toggle-btn'; aiHeader.appendChild(headerText); aiHeader.appendChild(toggleBtn); // 构建标题层级树 const headerTree = buildHeaderTree(headers); const treeContainer = createTreeStructure(headerTree, 0); treeContainer.style.display = GLOBAL_CONFIG.features.autoExpand ? 'block' : 'none'; // 添加展开/收起功能 toggleBtn.onclick = (e) => { e.stopPropagation(); const isExpanded = treeContainer.style.display !== 'none'; treeContainer.style.display = isExpanded ? 'none' : 'block'; toggleBtn.textContent = isExpanded ? '▶' : '▼'; toggleBtn.classList.toggle('collapsed', isExpanded); }; // 添加点击跳转功能 aiHeader.onclick = (e) => { e.stopPropagation(); messageElement.scrollIntoView({ behavior: 'smooth', block: 'start' }); highlightElement(messageElement); }; aiContainer.appendChild(aiHeader); aiContainer.appendChild(treeContainer); return aiContainer; } // 创建简单的AI消息项 function createSimpleAIItem(messageElement, messageIndex) { const aiItem = document.createElement('div'); aiItem.className = 'outline-ai-item'; const aiText = messageElement.textContent.substring(0, GLOBAL_CONFIG.features.textLength) + (messageElement.textContent.length > GLOBAL_CONFIG.features.textLength ? '...' : ''); aiItem.textContent = `🤖 ${messageIndex}. ${aiText}`; // 添加点击跳转功能 aiItem.onclick = () => { messageElement.scrollIntoView({ behavior: 'smooth', block: 'start' }); highlightElement(messageElement); }; return aiItem; } async function getEleWithRetry(getFunc, args=[], judgeRes=true, maxRetries = 10, retryDelay = 1000) { for(let attempt = 0; attempt < maxRetries; attempt++) { try { const res = getFunc(...args); if(!judgeRes) return true; if(res) { console.log(`成功获取到 chatArea,尝试次数: ${attempt + 1}`); return res; } if(attempt < maxRetries - 1) { console.log(`第 ${attempt + 1} 次获取 chatArea 失败,${retryDelay}ms 后重试...`); await new Promise(resolve => setTimeout(resolve, retryDelay)); } } catch(error) { console.error(`获取 chatArea 时发生错误 (尝试 ${attempt + 1}):`, error); if(attempt < maxRetries - 1) { await new Promise(resolve => setTimeout(resolve, retryDelay)); } } } console.error(`经过 ${maxRetries} 次尝试后仍无法获取到 chatArea`); return null; } // 设置 MutationObserver 监听 function setupMutationObserver(chatArea) { // 如果已有观察者,先断开连接 if (GLOBAL_OBJ.currentObserver) { GLOBAL_OBJ.currentObserver.disconnect(); console.log('已断开原有的 MutationObserver'); } // 创建新的观察者 const observer = new MutationObserver((mutations) => { // 检查是否有实际的内容变化 let hasContentChange = false; for (const mutation of mutations) { if (mutation.type === 'childList') { // 检查是否有新增或删除的消息节点 if (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0) { hasContentChange = true; break; } } else if (mutation.type === 'characterData') { hasContentChange = true; break; } } if (hasContentChange && GLOBAL_OBJ.debouncedRefresh) { GLOBAL_OBJ.debouncedRefresh(); } }); // 开始观察 observer.observe(chatArea, { childList: true, subtree: true, // 监听子树变化以捕获消息内容更新 attributes: false, characterData: true // 监听文本内容变化 }); // 保存观察者引用 GLOBAL_OBJ.currentObserver = observer; GLOBAL_OBJ.currentChatArea = chatArea; console.log('已设置新的 MutationObserver 监听:', chatArea); return observer; } async function init() { // 加载保存的设置 loadSettings(); // 插入CSS样式 insertStyles(); // 创建显示按钮 createShowButton(); let platform = judgePlatform(); if(platform === 'unknown') { console.log('不支持的平台'); return; } const parserConfig = getParserConfig(platform); if(!parserConfig) { console.log('无法获取解析配置'); return; } const outlineEle = initOutlineEle(); try{ // 插入大纲到页面 const r=await getEleWithRetry(parserConfig.insertOutline, [outlineEle],false,5,1000); if(!r) throw new EvalError("多次尝试插入大纲失败") } catch(e){ console.error("大纲插入内容失败,将插入到body并固定在右侧:",e) // 当插入失败时,直接插入到body并固定在页面右侧 insertOutlineToBodyFixed(outlineEle); } // 使用重试机制获取 chatArea console.log('开始获取 chatArea...'); const chatArea = await getEleWithRetry(parserConfig.selectChatArea); if(!chatArea) { console.error('经过多次重试后仍未找到聊天区域,脚本初始化失败'); return; } console.log('成功定位到 chatArea:', chatArea); GLOBAL_OBJ.chatArea=chatArea; // 获取大纲内容容器 const outlineContent = outlineEle.querySelector('#outline-content'); // 保存到全局对象 GLOBAL_OBJ.outlineContent = outlineContent; GLOBAL_OBJ.parserConfig = parserConfig; // 创建防抖的刷新函数 const debouncedRefresh = debounce(() => { refreshOutlineItems(GLOBAL_OBJ.outlineContent, GLOBAL_OBJ.parserConfig.determineMessageOwner); }, GLOBAL_CONFIG.features.debouncedInterval); // 300ms 防抖延迟 GLOBAL_OBJ.debouncedRefresh = debouncedRefresh; GLOBAL_OBJ.getCachedChatArea=function(force_refresh=false){ // 缓存选择器结果 if (force_refresh||!this._cachedChatArea) { this._cachedChatArea = parserConfig.selectChatArea() || null; console.log('get chatArea:',this._cachedChatArea) } return this._cachedChatArea; } // 创建强制刷新函数 const forceRefresh = () => { console.log('执行强制刷新...'); // 清理所有缓存 clearAllCache(); // 强制重新获取chatArea const newChatArea = GLOBAL_OBJ.getCachedChatArea(true); if (newChatArea) { // 重新设置 MutationObserver 监听新的 chatArea setupMutationObserver(newChatArea); // 立即刷新大纲内容 refreshOutlineItems(GLOBAL_OBJ.outlineContent, GLOBAL_OBJ.parserConfig.determineMessageOwner); console.log('强制刷新完成,已重新监听新的 chatArea'); } else { console.error('强制刷新失败:无法获取到chatArea'); } }; // 将强制刷新函数设置为全局可访问 GLOBAL_OBJ.forceRefreshOutline = forceRefresh; // 将展开/收起函数设置为全局可访问 GLOBAL_OBJ.toggleAllNodes = toggleAllNodes; // 初始化大纲内容 refreshOutlineItems(outlineContent, parserConfig.determineMessageOwner); // 设置 MutationObserver 监听聊天区域变化 setupMutationObserver(chatArea); // 定期清理缓存 setInterval(cleanupCache, 2 * 60 * 1000); // 每2分钟清理一次 console.log('对话大纲生成脚本已启动'); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();