DeepWiki Local Chat History

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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);

})();