Claude Exporter 0.7

Export Claude conversations using API

当前为 2025-07-09 提交的版本,查看 最新版本

// ==UserScript==
// @name         Claude Exporter 0.7
// @namespace    http://tampermonkey.net/
// @version      0.7
// @description  Export Claude conversations using API
// @author       MRL
// @match        https://claude.ai/chat/*
// @grant        GM_registerMenuCommand
// @grant        GM_download
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // Utility functions
    function generateTimestamp() {
        const now = new Date();
        return now.getFullYear() +
               String(now.getMonth() + 1).padStart(2, '0') +
               String(now.getDate()).padStart(2, '0') +
               String(now.getHours()).padStart(2, '0') +
               String(now.getMinutes()).padStart(2, '0') +
               String(now.getSeconds()).padStart(2, '0');
    }

    function sanitizeFileName(name) {
        return name.replace(/[\\/:*?"<>|]/g, '_')
                  .replace(/\s+/g, '_')
                  .replace(/__+/g, '_')
                  .replace(/^_+|_+$/g, '')
                  .slice(0, 100);
    }

    function downloadFile(filename, content) {
        const blob = new Blob([content], { type: 'text/plain' });
        const url = URL.createObjectURL(blob);
        const link = document.createElement('a');
        link.href = url;
        link.download = filename;
        link.click();
        setTimeout(() => {
            URL.revokeObjectURL(url);
        }, 100);
    }

    function showNotification(message, type = "info") {
        const notification = document.createElement('div');
        notification.style.cssText = `
            position: fixed;
            top: 20px;
            right: 20px;
            padding: 15px 20px;
            border-radius: 5px;
            color: white;
            font-family: system-ui, -apple-system, sans-serif;
            font-size: 14px;
            z-index: 10000;
            max-width: 400px;
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
        `;

        if (type === "error") {
            notification.style.backgroundColor = '#f44336';
        } else if (type === "success") {
            notification.style.backgroundColor = '#4CAF50';
        } else {
            notification.style.backgroundColor = '#2196F3';
        }

        notification.textContent = message;
        document.body.appendChild(notification);

        setTimeout(() => {
            if (notification.parentNode) {
                notification.parentNode.removeChild(notification);
            }
        }, 5000);
    }

    // API functions
    function getConversationId() {
        const match = window.location.pathname.match(/\/chat\/([^/?]+)/);
        return match ? match[1] : null;
    }

    function getOrgId() {
        const cookies = document.cookie.split(';');
        for (const cookie of cookies) {
            const [name, value] = cookie.trim().split('=');
            if (name === 'lastActiveOrg') {
                return value;
            }
        }
        throw new Error('Could not find organization ID');
    }

    async function getConversationData() {
        const conversationId = getConversationId();
        if (!conversationId) {
            throw new Error('Not in a conversation');
        }

        const orgId = getOrgId();
        const response = await fetch(`/api/organizations/${orgId}/chat_conversations/${conversationId}?tree=true&rendering_mode=messages&render_all_tools=true`);

        if (!response.ok) {
            throw new Error(`API request failed: ${response.status}`);
        }

        return await response.json();
    }

    // Text processing functions
    async function getTextFromContent(content) {
        let textPieces = [];

        if (content.text) {
            textPieces.push(content.text);
        }
        if (content.input) {
            textPieces.push(JSON.stringify(content.input));
        }
        if (content.content) {
            if (Array.isArray(content.content)) {
                for (const nestedContent of content.content) {
                    textPieces = textPieces.concat(await getTextFromContent(nestedContent));
                }
            } else if (typeof content.content === 'object') {
                textPieces = textPieces.concat(await getTextFromContent(content.content));
            }
        }
        return textPieces;
    }

    // Artifact processing functions
    function extractArtifacts(conversationData) {
        const artifacts = new Map(); // Map<artifactId, Array<{version, command, uuid, content, title, old_str, new_str}>>

        conversationData.chat_messages.forEach(message => {
            message.content.forEach(content => {
                if (content.type === 'tool_use' && content.name === 'artifacts' && content.input) {
                    const input = content.input;
                    const artifactId = input.id;

                    if (!artifacts.has(artifactId)) {
                        artifacts.set(artifactId, []);
                    }

                    const versions = artifacts.get(artifactId);
                    versions.push({
                        version: versions.length + 1,
                        command: input.command,
                        uuid: input.version_uuid,
                        content: input.content || '',
                        old_str: input.old_str || '',
                        new_str: input.new_str || '',
                        title: input.title || `Artifact ${artifactId}`,
                        timestamp: message.created_at
                    });
                }
            });
        });

        return artifacts;
    }

    // Artifact processing functions for command update
    function applyUpdate(previousContent, oldStr, newStr) {
        if (!previousContent || !oldStr) {
            console.warn('Cannot apply update: missing previousContent or oldStr');
            return previousContent || '';
        }

        // Apply the string replacement
        const updatedContent = previousContent.replace(oldStr, newStr);

        if (updatedContent === previousContent) {
            console.warn('Update did not change content - old string not found');
            console.warn('Looking for:', oldStr.substring(0, 100) + '...');
            console.warn('In content length:', previousContent.length);
            
            // Попробуем найти похожие строки для отладки
            const lines = previousContent.split('\n');
            const oldLines = oldStr.split('\n');
            if (oldLines.length > 0) {
                const firstOldLine = oldLines[0].trim();
                const foundLine = lines.find(line => line.includes(firstOldLine));
                if (foundLine) {
                    console.warn('Found similar line:', foundLine);
                }
            }
        }

        return updatedContent;
    }

    function buildArtifactVersions(artifacts) {
        const processedArtifacts = new Map();

        artifacts.forEach((versions, artifactId) => {
            const processedVersions = [];
            let currentContent = '';

            versions.forEach((version, index) => {
                let changeDescription = '';
                
                switch (version.command) {
                    case 'create':
                        currentContent = version.content;
                        changeDescription = 'Created';
                        break;
                    case 'rewrite':
                        currentContent = version.content;
                        changeDescription = 'Rewritten';
                        break;
                    case 'update':
                        const oldContent = currentContent;
                        currentContent = applyUpdate(currentContent, version.old_str, version.new_str);
                        
                        // Создаем более информативное описание изменений
                        const oldPreview = version.old_str ? version.old_str.substring(0, 100) + '...' : '';
                        const newPreview = version.new_str ? version.new_str.substring(0, 100) + '...' : '';
                        changeDescription = `Updated: "${oldPreview}" → "${newPreview}"`;
                        
                        // Добавляем информацию о том, сколько символов изменилось
                        const oldLength = oldContent.length;
                        const newLength = currentContent.length;
                        const lengthDiff = newLength - oldLength;
                        if (lengthDiff > 0) {
                            changeDescription += ` (+${lengthDiff} chars)`;
                        } else if (lengthDiff < 0) {
                            changeDescription += ` (${lengthDiff} chars)`;
                        }
                        break;
                    default:
                        console.warn(`Unknown command: ${version.command}`);
                        break;
                }

                processedVersions.push({
                    ...version,
                    fullContent: currentContent,
                    changeDescription: changeDescription
                });
            });

            processedArtifacts.set(artifactId, processedVersions);
        });

        return processedArtifacts;
    }

    // Export functions
    function generateConversationMarkdown(conversationData) {
        let markdown = '';

        // Header
        markdown += `# ${conversationData.name}\n\n`;
        markdown += `*URL: https://claude.ai/chat/${conversationData.uuid} *\n`;
        
        // Project info (if available)
        if (conversationData.project) {
            markdown += `*Project:* [${conversationData.project.name}] (https://claude.ai/project/${conversationData.project.uuid})\n`;
        }
        
        markdown += `*Сreated: ${conversationData.created_at}*\n`;
        markdown += `*Updated: ${conversationData.updated_at}*\n`;
        markdown += `*Exported on: ${new Date().toLocaleString()}*\n`;
        
        if (conversationData.model) {
            markdown += `*Model: ${conversationData.model}*\n`;
        }
        
        markdown += `\n`;
        
        // Messages
        conversationData.chat_messages.forEach(message => {
            const role = message.sender === 'human' ? 'Human' : 'Claude';
            markdown += `## ${role}\n`;
            markdown += `*UUID:* \`${message.uuid}\`\n`;
            markdown += `*Created:* ${message.created_at}\n\n`;

            message.content.forEach(content => {
                if (content.type === 'text') {
                    markdown += content.text + '\n\n';
                } else if (content.type === 'tool_use' && content.name === 'artifacts') {
                    const input = content.input;
                    markdown += `**Artifact Created:** ${input.title}\n`;
                    markdown += `*ID:* \`${input.id}\`\n`;
                    markdown += `*Command:* \`${input.command}\`\n\n`;
                } else if (content.type === 'thinking') {
                    if (content.thinking) {
                        markdown += `*[Claude thinking...]*\n\n`;
                        markdown += `<details>\n<summary>Thinking process</summary>\n\n`;
                        markdown += content.thinking + '\n\n';
                        markdown += `</details>\n\n`;
                    } else {
                        markdown += `*[Claude thinking...]*\n\n`;
                    }
                }
            });
        });

        return markdown;
    }

    async function exportConversation(finalVersionsOnly = false) {
        try {
            showNotification('Fetching conversation data...', 'info');

            const conversationData = await getConversationData();
            const timestamp = generateTimestamp();
            const conversationId = conversationData.uuid;
            const safeTitle = sanitizeFileName(conversationData.name);

            // Export main conversation
            const conversationMarkdown = generateConversationMarkdown(conversationData);
            const conversationFilename = `${timestamp}_${conversationId}__${safeTitle}.md`;
            downloadFile(conversationFilename, conversationMarkdown);

            // Extract and process artifacts
            const rawArtifacts = extractArtifacts(conversationData);
            const processedArtifacts = buildArtifactVersions(rawArtifacts);

            if (processedArtifacts.size === 0) {
                showNotification('No artifacts found in conversation', 'info');
                return;
            }

            let totalExported = 0;

            // Export artifacts
            processedArtifacts.forEach((versions, artifactId) => {
                const versionsToExport = finalVersionsOnly ?
                    [versions[versions.length - 1]] : // Only last version
                    versions; // All versions

                versionsToExport.forEach(version => {
                    const safeArtifactTitle = sanitizeFileName(version.title);
                    const filename = `${timestamp}_${conversationId}_${artifactId}_v${version.version}_${safeArtifactTitle}.md`;

                    let content = `# ${version.title}\n\n`;
                    content += `*Artifact ID:* \`${artifactId}\`\n`;
                    content += `*Version:* ${version.version}\n`;
                    content += `*Command:* \`${version.command}\`\n`;
                    content += `*UUID:* \`${version.uuid}\`\n`;
                    content += `*Created:* ${version.timestamp}\n`;
                    
                    // Добавляем информацию об изменениях
                    if (version.changeDescription) {
                        content += `*Change:* ${version.changeDescription}\n`;
                    }
                    
                    content += '\n---\n\n';
                    content += version.fullContent;

                    downloadFile(filename, content);
                    totalExported++;
                });
            });

            const mode = finalVersionsOnly ? 'final versions' : 'all versions';
            showNotification(`Export completed! Downloaded conversation + ${totalExported} artifacts (${mode})`, 'success');

        } catch (error) {
            console.error('Export failed:', error);
            showNotification(`Export failed: ${error.message}`, 'error');
        }
    }

    // Initialize
    function init() {
        console.log('[Claude API Exporter] Initializing...');

        // Register menu commands
        GM_registerMenuCommand('Export Conversation + Final Artifact Versions', () => exportConversation(true));
        GM_registerMenuCommand('Export Conversation + All Artifact Versions', () => exportConversation(false));
    }

    // Start when DOM is ready
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();