GPT 大纲生成器

为 GPT 对话生成右侧大纲视图,提取问题前16个字作为标题

// ==UserScript==
// @name         GPT 大纲生成器
// @namespace    http://tampermonkey.net/
// @version      1.7
// @description  为 GPT 对话生成右侧大纲视图,提取问题前16个字作为标题
// @author       YungVenuz
// @license      AGPL-3.0-or-later
// @match        https://chatgpt.com/*
// @match        https://chat.deepseek.com/*
// @match        https://gemini.google.com/*
// @match        https://kimi.moonshot.cn/*
// @include      https://ying.baichuan-ai.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=openai.com
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    function $(param) {
        // DOM Ready 逻辑
        if (typeof param === 'function') {
            if (document.readyState === 'complete' || document.readyState === 'interactive') {
                param();
            } else {
                document.addEventListener('DOMContentLoaded', param);
            }
            return;
        }

        // 选择器逻辑
        if (typeof param === 'string') {
            const elements = document.querySelectorAll(param);
            return {
                elements,
                hide() {
                    this.elements.forEach(el => el.style.display = 'none');
                    return this;
                },
                click(fn) {
                    this.elements.forEach(el => el.addEventListener('click', fn));
                    return this;
                }
            };
        }
    }

    /**
     * ChatGPT大纲生成器类
     */
    class ChatGPTOutlineGenerator {
        constructor() {
            this.outlineContainer = null;
            this.toggleButton = null;
            this.styleElement = null;
            this.cssStyles = `
                .outline-container {
                    color: #000000;
                    position: fixed;
                    top: 70px;
                    right: 20px;
                    width: 280px;
                    max-height: calc(100vh - 100px);
                    background-color: rgba(247, 247, 248, 0.85);
                    border-radius: 12px;
                    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
                    z-index: 1000;
                    overflow-y: auto;
                    overflow-x: hidden; /* 防止水平滚动 */
                    transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
                    font-family: 'Söhne', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, Ubuntu, Cantarell, 'Noto Sans', sans-serif;
                    opacity: 0.95;
                    backdrop-filter: blur(5px);
                    -webkit-backdrop-filter: blur(5px);
                }

                .outline-container:hover {
                    opacity: 1;
                    background-color: rgba(247, 247, 248, 0.98);
                    box-shadow: 0 6px 24px rgba(0, 0, 0, 0.12);
                    transform: translateY(-2px);
                }

                .dark .outline-container {
                    background-color: rgba(52, 53, 65, 0.85);
                    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
                }

                .dark .outline-container:hover {
                    background-color: rgba(52, 53, 65, 0.98);
                    box-shadow: 0 6px 24px rgba(0, 0, 0, 0.35);
                }

                .outline-header {
                    padding: 16px;
                    font-weight: 600;
                    font-size: 16px;
                    border-bottom: 1px solid rgba(0, 0, 0, 0.05);
                    display: flex;
                    justify-content: space-between;
                    align-items: center;
                    position: sticky;
                    top: 0;
                    background: inherit;
                    border-radius: 12px 12px 0 0;
                    z-index: 2;
                }

                .dark .outline-header {
                    border-bottom: 1px solid rgba(255, 255, 255, 0.05);
                    color: #ececf1;
                }

                .outline-title {
                    display: flex;
                    align-items: center;
                    gap: 8px;
                }

                .outline-title-icon {
                    color: #10a37f;
                }

                .outline-items {
                    padding: 8px 0;
                }

                .outline-item {
                    padding: 10px 16px;
                    cursor: pointer;
                    transition: all 0.2s ease;
                    font-size: 14px;
                    border-left: 3px solid transparent;
                    display: flex;
                    align-items: center;
                    margin: 2px 0;
                    border-radius: 0 4px 4px 0;
                    box-sizing: border-box; /* 确保padding不会增加元素宽度 */
                    width: 100%; /* 确保宽度不超过父容器 */
                }

                .outline-item:hover {
                    background-color: rgba(0, 0, 0, 0.05);
                    transform: translateX(2px);
                }

                .dark .outline-item:hover {
                    background-color: rgba(255, 255, 255, 0.05);
                }

                .outline-item.active {
                    border-left-color: #10a37f;
                    background-color: rgba(16, 163, 127, 0.1);
                    font-weight: 500;
                }

                .outline-item-icon {
                    margin-right: 10px;
                    color: #10a37f;
                    transition: transform 0.2s ease;
                }

                .outline-item:hover .outline-item-icon {
                    transform: scale(1.1);
                }

                .outline-item-text {
                    white-space: nowrap;
                    overflow: hidden;
                    text-overflow: ellipsis;
                    flex: 1;
                    line-height: 1.4;
                    max-width: calc(100% - 30px); /* 减去图标和边距的宽度 */
                }

                .dark .outline-item-text {
                    color: #ececf1;
                }

                .outline-toggle {
                    position: fixed;
                    top: 70px;
                    right: 20px;
                    width: 42px;
                    height: 42px;
                    border-radius: 50%;
                    background-color: rgba(16, 163, 127, 0.9);
                    color: white;
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    cursor: pointer;
                    z-index: 1001;
                    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);
                    transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
                }

                .outline-toggle:hover {
                    transform: scale(1.08);
                    background-color: #10a37f;
                    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
                }

                .outline-toggle svg {
                    width: 20px;
                    height: 20px;
                    transition: transform 0.3s ease;
                }

                .outline-toggle:hover svg {
                    transform: rotate(90deg);
                }

                .outline-close {
                    cursor: pointer;
                    opacity: 0.7;
                    transition: all 0.2s ease;
                    padding: 4px;
                    border-radius: 4px;
                }

                .outline-close:hover {
                    opacity: 1;
                    background-color: rgba(0, 0, 0, 0.05);
                }

                .dark .outline-close:hover {
                    background-color: rgba(255, 255, 255, 0.1);
                }

                .outline-empty {
                    padding: 20px 16px;
                    text-align: center;
                    color: #888;
                    font-style: italic;
                    font-size: 14px;
                }

                .dark .outline-empty {
                    color: #aaa;
                }

                @media (max-width: 1400px) {
                    .outline-container {
                        width: 250px;
                    }
                }

                @media (max-width: 1200px) {
                    .outline-container {
                        width: 220px;
                    }
                }

                @media (max-width: 768px) {
                    .outline-container {
                        display: none;
                    }
                }`;
        }

        /**
          * 初始化大纲生成器
          */
        init() {
            // 等待页面加载完成
            if (!document.querySelector('main')) {
                setTimeout(() => this.init(), 50);
                return;
            }

            this.addStyles();
            this.outlineContainer = this.createOutlineContainer();
            this.toggleButton = this.createToggleButton();

            // 初始化大纲
            setTimeout(() => this.generateOutlineItems(), 50);

            // 设置初始暗黑模式状态
            this.outlineContainer.classList.toggle('dark', this.detectDarkMode());

            // 监听暗黑模式变化
            this.observeDarkModeChanges();

            // 监听新消息
            this.observeNewMessages();

            // 监听滚动以高亮当前可见的消息
            this.observeScroll();
        }


        /**
         * 添加样式到页面
         */
        addStyles() {
            this.styleElement = document.createElement('style');
            this.styleElement.textContent = this.cssStyles;
            document.head.appendChild(this.styleElement);
        }

        /**
         * 创建大纲容器 - 使用DOM API
         * @returns {HTMLElement} 大纲容器元素
         */
        createOutlineContainer() {
            const container = document.createElement('div');
            container.className = 'outline-container';

            // 创建头部
            const header = document.createElement('div');
            header.className = 'outline-header';

            // 创建标题
            const title = document.createElement('div');
            title.className = 'outline-title';

            // 创建标题图标
            const titleIcon = document.createElement('span');
            titleIcon.className = 'outline-title-icon';
            const titleSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
            titleSvg.setAttribute('width', '16');
            titleSvg.setAttribute('height', '16');
            titleSvg.setAttribute('viewBox', '0 0 24 24');
            titleSvg.setAttribute('fill', 'none');
            const titlePath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
            titlePath.setAttribute('d', 'M4 6H20M4 12H20M4 18H14');
            titlePath.setAttribute('stroke', 'currentColor');
            titlePath.setAttribute('stroke-width', '2');
            titlePath.setAttribute('stroke-linecap', 'round');
            titlePath.setAttribute('stroke-linejoin', 'round');
            titleSvg.appendChild(titlePath);
            titleIcon.appendChild(titleSvg);

            // 创建标题文本
            const titleText = document.createElement('span');
            titleText.textContent = '对话大纲';

            // 组装标题
            title.appendChild(titleIcon);
            title.appendChild(titleText);

            // 创建关闭按钮
            const closeBtn = document.createElement('span');
            closeBtn.className = 'outline-close';
            const closeSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
            closeSvg.setAttribute('width', '16');
            closeSvg.setAttribute('height', '16');
            closeSvg.setAttribute('viewBox', '0 0 24 24');
            closeSvg.setAttribute('fill', 'none');
            const closePath1 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
            closePath1.setAttribute('d', 'M18 6L6 18');
            closePath1.setAttribute('stroke', 'currentColor');
            closePath1.setAttribute('stroke-width', '2');
            closePath1.setAttribute('stroke-linecap', 'round');
            closePath1.setAttribute('stroke-linejoin', 'round');
            const closePath2 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
            closePath2.setAttribute('d', 'M6 6L18 18');
            closePath2.setAttribute('stroke', 'currentColor');
            closePath2.setAttribute('stroke-width', '2');
            closePath2.setAttribute('stroke-linecap', 'round');
            closePath2.setAttribute('stroke-linejoin', 'round');
            closeSvg.appendChild(closePath1);
            closeSvg.appendChild(closePath2);
            closeBtn.appendChild(closeSvg);

            // 组装头部
            header.appendChild(title);
            header.appendChild(closeBtn);

            // 创建大纲项容器
            const outlineItems = document.createElement('div');
            outlineItems.className = 'outline-items';

            // 组装容器
            container.appendChild(header);
            container.appendChild(outlineItems);

            document.body.appendChild(container);

            // 添加关闭事件
            closeBtn.addEventListener('click', () => {
                container.style.display = 'none';
                this.toggleButton.style.display = 'flex';
            });

            return container;
        }

        /**
         * 创建切换按钮 - 使用DOM API
         * @returns {HTMLElement} 切换按钮元素
         */
        createToggleButton() {
            const button = document.createElement('div');
            button.className = 'outline-toggle';

            const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
            svg.setAttribute('width', '24');
            svg.setAttribute('height', '24');
            svg.setAttribute('viewBox', '0 0 24 24');
            svg.setAttribute('fill', 'none');

            const path1 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
            path1.setAttribute('d', 'M4 6H20');
            path1.setAttribute('stroke', 'currentColor');
            path1.setAttribute('stroke-width', '2');
            path1.setAttribute('stroke-linecap', 'round');

            const path2 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
            path2.setAttribute('d', 'M4 12H20');
            path2.setAttribute('stroke', 'currentColor');
            path2.setAttribute('stroke-width', '2');
            path2.setAttribute('stroke-linecap', 'round');

            const path3 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
            path3.setAttribute('d', 'M4 18H20');
            path3.setAttribute('stroke', 'currentColor');
            path3.setAttribute('stroke-width', '2');
            path3.setAttribute('stroke-linecap', 'round');

            svg.appendChild(path1);
            svg.appendChild(path2);
            svg.appendChild(path3);
            button.appendChild(svg);

            document.body.appendChild(button);

            // 添加点击事件
            button.addEventListener('click', () => {
                this.outlineContainer.style.display = 'block';
                button.style.display = 'none';

                // 重新生成大纲,确保最新状态
                this.generateOutlineItems();
            });

            return button;
        }

        /**
         * 提取问题文本的前20个字符
         * @param {string} text 问题文本
         * @returns {string} 提取后的标题
         */
        extractQuestionTitle(text) {
            // 去除空白字符
            const trimmed = text.trim();

            // 如果文本为空,返回默认文本
            if (!trimmed) return "空白问题";

            // 提取前20个字符,如果不足20个则全部返回
            return trimmed.length > 20 ? trimmed.substring(0, 20) + '...' : trimmed;
        }


        /**
          * 生成大纲项 - 使用DOM API
          */
        generateOutlineItems() {
            const outlineItems = this.outlineContainer.querySelector('.outline-items');
            // 清空现有内容
            while (outlineItems.firstChild) {
                outlineItems.removeChild(outlineItems.firstChild);
            }

            // 获取所有用户消息
            const userMessages = document.querySelectorAll('[data-message-author-role="user"]');

            if (userMessages.length === 0) {
                const emptyDiv = document.createElement('div');
                emptyDiv.className = 'outline-empty';
                emptyDiv.textContent = '暂无对话内容';
                outlineItems.appendChild(emptyDiv);
                return;
            }

            userMessages.forEach((message, index) => {
                const messageText = message.querySelector('.whitespace-pre-wrap')?.textContent || '';
                const title = this.extractQuestionTitle(messageText);

                const item = document.createElement('div');
                item.className = 'outline-item';
                item.dataset.index = index;
                item.dataset.messageId = message.id || `message-${index}`;

                // 创建图标
                const iconSpan = document.createElement('span');
                iconSpan.className = 'outline-item-icon';

                const iconSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
                iconSvg.setAttribute('width', '16');
                iconSvg.setAttribute('height', '16');
                iconSvg.setAttribute('viewBox', '0 0 24 24');
                iconSvg.setAttribute('fill', 'none');

                const iconPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
                iconPath.setAttribute('d', 'M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z');
                iconPath.setAttribute('stroke', 'currentColor');
                iconPath.setAttribute('fill', 'currentColor');

                iconSvg.appendChild(iconPath);
                iconSpan.appendChild(iconSvg);

                // 创建文本
                const textSpan = document.createElement('span');
                textSpan.className = 'outline-item-text';
                textSpan.textContent = `${index + 1}. ${title}`;

                // 组装项目
                item.appendChild(iconSpan);
                item.appendChild(textSpan);

                // 添加点击事件
                item.addEventListener('click', () => this.handleItemClick(item, message));

                outlineItems.appendChild(item);
            });

            // 检查是否有可见的消息,并高亮对应的大纲项
            this.highlightVisibleItem();
        }

        /**
          * 处理大纲项点击事件
          * @param {HTMLElement} item 点击的大纲项
          * @param {HTMLElement} message 对应的消息元素
          */
        handleItemClick(item, message) {
            // 滚动到对应的消息
            message.scrollIntoView({ behavior: 'smooth', block: 'center' });

            // 高亮当前项
            this.highlightItem(item);

            // 添加临时高亮效果
            message.style.transition = 'background-color 0.5s';
            message.style.backgroundColor = 'rgba(16, 163, 127, 0.1)';
            setTimeout(() => {
                message.style.backgroundColor = '';
            }, 1500);
        }

        /**
         * 高亮指定的大纲项
         * @param {HTMLElement} item 要高亮的大纲项
         */
        highlightItem(item) {
            document.querySelectorAll('.outline-item').forEach(el => {
                el.classList.remove('active');
            });
            item.classList.add('active');
        }

        /**
         * 监听页面滚动,高亮当前可见的消息对应的大纲项
         */
        observeScroll() {
            let scrollTimer = null;
            window.addEventListener('scroll', () => {
                // 使用防抖技术减少滚动事件处理频率
                if (scrollTimer) clearTimeout(scrollTimer);
                scrollTimer = setTimeout(() => {
                    this.highlightVisibleItem();
                }, 100);
            });
        }

        /**
         * 高亮当前可见的消息对应的大纲项
         */
        highlightVisibleItem() {
            const userMessages = document.querySelectorAll('[data-message-author-role="user"]');
            if (!userMessages.length) return;

            // 找到当前视口中最靠近顶部的消息
            let closestMessage = null;
            let closestDistance = Infinity;
            const viewportHeight = window.innerHeight;
            const viewportMiddle = viewportHeight / 2;

            userMessages.forEach(message => {
                const rect = message.getBoundingClientRect();
                // 计算消息中心点到视口中心的距离
                const distance = Math.abs((rect.top + rect.bottom) / 2 - viewportMiddle);

                // 如果消息在视口内且距离更近
                if (rect.top < viewportHeight && rect.bottom > 0 && distance < closestDistance) {
                    closestDistance = distance;
                    closestMessage = message;
                }
            });

            if (closestMessage) {
                // 找到对应的大纲项并高亮
                const index = Array.from(userMessages).indexOf(closestMessage);
                const outlineItem = this.outlineContainer.querySelector(`.outline-item[data-index="${index}"]`);
                if (outlineItem) {
                    this.highlightItem(outlineItem);
                }
            }
        }

        /**
         * 检测暗黑模式
         * @returns {boolean} 是否为暗黑模式
         */
        detectDarkMode() {
            return document.documentElement.classList.contains('dark');
        }

        /**
         * 监听暗黑模式变化
         */
        observeDarkModeChanges() {
            const observer = new MutationObserver((mutations) => {
                mutations.forEach((mutation) => {
                    if (mutation.attributeName === 'class') {
                        const isDarkMode = this.detectDarkMode();
                        this.outlineContainer.classList.toggle('dark', isDarkMode);
                    }
                });
            });

            observer.observe(document.documentElement, { attributes: true });
        }

        /**
         * 监听新消息
         */
        observeNewMessages() {
            const observer = new MutationObserver((mutations) => {
                let shouldUpdate = false;

                mutations.forEach(mutation => {
                    if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                        for (const node of mutation.addedNodes) {
                            if (node.nodeType === Node.ELEMENT_NODE &&
                                (node.querySelector('[data-message-author-role="user"]') ||
                                 node.hasAttribute && node.hasAttribute('data-message-author-role'))) {
                                shouldUpdate = true;
                                break;
                            }
                        }
                    }
                });

                if (shouldUpdate) {
                    setTimeout(() => this.generateOutlineItems(), 50); // 延迟执行,确保DOM已更新
                }
            });

            // 监听整个聊天容器
            const chatContainer = document.querySelector('main');
            if (chatContainer) {
                observer.observe(chatContainer, { childList: true, subtree: true });
            }
        }
    }

    /**
     * DeepSeek大纲生成器类
     */
    class DeepSeekOutlineGenerator extends ChatGPTOutlineGenerator {
        constructor() {
            super();
            // 继承ChatGPT大纲生成器的所有属性和方法
            this.currentUrl = window.location.href;
        }

        /**
     * 初始化大纲生成器
     */
        init() {
            // 等待页面加载完成
            if (!document.querySelector('.dad65929')) {
                setTimeout(() => this.init(), 50);
                return;
            }

            this.addStyles();
            this.outlineContainer = this.createOutlineContainer();
            this.toggleButton = this.createToggleButton();

            // 初始化大纲
            setTimeout(() => this.generateOutlineItems(), 50);

            // 监听新消息
            this.observeNewMessages();

            // 监听滚动以高亮当前可见的消息
            this.observeScroll();

            // 监听URL变化
            this.observeUrlChanges();
        }

        /**
     * 生成大纲项
     */
        generateOutlineItems() {
            const outlineItems = this.outlineContainer.querySelector('.outline-items');
            // 清空现有内容
            while (outlineItems.firstChild) {
                outlineItems.removeChild(outlineItems.firstChild);
            }

            // 获取所有用户消息 - 使用更新后的选择器
            const chatContainer = document.querySelector('.dad65929');
            if (!chatContainer) return;

            // 获取所有直接子div
            const allDivs = Array.from(chatContainer.children).filter(el => el.tagName === 'DIV');
            // 筛选出奇数位置的div(索引从0开始,所以是偶数索引)
            const userMessageContainers = allDivs.filter((_, index) => index % 2 === 0);

            // 从每个容器中提取第一个div作为用户消息
            const userMessages = userMessageContainers.map(container => {
                const firstDiv = container.querySelector('div');
                return firstDiv;
            }).filter(Boolean); // 过滤掉可能的null值

            if (userMessages.length === 0) {
                const emptyDiv = document.createElement('div');
                emptyDiv.className = 'outline-empty';
                emptyDiv.textContent = '暂无对话内容';
                outlineItems.appendChild(emptyDiv);
                return;
            }

            userMessages.forEach((message, index) => {
                // 提取消息文本
                const messageText = message.textContent || '';
                const title = this.extractQuestionTitle(messageText);

                const item = document.createElement('div');
                item.className = 'outline-item';
                item.dataset.index = index;
                item.dataset.messageId = `deepseek-message-${index}`;

                // 创建图标
                const iconSpan = document.createElement('span');
                iconSpan.className = 'outline-item-icon';

                const iconSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
                iconSvg.setAttribute('width', '16');
                iconSvg.setAttribute('height', '16');
                iconSvg.setAttribute('viewBox', '0 0 24 24');
                iconSvg.setAttribute('fill', 'none');

                const iconPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
                iconPath.setAttribute('d', 'M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z');
                iconPath.setAttribute('stroke', 'currentColor');
                iconPath.setAttribute('fill', 'currentColor');

                iconSvg.appendChild(iconPath);
                iconSpan.appendChild(iconSvg);

                // 创建文本
                const textSpan = document.createElement('span');
                textSpan.className = 'outline-item-text';
                textSpan.textContent = `${index + 1}. ${title}`;

                // 组装项目
                item.appendChild(iconSpan);
                item.appendChild(textSpan);

                // 添加点击事件
                item.addEventListener('click', () => this.handleItemClick(item, message));

                outlineItems.appendChild(item);
            });

            // 检查是否有可见的消息,并高亮对应的大纲项
            this.highlightVisibleItem();
        }

        /**
     * 监听新消息 - 更新后的DeepSeek版本
     */
        observeNewMessages() {
            const observer = new MutationObserver((mutations) => {
                let shouldUpdate = false;

                mutations.forEach(mutation => {
                    if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                        shouldUpdate = true;
                    }
                });

                if (shouldUpdate) {
                    setTimeout(() => this.generateOutlineItems(), 50);
                }
            });

            // 监听整个聊天容器
            const chatContainer = document.querySelector('.dad65929');
            if (chatContainer) {
                observer.observe(chatContainer, { childList: true, subtree: true });
            }
        }

        /**
     * 监听URL变化
     */
        observeUrlChanges() {
            // 使用setInterval定期检查URL是否变化
            setInterval(() => {
                const currentUrl = window.location.href;
                if (this.currentUrl !== currentUrl) {
                    console.log('URL changed, reinitializing outline generator');
                    this.currentUrl = currentUrl;

                    // 清空现有大纲
                    const outlineItems = this.outlineContainer.querySelector('.outline-items');
                    while (outlineItems && outlineItems.firstChild) {
                        outlineItems.removeChild(outlineItems.firstChild);
                    }

                    // 等待新页面加载完成后重新生成大纲
                    setTimeout(() => this.generateOutlineItems(), 500);
                }
            }, 500); // 每秒检查一次
        }

        /**
     * 高亮当前可见的消息对应的大纲项 - 更新后的DeepSeek版本
     */
        highlightVisibleItem() {
            // 获取所有用户消息
            const chatContainer = document.querySelector('.dad65929');
            if (!chatContainer) return;

            const allDivs = Array.from(chatContainer.children).filter(el => el.tagName === 'DIV');
            const userMessageContainers = allDivs.filter((_, index) => index % 2 === 0);
            const userMessages = userMessageContainers.map(container => {
                return container.querySelector('div');
            }).filter(Boolean);

            if (!userMessages.length) return;

            // 找到当前视口中最靠近中心的消息
            let closestMessage = null;
            let closestDistance = Infinity;
            const viewportHeight = window.innerHeight;
            const viewportMiddle = viewportHeight / 2;

            userMessages.forEach(message => {
                const rect = message.getBoundingClientRect();
                // 计算消息中心点到视口中心的距离
                const distance = Math.abs((rect.top + rect.bottom) / 2 - viewportMiddle);

                // 如果消息在视口内且距离更近
                if (rect.top < viewportHeight && rect.bottom > 0 && distance < closestDistance) {
                    closestDistance = distance;
                    closestMessage = message;
                }
            });

            if (closestMessage) {
                // 找到对应的大纲项并高亮
                const index = userMessages.indexOf(closestMessage);
                const outlineItem = this.outlineContainer.querySelector(`.outline-item[data-index="${index}"]`);
                if (outlineItem) {
                    this.highlightItem(outlineItem);
                }
            }
        }

        /**
     * 处理大纲项点击事件
     */
        handleItemClick(item, message) {
            // 高亮被点击的项
            this.highlightItem(item);

            // 滚动到对应的消息
            if (message) {
                message.scrollIntoView({ behavior: 'smooth', block: 'center' });
            }
        }
    }

    /**
     * Gemini大纲生成器类
     */
    class GeminiOutlineGenerator extends ChatGPTOutlineGenerator {
        constructor() {
            super();
            // 继承ChatGPT大纲生成器的所有属性和方法
        }
        /**
         * 初始化大纲生成器
         */
        init() {
            // 等待页面加载完成
            if (!document.querySelector('chat-window')) {
                setTimeout(() => this.init(), 50);
                return;
            }

            this.addStyles();
            this.outlineContainer = this.createOutlineContainer();
            this.toggleButton = this.createToggleButton();

            // 初始化大纲
            setTimeout(() => this.generateOutlineItems(), 50);

            // 监听新消息
            this.observeNewMessages();

            // 监听滚动以高亮当前可见的消息
            this.observeScroll();
        }


        /**
         * 生成大纲项 - 使用DOM API
         */
        generateOutlineItems() {
            const outlineItems = this.outlineContainer.querySelector('.outline-items');
            // 清空现有内容
            while (outlineItems.firstChild) {
                outlineItems.removeChild(outlineItems.firstChild);
            }

            // 获取所有用户消息 - 使用更新后的Gemini特定的选择器
            const userMessages = document.querySelectorAll('user-query .user-query-bubble-container');

            if (userMessages.length === 0) {
                const emptyDiv = document.createElement('div');
                emptyDiv.className = 'outline-empty';
                emptyDiv.textContent = '暂无对话内容';
                outlineItems.appendChild(emptyDiv);
                return;
            }

            userMessages.forEach((message, index) => {
                // 提取消息文本 - 适应Gemini的新DOM结构
                const messageText = message.querySelector('.query-text')?.textContent || '';
                const title = this.extractQuestionTitle(messageText);

                const item = document.createElement('div');
                item.className = 'outline-item';
                item.dataset.index = index;
                item.dataset.messageId = `gemini-message-${index}`;

                // 创建图标
                const iconSpan = document.createElement('span');
                iconSpan.className = 'outline-item-icon';

                const iconSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
                iconSvg.setAttribute('width', '16');
                iconSvg.setAttribute('height', '16');
                iconSvg.setAttribute('viewBox', '0 0 24 24');
                iconSvg.setAttribute('fill', 'none');

                const iconPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
                iconPath.setAttribute('d', 'M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z');
                iconPath.setAttribute('stroke', 'currentColor');
                iconPath.setAttribute('fill', 'currentColor');

                iconSvg.appendChild(iconPath);
                iconSpan.appendChild(iconSvg);

                // 创建文本
                const textSpan = document.createElement('span');
                textSpan.className = 'outline-item-text';
                textSpan.textContent = `${index + 1}. ${title}`;

                // 组装项目
                item.appendChild(iconSpan);
                item.appendChild(textSpan);

                // 添加点击事件
                item.addEventListener('click', () => this.handleItemClick(item, message));

                outlineItems.appendChild(item);
            });

            // 检查是否有可见的消息,并高亮对应的大纲项
            this.highlightVisibleItem();
        }

        /**
         * 监听新消息 - Gemini版本
         */
        observeNewMessages() {
            const observer = new MutationObserver((mutations) => {
                let shouldUpdate = false;

                mutations.forEach(mutation => {
                    if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                        for (const node of mutation.addedNodes) {
                            if (node.nodeType === Node.ELEMENT_NODE &&
                                (node.querySelector('user-query') ||
                                 node.tagName && node.tagName.toLowerCase() === 'user-query')) {
                                shouldUpdate = true;
                                break;
                            }
                        }
                    }
                });

                if (shouldUpdate) {
                    setTimeout(() => this.generateOutlineItems(), 50);
                }
            });

            // 监听整个聊天容器
            const chatContainer = document.querySelector('chat-window-content');
            if (chatContainer) {
                observer.observe(chatContainer, { childList: true, subtree: true });
            }

            // 监听URL变化,因为切换对话可能会改变URL
            this.observeUrlChanges();
        }

        /**
         * 监听URL变化 - 用于检测对话切换
         */
        observeUrlChanges() {
            let lastUrl = location.href;

            // 创建一个新的MutationObserver来监视URL变化
            const urlObserver = new MutationObserver(() => {
                if (location.href !== lastUrl) {
                    lastUrl = location.href;
                    // URL变化后,等待一段时间再更新大纲,确保新对话已加载
                    setTimeout(() => this.generateOutlineItems(), 50);
                }
            });

            // 监听整个文档的变化
            urlObserver.observe(document, { subtree: true, childList: true });

            // 使用history API监听导航事件
            const originalPushState = history.pushState;
            const originalReplaceState = history.replaceState;
            const self = this;

            history.pushState = function () {
                originalPushState.apply(this, arguments);
                setTimeout(() => self.generateOutlineItems(), 50);
            };

            history.replaceState = function () {
                originalReplaceState.apply(this, arguments);
                setTimeout(() => self.generateOutlineItems(), 50);
            };

            // 监听popstate事件(浏览器的前进/后退按钮)
            window.addEventListener('popstate', () => {
                setTimeout(() => this.generateOutlineItems(), 50);
            });
        }

    }

    /**
     * 百川AI大纲生成器类
     */
    class BaichuanOutlineGenerator extends ChatGPTOutlineGenerator {
        constructor() {
            super();
            // 继承ChatGPT大纲生成器的所有属性和方法
        }

        /**
         * 初始化大纲生成器
         */
        init() {
            // 等待页面加载完成
            if (!document.querySelector('#chat-list')) {
                setTimeout(() => this.init(), 50);
                return;
            }

            this.addStyles();
            this.outlineContainer = this.createOutlineContainer();
            this.toggleButton = this.createToggleButton();

            // 初始化大纲
            setTimeout(() => this.generateOutlineItems(), 50);

            // 监听新消息和对话切换
            this.observeNewMessages();

            // 监听滚动以高亮当前可见的消息
            this.observeScroll();

        }

        /**
         * 生成大纲项 - 使用DOM API
         */
        generateOutlineItems() {
            const outlineItems = this.outlineContainer.querySelector('.outline-items');
            // 清空现有内容
            while (outlineItems.firstChild) {
                outlineItems.removeChild(outlineItems.firstChild);
            }

            // 获取所有用户消息 - 使用百川AI特定的选择器
            const userMessages = document.querySelectorAll('#chat-list [data-type="prompt-item"]');

            if (userMessages.length === 0) {
                const emptyDiv = document.createElement('div');
                emptyDiv.className = 'outline-empty';
                emptyDiv.textContent = '暂无对话内容';
                outlineItems.appendChild(emptyDiv);
                return;
            }

            userMessages.forEach((message, index) => {
                // 提取消息文本 - 适应百川AI的DOM结构
                const messageText = message.querySelector('.prompt-text-item')?.textContent || '';
                const title = this.extractQuestionTitle(messageText);

                const item = document.createElement('div');
                item.className = 'outline-item';
                item.dataset.index = index;
                item.dataset.messageId = `baichuan-message-${index}`;

                // 创建图标
                const iconSpan = document.createElement('span');
                iconSpan.className = 'outline-item-icon';

                const iconSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
                iconSvg.setAttribute('width', '16');
                iconSvg.setAttribute('height', '16');
                iconSvg.setAttribute('viewBox', '0 0 24 24');
                iconSvg.setAttribute('fill', 'none');

                const iconPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
                iconPath.setAttribute('d', 'M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z');
                iconPath.setAttribute('stroke', 'currentColor');
                iconPath.setAttribute('fill', 'currentColor');

                iconSvg.appendChild(iconPath);
                iconSpan.appendChild(iconSvg);

                // 创建文本
                const textSpan = document.createElement('span');
                textSpan.className = 'outline-item-text';
                textSpan.textContent = `${index + 1}. ${title}`;

                // 组装项目
                item.appendChild(iconSpan);
                item.appendChild(textSpan);

                // 添加点击事件
                item.addEventListener('click', () => this.handleItemClick(item, message));

                outlineItems.appendChild(item);
            });

            // 检查是否有可见的消息,并高亮对应的大纲项
            this.highlightVisibleItem();
        }

        /**
         * 监听新消息 - 百川AI版本
         */
        observeNewMessages() {
            const observer = new MutationObserver((mutations) => {
                let shouldUpdate = false;

                mutations.forEach(mutation => {
                    if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                        for (const node of mutation.addedNodes) {
                            if (node.nodeType === Node.ELEMENT_NODE &&
                                (node.hasAttribute && node.hasAttribute('data-type') &&
                                 node.getAttribute('data-type') === 'prompt-item')) {
                                shouldUpdate = true;
                                break;
                            }
                        }
                    }

                    // 检查属性变化,可能是对话切换导致的变化
                    if (mutation.type === 'attributes' &&
                        (mutation.attributeName === 'data-type' ||
                         mutation.attributeName === 'class')) {
                        shouldUpdate = true;
                    }
                });

                if (shouldUpdate) {
                    setTimeout(() => this.generateOutlineItems(), 50);
                }
            });

            // 监听整个聊天容器
            const chatContainer = document.querySelector('#chat-list');
            if (chatContainer) {
                observer.observe(chatContainer, {
                    childList: true,
                    subtree: true,
                    attributes: true,  // 添加属性监听
                    attributeFilter: ['data-type', 'class'] // 监听特定属性变化
                });
            }

            // 监听URL变化,因为切换对话可能会改变URL
            this.observeUrlChanges();

            // 监听点击事件,可能是通过点击切换对话
            this.observeClickEvents();
        }

        /**
         * 监听URL变化 - 用于检测对话切换
         */
        observeUrlChanges() {
            let lastUrl = location.href;

            // 创建一个新的MutationObserver来监视URL变化
            const urlObserver = new MutationObserver(() => {
                if (location.href !== lastUrl) {
                    lastUrl = location.href;
                    // URL变化后,等待一段时间再更新大纲,确保新对话已加载
                    setTimeout(() => this.generateOutlineItems(), 50);
                }
            });

            // 监听整个文档的变化
            urlObserver.observe(document, { subtree: true, childList: true });

            // 使用history API监听导航事件
            const originalPushState = history.pushState;
            const originalReplaceState = history.replaceState;
            const self = this;

            history.pushState = function () {
                originalPushState.apply(this, arguments);
                setTimeout(() => self.generateOutlineItems(), 50);
            };

            history.replaceState = function () {
                originalReplaceState.apply(this, arguments);
                setTimeout(() => self.generateOutlineItems(), 50);
            };

            // 监听popstate事件(浏览器的前进/后退按钮)
            window.addEventListener('popstate', () => {
                setTimeout(() => this.generateOutlineItems(), 50);
            });
        }

        /**
         * 监听点击事件 - 用于检测对话切换
         */
        observeClickEvents() {
            // 监听可能导致对话切换的点击事件
            document.addEventListener('click', (event) => {
                // 检查是否点击了对话列表项
                const chatItem = event.target.closest('[role="button"]');
                if (chatItem) {
                    // 延迟更新大纲,确保对话已切换
                    setTimeout(() => this.generateOutlineItems(), 50);
                }
            });
        }

        /**
         * 高亮当前可见的消息对应的大纲项 - 百川AI版本
         */
        highlightVisibleItem() {
            const userMessages = document.querySelectorAll('#chat-list [data-type="prompt-item"]');
            if (!userMessages.length) return;

            // 找到当前视口中最靠近顶部的消息
            let closestMessage = null;
            let closestDistance = Infinity;
            const viewportHeight = window.innerHeight;
            const viewportMiddle = viewportHeight / 2;

            userMessages.forEach(message => {
                const rect = message.getBoundingClientRect();
                // 计算消息中心点到视口中心的距离
                const distance = Math.abs((rect.top + rect.bottom) / 2 - viewportMiddle);

                // 如果消息在视口内且距离更近
                if (rect.top < viewportHeight && rect.bottom > 0 && distance < closestDistance) {
                    closestDistance = distance;
                    closestMessage = message;
                }
            });

            if (closestMessage) {
                // 找到对应的大纲项并高亮
                const index = Array.from(userMessages).indexOf(closestMessage);
                const outlineItem = this.outlineContainer.querySelector(`.outline-item[data-index="${index}"]`);
                if (outlineItem) {
                    this.highlightItem(outlineItem);
                }
            }
        }
    }

    /**
     * Kimi大纲生成器类
     */
    class KimiOutlineGenerator extends ChatGPTOutlineGenerator {
        constructor() {
            super();
            // 继承ChatGPT大纲生成器的所有属性和方法
            this.currentUrl = window.location.href;
        }

        /**
         * 初始化大纲生成器
         */
        init() {
            // 等待页面加载完成
            if (!document.querySelector('.chat-content-list')) {
                setTimeout(() => this.init(), 50);
                return;
            }

            this.addStyles();
            this.outlineContainer = this.createOutlineContainer();
            this.toggleButton = this.createToggleButton();

            // 初始化大纲
            setTimeout(() => this.generateOutlineItems(), 50);

            // 监听新消息
            this.observeNewMessages();

            // 监听滚动以高亮当前可见的消息
            this.observeScroll();

            // 监听URL变化
            this.observeUrlChanges();
        }

        /**
         * 生成大纲项
         */
        generateOutlineItems() {
            const outlineItems = this.outlineContainer.querySelector('.outline-items');
            // 清空现有内容
            while (outlineItems.firstChild) {
                outlineItems.removeChild(outlineItems.firstChild);
            }

            // 获取所有用户消息 - 使用Kimi特定的选择器
            const userMessages = document.querySelectorAll('.segment-user');

            if (userMessages.length === 0) {
                const emptyDiv = document.createElement('div');
                emptyDiv.className = 'outline-empty';
                emptyDiv.textContent = '暂无对话内容';
                outlineItems.appendChild(emptyDiv);
                return;
            }

            userMessages.forEach((message, index) => {
                // 提取消息文本 - 适应Kimi的DOM结构
                const messageText = message.querySelector('.user-content')?.textContent || '';
                const title = this.extractQuestionTitle(messageText);

                const item = document.createElement('div');
                item.className = 'outline-item';
                item.dataset.index = index;
                item.dataset.messageId = `kimi-message-${index}`;

                // 创建图标
                const iconSpan = document.createElement('span');
                iconSpan.className = 'outline-item-icon';

                const iconSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
                iconSvg.setAttribute('width', '16');
                iconSvg.setAttribute('height', '16');
                iconSvg.setAttribute('viewBox', '0 0 24 24');
                iconSvg.setAttribute('fill', 'none');

                const iconPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
                iconPath.setAttribute('d', 'M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z');
                iconPath.setAttribute('stroke', 'currentColor');
                iconPath.setAttribute('fill', 'currentColor');

                iconSvg.appendChild(iconPath);
                iconSpan.appendChild(iconSvg);

                // 创建文本
                const textSpan = document.createElement('span');
                textSpan.className = 'outline-item-text';
                textSpan.textContent = `${index + 1}. ${title}`;

                // 组装项目
                item.appendChild(iconSpan);
                item.appendChild(textSpan);

                // 添加点击事件
                item.addEventListener('click', () => this.handleItemClick(item, message));

                outlineItems.appendChild(item);
            });

            // 检查是否有可见的消息,并高亮对应的大纲项
            this.highlightVisibleItem();
        }


        /**
         * 监听新消息 - Kimi版本
         */
        observeNewMessages() {
            const observer = new MutationObserver((mutations) => {
                let shouldUpdate = false;

                mutations.forEach(mutation => {
                    if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                        for (const node of mutation.addedNodes) {
                            if (node.nodeType === Node.ELEMENT_NODE &&
                                (node.classList.contains('chat-content-item') ||
                                 node.querySelector('.segment-user'))) {
                                shouldUpdate = true;
                                break;
                            }
                        }
                    }
                });

                if (shouldUpdate) {
                    setTimeout(() => this.generateOutlineItems(), 50);
                }
            });

            // 监听整个聊天容器
            const chatContainer = document.querySelector('.chat-content-list');
            if (chatContainer) {
                observer.observe(chatContainer, { childList: true, subtree: true });
            }

            // 监听URL变化,因为切换对话可能会改变URL
            this.observeUrlChanges();
        }

       /**
     * 监听URL变化
     */
        observeUrlChanges() {
            // 使用setInterval定期检查URL是否变化
            setInterval(() => {
                const currentUrl = window.location.href;
                if (this.currentUrl !== currentUrl) {
                    console.log('URL changed, reinitializing outline generator');
                    this.currentUrl = currentUrl;

                    // 清空现有大纲
                    const outlineItems = this.outlineContainer.querySelector('.outline-items');
                    while (outlineItems && outlineItems.firstChild) {
                        outlineItems.removeChild(outlineItems.firstChild);
                    }

                    // 等待新页面加载完成后重新生成大纲
                    setTimeout(() => this.generateOutlineItems(), 500);
                }
            }, 500); // 每秒检查一次
        }


        /**
         * 高亮当前可见的消息对应的大纲项 - Kimi版本
         */
        highlightVisibleItem() {
            const userMessages = document.querySelectorAll('.segment-user');
            if (!userMessages.length) return;

            // 找到当前视口中最靠近顶部的消息
            let closestMessage = null;
            let closestDistance = Infinity;
            const viewportHeight = window.innerHeight;
            const viewportMiddle = viewportHeight / 2;

            userMessages.forEach(message => {
                const rect = message.getBoundingClientRect();
                // 计算消息中心点到视口中心的距离
                const distance = Math.abs((rect.top + rect.bottom) / 2 - viewportMiddle);

                // 如果消息在视口内且距离更近
                if (rect.top < viewportHeight && rect.bottom > 0 && distance < closestDistance) {
                    closestDistance = distance;
                    closestMessage = message;
                }
            });

            if (closestMessage) {
                // 找到对应的大纲项并高亮
                const index = Array.from(userMessages).indexOf(closestMessage);
                const outlineItem = this.outlineContainer.querySelector(`.outline-item[data-index="${index}"]`);
                if (outlineItem) {
                    this.highlightItem(outlineItem);
                }
            }
        }
    }

    /**
     * 插件管理器类 - 用于管理多个插件
     */
    class PluginManager {
        constructor() {
            this.plugins = [];
        }

        /**
         * 注册(不可用)插件
         * @param {Object} plugin 插件实例
         */
        register(plugin) {
            this.plugins.push(plugin);
            return this;
        }

        /**
         * 初始化所有插件
         */
        initAll() {
            this.plugins.forEach(plugin => {
                if (typeof plugin.init === 'function') {
                    setTimeout(() => plugin.init(), 100); // 延迟启动,确保页面已加载
                }
            });
        }
    }

    /**
     * 创建并启动插件管理器
     */
    $(function () {
        // 添加延迟,确保所有DOM元素都已经渲染完成
        // 创建插件管理器
        const pluginManager = new PluginManager();

        // 定义支持的网站及其对应的生成器类
        const generatorMap = {
            'chatgpt.com': ChatGPTOutlineGenerator,
            'chat.deepseek.com': DeepSeekOutlineGenerator,
            'gemini.google.com': GeminiOutlineGenerator,
            'ying.baichuan-ai.com': BaichuanOutlineGenerator,
            'kimi.moonshot.cn': KimiOutlineGenerator
        };

        // 获取当前网站域名
        const currentUrl = window.location.href;

        // 遍历支持的网站,找到匹配的生成器并注册(不可用)
        for (const [domain, GeneratorClass] of Object.entries(generatorMap)) {
            if (currentUrl.includes(domain)) {
                // 注册(不可用)对应的大纲生成器插件
                pluginManager.register(new GeneratorClass());
                break; // 找到匹配的生成器后退出循环
            }
        }

        // 初始化所有插件
        pluginManager.initAll();
    });

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址