Lyra's Exporter Fetch

Claude conversation management companion script: One-click UUID extraction and complete JSON export with tree-branch mode support. Designed for power Claude users to easily manage hundreds of conversation windows, export complete timelines, thinking processes, tool calls and Artifacts. Works with Lyra's Exporter management app to turn every AI chat into your digital asset!

// ==UserScript==
// @name         Lyra's Exporter Fetch
// @namespace    userscript://lyra-conversation-exporter
// @version      2.3
// @description  Claude对话管理工具的配套脚本:一键获取对话UUID和完整JSON数据,支持树形分支模式导出。为重度Claude用户打造,轻松管理数百个对话窗口,导出完整时间线、思考过程、工具调用和Artifacts。配合Lyra's Exporter使用,让每次AI对话都成为您的数字资产!
// @description:en Claude conversation management companion script: One-click UUID extraction and complete JSON export with tree-branch mode support. Designed for power Claude users to easily manage hundreds of conversation windows, export complete timelines, thinking processes, tool calls and Artifacts. Works with Lyra's Exporter management app to turn every AI chat into your digital asset!
// @homepage     https://github.com/Yalums/Lyra-s-Claude-Exporter
// @supportURL   https://github.com/Yalums/Lyra-s-Claude-Exporter/issues
// @author       Yalums
// @match        https://claude.ai/*
// @run-at       document-start
// @grant        none
// @license      GNU General Public License v3.0
// ==/UserScript==

(function() {
    'use strict';

    // 存储请求到的用户ID
    let capturedUserId = '';
    // 存储工具栏的折叠状态
    let isCollapsed = localStorage.getItem('claudeToolCollapsed') === 'true';

    // 拦截XMLHttpRequest
    const originalXHROpen = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function(method, url) {
        const organizationsMatch = url.match(/api\/organizations\/([a-zA-Z0-9-]+)/);
        if (organizationsMatch && organizationsMatch[1]) {
            capturedUserId = organizationsMatch[1];
            console.log("✨ 已请求用户ID:", capturedUserId);
        }
        return originalXHROpen.apply(this, arguments);
    };

    // 拦截fetch请求
    const originalFetch = window.fetch;
    window.fetch = function(resource, options) {
        if (typeof resource === 'string') {
            const organizationsMatch = resource.match(/api\/organizations\/([a-zA-Z0-9-]+)/);
            if (organizationsMatch && organizationsMatch[1]) {
                capturedUserId = organizationsMatch[1];
                console.log("✨ 已请求用户ID:", capturedUserId);
            }
        }
        return originalFetch.apply(this, arguments);
    };

    const CONTROL_ID = "lyra-tool-container";
    const SWITCH_ID = "lyra-tree-mode";
    const TOGGLE_ID = "lyra-toggle-button";

    function injectCustomStyle() {
        const style = document.createElement('style');
        style.textContent = `
          #${CONTROL_ID} {
            position: fixed;
            right: 10px;
            bottom: 80px;
            display: flex;
            flex-direction: column;
            gap: 8px;
            z-index: 999999;
            transition: all 0.3s ease;
            background: rgba(255, 255, 255, 0.9);
            border-radius: 12px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            padding: 10px;
            border: 1px solid rgba(77, 171, 154, 0.3);
            max-width: 200px;
          }
          
          #${CONTROL_ID}.collapsed {
            transform: translateX(calc(100% - 40px));
          }
          
          #${TOGGLE_ID} {
            position: absolute;
            left: 0;
            top: 10px;
            width: 28px;
            height: 28px;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            background: rgba(77, 171, 154, 0.2);
            color: #4DAB9A;
            cursor: pointer;
            border: 1px solid rgba(77, 171, 154, 0.3);
            transition: all 0.3s;
            transform: translateX(-50%);
          }
          
          #${TOGGLE_ID}:hover {
            background: rgba(77, 171, 154, 0.3);
          }
          
          .lyra-main-controls {
            display: flex;
            flex-direction: column;
            gap: 8px;
            padding-left: 15px;
          }
          
          .lyra-button {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            padding: 8px 10px;
            border-radius: 8px;
            cursor: pointer;
            font-size: 14px;
            background-color: rgba(77, 171, 154, 0.1);
            color: #4DAB9A;
            border: 1px solid rgba(77, 171, 154, 0.2);
            transition: all 0.3s;
            text-align: left;
          }
          
          .lyra-button:hover {
            background-color: rgba(77, 171, 154, 0.2);
          }
          
          .lyra-toggle {
            display: flex;
            align-items: center;
            font-size: 13px;
            margin-bottom: 5px;
          }
          
          .lyra-switch {
            position: relative;
            display: inline-block;
            width: 32px;
            height: 16px;
            margin: 0 5px;
          }
          
          .lyra-switch input {
            opacity: 0;
            width: 0;
            height: 0;
          }
          
          .lyra-slider {
            position: absolute;
            cursor: pointer;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background-color: #ccc;
            transition: .4s;
            border-radius: 34px;
          }
          
          .lyra-slider:before {
            position: absolute;
            content: "";
            height: 12px;
            width: 12px;
            left: 2px;
            bottom: 2px;
            background-color: white;
            transition: .4s;
            border-radius: 50%;
          }
          
          input:checked + .lyra-slider {
            background-color: #4DAB9A;
          }
          
          input:checked + .lyra-slider:before {
            transform: translateX(16px);
          }
          
          .lyra-toast {
            position: fixed;
            bottom: 60px;
            right: 20px;
            background-color: #323232;
            color: white;
            padding: 8px 12px;
            border-radius: 6px;
            z-index: 1000000;
            opacity: 0;
            transition: opacity 0.3s ease-in-out;
            font-size: 13px;
          }
          
          .lyra-title {
            font-size: 12px;
            font-weight: 500;
            color: #4DAB9A;
            margin-bottom: 5px;
            text-align: center;
          }
        `;
        document.head.appendChild(style);
    }

    function showToast(message) {
        let toast = document.querySelector(".lyra-toast");
        if (!toast) {
            toast = document.createElement("div");
            toast.className = "lyra-toast";
            document.body.appendChild(toast);
        }
        toast.textContent = message;
        toast.style.opacity = "1";
        setTimeout(() => {
            toast.style.opacity = "0";
        }, 2000);
    }

    function getCurrentChatUUID() {
        const url = window.location.href;
        const match = url.match(/\/chat\/([a-zA-Z0-9-]+)/);
        return match ? match[1] : null;
    }

    function checkUrlForTreeMode() {
        return window.location.href.includes('?tree=True&rendering_mode=messages&render_all_tools=true') ||
               window.location.href.includes('&tree=True&rendering_mode=messages&render_all_tools=true');
    }

    function toggleCollapsed() {
        const container = document.getElementById(CONTROL_ID);
        if (container) {
            isCollapsed = !isCollapsed;
            if (isCollapsed) {
                container.classList.add('collapsed');
            } else {
                container.classList.remove('collapsed');
            }
            localStorage.setItem('claudeToolCollapsed', isCollapsed);
        }
    }

    function createUUIDControls() {
        // 如果控件已存在,则不再创建
        if (document.getElementById(CONTROL_ID)) return;

        // 创建主容器
        const container = document.createElement('div');
        container.id = CONTROL_ID;
        container.className = isCollapsed ? 'collapsed' : '';

        // 创建展开/折叠按钮
        const toggleButton = document.createElement('div');
        toggleButton.id = TOGGLE_ID;
        toggleButton.innerHTML = isCollapsed ? 
            '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>' : 
            '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>';
        toggleButton.addEventListener('click', () => {
            toggleCollapsed();
            toggleButton.innerHTML = isCollapsed ? 
                '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>' : 
                '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>';
        });
        
        container.appendChild(toggleButton);

        // 创建主控件区域
        const controlsArea = document.createElement('div');
        controlsArea.className = 'lyra-main-controls';
        
        // 添加标题
        const title = document.createElement('div');
        title.className = 'lyra-title';
        title.textContent = 'Lyra\'s Exporter Fetch';
        controlsArea.appendChild(title);

        // 创建模式切换开关
        const toggleContainer = document.createElement('div');
        toggleContainer.className = 'lyra-toggle';
        toggleContainer.innerHTML = `
          <span>   多分支模式(推荐)</span>
          <label class="lyra-switch">
            <input type="checkbox" id="${SWITCH_ID}" ${checkUrlForTreeMode() ? 'checked' : ''}>
            <span class="lyra-slider"></span>
          </label>
        `;
        controlsArea.appendChild(toggleContainer);

        // 创建获取UUID按钮
        const uuidButton = document.createElement('button');
        uuidButton.className = 'lyra-button';
        uuidButton.innerHTML = `
          <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 256 256" style="margin-right: 8px;">
            <path d="M128,128a32,32,0,1,0,32,32A32,32,0,0,0,128,128Zm0,48a16,16,0,1,1,16-16A16,16,0,0,1,128,176ZM128,80a32,32,0,1,0-32-32A32,32,0,0,0,128,80Zm0-48a16,16,0,1,1-16,16A16,16,0,0,1,128,32Z"/>
            <path d="M192,144a32,32,0,1,0,32,32A32,32,0,0,0,192,144Zm0,48a16,16,0,1,1,16-16A16,16,0,0,1,192,192Z"/>
            <path d="M192,128a32,32,0,1,0-32-32A32,32,0,0,0,192,128Zm0-48a16,16,0,1,1-16,16A16,16,0,0,1,192,80Z"/>
            <path d="M64,144a32,32,0,1,0,32,32A32,32,0,0,0,64,144Zm0,48a16,16,0,1,1,16-16A16,16,0,0,1,64,192Z"/>
            <path d="M64,128a32,32,0,1,0-32-32A32,32,0,0,0,64,128Zm0-48a16,16,0,1,1-16,16A16,16,0,0,1,64,80Z"/>
          </svg>
          获取对话UUID
        `;
        
        // 处理UUID按钮点击事件
        uuidButton.addEventListener('click', () => {
            const uuid = getCurrentChatUUID();
            if (uuid) {
                if (!capturedUserId) {
                    showToast("未能请求用户ID,请刷新页面或进行一些操作");
                    return;
                }

                navigator.clipboard.writeText(uuid).then(() => {
                    console.log("UUID 已复制:", uuid);
                    showToast("UUID已复制!");
                }).catch(err => {
                    console.error("复制失败:", err);
                    showToast("复制失败");
                });

                const treeMode = document.getElementById(SWITCH_ID).checked;
                const jumpUrl = `https://claude.ai/api/organizations/${capturedUserId}/chat_conversations/${uuid}${treeMode ? '?tree=True&rendering_mode=messages&render_all_tools=true' : ''}`;
                window.open(jumpUrl, "_blank");
            } else {
                showToast("未找到UUID!");
            }
        });
        
        controlsArea.appendChild(uuidButton);
        
        // 创建导出JSON按钮
        const downloadJsonButton = document.createElement('button');
        downloadJsonButton.className = 'lyra-button';
        downloadJsonButton.innerHTML = `
          <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 256 256" style="margin-right: 8px;">
            <path d="M224,152v56a16,16,0,0,1-16,16H48a16,16,0,0,1-16-16V152a8,8,0,0,1,16,0v56H208V152a8,8,0,0,1,16,0Zm-101.66,5.66a8,8,0,0,0,11.32,0l40-40a8,8,0,0,0-11.32-11.32L136,132.69V40a8,8,0,0,0-16,0v92.69L93.66,106.34a8,8,0,0,0-11.32,11.32Z"></path>
          </svg>
          导出对话JSON
        `;
        
        // 处理导出JSON按钮点击事件
        downloadJsonButton.addEventListener('click', async () => {
            const uuid = getCurrentChatUUID();
            if (uuid) {
                if (!capturedUserId) {
                    showToast("未能请求用户ID,请刷新页面或进行一些操作");
                    return;
                }

                try {
                    const treeMode = document.getElementById(SWITCH_ID).checked;
                    const apiUrl = `https://claude.ai/api/organizations/${capturedUserId}/chat_conversations/${uuid}${treeMode ? '?tree=True&rendering_mode=messages&render_all_tools=true' : ''}`;
                    
                    // 获取JSON数据
                    showToast("正在获取数据...");
                    const response = await fetch(apiUrl);
                    if (!response.ok) {
                        throw new Error(`请求失败: ${response.status}`);
                    }
                    
                    const data = await response.json();
                    
                    // 创建下载
                    const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
                    const url = URL.createObjectURL(blob);
                    const a = document.createElement('a');
                    a.href = url;
                    a.download = `claude_${uuid.substring(0, 8)}_${new Date().toISOString().slice(0,10)}.json`;
                    document.body.appendChild(a);
                    a.click();
                    document.body.removeChild(a);
                    URL.revokeObjectURL(url);
                    
                    showToast("JSON导出成功!");
                } catch (error) {
                    console.error("导出失败:", error);
                    showToast("导出失败: " + error.message);
                }
            } else {
                showToast("未找到对话UUID!");
            }
        });
        
        controlsArea.appendChild(downloadJsonButton);

        container.appendChild(controlsArea);
        document.body.appendChild(container);
    }

    // 初始化脚本
    function initScript() {
        injectCustomStyle();
        
        // 延迟执行,确保DOM加载完毕
        setTimeout(() => {
            if (/\/chat\/[a-zA-Z0-9-]+/.test(window.location.href)) {
                createUUIDControls();
            }
        }, 1000);

        // 监听 URL 变化(防止 SPA 页面跳转失效)
        let lastUrl = window.location.href;
        const observer = new MutationObserver(() => {
            if (window.location.href !== lastUrl) {
                lastUrl = window.location.href;
                setTimeout(() => {
                    if (/\/chat\/[a-zA-Z0-9-]+/.test(lastUrl)) {
                        createUUIDControls();
                    }
                }, 1000);
            }
        });

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

    // 等待DOM加载完成
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initScript);
    } else {
        initScript();
    }
})();