Greasy Fork is available in English.
通过点击按钮显示用户问题列表,支持导航到特定问题、分页功能、正序/倒序切换,智能脉冲式加载历史记录突破懒加载,自动适配暗黑模式,按钮可拖动并保存位置,悬浮窗智能展开方向
当前为
// ==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
// ==/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);
})();