Claude Conversation Exporter Plus

优雅导出 Claude 对话记录,支持 JSON 和 Markdown 格式

目前为 2024-11-19 提交的版本。查看 最新版本

// ==UserScript==
// @name         Claude Conversation Exporter Plus
// @namespace    http://tampermonkey.net/
// @version      4.6
// @description  优雅导出 Claude 对话记录,支持 JSON 和 Markdown 格式
// @author       Gao + GPT-4 + Claude
// @license      Custom License
// @match        https://*.claudesvip.top/chat/*
// @match        https://*.claude.ai/chat/*
// @match        https://*.fuclaude.com/chat/*
// @grant        none
// ==/UserScript==

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

(function() {
    'use strict';

    // 状态追踪
    let state = {
        targetResponse: null,
        lastUpdateTime: null,
        convertedMd: null
    };

    // 日志函数
    const log = {
        info: (msg) => console.log(`[Claude Saver] ${msg}`),
        error: (msg, e) => console.error(`[Claude Saver] ${msg}`, e)
    };

    // 正则表达式用于匹配目标 URL
    const targetUrlPattern = /\/chat_conversations\/[\w-]+\?tree=True&rendering_mode=messages&render_all_tools=true/;

    // 响应处理函数(处理符合匹配模式的响应)
    function processTargetResponse(text, url) {
        try {
            if (targetUrlPattern.test(url)) {
                state.targetResponse = text;
                state.lastUpdateTime = new Date().toLocaleTimeString();
                updateButtonStatus();
                log.info(`成功捕获目标响应 (${text.length} bytes) 来自: ${url}`);

                // 转换为Markdown
                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}
数据已准备好` : '等待目标响应中...';
            jsonButton.title = statusText;
            mdButton.title = statusText;
        }
    }

    // 创建下载按钮
    function createDownloadButtons() {
        // JSON 下载按钮
        const jsonButton = document.createElement('button');
        const mdButton = document.createElement('button');

        const buttonStyles = {
            padding: '10px 15px',
            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',
            marginRight: '10px'
        };

        jsonButton.id = 'downloadJsonButton';
        jsonButton.innerText = '下载 JSON';
        mdButton.id = 'downloadMdButton';
        mdButton.innerText = '下载 Markdown';

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

        // 鼠标悬停效果
        const onMouseOver = (button) => {
            button.style.transform = 'scale(1.05)';
            button.style.boxShadow = '0 4px 8px rgba(0,0,0,0.3)';
        };
        const onMouseOut = (button) => {
            button.style.transform = 'scale(1)';
            button.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)';
        };

        jsonButton.onmouseover = () => onMouseOver(jsonButton);
        jsonButton.onmouseout = () => onMouseOut(jsonButton);
        mdButton.onmouseover = () => onMouseOver(mdButton);
        mdButton.onmouseout = () => onMouseOut(mdButton);

        // 下载 JSON 功能
        jsonButton.onclick = function() {
            if (!state.targetResponse) {
                alert(`还没有发现有效的对话记录。
请等待目标响应或进行一些对话。`);
                return;
            }

            try {
                const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
                const chatName = document.title.trim().replace(/\s+-\s+Claude$/, '').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('下载过程中发生错误,请查看控制台了解详情。');
            }
        };

        // 下载 Markdown 功能
        mdButton.onclick = function() {
            if (!state.convertedMd) {
                alert(`还没有发现有效的对话记录。
请等待目标响应或进行一些对话。`);
                return;
            }

            try {
                const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
                const chatName = document.title.trim().replace(/\s+-\s+Claude$/, '').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('下载过程中发生错误,请查看控制台了解详情。');
            }
        };

        const buttonContainer = document.createElement('div');
        buttonContainer.style.position = 'fixed';
        buttonContainer.style.top = '45%';
        buttonContainer.style.right = '10px';
        buttonContainer.style.transform = 'translateY(-50%)';
        buttonContainer.style.zIndex = '9999';
        buttonContainer.style.display = 'flex';
        buttonContainer.style.flexDirection = 'row';

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

        updateButtonStatus();
    }

    // JSON 转 Markdown 转换函数
    function convertJsonToMd(data) {
        let mdContent = [];
        const title = document.title.trim().replace(/\s+-\s+Claude$/, '');
        mdContent.push(`# ${title}\n`);

        for (const message of data['chat_messages']) {
            const sender = message['sender'].charAt(0).toUpperCase() + message['sender'].slice(1);
            mdContent.push(`## ${sender}`);

            const createdAt = message['created_at'] || '';
            const updatedAt = message['updated_at'] || '';
            const timestamp = createdAt === updatedAt ? `*${createdAt}*` : `*${createdAt} (updated)*`;
            mdContent.push(timestamp);

            const content = processContent(message['content']);

            // 如果是assistant的消息,调整其中的标题级别
            if (sender.toLowerCase() === 'assistant') {
                mdContent.push(adjustHeadingLevel(content));
            } else {
                mdContent.push(`${content}\n`);
            }
        }

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

    // 调整Markdown标题级别
    function adjustHeadingLevel(text, increaseLevel = 2) {
        const codeBlockPattern = /```[\s\S]*?```/g;
        let segments = [];
        let match;

        // 提取代码块,并用占位符替代
        let lastIndex = 0;
        while ((match = codeBlockPattern.exec(text)) !== null) {
            segments.push(text.substring(lastIndex, match.index));
            segments.push(match[0]); // 保留代码块原样
            lastIndex = codeBlockPattern.lastIndex;
        }
        segments.push(text.substring(lastIndex));

        // 调整标题级别
        segments = segments.map(segment => {
            if (segment.startsWith('```')) {
                return segment; // 保留代码块原样
            } else {
                let lines = segment.split('\n');
                lines = lines.map(line => {
                    if (line.trim().startsWith('#')) {
                        const currentLevel = (line.match(/^#+/) || [''])[0].length;
                        return '#'.repeat(currentLevel + increaseLevel) + line.slice(currentLevel);
                    }
                    return line;
                });
                return lines.join('\n');
            }
        });

        return segments.join('');
    }

    // 处理消息内容,提取纯文本并处理LaTeX公式
    function processContent(content) {
        if (Array.isArray(content)) {
            let textParts = [];
            for (const item of content) {
                if (item.type === 'text') {
                    let text = item.text || '';
                    text = processLatex(text);
                    text = text.replace(/(?<!\n)(\n\| .*? \|\n\|[-| ]+\|\n(?:\| .*? \|\n)+)/g, '\n$1'); // 在表格前插入一个空行
                    textParts.push(text);
                }
            }
            return textParts.join('\n');
        }
        return String(content);
    }

    // 处理LaTeX公式
    function processLatex(text) {
        // 区分行内公式和独立公式
        text = text.replace(/\$\$(.+?)\$\$/gs, (match, formula) => {
            if (formula.includes('\n')) {
                // 这是独立公式
                return `$$${formula}$$`;
            } else {
                // 这是行内公式
                return `$${formula}$`;
            }
        });
        return text;
    }

    // 监听 fetch 请求
    const originalFetch = window.fetch;
    window.fetch = async function(...args) {
        const response = await originalFetch.apply(this, args);
        const url = args[0];

        log.info(`捕获到 fetch 请求: ${url}`);

        if (targetUrlPattern.test(url)) {
            try {
                log.info(`匹配到目标 URL: ${url}`);
                const clonedResponse = response.clone();
                clonedResponse.text().then(text => {
                    processTargetResponse(text, url);
                }).catch(e => {
                    log.error('解析fetch响应时出错:', e);
                });
            } catch (e) {
                log.error('克隆fetch响应时出错:', e);
            }
        }
        return response;
    };

    // 页面加载完成后立即创建按钮
    window.addEventListener('load', function() {
        createDownloadButtons();

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

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

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

QingJ © 2025

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