Greasy Fork is available in English.
通过点击按钮显示用户问题列表,支持导航到特定问题、分页功能、正序/倒序切换,智能脉冲式加载历史记录突破懒加载,自动适配暗黑模式,按钮可拖动并保存位置,悬浮窗智能展开方向,无极调整按钮大小,新增NotebookLM支持
// ==UserScript==
// @name webAI聊天问题列表导航
// @namespace http://tampermonkey.net/
// @version 3.9.6
// @description 通过点击按钮显示用户问题列表,支持导航到特定问题、分页功能、正序/倒序切换,智能脉冲式加载历史记录突破懒加载,自动适配暗黑模式,按钮可拖动并保存位置,悬浮窗智能展开方向,无极调整按钮大小,新增NotebookLM支持
// @author yutao
// @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/*
// @match https://notebooklm.google.com/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @grant GM_listValues
// 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";
// 配置工厂函数 - 创建统一的网站配置结构
function createSiteConfig(options) {
return {
messageSelector: options.messageSelector,
textSelector: options.textSelector !== undefined ? options.textSelector : null,
userCondition: options.userCondition || ((element) => true),
scrollContainerSelector: options.scrollContainerSelector ||
'div[class*="overflow"], div[class*="scroll"], main',
useScrollContainerForMessages: options.useScrollContainerForMessages || false,
};
}
// 预定义的用户条件函数
const userConditions = {
// 默认条件:所有消息都是用户消息
alwaysTrue: (element) => true,
// 检查类名是否包含特定字符串
hasClass: (className) => (element) => element.classList.contains(className),
};
// 配置对象,定义不同网站的聊天消息选择器和条件
const config = {
"chat.qwen.ai": createSiteConfig({
messageSelector: "div.rounded-3xl.bg-gray-50.dark\\:bg-gray-850",
textSelector: "p",
scrollContainerSelector: 'div.overflow-y-auto, div[class*="chat-content"]',
}),
"tongyi.com": createSiteConfig({
messageSelector: 'div[class*="questionItem"]',
textSelector: 'div[class*="contentBox"] div[class*="bubble"]',
scrollContainerSelector: 'div[class*="contentWrapper"], main, div[class*="chat-content"], div[class*="chatContent"]',
}),
"qianwen.com": createSiteConfig({
messageSelector: 'div[class*="questionItem"]',
textSelector: 'div[class*="contentBox"] div[class*="bubble"]',
scrollContainerSelector: 'div[class*="contentWrapper"], main, div[class*="chat-content"], div[class*="chatContent"]',
}),
"yuanbao.tencent.com": createSiteConfig({
messageSelector: "div.agent-chat__bubble__content",
textSelector: "div.hyc-content-text",
scrollContainerSelector: ".agent-chat__bubble-wrap",
}),
"doubao.com": createSiteConfig({
messageSelector: 'div[data-testid="send_message"]',
textSelector: 'div[data-testid="message_text_content"]',
scrollContainerSelector: 'div[class*="scrollable-"][class*="show-scrollbar-"]',
}),
"copilot.wps.cn": createSiteConfig({
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"]',
scrollContainerSelector: '.chat, .p__main, div[class*="scrollbar"], div[class*="chat-list"], div[class*="scroll"], .scroll-container',
}),
"www.kimi.com": createSiteConfig({
messageSelector: 'div.segment-user, div[class*="segment-user"]',
textSelector: '.user-content, div[class*="user-content"]',
scrollContainerSelector: 'div[class*="scrollbar"], div[class*="chat-history"]',
}),
"chatglm.cn": createSiteConfig({
messageSelector: 'div.conversation.question, div[id*="row-question"]',
textSelector: '.question-txt span, div[id*="row-question-p"] span',
scrollContainerSelector: 'div[class*="chat-history"], div[class*="scrollable"]',
}),
"chat.deepseek.com": createSiteConfig({
messageSelector: "div.fbb737a4",
scrollContainerSelector: ".scroll-container",
}),
"github.com": createSiteConfig({
messageSelector: "div.UserMessage-module__container--cAvvK.ChatMessage-module__userMessage--xvIFp",
userCondition: userConditions.hasClass("ChatMessage-module__userMessage--xvIFp"),
scrollContainerSelector: ".react-scroll-to-bottom--css-xgtui-79elbk",
}),
"copilot.microsoft.com": createSiteConfig({
messageSelector: "div.self-end.rounded-2xl",
userCondition: userConditions.hasClass("self-end"),
scrollContainerSelector: ".overflow-y-auto.flex-1",
}),
"chatgpt.com": createSiteConfig({
messageSelector: "div.rounded-3xl.bg-token-message-surface",
textSelector: "div.whitespace-pre-wrap",
scrollContainerSelector: "main div.overflow-y-auto",
}),
"notebooklm.google.com": createSiteConfig({
messageSelector: '.from-user-container',
textSelector: 'div, p, span',
userCondition: userConditions.hasClass('from-user-container'),
scrollContainerSelector: '.chat-panel-content, chat-pane, .chat-panel, main, div[class*="scroll"]',
useScrollContainerForMessages: true,
}),
};
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();
// 按钮样式工厂 - 类似 Vue 组件的按钮创建器
const ButtonFactory = {
// 按钮样式预设(类似 Vue 的 props)
presets: {
primary: {
background: colors.buttonPrimaryBg,
hoverBackground: colors.buttonPrimaryHover,
color: "#fff",
},
secondary: {
background: colors.buttonSecondaryBg,
hoverBackground: colors.buttonSecondaryHover,
color: "#fff",
},
default: {
background: colors.paginationBg,
hoverBackground: colors.itemHoverBg,
color: colors.paginationColor,
},
disabled: {
background: colors.paginationBg,
hoverBackground: colors.paginationBg,
color: colors.textSecondary,
},
},
// 创建按钮(类似 Vue 的 render 函数)
create(options = {}) {
const {
text = "",
preset = "default",
onClick = null,
disabled = false,
padding = "5px 10px",
fontSize = "12px",
borderRadius = "4px",
customStyle = {},
} = options;
const button = document.createElement("button");
const style = this.presets[disabled ? "disabled" : preset];
// 设置按钮文本
button.textContent = text;
// 应用基础样式
Object.assign(button.style, {
padding,
background: style.background,
color: style.color,
border: "none",
borderRadius,
cursor: disabled ? "not-allowed" : "pointer",
fontSize,
transition: "background 0.2s",
fontFamily: "Arial, sans-serif",
...customStyle,
});
// 设置禁用状态
button.disabled = disabled;
// 添加悬停效果(类似 Vue 的事件处理)
if (!disabled) {
button.addEventListener("mouseover", () => {
button.style.background = style.hoverBackground;
});
button.addEventListener("mouseout", () => {
button.style.background = style.background;
});
}
// 添加点击事件
if (onClick && !disabled) {
button.addEventListener("click", onClick);
}
return button;
},
// 创建分页按钮(特殊类型)
createPaginationButton(options = {}) {
const { page, isActive = false, onClick = null } = options;
return this.create({
text: String(page),
preset: isActive ? "secondary" : "default",
onClick,
customStyle: {
background: isActive ? colors.paginationActiveBg : colors.paginationBg,
color: isActive ? "#fff" : colors.paginationColor,
},
});
},
// 创建导航按钮(上一页/下一页)
createNavButton(options = {}) {
const { text, disabled = false, onClick = null } = options;
return this.create({
text,
preset: disabled ? "disabled" : "secondary",
disabled,
onClick,
customStyle: {
background: disabled ? colors.paginationBg : colors.paginationActiveBg,
color: disabled ? colors.textSecondary : "#fff",
},
});
},
};
// 右键菜单管理器
const contextMenuManager = {
menu: null,
// 创建菜单
create() {
if (this.menu) {
this.destroy();
}
this.menu = document.createElement('div');
this.menu.style.cssText = `
position: fixed;
background: ${colors.windowBg};
border: 1px solid ${colors.windowBorder};
border-radius: 8px;
box-shadow: ${colors.windowShadow};
padding: 8px 0;
min-width: 160px;
z-index: 10000;
font-family: Arial, sans-serif;
font-size: 13px;
display: none;
`;
document.body.appendChild(this.menu);
return this.menu;
},
// 添加菜单项
addItem(text, icon, onClick, isActive = false) {
const item = document.createElement('div');
item.style.cssText = `
padding: 8px 16px;
cursor: pointer;
color: ${colors.textPrimary};
display: flex;
align-items: center;
gap: 8px;
transition: background 0.2s;
${isActive ? `background: ${colors.itemHoverBg};` : ''}
`;
// 使用 DOM API 而不是 innerHTML,避免 Trusted Types 问题
const iconSpan = document.createElement('span');
iconSpan.style.cssText = 'width: 16px; text-align: center;';
iconSpan.textContent = icon;
const textSpan = document.createElement('span');
textSpan.style.cssText = 'flex: 1;';
textSpan.textContent = text;
item.appendChild(iconSpan);
item.appendChild(textSpan);
if (isActive) {
const checkSpan = document.createElement('span');
checkSpan.style.color = colors.buttonSecondaryBg;
checkSpan.textContent = '✓';
item.appendChild(checkSpan);
}
item.addEventListener('mouseover', () => {
item.style.background = colors.itemHoverBg;
});
item.addEventListener('mouseout', () => {
item.style.background = isActive ? colors.itemHoverBg : 'transparent';
});
item.addEventListener('click', (e) => {
e.stopPropagation();
onClick();
this.hide();
});
this.menu.appendChild(item);
},
// 添加分隔线
addSeparator() {
const separator = document.createElement('div');
separator.style.cssText = `
height: 1px;
background: ${colors.itemBorder};
margin: 4px 0;
`;
this.menu.appendChild(separator);
},
// 显示菜单
show(x, y) {
if (!this.menu) return;
// 清空现有内容(使用 DOM API 避免 Trusted Types 问题)
while (this.menu.firstChild) {
this.menu.removeChild(this.menu.firstChild);
}
// 构建菜单项
this.buildMenu();
// 调整位置防止超出屏幕
const rect = this.menu.getBoundingClientRect();
const maxX = window.innerWidth - rect.width - 10;
const maxY = window.innerHeight - rect.height - 10;
x = Math.min(x, maxX);
y = Math.min(y, maxY);
this.menu.style.left = x + 'px';
this.menu.style.top = y + 'px';
this.menu.style.display = 'block';
// 点击其他地方关闭菜单
setTimeout(() => {
document.addEventListener('click', this.hide.bind(this), { once: true });
}, 0);
},
// 构建菜单内容
buildMenu() {
// 按钮大小调整
this.addItem('调整按钮大小', '📏', () => this.openSizeAdjuster());
this.addSeparator();
// 位置和其他设置
this.addItem('重置位置', '📍', () => this.resetPosition());
this.addItem('重置所有设置', '🔄', () => this.resetAllSettings());
// 未来可扩展的设置项
// this.addSeparator();
// this.addItem('主题设置', '🎨', () => this.openThemeSettings());
// this.addItem('高级设置', '⚙️', () => this.openAdvancedSettings());
},
// 打开大小调整器
openSizeAdjuster() {
sizeAdjusterManager.show();
},
// 重置位置
resetPosition() {
const defaultPos = settingsManager.defaults.position;
settingsManager.set('position', defaultPos);
button.style.bottom = defaultPos.bottom + 'px';
button.style.right = defaultPos.right + 'px';
updateFloatWindowPosition();
},
// 重置所有设置
resetAllSettings() {
if (confirm('确定要重置所有设置吗?这将恢复默认的按钮大小和位置。')) {
settingsManager.reset();
location.reload(); // 简单粗暴的重置方法
}
},
// 隐藏菜单
hide() {
if (this.menu) {
this.menu.style.display = 'none';
}
},
// 销毁菜单
destroy() {
if (this.menu) {
this.menu.remove();
this.menu = null;
}
}
};
// 应用按钮大小
function applyButtonSize(scale) {
const style = settingsManager.getButtonStyle(scale);
button.style.padding = style.padding;
button.style.fontSize = style.fontSize;
button.style.borderRadius = style.borderRadius;
// 更新悬浮窗位置(因为按钮大小变了)
setTimeout(updateFloatWindowPosition, 0);
}
// 统一存储适配器 - 优先使用 GM API,回退到 localStorage
const StorageAdapter = {
// 检测是否支持 GM API
hasGMSupport() {
const hasSupport = typeof GM_setValue !== 'undefined' && typeof GM_getValue !== 'undefined';
// 首次检测时输出日志
if (!this._loggedSupport) {
console.log('[存储适配器] GM API 支持:', hasSupport ? '✅ 是' : '❌ 否,使用 localStorage');
this._loggedSupport = true;
}
return hasSupport;
},
// 设置值
set(key, value) {
try {
if (this.hasGMSupport()) {
GM_setValue(key, value);
} else {
localStorage.setItem(key, JSON.stringify(value));
}
return true;
} catch (e) {
console.warn(`存储失败: ${key}`, e);
return false;
}
},
// 获取值
get(key, defaultValue = null) {
try {
if (this.hasGMSupport()) {
return GM_getValue(key, defaultValue);
} else {
const value = localStorage.getItem(key);
return value ? JSON.parse(value) : defaultValue;
}
} catch (e) {
console.warn(`读取失败: ${key}`, e);
return defaultValue;
}
},
// 删除值
delete(key) {
try {
if (this.hasGMSupport()) {
GM_deleteValue(key);
} else {
localStorage.removeItem(key);
}
return true;
} catch (e) {
console.warn(`删除失败: ${key}`, e);
return false;
}
},
// 列出所有键
listKeys() {
try {
if (this.hasGMSupport()) {
return GM_listValues();
} else {
return Object.keys(localStorage);
}
} catch (e) {
console.warn('列出键失败', e);
return [];
}
}
};
// 设置管理器 - 统一管理所有用户设置
const settingsManager = {
storageKeys: {
position: 'questionListButton_position',
size: 'questionListButton_size',
theme: 'questionListButton_theme',
pageSize: 'questionListButton_pageSize'
},
// 默认设置
defaults: {
position: { bottom: 20, right: 20 },
buttonScale: 100, // 按钮缩放比例 (50-200)
theme: 'auto',
pageSize: 10
},
// 根据缩放比例计算按钮样式
getButtonStyle(scale) {
// 基础样式 (scale = 100 时的标准大小)
const basePadding = 10; // 10px
const baseFontSize = 14; // 14px
const baseBorderRadius = 8; // 8px
// 计算实际值
const factor = scale / 100;
const padding = Math.round(basePadding * factor);
const fontSize = Math.round(baseFontSize * factor);
const borderRadius = Math.round(baseBorderRadius * factor);
return {
padding: `${padding}px ${Math.round(padding * 1.5)}px`,
fontSize: `${fontSize}px`,
borderRadius: `${borderRadius}px`
};
},
// 获取设置
get(key) {
return StorageAdapter.get(this.storageKeys[key], this.defaults[key]);
},
// 保存设置
set(key, value) {
return StorageAdapter.set(this.storageKeys[key], value);
},
// 重置所有设置
reset() {
Object.keys(this.storageKeys).forEach(key => {
StorageAdapter.delete(this.storageKeys[key]);
});
}
};
// 创建美化后的浮动按钮
const button = document.createElement("button");
button.textContent = "问题列表";
button.style.position = "fixed";
button.style.zIndex = "1000";
button.style.background = colors.buttonBg;
button.style.color = colors.buttonColor;
button.style.border = "none";
button.style.boxShadow = "0 2px 4px rgba(0,0,0,0.2)";
button.style.cursor = "move";
button.style.fontFamily = "Arial, sans-serif";
button.style.transition = "transform 0.2s, box-shadow 0.2s";
button.style.userSelect = "none";
// 恢复保存的位置和大小
const savedPos = settingsManager.get('position');
const savedScale = settingsManager.get('buttonScale');
button.style.bottom = savedPos.bottom + "px";
button.style.right = savedPos.right + "px";
// 应用保存的按钮大小
applyButtonSize(savedScale);
// 创建右键菜单
contextMenuManager.create();
// 大小调整器管理器
const sizeAdjusterManager = {
panel: null,
slider: null,
input: null,
// 创建调整面板
create() {
if (this.panel) {
this.destroy();
}
this.panel = document.createElement('div');
this.panel.style.cssText = `
position: fixed;
background: ${colors.windowBg};
border: 1px solid ${colors.windowBorder};
border-radius: 12px;
box-shadow: ${colors.windowShadow};
padding: 20px;
width: 300px;
z-index: 10001;
font-family: Arial, sans-serif;
display: none;
`;
// 标题
const title = document.createElement('div');
title.textContent = '调整按钮大小';
title.style.cssText = `
font-size: 16px;
font-weight: bold;
color: ${colors.textPrimary};
margin-bottom: 15px;
text-align: center;
`;
// 滑块容器
const sliderContainer = document.createElement('div');
sliderContainer.style.cssText = `
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 15px;
`;
// 滑块
this.slider = document.createElement('input');
this.slider.type = 'range';
this.slider.min = '50';
this.slider.max = '200';
this.slider.step = '5';
this.slider.style.cssText = `
flex: 1;
height: 6px;
border-radius: 3px;
background: ${colors.itemBorder};
outline: none;
cursor: pointer;
`;
// 数值输入框
this.input = document.createElement('input');
this.input.type = 'number';
this.input.min = '50';
this.input.max = '200';
this.input.step = '5';
this.input.style.cssText = `
width: 60px;
padding: 6px 8px;
border: 1px solid ${colors.windowBorder};
border-radius: 4px;
background: ${colors.windowBg};
color: ${colors.textPrimary};
font-size: 13px;
text-align: center;
`;
// 按钮容器
const buttonContainer = document.createElement('div');
buttonContainer.style.cssText = `
display: flex;
gap: 10px;
justify-content: center;
`;
// 确定按钮
const okButton = document.createElement('button');
okButton.textContent = '确定';
okButton.style.cssText = `
padding: 8px 16px;
background: ${colors.buttonSecondaryBg};
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
`;
// 取消按钮
const cancelButton = document.createElement('button');
cancelButton.textContent = '取消';
cancelButton.style.cssText = `
padding: 8px 16px;
background: ${colors.paginationBg};
color: ${colors.textPrimary};
border: 1px solid ${colors.windowBorder};
border-radius: 6px;
cursor: pointer;
font-size: 13px;
`;
// 组装面板
sliderContainer.appendChild(this.slider);
sliderContainer.appendChild(this.input);
buttonContainer.appendChild(okButton);
buttonContainer.appendChild(cancelButton);
this.panel.appendChild(title);
this.panel.appendChild(sliderContainer);
this.panel.appendChild(buttonContainer);
document.body.appendChild(this.panel);
// 事件监听
this.setupEvents(okButton, cancelButton);
},
// 设置事件监听
setupEvents(okButton, cancelButton) {
// 滑块变化
this.slider.addEventListener('input', () => {
const value = parseInt(this.slider.value);
this.input.value = value;
this.previewSize(value);
});
// 输入框变化
this.input.addEventListener('input', () => {
let value = parseInt(this.input.value);
if (isNaN(value)) return;
value = Math.max(50, Math.min(200, value));
this.slider.value = value;
this.previewSize(value);
});
// 确定按钮
okButton.addEventListener('click', () => {
const value = parseInt(this.slider.value);
settingsManager.set('buttonScale', value);
this.hide();
});
// 取消按钮
cancelButton.addEventListener('click', () => {
// 恢复原始大小
const originalScale = settingsManager.get('buttonScale');
applyButtonSize(originalScale);
this.hide();
});
// 点击外部关闭
setTimeout(() => {
document.addEventListener('click', (e) => {
if (!this.panel.contains(e.target)) {
cancelButton.click();
}
}, { once: true });
}, 0);
},
// 预览大小变化
previewSize(scale) {
applyButtonSize(scale);
},
// 显示面板
show() {
if (!this.panel) {
this.create();
}
// 设置当前值
const currentScale = settingsManager.get('buttonScale');
this.slider.value = currentScale;
this.input.value = currentScale;
// 居中显示
const rect = this.panel.getBoundingClientRect();
const x = (window.innerWidth - 300) / 2;
const y = (window.innerHeight - rect.height) / 2;
this.panel.style.left = x + 'px';
this.panel.style.top = y + 'px';
this.panel.style.display = 'block';
// 聚焦到滑块
setTimeout(() => this.slider.focus(), 100);
},
// 隐藏面板
hide() {
if (this.panel) {
this.panel.style.display = 'none';
}
},
// 销毁面板
destroy() {
if (this.panel) {
this.panel.remove();
this.panel = null;
this.slider = null;
this.input = null;
}
}
};
// 拖动功能
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);
settingsManager.set('position', { 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)";
}
});
// 右键菜单事件
button.addEventListener("contextmenu", (e) => {
e.preventDefault();
e.stopPropagation();
contextMenuManager.show(e.clientX, e.clientY);
});
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);
// 存储测试函数(开发调试用)
window.testStorage = function() {
console.log('=== 存储测试开始 ===');
// 测试1: 检测 GM API
console.log('1. GM API 支持:', StorageAdapter.hasGMSupport());
// 测试2: 写入测试
const testKey = 'test_storage_' + Date.now();
const testValue = { time: Date.now(), data: '测试数据' };
console.log('2. 写入测试数据:', testValue);
const writeSuccess = StorageAdapter.set(testKey, testValue);
console.log(' 写入结果:', writeSuccess ? '✅ 成功' : '❌ 失败');
// 测试3: 读取测试
const readValue = StorageAdapter.get(testKey);
console.log('3. 读取测试数据:', readValue);
console.log(' 读取结果:', JSON.stringify(readValue) === JSON.stringify(testValue) ? '✅ 成功' : '❌ 失败');
// 测试4: 删除测试
StorageAdapter.delete(testKey);
const afterDelete = StorageAdapter.get(testKey);
console.log('4. 删除后读取:', afterDelete);
console.log(' 删除结果:', afterDelete === null ? '✅ 成功' : '❌ 失败');
// 测试5: 收藏功能
console.log('5. 当前页面ID:', FavoriteManager.getPageId());
console.log(' 当前收藏:', FavoriteManager.getAll());
console.log('=== 存储测试完成 ===');
console.log('提示: 刷新页面后再次运行 testStorage() 检查数据是否持久化');
};
// 消息计数管理器 - 记录每个页面的消息总数
const MessageCountManager = {
storageKey: 'questionList_messageCounts',
// 获取当前页面的唯一标识
getPageId() {
return window.location.pathname + window.location.search;
},
// 获取记录的消息总数
getCount() {
const pageId = this.getPageId();
const allCounts = StorageAdapter.get(this.storageKey, {});
return allCounts[pageId] || 0;
},
// 保存消息总数
saveCount(count) {
const pageId = this.getPageId();
const allCounts = StorageAdapter.get(this.storageKey, {});
allCounts[pageId] = count;
StorageAdapter.set(this.storageKey, allCounts);
console.log('[消息计数] 保存消息总数:', {
pageId,
消息总数: count
});
},
// 检查是否需要加载历史
shouldLoadHistory(currentCount) {
const savedCount = this.getCount();
const needLoad = savedCount > 0 && currentCount < savedCount;
console.log('[消息计数] 检查是否需要加载历史:', {
当前消息数: currentCount,
记录的总数: savedCount,
需要加载: needLoad
});
return needLoad;
}
};
// 收藏管理器
const FavoriteManager = {
storageKey: 'questionList_favorites',
// 获取当前页面的唯一标识(用于区分不同对话)
getPageId() {
return window.location.pathname + window.location.search;
},
// 获取所有收藏
getAll() {
const pageId = this.getPageId();
const allFavorites = StorageAdapter.get(this.storageKey, {});
const favorites = allFavorites[pageId] || [];
console.log('[收藏管理器] 读取收藏:', {
pageId,
收藏数量: favorites.length,
收藏列表: favorites
});
return favorites;
},
// 保存收藏
saveAll(favorites) {
const pageId = this.getPageId();
const allFavorites = StorageAdapter.get(this.storageKey, {});
allFavorites[pageId] = favorites;
const success = StorageAdapter.set(this.storageKey, allFavorites);
console.log('[收藏管理器] 保存收藏:', {
pageId,
收藏数量: favorites.length,
保存成功: success
});
},
// 检查是否已收藏
isFavorite(questionText) {
return this.getAll().includes(questionText);
},
// 添加收藏
add(questionText) {
const favorites = this.getAll();
if (!favorites.includes(questionText)) {
favorites.push(questionText);
this.saveAll(favorites);
}
},
// 移除收藏
remove(questionText) {
const favorites = this.getAll().filter(text => text !== questionText);
this.saveAll(favorites);
},
// 切换收藏状态
toggle(questionText) {
if (this.isFavorite(questionText)) {
this.remove(questionText);
return false;
} else {
this.add(questionText);
return true;
}
},
// 获取收藏的问题对象
getFavoriteQuestions(allQuestions) {
const favorites = this.getAll();
return allQuestions.filter(q => favorites.includes(q.text));
}
};
// 分页相关变量
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 = ButtonFactory.create({
text: "加载历史",
preset: "primary",
onClick: () => loadHistoryRecords(),
});
// 使用按钮工厂创建排序切换按钮
const sortButton = ButtonFactory.create({
text: "正序",
preset: "secondary",
onClick: () => {
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 favoriteContainer = document.createElement("div");
favoriteContainer.style.marginBottom = "10px";
favoriteContainer.style.display = "none"; // 默认隐藏,有收藏时才显示
const favoriteTitle = document.createElement("div");
favoriteTitle.style.fontSize = "12px";
favoriteTitle.style.fontWeight = "bold";
favoriteTitle.style.color = colors.textPrimary;
favoriteTitle.style.padding = "5px 0";
favoriteTitle.style.borderBottom = `2px solid ${colors.buttonPrimaryBg}`;
favoriteTitle.style.marginBottom = "5px";
favoriteTitle.textContent = "📌 收藏";
const favoriteList = document.createElement("ul");
favoriteList.style.listStyle = "none";
favoriteList.style.padding = "0";
favoriteList.style.margin = "0 0 10px 0";
favoriteContainer.appendChild(favoriteTitle);
favoriteContainer.appendChild(favoriteList);
floatWindow.appendChild(favoriteContainer);
// 创建问题计数显示区域
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.appendChild(questionCountDisplay);
// 问题列表容器
const listContainer = document.createElement("ul");
listContainer.style.listStyle = "none";
listContainer.style.padding = "0";
listContainer.style.margin = "0";
floatWindow.appendChild(listContainer);
floatWindow.appendChild(paginationContainer);
// 更新问题计数显示
function updateQuestionCountDisplay() {
questionCountDisplay.textContent = `共找到 ${questions.length} 个问题`;
}
// 获取文本内容的辅助函数
function getTextContent(element) {
return element ? element.textContent.trim() : "";
}
// 查找所有用户问题并去重的函数
function findAllQuestionsWithDeduplication() {
// 选择聊天容器
let chatContainer = null;
// 检查配置是否要求使用滚动容器来查找消息
if (currentConfig.useScrollContainerForMessages && currentConfig.scrollContainerSelector) {
// 使用配置的滚动容器选择器
const selectors = currentConfig.scrollContainerSelector.split(',');
for (const selector of selectors) {
chatContainer = document.querySelector(selector.trim());
if (chatContainer) break;
}
}
// 如果没找到,使用通用选择器
if (!chatContainer) {
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 lastQuestionCount = questions.length;
let iteration = 0;
const maxRetryAfterNoChange = 2; // 没有新内容后,再尝试2次确认
// 脉冲式滚动加载循环
while (isLoading && consecutiveNoChange <= maxRetryAfterNoChange) {
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 = consecutiveNoChange === 0 ? 600 : 1000;
await new Promise((resolve) => setTimeout(resolve, waitTime));
// 4. 扫描新内容
const preCount = questions.length;
findAllQuestionsWithDeduplication();
const postCount = questions.length;
// 5. 检测变化
const questionsChanged = postCount > lastQuestionCount;
const newQuestionsCount = postCount - lastQuestionCount;
if (questionsChanged) {
// 发现新内容
consecutiveNoChange = 0;
lastQuestionCount = postCount;
statusLabel.textContent = `已加载 ${postCount} 个问题 (+${newQuestionsCount})`;
console.log(`[历史加载] 第${iteration}次: 新增 ${newQuestionsCount} 个问题,总计 ${postCount} 个`);
} else {
// 没有新内容
consecutiveNoChange++;
statusLabel.textContent = `检查中... (${consecutiveNoChange}/${maxRetryAfterNoChange + 1})`;
console.log(`[历史加载] 第${iteration}次: 没有新内容,连续 ${consecutiveNoChange} 次`);
// 如果已经连续多次没有新内容,说明已经到底了
if (consecutiveNoChange > maxRetryAfterNoChange) {
console.log(`[历史加载] 连续 ${consecutiveNoChange} 次没有新内容,停止加载`);
break;
}
}
// 6. 额外的触发机制:模拟鼠标滚轮事件(每3次触发一次)
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;
// 生成加载结果提示
let resultMessage;
if (newQuestions > 0) {
resultMessage = `✓ 成功加载 ${newQuestions} 条新记录 (共${questions.length}条)`;
console.log(`[历史加载] 完成: 新增 ${newQuestions} 条,总计 ${questions.length} 条,共尝试 ${iteration} 次`);
} else {
resultMessage = consecutiveNoChange > maxRetryAfterNoChange
? "已加载所有历史记录"
: "未找到更多历史记录";
console.log(`[历史加载] 完成: 没有新内容,共尝试 ${iteration} 次`);
}
statusLabel.textContent = resultMessage;
// 保存消息总数(用于下次刷新时判断)
if (questions.length > 0) {
MessageCountManager.saveCount(questions.length);
}
// 延迟隐藏状态标签
setTimeout(() => {
statusLabel.style.display = "none";
}, 4000);
}
// 创建问题列表项(带收藏功能)
function createQuestionItem(q, index, isFavoriteItem = false) {
const listItem = document.createElement("li");
listItem.style.padding = "8px 12px";
listItem.style.fontSize = "13px";
listItem.style.color = colors.textPrimary;
listItem.style.borderBottom = `1px solid ${colors.itemBorder}`;
listItem.style.transition = "background 0.2s";
listItem.style.borderRadius = "4px";
listItem.style.display = "flex";
listItem.style.alignItems = "center";
listItem.style.gap = "8px";
listItem.title = q.text;
// 问题文本容器
const textContainer = document.createElement("div");
textContainer.style.flex = "1";
textContainer.style.cursor = "pointer";
textContainer.style.whiteSpace = "nowrap";
textContainer.style.overflow = "hidden";
textContainer.style.textOverflow = "ellipsis";
const shortText = q.text.substring(0, 20) + (q.text.length > 20 ? "..." : "");
textContainer.textContent = `${index}: ${shortText}`;
// 星标按钮
const starButton = document.createElement("span");
starButton.textContent = FavoriteManager.isFavorite(q.text) ? "⭐" : "☆";
starButton.style.cursor = "pointer";
starButton.style.fontSize = "16px";
starButton.style.flexShrink = "0";
starButton.style.transition = "transform 0.2s";
starButton.title = FavoriteManager.isFavorite(q.text) ? "取消收藏" : "收藏";
// 星标按钮事件
starButton.addEventListener("click", (e) => {
e.stopPropagation();
const isFavorited = FavoriteManager.toggle(q.text);
starButton.textContent = isFavorited ? "⭐" : "☆";
starButton.title = isFavorited ? "取消收藏" : "收藏";
// 重新渲染收藏区域和当前页
renderFavorites();
renderPage(currentPage);
});
starButton.addEventListener("mouseover", () => {
starButton.style.transform = "scale(1.2)";
});
starButton.addEventListener("mouseout", () => {
starButton.style.transform = "scale(1)";
});
// 文本容器点击事件
textContainer.addEventListener("click", () => {
q.element.scrollIntoView({ behavior: "smooth", block: "start" });
floatWindow.style.opacity = "0";
setTimeout(() => (floatWindow.style.display = "none"), 200);
button.textContent = "问题列表";
});
// 悬停效果
listItem.addEventListener("mouseover", () => {
listItem.style.background = colors.itemHoverBg;
});
listItem.addEventListener("mouseout", () => {
listItem.style.background = "none";
});
listItem.appendChild(textContainer);
listItem.appendChild(starButton);
return listItem;
}
// 渲染收藏区域
function renderFavorites() {
// 清空收藏列表
while (favoriteList.firstChild) {
favoriteList.removeChild(favoriteList.firstChild);
}
const favoriteQuestions = FavoriteManager.getFavoriteQuestions(questions);
if (favoriteQuestions.length > 0) {
favoriteContainer.style.display = "block";
favoriteTitle.textContent = `📌 收藏 (${favoriteQuestions.length})`;
favoriteQuestions.forEach((q) => {
// 找到问题的原始索引
const originalIndex = questions.findIndex(item => item.text === q.text);
const displayIndex = isReversed ? questions.length - originalIndex : originalIndex + 1;
const item = createQuestionItem(q, displayIndex, true);
favoriteList.appendChild(item);
});
} else {
favoriteContainer.style.display = "none";
}
}
// 使找到的问题定位在屏幕中
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 displayIndex = isReversed ? questions.length - start - idx : start + idx + 1;
const item = createQuestionItem(q, displayIndex);
listContainer.appendChild(item);
});
// 同时更新收藏区域
renderFavorites();
}
// 更新分页控件
function updatePagination() {
// 清空分页容器
while (paginationContainer.firstChild) {
paginationContainer.removeChild(paginationContainer.firstChild);
}
const totalPages = Math.ceil(questions.length / pageSize);
if (totalPages) {
// 使用按钮工厂创建上一页按钮
const prevButton = ButtonFactory.createNavButton({
text: "上一页",
disabled: currentPage === 1,
onClick: () => {
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 = ButtonFactory.createPaginationButton({
page: 1,
isActive: false,
onClick: () => {
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 = ButtonFactory.createPaginationButton({
page: i,
isActive: currentPage === i,
onClick: () => {
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 = ButtonFactory.createPaginationButton({
page: totalPages,
isActive: false,
onClick: () => {
currentPage = totalPages;
renderPage(currentPage);
updatePagination();
},
});
paginationContainer.appendChild(lastPageButton);
}
// 使用按钮工厂创建下一页按钮
const nextButton = ButtonFactory.createNavButton({
text: "下一页",
disabled: currentPage === totalPages,
onClick: () => {
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", () => {
// 先找一次所有问题
// NotebookLM 等 Angular 应用需要更长的加载时间
const isAngularApp = hostname.includes('notebooklm') || document.querySelector('[ng-version]');
const delay = isAngularApp ? 5000 : 1000;
setTimeout(() => {
findAllQuestionsWithDeduplication();
setupInputListener();
// NotebookLM 特殊处理:如果第一次没找到,再尝试几次
if (isAngularApp && questions.length === 0) {
let retryCount = 0;
const retryInterval = setInterval(() => {
findAllQuestionsWithDeduplication();
retryCount++;
if (questions.length > 0 || retryCount >= 3) {
clearInterval(retryInterval);
if (questions.length > 0) {
console.log('[问题列表导航] NotebookLM 重试成功,找到', questions.length, '个问题');
}
}
}, 2000); // 每2秒重试一次,最多3次
}
// 智能加载历史记录
setTimeout(() => {
const currentCount = questions.length;
const shouldLoad = MessageCountManager.shouldLoadHistory(currentCount);
if (shouldLoad) {
console.log('[问题列表导航] 检测到消息数量减少,自动加载历史记录...');
// 自动加载历史,但不显示状态(静默加载)
const originalDisplay = statusLabel.style.display;
statusLabel.style.display = 'none';
loadHistoryRecords().finally(() => {
statusLabel.style.display = originalDisplay;
console.log('[问题列表导航] 历史记录加载完成');
});
} else {
console.log('[问题列表导航] 消息数量正常,无需加载历史');
// 即使不需要加载历史,也保存当前的消息数量
if (currentCount > 0) {
MessageCountManager.saveCount(currentCount);
}
}
}, delay + 2000); // 等待初始扫描完成后再检查
}, delay);
});
// 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);
})();