AI 导航栏(Gemini & ChatGPT & DeepSeek)

🚀 三合一 AI 侧边导航栏。支持 Gemini、ChatGPT、DeepSeek。提供长对话索引、极速跳转与本地收藏功能。

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         AI 导航栏(Gemini & ChatGPT & DeepSeek)
// @name:en      AI Chat Sidebar (Gemini & ChatGPT & DeepSeek)
// @namespace    https://github.com/kuilei98/ai-conversation-sidebar
// @version      2.0.0.1213
// @uuid         7a3b9c1d-8e4f-4a5b-9c6d-1e2f3a4b5c6d
// @description  🚀 三合一 AI 侧边导航栏。支持 Gemini、ChatGPT、DeepSeek。提供长对话索引、极速跳转与本地收藏功能。
// @description:en 🚀 AI Sidebar for Gemini, ChatGPT, and DeepSeek. Features: conversation navigation, jump to message, and bookmarking.
// @author       kuilei98
// @match        https://gemini.google.com/*
// @match        https://chatgpt.com/*
// @match        https://chat.openai.com/*
// @match        https://chat.deepseek.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=gemini.google.com
// @supportURL   https://github.com/kuilei98/ai-conversation-sidebar/issues
// @homepageURL  https://github.com/kuilei98/ai-conversation-sidebar
// @grant        none
// @license      MIT
// @run-at       document-end
// @noframes
// ==/UserScript==

(function() {
    'use strict';

    // --- 1. 全局配置与样式 ---
    const NAV_CONTAINER_ID = 'ai-nav-container-universal';
    const NAV_CONTENT_ID = 'ai-nav-content-list';
    const CONTROL_TEXT = '按住拖动 | 单击折叠';
    
    // CSS变量解耦,适配深色模式
    const STATIC_CSS = `
        :root {
            --ai-accent: #0b57d0;
            --ai-highlight: rgba(11, 87, 208, 0.15);
            --ai-top-offset: 100px;
            --ai-bg: #ffffff;
            --ai-border: rgba(0,0,0,0.08);
            --ai-shadow: 0 2px 6px rgba(0,0,0,0.08);
            --ai-shadow-hover: 0 8px 20px rgba(0,0,0,0.12);
            --ai-text-primary: #1f1f1f;
            --ai-text-secondary: #444746;
            --ai-star-inactive: #c4c7c5;
            --ai-star-active: #fbbc04;
            --ai-star-bg: #fff8e1;
            --ai-star-text: #b06000;
        }

        @media (prefers-color-scheme: dark) {
            :root {
                --ai-bg: #1e1f20;
                --ai-border: rgba(255,255,255,0.1);
                --ai-shadow: 0 2px 6px rgba(0,0,0,0.4);
                --ai-shadow-hover: 0 8px 24px rgba(0,0,0,0.6);
                --ai-text-primary: #e3e3e3;
                --ai-text-secondary: #c4c7c5;
                --ai-star-inactive: #8e918f;
                --ai-star-bg: #3f3a2c;
                --ai-star-text: #fdd663;
            }
        }

        #${NAV_CONTAINER_ID} {
            position: fixed; top: var(--ai-top-offset); right: 15px; width: auto; max-height: 98vh;
            display: flex; flex-direction: column; gap: 4px; align-items: flex-end; z-index: 2147483647; padding-bottom: 20px;
        }
        #${NAV_CONTAINER_ID}.ai-left-side { align-items: flex-start; }
        #${NAV_CONTENT_ID} { display: flex; flex-direction: column; gap: 4px; align-items: inherit; width: 100%; overflow: visible; }
        #${NAV_CONTENT_ID}.hidden { display: none; }

        .nav-capsule {
            pointer-events: auto; background-color: var(--ai-bg); border: 1px solid var(--ai-border); color: var(--ai-text-primary);
            width: 34px; height: 34px; padding: 0; display: flex; align-items: center; justify-content: center;
            box-shadow: var(--ai-shadow); cursor: pointer; transition: width 0.25s cubic-bezier(0.2, 0, 0, 1), background-color 0.2s, box-shadow 0.2s, border-radius 0.2s;
            overflow: hidden; white-space: nowrap; position: relative; flex-shrink: 0; box-sizing: border-box;
            border-radius: 5px 0 0 5px; flex-direction: row;
        }
        #${NAV_CONTAINER_ID}.ai-left-side .nav-capsule { border-radius: 0 5px 5px 0; flex-direction: row-reverse; }

        .nav-capsule:hover, .nav-capsule.is-dragging { 
            width: 280px; padding: 0 12px; justify-content: space-between; 
            background-color: var(--ai-bg); box-shadow: var(--ai-shadow-hover); 
            z-index: 10000; border-color: transparent; border-radius: 5px 0 0 5px; 
        }
        #${NAV_CONTAINER_ID}.ai-left-side .nav-capsule:hover, 
        #${NAV_CONTAINER_ID}.ai-left-side .nav-capsule.is-dragging { border-radius: 0 5px 5px 0; }

        .nav-capsule.control-capsule {
            cursor: grab; border-left: 3px solid var(--ai-accent); z-index: 10001; touch-action: none; user-select: none;
        }
        #${NAV_CONTAINER_ID}.ai-left-side .nav-capsule.control-capsule { border-left: 1px solid var(--ai-border); border-right: 3px solid var(--ai-accent); }
        .nav-capsule.control-capsule:active { cursor: grabbing; }
        .nav-capsule.control-capsule .capsule-index { font-size: 18px; transform: translateY(-1px); }

        .capsule-index { font-weight: 700; font-size: 13px; color: var(--ai-accent); text-align: center; min-width: auto; flex-shrink: 0; transition: transform 0.2s; }
        .capsule-text { display: none; font-size: 13px; color: var(--ai-text-secondary); flex: 1; margin: 0 12px; overflow: hidden; text-overflow: ellipsis; text-align: left; }
        #${NAV_CONTAINER_ID}.ai-left-side .capsule-text { text-align: right; }
        .nav-capsule:hover .capsule-text, .nav-capsule.is-dragging .capsule-text { display: block; animation: fadeIn 0.2s forwards; }

        .capsule-star { display: none; font-size: 16px; color: var(--ai-star-inactive); width: 18px; text-align: center; cursor: pointer; transition: all 0.2s ease; flex-shrink: 0; }
        .nav-capsule:hover .capsule-star, .nav-capsule.is-dragging .capsule-star { display: block; }
        .capsule-star.unlocked { opacity: 1 !important; color: var(--ai-text-primary); transform: scale(1.1); }
        .capsule-star.denied { animation: shake 0.3s ease; }
        
        .nav-capsule.starred { background-color: var(--ai-star-bg); border-color: transparent; }
        .nav-capsule.starred .capsule-index { color: var(--ai-star-text); }
        .nav-capsule.starred .capsule-star { display: block !important; opacity: 1 !important; color: var(--ai-star-active) !important; transform: scale(1.1); }

        .nav-capsule.active {
            background-color: var(--ai-highlight);
            border-left: 3px solid var(--ai-accent);
            border-right: 1px solid var(--ai-border); border-top: 1px solid var(--ai-border); border-bottom: 1px solid var(--ai-border);
        }
        #${NAV_CONTAINER_ID}.ai-left-side .nav-capsule.active {
            border-left: 1px solid var(--ai-border); border-right: 3px solid var(--ai-accent); 
        }
        .nav-capsule.active .capsule-index { transform: scale(1.1); }

        .ai-active-message {
            background-color: var(--ai-highlight) !important;
            box-shadow: -4px 0 0 var(--ai-accent); transition: background-color 0.3s ease; border-radius: 4px; 
        }

        @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
        @keyframes shake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-2px); } 75% { transform: translateX(2px); } }
        [data-ai-index]::before { content: attr(data-ai-index); display: inline-block; font-family: Consolas, monospace; font-weight: bold; color: var(--ai-accent); margin-right: 10px; font-size: 1.1em; opacity: 1; user-select: none; vertical-align: middle; }
    `;

    // --- 2. 平台适配策略 ---
    const PLATFORMS = {
        'gemini': {
            color: '#0b57d0', highlight: 'rgba(11, 87, 208, 0.15)', top: '140px',
            selectors: ['user-query', '.user-query', '[data-test-id="user-query"]', 'div.user-query-container']
        },
        'chatgpt': {
            color: '#10a37f', highlight: 'rgba(16, 163, 127, 0.15)', top: '140px', darkModeBg: '#212121',
            selectors: ['[data-message-author-role="user"]', '.group\\/conversation-turn:has([data-message-author-role="user"])']
        },
        'deepseek': {
            color: '#4d8aff', highlight: 'rgba(77, 138, 255, 0.15)', top: '100px',
            customQueryList: () => {
                const questions = [];
                document.querySelectorAll('.ds-message:has(> .ds-markdown)').forEach(answerMsg => {
                    const prevSibling = answerMsg.parentElement?.previousElementSibling;
                    const questionMsg = prevSibling?.querySelector('.ds-message');
                    if (questionMsg) questions.push(questionMsg);
                });
                return questions;
            }
        }
    };

    // --- 3. 环境检测与初始化 ---
    const host = window.location.hostname;
    let currentPlatform, siteKey;
    if (host.includes('gemini.google')) { currentPlatform = PLATFORMS.gemini; siteKey = 'gemini'; }
    else if (host.includes('chatgpt') || host.includes('openai')) { currentPlatform = PLATFORMS.chatgpt; siteKey = 'chatgpt'; }
    else if (host.includes('deepseek')) { currentPlatform = PLATFORMS.deepseek; siteKey = 'deepseek'; }
    else return;

    const styleEl = document.createElement('style');
    styleEl.textContent = STATIC_CSS;
    document.head.appendChild(styleEl);

    const rootStyle = document.documentElement.style;
    rootStyle.setProperty('--ai-accent', currentPlatform.color);
    rootStyle.setProperty('--ai-highlight', currentPlatform.highlight);
    rootStyle.setProperty('--ai-top-offset', currentPlatform.top);
    if (currentPlatform.darkModeBg) {
        const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
        if (isDark) rootStyle.setProperty('--ai-bg', currentPlatform.darkModeBg);
    }

    let currentChatId = '';
    let cachedStars = {};
    let isCollapsed = false;
    let cachedSelector = null;
    let windowStartIndex = 0;
    let activeGlobalIndex = -1;
    let maxVisible = 10;
    let dragOffsetX = 0, dragOffsetY = 0, rafId = null;

    // --- 4. 常用工具函数 ---
    const getClientXY = (e) => (e.touches && e.touches.length > 0) ? { x: e.touches[0].clientX, y: e.touches[0].clientY } : { x: e.clientX, y: e.clientY };
    const defaultGetText = (el) => el.textContent || el.innerText || '';
    
    function throttle(func, wait) {
        let timeout = null;
        return function(...args) {
            if (!timeout) {
                timeout = setTimeout(() => { func.apply(this, args); timeout = null; }, wait);
            }
        };
    }

    // --- 5. 核心逻辑:自适应扫描与布局 ---
    function getBestQueryList() {
        if (currentPlatform.customQueryList) return currentPlatform.customQueryList();
        if (cachedSelector) {
            const res = document.querySelectorAll(cachedSelector);
            if (res.length > 0) return res;
            cachedSelector = null;
        }
        let bestQueries = [];
        for (let s of currentPlatform.selectors) {
            const res = document.querySelectorAll(s);
            if (res.length > bestQueries.length) { bestQueries = res; cachedSelector = s; }
        }
        return bestQueries;
    }

    const fitToScreenAndScan = (force = false) => {
        const container = document.getElementById(NAV_CONTAINER_ID);
        if (!container) return;
        
        const totalItems = getBestQueryList().length;
        if (totalItems === 0) return;

        const rect = container.getBoundingClientRect();
        const containerTop = rect.top > 0 ? rect.top : (parseInt(currentPlatform.top) || 100);
        const availableHeight = window.innerHeight - containerTop - 40;
        const listAvailableHeight = availableHeight - 38; 
        
        let calculatedCapacity = Math.floor(listAvailableHeight / 38);
        if (calculatedCapacity < 1) calculatedCapacity = 1;
        
        maxVisible = Math.min(totalItems, calculatedCapacity);
        scanMessages(force);
    };

    const throttledScan = throttle(() => fitToScreenAndScan(), 1000);
    const throttledResize = throttle(() => fitToScreenAndScan(true), 200);

    // --- 6. 观察者 DOM 监听 ---
    const initObserver = () => {
        const observer = new MutationObserver((mutations) => {
            let shouldUpdate = false;
            for (let m of mutations) {
                let node = m.target;
                let isInsideNav = false;
                while(node && node !== document) {
                    if (node.id === NAV_CONTAINER_ID) { isInsideNav = true; break; }
                    node = node.parentElement;
                }
                if (!isInsideNav) { shouldUpdate = true; break; }
            }
            if (shouldUpdate) throttledScan();
        });
        observer.observe(document.body, { childList: true, subtree: true });
    };

    window.addEventListener('resize', throttledResize);
    setInterval(() => { try { updateChatId(); ensureContainer(); } catch (e) { console.error("AI Nav Error:", e); } }, 1000);
    initObserver();

    function updateChatId() {
        const newPath = window.location.pathname;
        if (newPath !== currentChatId) {
            currentChatId = newPath;
            const storageKey = `${siteKey}_stars_${currentChatId}`;
            cachedStars = JSON.parse(localStorage.getItem(storageKey) || '{}');
            cachedSelector = null;
            windowStartIndex = 0;
            activeGlobalIndex = -1;
            document.querySelectorAll('.ai-active-message').forEach(el => el.classList.remove('ai-active-message'));
            const contentList = document.getElementById(NAV_CONTENT_ID);
            if(contentList) fitToScreenAndScan(true);
        }
    }

    // --- 7. 渲染胶囊与交互反馈 ---
    function createOrUpdateCapsule(capsule, data) {
        const { rawText, shortText, indexLabel, isStarred, uniqueKey, globalIndex } = data;
        
        // 使用标准 DOM API 防止 Trusted Types 错误
        if (!capsule) {
            capsule = document.createElement('div');
            capsule.className = 'nav-capsule';
            
            const indexSpan = document.createElement('span'); indexSpan.className = 'capsule-index';
            const textSpan = document.createElement('span'); textSpan.className = 'capsule-text';
            const starSpan = document.createElement('span'); starSpan.className = 'capsule-star';
            
            starSpan.textContent = '☆';
            starSpan.title = '点击两次以收藏';
            
            capsule.append(indexSpan, textSpan, starSpan);
        }

        if (capsule.title !== rawText) {
            capsule.title = rawText;
            capsule.querySelector('.capsule-text').textContent = shortText;
        }

        capsule.dataset.key = uniqueKey;
        capsule.dataset.index = globalIndex;
        capsule.querySelector('.capsule-index').textContent = indexLabel;

        if (globalIndex === activeGlobalIndex) capsule.classList.add('active');
        else capsule.classList.remove('active');

        const starIcon = capsule.querySelector('.capsule-star');
        if (isStarred) {
             capsule.classList.add('starred');
             starIcon.textContent = '★';
             starIcon.title = '取消收藏';
        } else {
             capsule.classList.remove('starred');
             starIcon.textContent = '☆';
             starIcon.title = '点击两次以收藏';
        }
        return capsule;
    }

    function scanMessages(force = false) {
        const contentList = document.getElementById(NAV_CONTENT_ID);
        if (!contentList) return;

        const queries = getBestQueryList();
        const totalLen = queries.length;

        if (totalLen > maxVisible) {
            if (windowStartIndex + maxVisible > totalLen) {
                windowStartIndex = totalLen - maxVisible;
                force = true;
            }
        } else {
            windowStartIndex = 0;
        }

        const visibleQueries = Array.from(queries).slice(windowStartIndex, windowStartIndex + maxVisible);
        if (force) contentList.replaceChildren();
        while (contentList.children.length > visibleQueries.length) contentList.lastElementChild.remove();

        const getText = currentPlatform.getText || defaultGetText;

        visibleQueries.forEach((el, relativeIndex) => {
            const globalIndex = windowStartIndex + relativeIndex;
            const indexLabel = `Q${globalIndex + 1}`;
            if (el.getAttribute('data-ai-index') !== indexLabel) el.setAttribute('data-ai-index', indexLabel);

            let rawText = getText(el) || `Question ${globalIndex + 1}`;
            rawText = rawText.replace(/\s+/g, ' ').trim();
            const uniqueKey = rawText ? (rawText.substring(0, 50) + "_uid_" + globalIndex) : "empty_node";

            const data = {
                rawText,
                shortText: rawText.length > 25 ? rawText.substring(0, 25) + '...' : rawText,
                indexLabel,
                isStarred: !!cachedStars[uniqueKey],
                uniqueKey,
                globalIndex
            };

            const existingCapsule = contentList.children[relativeIndex];
            const resultCapsule = createOrUpdateCapsule(existingCapsule, data);
            if (!existingCapsule) contentList.appendChild(resultCapsule);
        });
    }

    function handleJump(index) {
        const bestQueries = getBestQueryList();
        const target = bestQueries[index];
        if (target) {
            target.scrollIntoView({ behavior: 'instant', block: 'center' });
            document.querySelectorAll('.ai-active-message').forEach(el => el.classList.remove('ai-active-message'));
            target.classList.add('ai-active-message');
            activeGlobalIndex = index;
            scanMessages(); 
        }
    }

    // --- 8. 事件委托(点击、滚动、触摸) ---
    function setupDelegation(listContainer) {
        listContainer.addEventListener('click', (e) => {
            const capsule = e.target.closest('.nav-capsule');
            if (!capsule) return;
            const index = parseInt(capsule.dataset.index, 10);
            const totalItems = getBestQueryList().length;

            const checkAndTriggerPageFlip = () => {
                const isFirstChild = (capsule === listContainer.firstElementChild);
                const isLastChild = (capsule === listContainer.lastElementChild);
                
                if (isFirstChild && windowStartIndex > 0) {
                    windowStartIndex = Math.max(0, windowStartIndex - (maxVisible - 1));
                    scanMessages(true);
                } 
                else if (isLastChild && index < totalItems - 1) {
                    windowStartIndex = index;
                    scanMessages(true);
                }
            };

            // 收藏功能
            if (e.target.classList.contains('capsule-star')) {
                const key = capsule.dataset.key;
                const starEl = e.target;
                const isStarred = !!cachedStars[key];
                if (!isStarred && !starEl.classList.contains('unlocked')) {
                    starEl.classList.add('unlocked'); starEl.classList.remove('denied');
                    void starEl.offsetWidth; starEl.classList.add('denied');
                    if (!isNaN(index)) {
                        handleJump(index);
                        checkAndTriggerPageFlip();
                    }
                    return;
                }
                if (cachedStars[key]) delete cachedStars[key];
                else cachedStars[key] = true;
                localStorage.setItem(`${siteKey}_stars_${currentChatId}`, JSON.stringify(cachedStars));
                scanMessages();
                checkAndTriggerPageFlip();
                return;
            }
            
            // 跳转功能
            if (!isNaN(index)) {
                handleJump(index);
                checkAndTriggerPageFlip();
            }
        });

        // 鼠标移出重置星星锁
        listContainer.addEventListener('mouseout', (e) => {
            const capsule = e.target.closest('.nav-capsule');
            if (capsule && !capsule.contains(e.relatedTarget)) {
                capsule.querySelector('.capsule-star')?.classList.remove('unlocked');
            }
        });

        // 鼠标滚轮翻页
        listContainer.addEventListener('wheel', (e) => {
            const totalItems = getBestQueryList().length;
            if (totalItems <= maxVisible) return;
            e.preventDefault();
            if (listContainer._isScrolling) return;
            listContainer._isScrolling = true;
            setTimeout(() => listContainer._isScrolling = false, 50);
            
            if (e.deltaY > 0) { if (windowStartIndex + maxVisible < totalItems) { windowStartIndex++; scanMessages(true); } } 
            else { if (windowStartIndex > 0) { windowStartIndex--; scanMessages(true); } }
        }, { passive: false });

        // 触摸滑动翻页
        let touchStartY = 0;
        listContainer.addEventListener('touchstart', (e) => { touchStartY = e.touches[0].clientY; }, { passive: true });
        listContainer.addEventListener('touchmove', (e) => { if (getBestQueryList().length > maxVisible && e.cancelable) e.preventDefault(); }, { passive: false });
        listContainer.addEventListener('touchend', (e) => {
            const totalItems = getBestQueryList().length;
            if (totalItems <= maxVisible) return;
            const deltaY = e.changedTouches[0].clientY - touchStartY;
            if (Math.abs(deltaY) > 30) {
                const steps = Math.ceil(Math.abs(deltaY) / 38);
                if (deltaY < 0) { if (windowStartIndex + maxVisible < totalItems) windowStartIndex = Math.min(windowStartIndex + steps, totalItems - maxVisible); }
                else { if (windowStartIndex > 0) windowStartIndex = Math.max(0, windowStartIndex - steps); }
                scanMessages(true);
            }
        });
    }

    // --- 9. 拖拽与折叠控制 ---
    function setupDragAndFold(controlEl) {
        const container = document.getElementById(NAV_CONTAINER_ID);
        let hasMoved = false, isDragging = false;

        const onStart = (e) => {
            if (e.type === 'mousedown' && e.button !== 0) return;
            isDragging = true; hasMoved = false;
            controlEl.classList.add('is-dragging');
            const { x, y } = getClientXY(e);
            controlEl._startX = x; controlEl._startY = y;
        };

        const onMove = (e) => {
            if (!isDragging) return;
            const { x, y } = getClientXY(e);
            const dx = x - controlEl._startX, dy = y - controlEl._startY;
            if (!hasMoved && (dx * dx + dy * dy) > 16) hasMoved = true;

            if (hasMoved) {
                if(e.cancelable) e.preventDefault();
                if (!rafId) {
                    rafId = requestAnimationFrame(() => {
                        const rect = container.getBoundingClientRect();
                        if (dragOffsetX === 0 && dragOffsetY === 0 && controlEl._startX) { 
                             const cur = container.getBoundingClientRect();
                             dragOffsetX = controlEl._startX - cur.left; dragOffsetY = controlEl._startY - cur.top;
                        }
                        let newLeft = x - dragOffsetX, newTop = y - dragOffsetY;
                        const winW = window.innerWidth, winH = window.innerHeight;

                        if (newLeft < 0) newLeft = 0; if (newLeft + rect.width > winW) newLeft = winW - rect.width;
                        if (newTop < 0) newTop = 0; if (newTop + rect.height > winH) newTop = winH - rect.height;

                        container.style.left = newLeft + 'px'; container.style.top = newTop + 'px';
                        container.style.right = 'auto'; container.style.bottom = 'auto';
                        
                        if (newLeft + (rect.width / 2) < winW / 2) container.classList.add('ai-left-side');
                        else container.classList.remove('ai-left-side');
                        rafId = null;
                    });
                }
            }
        };

        const onEnd = (e) => {
            if (!isDragging) return;
            isDragging = false; controlEl.classList.remove('is-dragging');
            if (rafId) { cancelAnimationFrame(rafId); rafId = null; }
            if (hasMoved) {
                const rect = container.getBoundingClientRect();
                if (rect.left + (rect.width / 2) < window.innerWidth / 2) {
                    container.classList.add('ai-left-side'); container.style.left = rect.left + 'px'; container.style.right = 'auto';
                } else {
                    container.classList.remove('ai-left-side'); container.style.right = (window.innerWidth - rect.right) + 'px'; container.style.left = 'auto';
                }
                const preventClick = (ce) => { ce.preventDefault(); ce.stopPropagation(); controlEl.removeEventListener('click', preventClick, true); };
                controlEl.addEventListener('click', preventClick, true);
                fitToScreenAndScan(true);
            }
            delete controlEl._startX; delete controlEl._startY;
        };

        controlEl.addEventListener('mousedown', onStart);
        controlEl.addEventListener('touchstart', onStart, { passive: false });
        document.addEventListener('mousemove', onMove);
        document.addEventListener('touchmove', onMove, { passive: false });
        document.addEventListener('mouseup', onEnd);
        document.addEventListener('touchend', onEnd);
        controlEl.addEventListener('click', (e) => { if(!hasMoved) toggleCollapse(); });
    }

    function toggleCollapse() {
        const contentList = document.getElementById(NAV_CONTENT_ID);
        const iconSpan = document.querySelector('.control-capsule .capsule-index');
        if (!contentList) return;
        isCollapsed = !isCollapsed;
        if (isCollapsed) { contentList.classList.add('hidden'); if(iconSpan) iconSpan.textContent = '+'; } 
        else { contentList.classList.remove('hidden'); if(iconSpan) iconSpan.textContent = '≡'; setTimeout(() => fitToScreenAndScan(true), 50); }
    }

    // --- 10. 启动入口 ---
    function ensureContainer() {
        let container = document.getElementById(NAV_CONTAINER_ID);
        if (!container) {
            container = document.createElement('div'); container.id = NAV_CONTAINER_ID; document.documentElement.appendChild(container);
            const controlCapsule = document.createElement('div'); controlCapsule.className = 'nav-capsule control-capsule';
            
            const iconSpan = document.createElement('span'); iconSpan.className = 'capsule-index'; iconSpan.textContent = '≡';
            const textSpan = document.createElement('span'); textSpan.className = 'capsule-text'; textSpan.textContent = CONTROL_TEXT;
            
            controlCapsule.append(iconSpan, textSpan);
            
            setupDragAndFold(controlCapsule); container.appendChild(controlCapsule);
            const listContainer = document.createElement('div'); listContainer.id = NAV_CONTENT_ID; container.appendChild(listContainer);
            setupDelegation(listContainer);
        }
    }
})();