deepseek-question-list

展示网页版deepseek当前对话的所有提问

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

// ==UserScript==
// @name         deepseek-question-list
// @namespace    https://github.com/firesahc/webai-question-list
// @version      1.17.2
// @description  展示网页版deepseek当前对话的所有提问
// @author       firesahc
// @match        https://chat.deepseek.com/*
// @license      MIT
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

let observer = null;
let isObserving = false;
let debounceTimer = null;
let spaObserver = null;

function createParserInit() {
    const existingList = document.getElementById('xpath-parser-list');
    if (existingList) existingList.remove();

    const listContainer = document.createElement('div');
    listContainer.id = 'xpath-parser-list';
    listContainer.style.cssText = `
        position: static;
        top: 12px;
        right: 65px;
        width: 260px;
        flex-shrink: 0;
        z-index: 1005;
        background: var(--dsw-specific-sidebar-fill, #f9fafb);
        border-left: 1px solid var(--dsw-alias-border-l1, rgba(0,0,0,.04));
        overflow-y: auto;
        display: flex;
        flex-direction: column;
        font-family: Arial, sans-serif;
        font-size: 14px;
        padding: 6px 12px 10px;
        transition: width 0.3s var(--ds-ease-in-out, ease), box-shadow 0.3s ease;
        box-shadow: none;
        max-height: 100vh;
    `;

    const topButtonBar = document.createElement('div');
    topButtonBar.style.cssText = `
        display: flex;
        gap: 6px;
        flex-wrap: wrap;
        padding-bottom: 10px;
        margin-bottom: 4px;
        border-bottom: 1px solid var(--dsw-alias-border-l1, rgba(0,0,0,.04));
        flex-shrink: 0;
    `;

    const contentArea = document.createElement('div');
    contentArea.id = 'xpath-list-content';
    contentArea.style.cssText = `
        flex: 1;
        overflow-y: auto;
        padding: 4px 0;
        min-height: 0;
    `;
    
    addTopButtons(topButtonBar, listContainer, contentArea);
    
    listContainer.appendChild(topButtonBar);
    listContainer.appendChild(contentArea);
    
    // 延迟启动观察器
    clearTimeout(debounceTimer);
    debounceTimer = setTimeout(() => {
        addQuestionCollapseButtons();

        // 将列表框添加到 class="c3ecdb44" 的元素内部
        const targetContainer = document.querySelector('.c3ecdb44');
        if (targetContainer) {
            targetContainer.appendChild(listContainer);
            // SPA 导航检测:当 React 切换对话时,我们的元素会被移除,此时重新初始化
            setupSPAObserver(targetContainer);
        }
        else {
            console.warn("未找到框架元素class=\"c3ecdb44\"");
        }

        // 自动开始解析(首次加载时)
        const contentVisible = GM_getValue('isContentVisible', true);
        if (!isObserving && contentVisible) {
            const contentArea = document.getElementById('xpath-list-content');
            if (contentArea) {
                startObservation(contentArea);
            }
        }
    }, 500)
}

function startObservation(contentArea) {
    if (isObserving) return true;

    observer = new MutationObserver((mutations) => {
        let shouldParse = false;
        for (const mutation of mutations) {
            // 检查目标元素的类名
            const targetClass = mutation.target.className;
            
            // 情况1: 直接检测到 ds-virtual-list-visible-items 的变化
            if (mutation.type === 'childList' &&
                typeof targetClass === 'string' &&
                targetClass.includes('ds-virtual-list-visible-items')) {
                shouldParse = true;
                break;
            }
            
            // 情况2: 检测到滚动区域的变化,且涉及 ds-virtual-list-visible-items 节点
            if (mutation.type === 'childList' &&
                typeof targetClass === 'string' &&
                targetClass.includes('ds-virtual-list-items')) {
                // 检查添加的节点
                if (mutation.addedNodes.length > 0) {
                    for (const node of mutation.addedNodes) {
                        if (node.nodeType === Node.ELEMENT_NODE &&
                            node.classList &&
                            node.classList.contains('ds-virtual-list-visible-items')) {
                            shouldParse = true;
                            break;
                        }
                    }
                }
                // 检查移除的节点
                else if (mutation.removedNodes.length > 0) {
                    for (const node of mutation.removedNodes) {
                        if (node.nodeType === Node.ELEMENT_NODE &&
                            node.classList &&
                            node.classList.contains('ds-virtual-list-visible-items')) {
                            shouldParse = true;
                            break;
                        }
                    }
                }
                if (shouldParse) break;
            }
        }

        if (shouldParse) {
            clearTimeout(debounceTimer);
            debounceTimer = setTimeout(() => {
                const messageElements = parseElements();
                if (messageElements.length === 0) return;
                contentArea.innerHTML = '';
                addListMessages(contentArea, messageElements);

                // 为每个目标元素添加收起按钮(仅当内容较长时)
                addElementCollapseButtons(messageElements);
            }, 300);
        }
    });

    // 获取目标元素(._765a5cd 不含 ds-scroll-area 类,该类位于其子元素 _2bd7b35 上)
    const targetElement = document.querySelector('._765a5cd');
    if (!targetElement) {
        console.error("未找到监听元素class=\"_765a5cd\"");
        return false;
    }
    
    try {
        // 开始观察目标元素(仅监听子节点变化,不监听属性)
        observer.observe(targetElement, {
            childList: true,
            subtree: true
        });
        isObserving = true;
        return true;
    } catch (error) {
        isObserving = false;
        return false;
    }
}

function stopObservation() {
    if (observer) {
        observer.disconnect();
        observer = null;
        isObserving = false;
        clearTimeout(debounceTimer);
    }
}

function setupSPAObserver(targetContainer) {
    // 清理旧的 SPA 观察器
    if (spaObserver) spaObserver.disconnect();
    // 监听 .c3ecdb44 的子节点移除 → React 切换对话时重建整个脚本
    spaObserver = new MutationObserver((mutations) => {
        for (const m of mutations) {
            for (const node of m.removedNodes) {
                if (node.nodeType === Node.ELEMENT_NODE && node.id === 'xpath-parser-list') {
                    spaObserver.disconnect();
                    spaObserver = null;
                    stopObservation();
                    createParserInit();
                    return;
                }
            }
        }
    });
    spaObserver.observe(targetContainer, { childList: true });
}

function parseElements() {
    try {
        const targetElements = document.querySelectorAll('.fbb737a4');
        if (targetElements.length === 0) {
            return [];
        }
        return Array.from(targetElements);
    } catch (error) {
        console.warn("parseElements error:", error);
        return [];
    }
}

function addElementCollapseButtons(messageElements) {
    messageElements.forEach((element) => {
        // 检查是否已经添加过按钮
        if (element.hasAttribute('data-collapse-button-added')) {
            return;
        }

        // 如果内容高度不超过400px,不需要添加收起按钮
        if (element.scrollHeight <= 400) {
            return;
        }

        // 标记已添加按钮
        element.setAttribute('data-collapse-button-added', 'true');

        // 确保元素有相对定位,以便按钮可以绝对定位
        const originalPosition = element.style.position;
        if (!originalPosition || originalPosition === 'static') {
            element.style.position = 'relative';
        }

        // 创建收起按钮
        const collapseButton = document.createElement('button');
        collapseButton.textContent = '收起';
        collapseButton.style.cssText = `
            position: absolute;
            top: 5px;
            left: 5px;
            z-index: 1000;
            background: rgba(100, 100, 100, 0.8);
            color: white;
            border: none;
            border-radius: 4px;
            padding: 4px 8px;
            font-size: 12px;
            cursor: pointer;
            opacity: 0.8;
            transition: opacity 0.2s;
        `;

        // 存储原始高度和溢出状态
        const originalHeight = element.style.height;
        const originalOverflow = element.style.overflow;
        let isCollapsed = false;

        collapseButton.addEventListener('mouseenter', () => {
            collapseButton.style.opacity = '1';
        });

        collapseButton.addEventListener('mouseleave', () => {
            collapseButton.style.opacity = '0.8';
        });

        collapseButton.addEventListener('click', (e) => {
            e.stopPropagation();
            if (isCollapsed) {
                // 展开
                element.style.height = originalHeight || '';
                element.style.overflow = originalOverflow || '';
                collapseButton.textContent = '收起';
                isCollapsed = false;
            } else {
                // 收起
                element.style.height = '110px';
                element.style.overflow = 'hidden';
                collapseButton.textContent = '展开';
                isCollapsed = true;
            }
        });

        // 添加按钮到元素
        element.appendChild(collapseButton);
    });
}

function addQuestionCollapseButtons(){
    // 检查是否已添加过按钮
    if (document.getElementById('question-collapse-toggle')) {
        return;
    }

    // 尝试查找目标元素
    const questionElement = document.querySelector('._871cbca');
    if (!questionElement) {
        console.error("未找到输入框框架元素class=\"_871cbca\"");
        return;
    }
    
    // 保存原始 display 值(CSS 中为 flex),供展开时恢复
    const originalDisplay = getComputedStyle(questionElement).display;

    const toggleButton = document.createElement('button');
    toggleButton.id = 'question-collapse-toggle';
    // 获取目标容器元素
    const containerElement = document.querySelector('._7780f2e');
    if (!containerElement) {
        console.error("未找到输入框元素class=\"_7780f2e\"");
        return;
    } else {
        toggleButton.textContent = '▼';
        // 设置容器元素为相对定位,以便按钮可以相对于它定位
        containerElement.style.position = 'relative';
        // 将按钮添加到容器元素内部
        containerElement.appendChild(toggleButton);
        // 设置按钮样式 - 在容器内水平居中
        toggleButton.style.cssText = `
            position: absolute;
            bottom: 0; 
            left: 50%;
            transform: translateX(-50%);
            z-index: 1000;
            padding: 8px 20px;
            background-color: rgba(255, 255, 255, 0.3);
            color: #000;
            border: 1px solid rgba(0, 0, 0, 0.1);
            border-bottom: none;
            border-radius: 8px 8px 0 0;
            cursor: pointer;
            backdrop-filter: blur(12px);
            -webkit-backdrop-filter: blur(12px);
            font-size: 10px;
            font-weight: bold;
            box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
            transition: all 0.3s ease;
        `;
    }

    // 添加点击事件
    toggleButton.addEventListener('click', function () {
        if (questionElement.style.display === 'none') {
            // 展开:恢复原始 display 值 (flex)
            questionElement.style.display = originalDisplay;
            toggleButton.textContent = '▼';
        } else {
            // 收起:保存当前 inline display 并设为 none
            questionElement.style.display = 'none';
            toggleButton.textContent = '▲';
        };
    });
}

function addListMessages(contentArea, messageElements) {
    const list = document.createElement('ul');
    list.style.cssText = `
        list-style: none;
        margin: 0;
        padding: 0;
    `;

    messageElements.forEach((item, index) => {
        const listItem = createListItem(item, index);
        list.appendChild(listItem);
    });

    contentArea.appendChild(list);
}

function createListItem(item, index) {
    const listItem = document.createElement('li');
    listItem.style.cssText = `
        margin-bottom: 6px;
        padding: 6px 8px;
        border: 1px solid var(--dsw-alias-border-l1, #d0d5dd);
        border-left: 3px solid var(--dsw-alias-brand-primary, #4a6cf7);
        border-radius: 6px;
        background: var(--dsw-alias-bg-base, #f8f9fb);
        cursor: pointer;
        transition: all 0.2s ease;
    `;

    listItem.addEventListener('mouseenter', () => {
        listItem.style.background = 'var(--dsw-specific-sidebar-nav-item-hover, #eef1f6)';
        listItem.style.borderColor = 'var(--dsw-specific-sidebar-nav-item-active-accent, #4a6cf7)';
        listItem.style.borderLeftColor = 'var(--dsw-specific-sidebar-nav-item-active-accent, #2d4cc8)';
    });

    listItem.addEventListener('mouseleave', () => {
        listItem.style.background = 'var(--dsw-alias-bg-base, #f8f9fb)';
        listItem.style.borderColor = 'var(--dsw-alias-border-l1, #d0d5dd)';
        listItem.style.borderLeftColor = 'var(--dsw-alias-brand-primary, #4a6cf7)';
    });

    const indexInfo = document.createElement('div');
    indexInfo.style.cssText = `
        font-weight: 700;
        color: #1a1a2e;
        font-size: 13px;
        margin-bottom: 2px;
    `;
    indexInfo.textContent = `问题 ${index + 1}`;

    const contentPreview = document.createElement('div');
    contentPreview.style.cssText = `
        color: #2d2d3f;
        font-size: 13px;
        line-height: 1.45;
        font-weight: 450;
        background: #fff;
        padding: 5px 7px;
        border-radius: 4px;
        border: 1px solid var(--dsw-alias-border-l1, #e8ecf1);
    `;
    
    const textContent = item.textContent?.trim() || '';
    contentPreview.textContent = textContent ?
        (textContent.length > 120 ? 
             textContent.substring(0, 120) + '...' : 
             textContent
        ) :
        '[空内容]';

    listItem.appendChild(indexInfo);
    listItem.appendChild(contentPreview);

    //点击跳转到问题起始
    listItem.addEventListener('click', () => {
        item.scrollIntoView({ behavior: 'smooth', block: 'start' });
    });

    return listItem;
}

function addTopButtons(buttonContainer, listContainer, contentArea) {
    // 从油猴存储中读取 isContentVisible 的值,默认值为 true(显示状态)
    let isContentVisible = GM_getValue('isContentVisible', true);

    // 根据存储的值初始化布局状态
    if (isContentVisible) {
        applyExpandedState(listContainer, contentArea, buttonContainer);
    } else {
        applyCollapsedState(listContainer, contentArea, buttonContainer);
    }

    // ── 解析按钮:仿 DeepSeek capsule 样式(primary 标签色)──
    const playIcon = '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.2"/><path d="M6.2 4.8L11 8L6.2 11.2V4.8Z" fill="currentColor"/></svg>';
    const stopIcon = '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.2"/><rect x="4.8" y="4.8" width="6.4" height="6.4" rx="1.2" fill="currentColor"/></svg>';

    const parseButton = createCapsuleButton(
        isObserving ? stopIcon : playIcon,
        true,
        () => {
            if (isObserving) {
                stopObservation();
                parseButton.innerHTML = playIcon;
            } else {
                const success = startObservation(contentArea);
                if (success) {
                    parseButton.innerHTML = stopIcon;
                    const messageElements = parseElements();
                    if (messageElements.length > 0) {
                        contentArea.innerHTML = '';
                        addListMessages(contentArea, messageElements);
                        addElementCollapseButtons(messageElements);
                    }
                }
            }
        }
    );
    parseButton.title = isObserving ? '停止解析' : '开始解析';

    // ── 折叠按钮:仿 DeepSeek capsule 样式(tertiary 标签色,更低调)──
    // 展开态 → 点击收起(双右箭头 »)
    const closeIcon = '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M4.5 3.5L9 8L4.5 12.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/><path d="M9 3.5L12 8L9 12.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>';
    // 收起态 → 点击展开(双左箭头 «)
    const openIcon  = '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M11.5 3.5L7 8L11.5 12.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/><path d="M7 3.5L4 8L7 12.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>';

    const toggleButton = createCapsuleButton(
        isContentVisible ? closeIcon : openIcon,
        false,
        () => {
            isContentVisible = !isContentVisible;
            toggleButton.innerHTML = isContentVisible ? closeIcon : openIcon;
            if (isContentVisible) {
                applyExpandedState(listContainer, contentArea, buttonContainer);
            } else {
                applyCollapsedState(listContainer, contentArea, buttonContainer);
            }
            GM_setValue('isContentVisible', isContentVisible);
        }
    );
    toggleButton.title = isContentVisible ? '收起列表' : '展开列表';

    buttonContainer.appendChild(parseButton);
    buttonContainer.appendChild(toggleButton);
}

function applyCollapsedState(listContainer, contentArea, buttonContainer) {
    // ── 仿 DeepSeek .e5bf614e 胶囊容器 ──
    listContainer.style.position = 'fixed';
    listContainer.style.top = '12px';
    listContainer.style.right = '65px';
    listContainer.style.width = 'auto';
    listContainer.style.minWidth = '';
    listContainer.style.maxHeight = 'none';
    listContainer.style.height = '40px';
    listContainer.style.padding = '0 4px';
    listContainer.style.borderRadius = '100px';
    listContainer.style.border = '1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.08))';
    listContainer.style.background = 'var(--dsw-alias-bg-layer-3, #fff)';
    listContainer.style.boxShadow = '0 4px 12px rgba(0,0,0,.04)';
    listContainer.style.overflowY = 'visible';
    listContainer.style.backdropFilter = '';
    listContainer.style.webkitBackdropFilter = '';
    listContainer.style.display = 'flex';
    listContainer.style.flexDirection = 'row';
    listContainer.style.alignItems = 'center';
    contentArea.style.display = 'none';
    buttonContainer.style.flexDirection = 'row';
    buttonContainer.style.borderBottom = 'none';
    buttonContainer.style.paddingBottom = '0';
    buttonContainer.style.marginBottom = '0';
    buttonContainer.style.gap = '2px';
    // 调整按钮内边距(更紧凑的图标按钮)
    if (buttonContainer.children[0]) buttonContainer.children[0].style.padding = '0 6px';
    if (buttonContainer.children[1]) buttonContainer.children[1].style.padding = '0 5px';
}

function applyExpandedState(listContainer, contentArea, buttonContainer) {
    // 回到文档流 → flex 子元素,占据右侧栏位
    listContainer.style.position = '';
    listContainer.style.top = '';
    listContainer.style.right = '';
    listContainer.style.width = '260px';
    listContainer.style.minWidth = '';
    listContainer.style.maxHeight = '100vh';
    listContainer.style.height = '';
    listContainer.style.padding = '6px 12px 10px';
    listContainer.style.boxShadow = 'none';
    listContainer.style.borderRadius = '';
    listContainer.style.border = '';
    listContainer.style.overflowY = 'auto';
    listContainer.style.display = '';
    listContainer.style.flexDirection = '';
    listContainer.style.alignItems = '';
    listContainer.style.background = 'var(--dsw-specific-sidebar-fill, #f9fafb)';
    listContainer.style.backdropFilter = '';
    listContainer.style.webkitBackdropFilter = '';
    contentArea.style.display = 'block';
    buttonContainer.style.flexDirection = 'row';
    buttonContainer.style.borderBottom = '1px solid var(--dsw-alias-border-l1, rgba(0,0,0,.04))';
    buttonContainer.style.paddingBottom = '10px';
    buttonContainer.style.marginBottom = '4px';
    buttonContainer.style.gap = '6px';
    // 恢复默认内边距
    if (buttonContainer.children[0]) buttonContainer.children[0].style.padding = '';
    if (buttonContainer.children[1]) buttonContainer.children[1].style.padding = '';
}

// ── DeepSeek 风格胶囊按钮 ──
function createCapsuleButton(innerHTML, isPrimary, clickHandler) {
    const btn = document.createElement('button');
    btn.innerHTML = innerHTML;
    const labelVar = isPrimary ? 'primary' : 'tertiary';
    btn.style.cssText = `
        height: 34px;
        padding: 0 12px;
        border: none;
        border-radius: 4096px;
        cursor: pointer;
        display: inline-flex;
        align-items: center;
        gap: 4px;
        font-size: 14px;
        font-weight: 400;
        line-height: 1;
        white-space: nowrap;
        box-sizing: border-box;
        transition: background 0.2s ease;
        color: var(--dsw-alias-label-${labelVar}, ${isPrimary ? '#1a1a2e' : '#8b95a1'});
        background: transparent;
        outline: none;
    `;
    btn.addEventListener('mouseenter', () => {
        btn.style.background = 'var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,0.05))';
    });
    btn.addEventListener('mouseleave', () => {
        btn.style.background = 'transparent';
    });
    btn.addEventListener('click', clickHandler);
    return btn;
}

if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', createParserInit);
} else {
    createParserInit();
}

window.createParser = createParserInit;
window.parseTarget = function() {
    const contentArea = document.getElementById('xpath-list-content');
    if (contentArea) {
        const messageElements = parseElements();
        if (messageElements.length > 0) {
            contentArea.innerHTML = '';
            addListMessages(contentArea, messageElements);

            // 为每个目标元素添加收起按钮(仅当内容较长时)
            addElementCollapseButtons(messageElements);
        }
    }
};