DeepWiki Local Chat History

自动保存在 DeepWiki 上的聊天记录到本地, 并在仓库页面显示历史列表。(SPA 修复版)

// ==UserScript==
// @name         DeepWiki Local Chat History
// @namespace    http://tampermonkey.net/
// @version      4.0
// @description  自动保存在 DeepWiki 上的聊天记录到本地, 并在仓库页面显示历史列表。(SPA 修复版)
// @author       Gemini (Updated)
// @match        https://deepwiki.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

/* eslint-env es2017 */

(async function() {
    'use strict';

    const HISTORY_KEY = 'deepWikiHistory';
    let currentPath = ''; // 跟踪当前路径, 防止重复运行
    let saveInterval = null; // 跟踪保存逻辑的轮询
    let displayInterval = null; // 跟踪显示逻辑的轮询

    // --- 1. 存储辅助函数 (GM_ functions) ---

    async function getHistory() {
        const historyJson = await GM_getValue(HISTORY_KEY, '[]');
        try {
            return JSON.parse(historyJson);
        } catch (e) {
            console.error('DeepWiki History: Failed to parse history', e);
            return [];
        }
    }

    async function saveHistory(historyArray) {
        await GM_setValue(HISTORY_KEY, JSON.stringify(historyArray));
    }

    async function addHistoryEntry(entry) {
        const history = await getHistory();
        if (!history.some(e => e.sessionId === entry.sessionId)) {
            history.push(entry);
            await saveHistory(history);
            console.log('DeepWiki History: Saved new chat.', entry);
        }
    }

    async function removeHistoryEntry(sessionId) {
        let history = await getHistory();
        history = history.filter(entry => entry.sessionId !== sessionId);
        await saveHistory(history);
        console.log('DeepWiki History: Removed chat.', sessionId);
    }

    // --- 2. 停止所有轮询 (新) ---
    // 这是修复 Bug 的关键: 停止所有正在运行的 interval
    function stopAllPollers() {
        if (saveInterval) {
            clearInterval(saveInterval);
            saveInterval = null;
        }
        if (displayInterval) {
            clearInterval(displayInterval);
            displayInterval = null;
        }
    }

    // --- 3. 页面逻辑路由 (新) ---
    // 这个主函数现在会在每次页面导航时运行
    function mainRouter() {
        const path = window.location.pathname;
        if (path === currentPath) {
            return; // 路径没变, 不做任何事
        }
        currentPath = path;

        // 关键: 在决定新路由前, 停止所有旧的轮询
        stopAllPollers();

        if (path.startsWith('/search/')) {
            // 场景 A: 聊天页面
            console.log('DeepWiki History: Router -> Chat Page');
            runSaveLogic();
        } else {
            const parts = path.split('/').filter(p => p.length > 0);
            if (parts.length === 2) {
                // 场景 B: 仓库主页
                console.log('DeepWiki History: Router -> Repo Page');
                runDisplayLogic(parts.join('/'));
            } else {
                // 其他页面 (如 /)
                console.log('DeepWiki History: Router -> Other Page, doing nothing.');
            }
        }
    }

    // --- 4. 逻辑实现 ---

    /**
     * 场景 A: 在聊天页面 (/search/...) 自动保存记录
     */
    function runSaveLogic() {
        const sessionId = window.location.pathname.split('/').pop();
        if (!sessionId) return;

        (async () => {
            const history = await getHistory();
            if (history.some(entry => entry.sessionId === sessionId)) {
                return;
            }

            let attempt = 0;
            // 启动轮询, 并保存句柄
            saveInterval = setInterval(async () => {
                attempt++;

                const repoLinkElement = document.querySelector('a.text-neutral-400[href*="/"]');
                let repoName = null;
                if (repoLinkElement) {
                    const href = repoLinkElement.getAttribute('href');
                    if (href && href.startsWith('/') && href.split('/').length === 3) {
                         repoName = href.substring(1);
                    }
                }

                const promptElement = document.querySelector('span.text-xl');
                let firstPromptText = null;
                if (promptElement) {
                    const textNode = Array.from(promptElement.childNodes).find(node => node.nodeType === Node.TEXT_NODE);
                    if (textNode) {
                        firstPromptText = textNode.textContent.trim();
                    }
                }

                if (repoName && firstPromptText && sessionId) {
                    clearInterval(saveInterval); // 停止轮询
                    saveInterval = null; // 清理句柄
                    await addHistoryEntry({
                        repo: repoName,
                        prompt: firstPromptText,
                        sessionId: sessionId,
                        fullUrl: window.location.href,
                        timestamp: new Date().toISOString()
                    });
                } else if (attempt > 20) {
                    clearInterval(saveInterval); // 超时停止
                    saveInterval = null;
                    console.log('DeepWiki History: Polling timed out. Could not find all elements.');
                }
            }, 500);
        })();
    }

    /**
     * 场景 B: 在仓库主页 (/owner/repo) 显示历史列表
     */
    function runDisplayLogic(currentRepo) {
        let attempt = 0;
        // 启动轮询, 并保存句柄
        displayInterval = setInterval(async () => {
            attempt++;

            const wikiList = document.querySelector('ul.overflow-y-auto');

            if (wikiList && !document.getElementById('tampermonkey-history-container')) {
                clearInterval(displayInterval); // 停止轮询
                displayInterval = null; // 清理句柄

                const sidebarContainer = wikiList.parentNode;
                if (!sidebarContainer) {
                    console.log('DeepWiki History: Found list but no parent container.');
                    return;
                }

                const history = await getHistory();
                const repoHistory = history
                    .filter(entry => entry.repo === currentRepo)
                    .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));

                if (repoHistory.length > 0) {
                    const container = document.createElement('div');
                    container.id = 'tampermonkey-history-container';
                    container.innerHTML = `
                        <hr style="border-top: 1px solid #eee; margin: 12px 0;">
                        <h3 style="padding: 0 8px; margin-top: 16px; margin-bottom: 8px; font-weight: 600; font-size: 14px; color: #888;">本地对话记录</h3>
                    `;

                    const list = document.createElement('ul');
                    list.style.listStyle = 'none';
                    list.style.padding = '0';
                    list.style.margin = '0';

                    repoHistory.forEach(entry => {
                        const li = document.createElement('li');
                        li.style.display = 'flex';
                        li.style.justifyContent = 'space-between';
                        li.style.alignItems = 'center';
                        li.style.padding = '0 8px';
                        li.style.marginBottom = '4px';
                        li.style.fontSize = '14px';

                        const link = document.createElement('a');
                        link.href = entry.fullUrl;
                        link.textContent = entry.prompt;
                        link.title = `保存于: ${new Date(entry.timestamp).toLocaleString()}\n点击跳转: ${entry.fullUrl}`;
                        link.className = 'hover:bg-hover block w-full rounded px-2 py-1.5 text-left text-sm transition-none text-secondary';
                        link.style.flex = '1';
                        link.style.overflow = 'hidden';
                        link.style.textOverflow = 'ellipsis';
                        link.style.whiteSpace = 'nowrap';
                        link.style.textDecoration = 'none';

                        const deleteBtn = document.createElement('button');
                        deleteBtn.textContent = '✕';
                        deleteBtn.title = '移除此条记录';
                        deleteBtn.style.marginLeft = '10px';
                        deleteBtn.style.cursor = 'pointer';
                        deleteBtn.style.border = 'none';
                        deleteBtn.style.background = 'transparent';
                        deleteBtn.style.color = '#999';
                        deleteBtn.style.fontSize = '16px';
                        deleteBtn.style.padding = '0 5px';
                        deleteBtn.addEventListener('mouseover', () => deleteBtn.style.color = '#333');
                        deleteBtn.addEventListener('mouseout', () => deleteBtn.style.color = '#999');

                        deleteBtn.addEventListener('click', async (e) => {
                            e.preventDefault();
                            e.stopPropagation();
                            if (confirm(`是否移除 "${entry.prompt}" 这条记录?\n(这只会从您的本地存储中删除)`)) {
                                await removeHistoryEntry(entry.sessionId);
                                li.remove();
                                if (list.children.length === 0) {
                                    container.remove();
                                }
                            }
                        });

                        li.appendChild(link);
                        li.appendChild(deleteBtn);
                        list.appendChild(li);
                    });

                    container.appendChild(list);
                    sidebarContainer.appendChild(container);
                }
            } else if (attempt > 20) {
                clearInterval(displayInterval); // 超时停止
                displayInterval = null;
                console.log('DeepWiki History: Polling timed out. Could not find sidebar (ul.overflow-y-auto).');
            }
        }, 500);
    }

    // --- 5. 启动器 (新) ---
    // 使用 MutationObserver 监视 URL 变化 (SPA 兼容)
    // 我们监视 <title> 元素的变化, 这是一个非常可靠的 SPA 导航信号
    const observer = new MutationObserver((mutations) => {
        mainRouter(); // 每次 <title> 变化时, 重新运行路由
    });

    // 等待 <title> 元素出现, 然后开始监视
    let titlePoll = setInterval(() => {
        const titleElement = document.querySelector('head > title');
        if (titleElement) {
            clearInterval(titlePoll);
            observer.observe(titleElement, { childList: true });
            // 立即运行一次路由, 以处理当前页面
            mainRouter();
        }
    }, 100);

})();

QingJ © 2025

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