Greasy Fork

Greasy Fork is available in English.

Lyra's Exporter Fetch

获取Claude的UUID并跳转到API页面,支持树形模式切换,收纳隐藏,UI更友好!

当前为 2025-06-16 提交的版本,查看 最新版本

// ==UserScript==
// @name         Lyra's Exporter Fetch
// @namespace    userscript://lyra-conversation-exporter
// @version      2.1
// @description  获取Claude的UUID并跳转到API页面,支持树形模式切换,收纳隐藏,UI更友好!
// @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 Fetch Exporter';
        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();
    }
})();