DeepSeek 对话导出器 | DeepSeek Conversation Exporter Plus

优雅导出 DeepSeek 对话记录,支持 JSON 和 Markdown 格式。Elegantly export DeepSeek conversation records, supporting JSON and Markdown formats.

// ==UserScript==
// @name         DeepSeek 对话导出器 | DeepSeek Conversation Exporter Plus
// @namespace    http://tampermonkey.net/
// @version      0.0.5
// @description  优雅导出 DeepSeek 对话记录,支持 JSON 和 Markdown 格式。Elegantly export DeepSeek conversation records, supporting JSON and Markdown formats.
// @author       Gao + GPT-4 + Claude
// @license      Custom License
// @match        https://*.deepseek.com/a/chat/s/*
// @grant        none
// ==/UserScript==

/*
 您可以在个人设备上使用和修改该代码。
 不得将该代码或其修改版本重新分发、再发布或用于其他公众渠道。
 保留所有权利,未经授权不得用于商业用途。
*/

/*
You may use and modify this code on your personal devices.
You may not redistribute, republish, or use this code or its modified versions in other public channels.
All rights reserved. Unauthorized commercial use is prohibited.
*/

(function() {
    'use strict';

    let state = {
        targetResponse: null,
        lastUpdateTime: null,
        convertedMd: null
    };

    const log = {
        info: (msg) => console.log(`[DeepSeek Saver] ${msg}`),
        error: (msg, e) => console.error(`[DeepSeek Saver] ${msg}`, e)
    };

    const targetUrlPattern = /chat_session_id=/;

    function processTargetResponse(text, url) {
        try {
            if (text.length < 1024) {
                log.error('缓存故障,请重新发一条消息或者删除网站数据重新登录!');
                alert('缓存故障,请重新发一条消息或者删除网站数据重新登录!');
                return;
            }

            if (targetUrlPattern.test(url)) {
                state.targetResponse = text;
                state.lastUpdateTime = new Date().toLocaleTimeString();
                updateButtonStatus();
                log.info(`成功捕获目标响应 (${text.length} bytes) 来自: ${url}`);

                state.convertedMd = convertJsonToMd(JSON.parse(text));
                log.info('成功将JSON转换为Markdown');
            }
        } catch (e) {
            log.error('处理目标响应时出错:', e);
        }
    }

    function updateButtonStatus() {
        const jsonButton = document.getElementById('downloadJsonButton');
        const mdButton = document.getElementById('downloadMdButton');
        if (jsonButton && mdButton) {
            const hasResponse = state.targetResponse !== null;
            jsonButton.style.backgroundColor = hasResponse ? '#28a745' : '#007bff';
            mdButton.style.backgroundColor = state.convertedMd ? '#28a745' : '#007bff';
            const statusText = hasResponse ? `最后更新: ${state.lastUpdateTime}\n数据已准备好` : '等待目标响应中...';
            jsonButton.title = statusText;
            mdButton.title = statusText;
        }
    }

 function createDownloadButtons() {
        const buttonContainer = document.createElement('div');
        const jsonButton = document.createElement('button');
        const mdButton = document.createElement('button');

        Object.assign(buttonContainer.style, {
            position: 'fixed',
            top: '45%',
            right: '10px',
            zIndex: '9999',
            display: 'flex',
            flexDirection: 'column',
            gap: '10px',
            opacity: '0.5',
            transition: 'opacity 0.3s ease',
            cursor: 'move'
        });

        const buttonStyles = {
            padding: '8px 12px',
            backgroundColor: '#007bff',
            color: '#ffffff',
            border: 'none',
            borderRadius: '5px',
            cursor: 'pointer',
            transition: 'all 0.3s ease',
            fontFamily: 'Arial, sans-serif',
            boxShadow: '0 2px 5px rgba(0,0,0,0.2)',
            whiteSpace: 'nowrap',
            fontSize: '14px'
        };

        jsonButton.id = 'downloadJsonButton';
        jsonButton.innerText = 'JSON';
        mdButton.id = 'downloadMdButton';
        mdButton.innerText = 'MD';

        Object.assign(jsonButton.style, buttonStyles);
        Object.assign(mdButton.style, buttonStyles);

        buttonContainer.onmouseenter = () => buttonContainer.style.opacity = '1';
        buttonContainer.onmouseleave = () => buttonContainer.style.opacity = '0.5';

        let isDragging = false;
        let currentX;
        let currentY;
        let initialX;
        let initialY;
        let xOffset = 0;
        let yOffset = 0;

        buttonContainer.onmousedown = dragStart;
        document.onmousemove = drag;
        document.onmouseup = dragEnd;

        function dragStart(e) {
            initialX = e.clientX - xOffset;
            initialY = e.clientY - yOffset;
            if (e.target === buttonContainer) {
                isDragging = true;
            }
        }

        function drag(e) {
            if (isDragging) {
                e.preventDefault();
                currentX = e.clientX - initialX;
                currentY = e.clientY - initialY;
                xOffset = currentX;
                yOffset = currentY;
                setTranslate(currentX, currentY, buttonContainer);
            }
        }

        function dragEnd() {
            isDragging = false;
        }

        function setTranslate(xPos, yPos, el) {
            el.style.transform = `translate(${xPos}px, ${yPos}px)`;
        }

        jsonButton.onclick = function() {
            if (!state.targetResponse) {
                alert('还没有发现有效的对话记录。\n请等待目标响应或进行一些对话。');
                return;
            }
            try {
                const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
                const jsonData = JSON.parse(state.targetResponse);
                const chatName = `DeepSeek - ${jsonData.data.biz_data.chat_session.title || 'Untitled Chat'}`.replace(/[\/\\?%*:|"<>]/g, '-');
                const fileName = `${chatName}_${timestamp}.json`;

                const blob = new Blob([state.targetResponse], { type: 'application/json' });
                const link = document.createElement('a');
                link.href = URL.createObjectURL(blob);
                link.download = fileName;
                link.click();

                log.info(`成功下载文件: ${fileName}`);
            } catch (e) {
                log.error('下载过程中出错:', e);
                alert('下载过程中发生错误,请查看控制台了解详情。');
            }
        };

        mdButton.onclick = function() {
            if (!state.convertedMd) {
                alert('还没有发现有效的对话记录。\n请等待目标响应或进行一些对话。');
                return;
            }
            try {
                const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
                const jsonData = JSON.parse(state.targetResponse);
                const chatName = `DeepSeek - ${jsonData.data.biz_data.chat_session.title || 'Untitled Chat'}`.replace(/[\/\\?%*:|"<>]/g, '-');
                const fileName = `${chatName}_${timestamp}.md`;

                const blob = new Blob([state.convertedMd], { type: 'text/markdown' });
                const link = document.createElement('a');
                link.href = URL.createObjectURL(blob);
                link.download = fileName;
                link.click();

                log.info(`成功下载文件: ${fileName}`);
            } catch (e) {
                log.error('下载过程中出错:', e);
                alert('下载过程中发生错误,请查看控制台了解详情。');
            }
        };

        buttonContainer.appendChild(jsonButton);
        buttonContainer.appendChild(mdButton);
        document.body.appendChild(buttonContainer);

        updateButtonStatus();
    }

    function convertJsonToMd(data) {
        let mdContent = [];
        const title = data.data.biz_data.chat_session.title || 'Untitled Chat';
        const totalTokens = data.data.biz_data.chat_messages.reduce((acc, msg) => acc + msg.accumulated_token_usage, 0);
        mdContent.push(`# DeepSeek - ${title} (Total Tokens: ${totalTokens})\n`);

        data.data.biz_data.chat_messages.forEach(msg => {
            const role = msg.role === 'USER'? 'Human' : 'Assistant';
            mdContent.push(`### ${role}`);

            const timestamp = new Date(msg.inserted_at * 1000).toISOString();
            mdContent.push(`*${timestamp}*\n`);

            if (msg.files && msg.files.length > 0) {
                msg.files.forEach(file => {
                    const insertTime = new Date(file.inserted_at * 1000).toISOString();
                    const updateTime = new Date(file.updated_at * 1000).toISOString();
                    mdContent.push(`### File Information`);
                    mdContent.push(`- Name: ${file.file_name}`);
                    mdContent.push(`- Size: ${file.file_size} bytes`);
                    mdContent.push(`- Token Usage: ${file.token_usage}`);
                    mdContent.push(`- Upload Time: ${insertTime}`);
                    mdContent.push(`- Last Update: ${updateTime}\n`);
                });
            }

            let content = msg.content;

            if (msg.search_results && msg.search_results.length > 0) {
                const citations = {};
                msg.search_results.forEach((result, index) => {
                    if (result.cite_index !== null) {
                        citations[result.cite_index] = result.url;
                    }
                });
                content = content.replace(/\[citation:(\d+)\]/g, (match, p1) => {
                    const url = citations[parseInt(p1)];
                    return url? ` [${p1}](${url})` : match;
                });
                content = content.replace(/\s+,/g, ',').replace(/\s+\./g, '.');
            }

            if (msg.thinking_content) {
                const thinkingTime = msg.thinking_elapsed_secs? `(${msg.thinking_elapsed_secs}s)` : '';
                content += `\n\n**Thinking Process ${thinkingTime}:**\n${msg.thinking_content}`;
            }

            content = content.replace(/\$\$(.*?)\$\$/gs, (match, formula) => {
                return formula.includes('\n')? `\n$$\n${formula}\n$$\n` : `$$${formula}$$`;
            });

            mdContent.push(content + '\n');
        });

        return mdContent.join('\n');
    }

    const hookXHR = () => {
        const originalXHR = window.XMLHttpRequest.prototype.open;
        window.XMLHttpRequest.prototype.open = function() {
            this.addEventListener('load', function() {
                const url = this.responseURL;
                if (url && url.includes('history_messages?chat_session_id')) {
                    processTargetResponse(this.responseText, url);
                }
            });
            originalXHR.apply(this, arguments);
        };
    };
    hookXHR();

    window.addEventListener('load', function() {
        createDownloadButtons();

        const observer = new MutationObserver(() => {
            if (!document.getElementById('downloadJsonButton') || !document.getElementById('downloadMdButton')) {
                log.info('检测到按钮丢失,正在重新创建...');
                createDownloadButtons();
            }
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });

        log.info('DeepSeek 保存脚本已启动');
    });
})();