// ==UserScript== // @name webAI聊天问题列表导航 // @namespace http://tampermonkey.net/ // @version 3.6.1 // @description 通过点击按钮显示用户问题列表,支持导航到特定问题、分页功能、正序/倒序切换,智能脉冲式加载历史记录突破懒加载,自动适配暗黑模式,按钮可拖动并保存位置,悬浮窗智能展开方向 // @author yutao // @match https://grok.com/* // @match https://github.com/copilot/* // @match https://yuanbao.tencent.com/chat/* // @match https://chat.qwen.ai/c/* // @match https://copilot.microsoft.com/chats/* // @match https://chatgpt.com/c/* // @match https://chat.deepseek.com/a/chat/* // @match https://www.tongyi.com/* // @match https://www.qianwen.com/* // @match https://www.doubao.com/* // @match https://www.chatglm.cn/* // @match https://www.kimi.com/chat/* // @match https://copilot.wps.cn/* // @grant none // MIT License // // Copyright (c) [2025] [yutao] // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE.@license // @downloadURL none // ==/UserScript== (function () { "use strict"; // 配置对象,定义不同网站的聊天消息选择器和条件 const config = { "chat.qwen.ai": { messageSelector: "div.rounded-3xl.bg-gray-50.dark\\:bg-gray-850", textSelector: "p", userCondition: (element) => true, scrollContainerSelector: 'div.overflow-y-auto, div[class*="chat-content"]', }, "tongyi.com": { messageSelector: 'div[class*="questionItem"]', textSelector: 'div[class*="contentBox"] div[class*="bubble"]', userCondition: (element) => true, scrollContainerSelector: 'div[class*="contentWrapper"], main, div[class*="chat-content"], div[class*="chatContent"]', }, "qianwen.com": { messageSelector: 'div[class*="questionItem"]', textSelector: 'div[class*="contentBox"] div[class*="bubble"]', userCondition: (element) => true, scrollContainerSelector: 'div[class*="contentWrapper"], main, div[class*="chat-content"], div[class*="chatContent"]', }, "yuanbao.tencent.com": { messageSelector: "div.agent-chat__bubble__content", textSelector: "div.hyc-content-text", userCondition: (element) => true, scrollContainerSelector: ".agent-chat__bubble-wrap", }, "doubao.com": { messageSelector: 'div[data-testid="send_message"]', textSelector: 'div[data-testid="message_text_content"]', userCondition: (element) => true, scrollContainerSelector: 'div[class*="scrollable-"][class*="show-scrollbar-"]', }, "copilot.wps.cn": { messageSelector: 'li.item--user, div[class*="item--user"], li[class*="item--user"], .item.item--user', textSelector: '.item__value span, div[class*="item__value"] span, .item__value, [class*="item__value"]', userCondition: (element) => true, scrollContainerSelector: '.chat, .p__main, div[class*="scrollbar"], div[class*="chat-list"], div[class*="scroll"], .scroll-container', }, "www.kimi.com": { messageSelector: 'div.segment-user, div[class*="segment-user"]', textSelector: '.user-content, div[class*="user-content"]', userCondition: (element) => true, scrollContainerSelector: 'div[class*="scrollbar"], div[class*="chat-history"]', }, "chatglm.cn": { messageSelector: 'div.conversation.question, div[id*="row-question"]', textSelector: '.question-txt span, div[id*="row-question-p"] span', userCondition: (element) => true, scrollContainerSelector: 'div[class*="chat-history"], div[class*="scrollable"]', }, "chat.deepseek.com": { messageSelector: "div.fbb737a4", textSelector: null, userCondition: (element) => true, scrollContainerSelector: ".scroll-container", }, "grok.com": { messageSelector: 'div[class*="message"], div[data-testid*="message"], div[class*="chat-message"]', textSelector: 'div[class*="content"], span[class*="text"], p', userCondition: (element) => { // Grok 用户消息通常在右侧或有特定的类名 const classes = element.className.toLowerCase(); const hasUserClass = classes.includes('user') || classes.includes('human') || classes.includes('sender'); // 检查是否在右侧(用户消息通常右对齐) const style = window.getComputedStyle(element); const isRightAligned = style.justifyContent === 'flex-end' || style.alignSelf === 'flex-end' || style.marginLeft === 'auto'; // 检查背景色(用户消息通常有不同的背景色) const bgColor = style.backgroundColor; const isUserBg = bgColor && bgColor !== 'rgba(0, 0, 0, 0)' && bgColor !== 'transparent'; return hasUserClass || isRightAligned || (isUserBg && !classes.includes('assistant') && !classes.includes('bot')); }, scrollContainerSelector: 'main, div[class*="scroll"], div[class*="chat-container"], div[class*="messages"]', }, "github.com": { messageSelector: "div.UserMessage-module__container--cAvvK.ChatMessage-module__userMessage--xvIFp", textSelector: null, userCondition: (element) => element.classList.contains("ChatMessage-module__userMessage--xvIFp"), scrollContainerSelector: ".react-scroll-to-bottom--css-xgtui-79elbk", }, "copilot.microsoft.com": { messageSelector: "div.self-end.rounded-2xl", textSelector: null, userCondition: (element) => element.classList.contains("self-end"), scrollContainerSelector: ".overflow-y-auto.flex-1", }, "chatgpt.com": { messageSelector: "div.rounded-3xl.bg-token-message-surface", textSelector: "div.whitespace-pre-wrap", userCondition: (element) => true, scrollContainerSelector: "main div.overflow-y-auto", }, }; const genericConfig = { // 消息选择器:匹配常见的消息元素模式 messageSelector: 'div[class*="message"], div[class*="chat"], div[class*="user"], div[class*="question"], div[class*="questionItem"]', // 文本选择器:匹配常见的文本容器 textSelector: 'div[class*="text"], div[class*="content"], p, span[class*="content"], div[class*="contentBox"]', // 用户消息条件:使用多种通用的方法识别用户消息 userCondition: (element) => { // 检查常见的用户消息类名 if ( element.classList.toString().includes("user") || element.classList.toString().includes("question") || element.classList.toString().includes("self") || element.classList.toString().includes("right") || element.classList.toString().includes("message") ) return true; // 检查常见的用户角色属性 if ( element.getAttribute("data-role") === "user" || element.getAttribute("data-author") === "user" || element.getAttribute("data-message-author-role") === "user" ) return true; // 检查布局特征 (右对齐通常表示用户消息) const style = window.getComputedStyle(element); if ( style.justifyContent === "flex-end" || style.textAlign === "right" || style.alignSelf === "flex-end" ) return true; // 检查文本内容特征:如果没有包含AI常用的前缀标识 const text = element.textContent.trim().toLowerCase(); if ( text && text.length > 0 && !text.startsWith("ai:") && !text.startsWith("assistant:") && !text.startsWith("bot:") ) return true; return false; }, // 滚动容器选择器:匹配常见的滚动容器 scrollContainerSelector: 'div[class*="overflow"], div[class*="scroll"], div[class*="chat-container"], div[class*="message-container"], #messages-container, main', }; // 获取当前域名并选择配置 const hostname = window.location.hostname; // 获取当前网站的配置,如果没有特定配置则使用通用配置 const currentConfig = config[hostname] || genericConfig; // 暗黑模式检测和主题管理 const themeManager = { isDark: false, // 检测暗黑模式 detectDarkMode() { // 1. 检查系统偏好 const systemDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; // 2. 检查网站是否使用暗黑模式 const htmlDark = document.documentElement.classList.contains('dark') || document.documentElement.getAttribute('data-theme') === 'dark'; const bodyDark = document.body.classList.contains('dark') || document.body.getAttribute('data-theme') === 'dark'; // 3. 检查背景色 const bodyBg = window.getComputedStyle(document.body).backgroundColor; const bgDark = this.isColorDark(bodyBg); this.isDark = htmlDark || bodyDark || bgDark || systemDark; return this.isDark; }, // 判断颜色是否为暗色 isColorDark(color) { const rgb = color.match(/\d+/g); if (!rgb || rgb.length < 3) return false; const brightness = (parseInt(rgb[0]) * 299 + parseInt(rgb[1]) * 587 + parseInt(rgb[2]) * 114) / 1000; return brightness < 128; }, // 获取主题颜色 getColors() { if (this.isDark) { return { // 暗黑模式 buttonBg: "linear-gradient(135deg, #1e40af, #0ea5e9)", buttonColor: "#e5e7eb", windowBg: "#1f2937", windowBorder: "#374151", windowShadow: "0 4px 12px rgba(0,0,0,0.5)", textPrimary: "#f3f4f6", textSecondary: "#9ca3af", itemHoverBg: "#374151", itemBorder: "#4b5563", buttonPrimaryBg: "#10b981", buttonPrimaryHover: "#059669", buttonSecondaryBg: "#3b82f6", buttonSecondaryHover: "#2563eb", statusBg: "#374151", statusBorder: "#4b5563", paginationBg: "#374151", paginationActiveBg: "#3b82f6", paginationColor: "#e5e7eb", }; } else { return { // 亮色模式 buttonBg: "linear-gradient(135deg, #007BFF, #00C4FF)", buttonColor: "#fff", windowBg: "#ffffff", windowBorder: "#e0e0e0", windowShadow: "0 4px 12px rgba(0,0,0,0.15)", textPrimary: "#333", textSecondary: "#666", itemHoverBg: "#f5f5f5", itemBorder: "#f0f0f0", buttonPrimaryBg: "#28a745", buttonPrimaryHover: "#218838", buttonSecondaryBg: "#007BFF", buttonSecondaryHover: "#0069d9", statusBg: "#f8f9fa", statusBorder: "#e0e0e0", paginationBg: "#f0f0f0", paginationActiveBg: "#007BFF", paginationColor: "#333", }; } } }; // 初始化主题 themeManager.detectDarkMode(); const colors = themeManager.getColors(); // 位置管理器 - 保存和恢复按钮位置 const positionManager = { storageKey: 'questionListButton_position', // 获取保存的位置 getSavedPosition() { try { const saved = localStorage.getItem(this.storageKey); return saved ? JSON.parse(saved) : null; } catch (e) { return null; } }, // 保存位置 savePosition(bottom, right) { try { localStorage.setItem(this.storageKey, JSON.stringify({ bottom, right })); } catch (e) { console.warn('无法保存按钮位置'); } }, // 获取默认位置 getDefaultPosition() { return { bottom: 20, right: 20 }; } }; // 创建美化后的浮动按钮 const button = document.createElement("button"); button.textContent = "问题列表"; button.style.position = "fixed"; button.style.zIndex = "1000"; button.style.padding = "10px 15px"; button.style.background = colors.buttonBg; button.style.color = colors.buttonColor; button.style.border = "none"; button.style.borderRadius = "8px"; button.style.boxShadow = "0 2px 4px rgba(0,0,0,0.2)"; button.style.cursor = "move"; button.style.fontFamily = "Arial, sans-serif"; button.style.fontSize = "14px"; button.style.transition = "transform 0.2s, box-shadow 0.2s"; button.style.userSelect = "none"; // 恢复保存的位置或使用默认位置 const savedPos = positionManager.getSavedPosition() || positionManager.getDefaultPosition(); button.style.bottom = savedPos.bottom + "px"; button.style.right = savedPos.right + "px"; // 拖动功能 let isDragging = false; let dragStartX = 0; let dragStartY = 0; let buttonStartBottom = 0; let buttonStartRight = 0; button.addEventListener("mousedown", (e) => { // 只在左键点击时开始拖动 if (e.button !== 0) return; isDragging = true; dragStartX = e.clientX; dragStartY = e.clientY; buttonStartBottom = parseInt(button.style.bottom); buttonStartRight = parseInt(button.style.right); button.style.cursor = "grabbing"; e.preventDefault(); }); document.addEventListener("mousemove", (e) => { if (!isDragging) return; const deltaX = dragStartX - e.clientX; const deltaY = dragStartY - e.clientY; // 修正:向下拖动时 deltaY 应该为负 let newBottom = buttonStartBottom + deltaY; let newRight = buttonStartRight + deltaX; // 限制在窗口范围内 const maxBottom = window.innerHeight - button.offsetHeight - 10; const maxRight = window.innerWidth - button.offsetWidth - 10; newBottom = Math.max(10, Math.min(newBottom, maxBottom)); newRight = Math.max(10, Math.min(newRight, maxRight)); button.style.bottom = newBottom + "px"; button.style.right = newRight + "px"; e.preventDefault(); }); document.addEventListener("mouseup", (e) => { if (isDragging) { isDragging = false; button.style.cursor = "move"; // 保存位置 const bottom = parseInt(button.style.bottom); const right = parseInt(button.style.right); positionManager.savePosition(bottom, right); // 更新悬浮窗位置 updateFloatWindowPosition(); // 如果移动距离很小,视为点击 const moveDistance = Math.sqrt( Math.pow(e.clientX - dragStartX, 2) + Math.pow(e.clientY - dragStartY, 2) ); if (moveDistance < 5) { // 触发点击事件 setTimeout(() => { toggleFloatWindow(); }, 0); } } }); button.addEventListener("mouseover", () => { if (!isDragging) { button.style.transform = "scale(1.05)"; button.style.boxShadow = "0 4px 8px rgba(0,0,0,0.3)"; } }); button.addEventListener("mouseout", () => { if (!isDragging) { button.style.transform = "scale(1)"; button.style.boxShadow = "0 2px 4px rgba(0,0,0,0.2)"; } }); document.body.appendChild(button); // 创建美化后的悬浮窗 const floatWindow = document.createElement("div"); floatWindow.style.position = "fixed"; floatWindow.style.width = "320px"; floatWindow.style.maxHeight = "420px"; floatWindow.style.background = colors.windowBg; floatWindow.style.border = `1px solid ${colors.windowBorder}`; floatWindow.style.borderRadius = "10px"; floatWindow.style.boxShadow = colors.windowShadow; floatWindow.style.padding = "15px"; floatWindow.style.overflowY = "auto"; floatWindow.style.display = "none"; floatWindow.style.zIndex = "1000"; floatWindow.style.fontFamily = "Arial, sans-serif"; floatWindow.style.transition = "opacity 0.2s"; // 更新悬浮窗位置的函数 - 智能选择展开方向 function updateFloatWindowPosition() { const buttonBottom = parseInt(button.style.bottom); const buttonRight = parseInt(button.style.right); const buttonWidth = button.offsetWidth; const buttonHeight = button.offsetHeight; const windowWidth = window.innerWidth; const windowHeight = window.innerHeight; const floatWindowWidth = 320; const floatWindowHeight = 420; // maxHeight const gap = 10; // 间距 // 计算按钮在屏幕上的实际位置 const buttonLeft = windowWidth - buttonRight - buttonWidth; const buttonTop = windowHeight - buttonBottom - buttonHeight; // 重置之前的定位属性 floatWindow.style.top = 'auto'; floatWindow.style.bottom = 'auto'; floatWindow.style.left = 'auto'; floatWindow.style.right = 'auto'; // 1. 判断垂直方向:上方还是下方展开 const spaceAbove = buttonTop; // 按钮上方的空间 const spaceBelow = windowHeight - buttonTop - buttonHeight; // 按钮下方的空间 if (spaceAbove >= floatWindowHeight || spaceAbove >= spaceBelow) { // 上方空间足够,在按钮上方展开 floatWindow.style.bottom = (buttonBottom + buttonHeight + gap) + "px"; } else { // 上方空间不足,在按钮下方展开 floatWindow.style.top = (buttonTop + buttonHeight + gap) + "px"; } // 2. 判断水平方向:左侧还是右侧对齐 const spaceOnRight = buttonLeft + buttonWidth; const spaceOnLeft = windowWidth - buttonLeft; if (spaceOnRight >= floatWindowWidth) { // 右对齐(悬浮窗在按钮左侧或与按钮右边缘对齐) floatWindow.style.right = buttonRight + "px"; } else if (spaceOnLeft >= floatWindowWidth) { // 左对齐(悬浮窗在按钮右侧或与按钮左边缘对齐) floatWindow.style.left = buttonLeft + "px"; } else { // 空间不足,居中显示 const centerLeft = Math.max(gap, (windowWidth - floatWindowWidth) / 2); floatWindow.style.left = centerLeft + "px"; } } // 初始化悬浮窗位置 updateFloatWindowPosition(); document.body.appendChild(floatWindow); // 分页相关变量 let questions = []; const pageSize = 10; let currentPage = 1; let isReversed = false; let isLoading = false; // 加载状态标志 let autoLoadCompleted = false; // 标记自动加载是否已完成 // 创建顶部按钮容器 const topButtonContainer = document.createElement("div"); topButtonContainer.style.display = "flex"; topButtonContainer.style.justifyContent = "space-between"; topButtonContainer.style.marginBottom = "15px"; // 创建加载历史按钮 const loadButton = document.createElement("button"); loadButton.textContent = "加载历史"; loadButton.style.padding = "5px 10px"; loadButton.style.background = colors.buttonPrimaryBg; loadButton.style.color = "#fff"; loadButton.style.border = "none"; loadButton.style.borderRadius = "4px"; loadButton.style.cursor = "pointer"; loadButton.style.fontSize = "12px"; loadButton.style.transition = "background 0.2s"; loadButton.addEventListener("mouseover", () => { loadButton.style.background = colors.buttonPrimaryHover; }); loadButton.addEventListener("mouseout", () => { loadButton.style.background = colors.buttonPrimaryBg; }); loadButton.addEventListener("click", () => { loadHistoryRecords(); }); // 创建排序切换按钮 const sortButton = document.createElement("button"); sortButton.textContent = "正序"; sortButton.style.padding = "5px 10px"; sortButton.style.background = colors.buttonSecondaryBg; sortButton.style.color = "#fff"; sortButton.style.border = "none"; sortButton.style.borderRadius = "4px"; sortButton.style.cursor = "pointer"; sortButton.style.fontSize = "12px"; sortButton.style.transition = "background 0.2s"; sortButton.addEventListener("mouseover", () => { sortButton.style.background = colors.buttonSecondaryHover; }); sortButton.addEventListener("mouseout", () => { sortButton.style.background = colors.buttonSecondaryBg; }); sortButton.addEventListener("click", () => { isReversed = !isReversed; sortButton.textContent = isReversed ? "倒序" : "正序"; findAllQuestionsWithDeduplication(); }); // 状态显示标签 const statusLabel = document.createElement("div"); statusLabel.textContent = "正在加载历史..."; statusLabel.style.fontSize = "12px"; statusLabel.style.color = colors.textSecondary; statusLabel.style.padding = "5px 0"; statusLabel.style.display = "none"; // 默认隐藏 // 将按钮添加到容器中 topButtonContainer.appendChild(statusLabel); topButtonContainer.appendChild(loadButton); topButtonContainer.appendChild(sortButton); floatWindow.appendChild(topButtonContainer); // 创建分页控件 const paginationContainer = document.createElement("div"); paginationContainer.style.display = "flex"; paginationContainer.style.justifyContent = "center"; paginationContainer.style.marginTop = "10px"; paginationContainer.style.gap = "5px"; // 问题列表容器 const listContainer = document.createElement("ul"); listContainer.style.listStyle = "none"; listContainer.style.padding = "0"; listContainer.style.margin = "0"; floatWindow.appendChild(listContainer); floatWindow.appendChild(paginationContainer); // 创建问题计数显示区域 const questionCountDisplay = document.createElement("div"); questionCountDisplay.style.fontSize = "12px"; questionCountDisplay.style.color = colors.textSecondary; questionCountDisplay.style.textAlign = "center"; questionCountDisplay.style.margin = "5px 0 10px 0"; floatWindow.insertBefore(questionCountDisplay, listContainer); // 更新问题计数显示 function updateQuestionCountDisplay() { questionCountDisplay.textContent = `共找到 ${questions.length} 个问题`; } // 获取文本内容的辅助函数 function getTextContent(element) { return element ? element.textContent.trim() : ""; } // 查找所有用户问题并去重的函数 function findAllQuestionsWithDeduplication() { const chatContainer = document.querySelector(".chat-container, #chat, main, article") || document.body; const potentialMessages = chatContainer.querySelectorAll( currentConfig.messageSelector ); // 调试信息(仅在找不到消息时输出) if (potentialMessages.length === 0) { console.log('[问题列表导航] 调试信息:', { 网站: hostname, 消息选择器: currentConfig.messageSelector, 找到的元素数量: potentialMessages.length, 提示: '如果一直为0,说明选择器不匹配当前页面结构' }); } // 临时存储所有找到的问题 const foundQuestions = []; const seenTexts = new Set(); // 用于去重 let filteredCount = 0; // 被过滤掉的消息数量 for (let i = 0; i < potentialMessages.length; i++) { const element = potentialMessages[i]; const textElement = currentConfig.textSelector ? element.querySelector(currentConfig.textSelector) : element; const text = getTextContent(textElement); // 如果文本内容有效且符合用户消息条件 if (text && text.length > 2) { if (currentConfig.userCondition(element)) { // 使用文本内容进行去重 if (!seenTexts.has(text)) { seenTexts.add(text); foundQuestions.push({ element, text }); } } else { filteredCount++; } } } // 调试信息(仅在找到元素但没有用户消息时输出) if (potentialMessages.length > 0 && foundQuestions.length === 0) { console.log('[问题列表导航] 调试信息:', { 网站: hostname, 找到的消息元素: potentialMessages.length, 通过用户条件的: foundQuestions.length, 被过滤的: filteredCount, 提示: '找到了消息元素,但 userCondition 过滤掉了所有消息。可能需要调整 userCondition 逻辑' }); } // 更新全局问题列表 questions = foundQuestions; // 确保排序正确 if (isReversed) { questions.reverse(); } // 更新界面 updateQuestionCountDisplay(); renderPage(currentPage); updatePagination(); } // 改进的懒加载突破函数 - 使用脉冲式滚动和智能检测 async function loadHistoryRecords() { if (isLoading) { // 如果正在加载,点击按钮可以停止加载 isLoading = false; statusLabel.textContent = "已停止加载"; setTimeout(() => { statusLabel.style.display = "none"; }, 2000); return; } isLoading = true; statusLabel.textContent = "正在加载历史... (再次点击停止)"; statusLabel.style.display = "block"; // 智能查找滚动容器(排除侧边栏) function findScrollContainer() { // 辅助函数:判断是否是侧边栏(通常宽度较小,在左侧) function isSidebar(element) { const rect = element.getBoundingClientRect(); const windowWidth = window.innerWidth; // 侧边栏特征:宽度小于窗口的30%,且在左侧 return rect.width < windowWidth * 0.3 && rect.left < 100; } // 1. 尝试配置的选择器(支持多个选择器,用逗号分隔) const selectors = currentConfig.scrollContainerSelector.split(","); for (const selector of selectors) { const container = document.querySelector(selector.trim()); if ( container && container.scrollHeight > container.clientHeight && !isSidebar(container) ) { return container; } } // 2. 尝试常见的容器 const commonSelectors = [ "main", "#chat-history", '[class*="chat-content"]', '[class*="message-container"]', '[class*="chatContent"]', ]; for (const selector of commonSelectors) { const container = document.querySelector(selector); if ( container && container.scrollHeight > container.clientHeight && !isSidebar(container) ) { return container; } } // 3. 启发式查找:找到最后一条消息的可滚动父元素(排除侧边栏) const lastMessage = document.querySelector( currentConfig.messageSelector ); if (lastMessage) { let parent = lastMessage.parentElement; while (parent && parent !== document.body) { const style = window.getComputedStyle(parent); if ( (style.overflowY === "auto" || style.overflowY === "scroll") && parent.scrollHeight > parent.clientHeight && !isSidebar(parent) ) { return parent; } parent = parent.parentElement; } } // 4. 回退到 documentElement return document.documentElement; } const container = findScrollContainer(); const originalScrollTop = container.scrollTop; const initialQuestionCount = questions.length; let consecutiveNoChange = 0; let lastHeight = container.scrollHeight; let lastQuestionCount = questions.length; let iteration = 0; const maxIterations = 20; // 最多尝试20次 // 脉冲式滚动加载循环 while (isLoading && consecutiveNoChange < 5 && iteration < maxIterations) { iteration++; // 1. 脉冲式滚动 - 模拟用户滚动行为 // 先向下滚动一点,再滚动到顶部,触发懒加载机制 container.scrollTop = Math.min(100, container.scrollHeight * 0.1); await new Promise((resolve) => setTimeout(resolve, 100)); container.scrollTop = 0; await new Promise((resolve) => setTimeout(resolve, 100)); // 2. 触发滚动事件(某些框架需要) container.dispatchEvent(new Event("scroll", { bubbles: true })); // 3. 动态等待 - 根据网络状况调整 const waitTime = iteration < 5 ? 800 : 1200; // 前几次快速,后面慢一点 await new Promise((resolve) => setTimeout(resolve, waitTime)); // 4. 扫描新内容 const preCount = questions.length; findAllQuestionsWithDeduplication(); const postCount = questions.length; // 5. 检测变化 const newHeight = container.scrollHeight; const heightChanged = newHeight > lastHeight; const questionsChanged = postCount > lastQuestionCount; if (heightChanged || questionsChanged) { // 发现新内容 consecutiveNoChange = 0; lastHeight = newHeight; lastQuestionCount = postCount; statusLabel.textContent = `已加载 ${postCount} 个问题... (${iteration}/${maxIterations})`; } else { // 没有新内容 consecutiveNoChange++; statusLabel.textContent = `检查中... (${consecutiveNoChange}/5) - 第${iteration}次`; } // 6. 额外的触发机制:模拟鼠标滚轮事件 if (iteration % 3 === 0) { const wheelEvent = new WheelEvent("wheel", { deltaY: -100, bubbles: true, cancelable: true, }); container.dispatchEvent(wheelEvent); } } // 恢复原始滚动位置 container.scrollTop = originalScrollTop; // 完成加载 const newQuestions = questions.length - initialQuestionCount; isLoading = false; autoLoadCompleted = true; statusLabel.textContent = newQuestions > 0 ? `✓ 成功加载 ${newQuestions} 条新记录 (共${questions.length}条)` : iteration >= maxIterations ? "已达到最大尝试次数" : "未找到更多历史记录"; // 延迟隐藏状态标签 setTimeout(() => { statusLabel.style.display = "none"; }, 4000); } // 使找到的问题定位在屏幕中 function renderPage(page) { // 清空列表容器 while (listContainer.firstChild) { listContainer.removeChild(listContainer.firstChild); } const start = (page - 1) * pageSize; const end = page * pageSize; const pageQuestions = questions.slice(start, end); pageQuestions.forEach((q, idx) => { const listItem = document.createElement("li"); const shortText = q.text.substring(0, 20) + (q.text.length > 20 ? "..." : ""); listItem.textContent = `${ isReversed ? questions.length - start - idx : start + idx + 1 }: ${shortText}`; listItem.style.padding = "8px 12px"; listItem.style.cursor = "pointer"; listItem.style.fontSize = "13px"; listItem.style.color = colors.textPrimary; listItem.style.whiteSpace = "nowrap"; listItem.style.overflow = "hidden"; listItem.style.textOverflow = "ellipsis"; listItem.style.borderBottom = `1px solid ${colors.itemBorder}`; listItem.style.transition = "background 0.2s"; listItem.style.borderRadius = "4px"; listItem.title = q.text; listItem.addEventListener("mouseover", () => { listItem.style.background = colors.itemHoverBg; }); listItem.addEventListener("mouseout", () => { listItem.style.background = "none"; }); listItem.addEventListener("click", () => { q.element.scrollIntoView({ behavior: "smooth", block: "start" }); floatWindow.style.opacity = "0"; setTimeout(() => (floatWindow.style.display = "none"), 200); button.textContent = "问题列表"; }); listContainer.appendChild(listItem); }); } // 更新分页控件 function updatePagination() { // 清空分页容器 while (paginationContainer.firstChild) { paginationContainer.removeChild(paginationContainer.firstChild); } const totalPages = Math.ceil(questions.length / pageSize); if (totalPages) { const prevButton = document.createElement("button"); prevButton.textContent = "上一页"; prevButton.style.padding = "5px 10px"; prevButton.style.border = "none"; prevButton.style.background = currentPage === 1 ? colors.paginationBg : colors.paginationActiveBg; prevButton.style.color = currentPage === 1 ? colors.textSecondary : "#fff"; prevButton.style.cursor = currentPage === 1 ? "not-allowed" : "pointer"; prevButton.style.borderRadius = "4px"; prevButton.style.transition = "background 0.2s"; prevButton.disabled = currentPage === 1; prevButton.addEventListener("click", () => { if (currentPage > 1) { currentPage--; renderPage(currentPage); updatePagination(); } }); paginationContainer.appendChild(prevButton); // 显示页码按钮,但限制最多显示5个 const maxButtons = 5; let startPage = Math.max( 1, Math.min( currentPage - Math.floor(maxButtons / 2), totalPages - maxButtons + 1 ) ); if (startPage < 1) startPage = 1; const endPage = Math.min(startPage + maxButtons - 1, totalPages); if (startPage > 1) { const firstPageButton = document.createElement("button"); firstPageButton.textContent = "1"; firstPageButton.style.padding = "5px 10px"; firstPageButton.style.border = "none"; firstPageButton.style.background = colors.paginationBg; firstPageButton.style.color = colors.paginationColor; firstPageButton.style.cursor = "pointer"; firstPageButton.style.borderRadius = "4px"; firstPageButton.style.transition = "background 0.2s"; firstPageButton.addEventListener("click", () => { currentPage = 1; renderPage(currentPage); updatePagination(); }); paginationContainer.appendChild(firstPageButton); if (startPage > 2) { const ellipsis = document.createElement("span"); ellipsis.textContent = "..."; ellipsis.style.padding = "5px"; ellipsis.style.color = colors.textSecondary; paginationContainer.appendChild(ellipsis); } } for (let i = startPage; i <= endPage; i++) { const pageButton = document.createElement("button"); pageButton.textContent = i; pageButton.style.padding = "5px 10px"; pageButton.style.border = "none"; pageButton.style.background = currentPage === i ? colors.paginationActiveBg : colors.paginationBg; pageButton.style.color = currentPage === i ? "#fff" : colors.paginationColor; pageButton.style.cursor = "pointer"; pageButton.style.borderRadius = "4px"; pageButton.style.transition = "background 0.2s"; pageButton.addEventListener("click", () => { currentPage = i; renderPage(currentPage); updatePagination(); }); paginationContainer.appendChild(pageButton); } if (endPage < totalPages) { if (endPage < totalPages - 1) { const ellipsis = document.createElement("span"); ellipsis.textContent = "..."; ellipsis.style.padding = "5px"; ellipsis.style.color = colors.textSecondary; paginationContainer.appendChild(ellipsis); } const lastPageButton = document.createElement("button"); lastPageButton.textContent = totalPages; lastPageButton.style.padding = "5px 10px"; lastPageButton.style.border = "none"; lastPageButton.style.background = colors.paginationBg; lastPageButton.style.color = colors.paginationColor; lastPageButton.style.cursor = "pointer"; lastPageButton.style.borderRadius = "4px"; lastPageButton.style.transition = "background 0.2s"; lastPageButton.addEventListener("click", () => { currentPage = totalPages; renderPage(currentPage); updatePagination(); }); paginationContainer.appendChild(lastPageButton); } const nextButton = document.createElement("button"); nextButton.textContent = "下一页"; nextButton.style.padding = "5px 10px"; nextButton.style.border = "none"; nextButton.style.background = currentPage === totalPages ? colors.paginationBg : colors.paginationActiveBg; nextButton.style.color = currentPage === totalPages ? colors.textSecondary : "#fff"; nextButton.style.cursor = currentPage === totalPages ? "not-allowed" : "pointer"; nextButton.style.borderRadius = "4px"; nextButton.style.transition = "background 0.2s"; nextButton.disabled = currentPage === totalPages; nextButton.addEventListener("click", () => { if (currentPage < totalPages) { currentPage++; renderPage(currentPage); updatePagination(); } }); paginationContainer.appendChild(nextButton); } } // 切换悬浮窗显示状态的函数 function toggleFloatWindow() { if ( floatWindow.style.display === "none" || floatWindow.style.display === "" ) { findAllQuestionsWithDeduplication(); updateFloatWindowPosition(); // 更新位置 floatWindow.style.display = "block"; floatWindow.style.opacity = "1"; button.textContent = "隐藏列表"; } else { floatWindow.style.opacity = "0"; setTimeout(() => { floatWindow.style.display = "none"; button.textContent = "问题列表"; }, 200); } } // 注意:点击事件已在拖动逻辑中处理(mouseup 事件) // 监听用户输入新问题后触发查找 function setupInputListener() { const input = document.querySelector( 'textarea, input[type="text"], [contenteditable]' ); if (input) { input.addEventListener("keypress", (e) => { if (e.key === "Enter" && !e.shiftKey) { setTimeout(findAllQuestionsWithDeduplication, 1000); } }); } // 监听可能的发送按钮点击 const sendButtons = document.querySelectorAll( 'button[type="submit"], button[aria-label*="send"], button[aria-label*="发送"]' ); sendButtons.forEach((btn) => { btn.addEventListener("click", () => { setTimeout(findAllQuestionsWithDeduplication, 1000); }); }); } // 页面加载后初始化 window.addEventListener("load", () => { // 先找一次所有问题 setTimeout(() => { findAllQuestionsWithDeduplication(); setupInputListener(); }, 1000); }); // MutationObserver 监听DOM变化,动态更新问题列表 const observerConfig = { childList: true, subtree: true }; const observer = new MutationObserver((mutationsList) => { // 检查是否需要更新问题列表 for (const mutation of mutationsList) { if (mutation.type === "childList" && mutation.addedNodes.length > 0) { // 检查是否添加了新的消息元素 const hasNewMessages = Array.from(mutation.addedNodes).some((node) => { if (node.nodeType === Node.ELEMENT_NODE) { return ( (node.matches && node.matches(currentConfig.messageSelector)) || (node.querySelector && node.querySelector(currentConfig.messageSelector)) ); } return false; }); if (hasNewMessages) { // 使用节流技术避免频繁更新 if (!observer.updateTimeout) { observer.updateTimeout = setTimeout(() => { findAllQuestionsWithDeduplication(); observer.updateTimeout = null; }, 500); } break; } } } }); // 开始观察DOM变化 setTimeout(() => { const chatContainer = document.querySelector(".chat-container, #chat, main, article") || document.body; observer.observe(chatContainer, observerConfig); }, 1500); })();