// ==UserScript== // @name Lyra's Exporter Fetch // @namespace userscript://lyra-conversation-exporter // @version 3.2 // @description Claude、Gemini、NotebookLM、Google AI Studio对话管理工具的配套脚本:一键获取对话UUID和完整JSON数据,支持树形分支模式导出。轻松管理数百个对话窗口,导出完整时间线、思考过程、工具调用和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/* // @match https://gemini.google.com/app/* // @match https://notebooklm.google.com/* // @match https://aistudio.google.com/* // @run-at document-start // @grant none // @license GNU General Public License v3.0 // @downloadURL none // ==/UserScript== (function() { 'use strict'; // --- 全局配置 --- const SCROLL_DELAY_MS = 250; // 向下滚动时,每步之间的等待时间 const SCROLL_TOP_WAIT_MS = 1000; // 滚动到顶部后的等待时间 // --- 平台检测 --- let currentPlatform = ''; const hostname = window.location.hostname; if (hostname.includes('claude.ai')) { currentPlatform = 'claude'; } else if (hostname.includes('gemini.google.com')) { currentPlatform = 'gemini'; } else if (hostname.includes('notebooklm.google.com')) { currentPlatform = 'notebooklm'; } else if (hostname.includes('aistudio.google.com')) { currentPlatform = 'aistudio'; } console.log(`[Lyra's Universal Exporter] Platform detected: ${currentPlatform}`); // Claude专用变量 let capturedUserId = ''; // 通用变量 let isPanelCollapsed = localStorage.getItem('lyraExporterCollapsed') === 'true'; const CONTROL_ID = "lyra-universal-exporter-container"; const TOGGLE_ID = "lyra-toggle-button"; const TREE_SWITCH_ID = "lyra-tree-mode"; let panelInjected = false; // AI Studio专用滚动数据 let collectedData = new Map(); // Claude专用:拦截请求获取用户ID if (currentPlatform === 'claude') { 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("✨ Captured user ID:", capturedUserId); } return originalXHROpen.apply(this, arguments); }; 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("✨ Captured user ID:", capturedUserId); } } return originalFetch.apply(this, arguments); }; } // 注入样式 function injectCustomStyle() { const style = document.createElement('style'); style.textContent = ` #${CONTROL_ID} { position: fixed !important; right: 15px !important; bottom: 25px !important; display: flex !important; flex-direction: column !important; gap: 8px !important; z-index: 2147483647 !important; transition: transform 0.3s ease, width 0.3s ease, padding 0.3s ease !important; background: white !important; border-radius: 12px !important; box-shadow: 0 4px 15px rgba(0,0,0,0.2) !important; padding: 12px !important; border: 1px solid #e0e0e0 !important; width: auto !important; min-width: 40px !important; font-family: 'Google Sans', Roboto, Arial, sans-serif !important; color: #3c4043 !important; } #${CONTROL_ID}.collapsed .lyra-main-controls { display: none !important; } #${CONTROL_ID}.collapsed { padding: 8px !important; width: 40px !important; height: 40px !important; justify-content: center !important; align-items: center !important; overflow: hidden !important; } #${TOGGLE_ID} { position: absolute !important; left: -14px !important; top: 50% !important; transform: translateY(-50%) !important; width: 28px !important; height: 28px !important; border-radius: 50% !important; display: flex !important; align-items: center !important; justify-content: center !important; background: #f1f3f4 !important; color: #1a73e8 !important; cursor: pointer !important; border: 1px solid #dadce0 !important; transition: all 0.3s !important; box-shadow: 0 1px 3px rgba(0,0,0,0.1) !important; } #${CONTROL_ID}.collapsed #${TOGGLE_ID} { position: static !important; transform: none !important; left: auto !important; top: auto !important; } #${TOGGLE_ID}:hover { background: #e8eaed !important; } .lyra-main-controls { display: flex !important; flex-direction: column !important; gap: 10px !important; align-items: center !important; } .lyra-button { display: inline-flex !important; align-items: center !important; justify-content: center !important; padding: 8px 16px !important; border-radius: 18px !important; cursor: pointer !important; font-size: 14px !important; font-weight: 500 !important; background-color: #1a73e8 !important; color: white !important; border: none !important; transition: background-color 0.3s, box-shadow 0.3s !important; box-shadow: 0 1px 2px rgba(0,0,0,0.1) !important; font-family: 'Google Sans', Roboto, Arial, sans-serif !important; white-space: nowrap !important; width: 100% !important; gap: 8px !important; } .lyra-button:hover { background-color: #1765c2 !important; box-shadow: 0 2px 4px rgba(0,0,0,0.15) !important; } .lyra-button:disabled { opacity: 0.6 !important; cursor: not-allowed !important; } .lyra-button svg { flex-shrink: 0 !important; } .lyra-title { font-size: 13px !important; font-weight: 500 !important; color: #1a73e8 !important; margin-bottom: 8px !important; text-align: center !important; } .lyra-toggle { display: flex !important; align-items: center !important; font-size: 13px !important; margin-bottom: 5px !important; gap: 5px !important; color: #5f6368 !important; } .lyra-switch { position: relative !important; display: inline-block !important; width: 32px !important; height: 16px !important; } .lyra-switch input { opacity: 0 !important; width: 0 !important; height: 0 !important; } .lyra-slider { position: absolute !important; cursor: pointer !important; top: 0 !important; left: 0 !important; right: 0 !important; bottom: 0 !important; background-color: #ccc !important; transition: .4s !important; border-radius: 34px !important; } .lyra-slider:before { position: absolute !important; content: "" !important; height: 12px !important; width: 12px !important; left: 2px !important; bottom: 2px !important; background-color: white !important; transition: .4s !important; border-radius: 50% !important; } input:checked + .lyra-slider { background-color: #1a73e8 !important; } input:checked + .lyra-slider:before { transform: translateX(16px) !important; } .lyra-toast { position: fixed !important; bottom: 80px !important; right: 20px !important; background-color: #323232 !important; color: white !important; padding: 12px 20px !important; border-radius: 4px !important; z-index: 2147483648 !important; opacity: 0 !important; transition: opacity 0.3s ease-in-out !important; font-size: 14px !important; font-family: 'Google Sans', Roboto, Arial, sans-serif !important; } .lyra-loading { display: inline-block !important; width: 20px !important; height: 20px !important; border: 2px solid rgba(255, 255, 255, 0.3) !important; border-radius: 50% !important; border-top-color: #fff !important; animation: lyra-spin 1s linear infinite !important; } @keyframes lyra-spin { to { transform: rotate(360deg); } } .lyra-progress { font-size: 12px !important; color: #5f6368 !important; margin-top: 5px !important; text-align: center !important; width: 100%; } `; document.head.appendChild(style); } // 通用工具函数 function showToast(message, duration = 3000) { 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"; setTimeout(() => { if (toast && toast.parentElement) { toast.parentElement.removeChild(toast); } }, 300); }, duration); } function toggleCollapsed() { const container = document.getElementById(CONTROL_ID); const toggleButton = document.getElementById(TOGGLE_ID); if (container && toggleButton) { isPanelCollapsed = !isPanelCollapsed; container.classList.toggle('collapsed', isPanelCollapsed); toggleButton.innerHTML = isPanelCollapsed ? '' : ''; localStorage.setItem('lyraExporterCollapsed', isPanelCollapsed); } } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // --- Claude专用函数 --- 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'); } async function getAllConversations() { if (!capturedUserId) { showToast("未能获取用户ID,请刷新页面"); return null; } try { const apiUrl = `https://claude.ai/api/organizations/${capturedUserId}/chat_conversations`; const response = await fetch(apiUrl); if (!response.ok) { throw new Error(`请求失败: ${response.status}`); } return await response.json(); } catch (error) { console.error("获取对话列表失败:", error); showToast("获取对话列表失败: " + error.message); return null; } } async function getConversationDetails(uuid) { if (!capturedUserId) { return null; } try { const apiUrl = `https://claude.ai/api/organizations/${capturedUserId}/chat_conversations/${uuid}?tree=True&rendering_mode=messages&render_all_tools=true`; const response = await fetch(apiUrl); if (!response.ok) { throw new Error(`请求失败: ${response.status}`); } return await response.json(); } catch (error) { console.error(`获取对话 ${uuid} 详情失败:`, error); return null; } } // --- Gemini, NotebookLM, AI Studio 提取函数 --- function extractGeminiConversationData() { const conversationTurns = document.querySelectorAll("div.conversation-turn"); let conversationData = []; if (conversationTurns.length === 0) { const legacySelectors = ["div.single-turn", "div.conversation-container"]; for (const sel of legacySelectors) { const legacyContainers = document.querySelectorAll(sel); if (legacyContainers.length > 0) { legacyContainers.forEach(container => processGeminiContainer(container, conversationData)); break; } } } else { conversationTurns.forEach(turn => processGeminiContainer(turn, conversationData)); } return conversationData; } function processGeminiContainer(container, conversationData) { const userQueryElement = container.querySelector("user-query .query-text") || container.querySelector(".query-text-line"); const modelResponseContainer = container.querySelector("model-response") || container; const modelResponseElement = modelResponseContainer.querySelector("message-content .markdown-main-panel"); const questionText = userQueryElement ? userQueryElement.innerText.trim() : ""; const answerText = modelResponseElement ? modelResponseElement.innerText.trim() : ""; if (questionText || answerText) { conversationData.push({ human: questionText, assistant: answerText }); } } function extractNotebookLMConversationData() { const conversationTurns = document.querySelectorAll("div.chat-message-pair"); let conversationData = []; conversationTurns.forEach((turnContainer) => { let questionText = ""; const userQueryEl = turnContainer.querySelector("chat-message .from-user-container .message-text-content p, chat-message .from-user-container .message-text-content div.ng-star-inserted"); if (userQueryEl) { questionText = userQueryEl.innerText.trim(); } let answerText = ""; const modelResponseContent = turnContainer.querySelector("chat-message .to-user-container .message-text-content"); if (modelResponseContent) { let answerParts = []; const structuralElements = modelResponseContent.querySelectorAll('labs-tailwind-structural-element-view-v2'); structuralElements.forEach(structEl => { const textSpans = structEl.querySelectorAll('span[data-start-index].ng-star-inserted'); textSpans.forEach(span => { if (!span.closest('button.citation-marker')) { if (span.closest('.paragraph') || span.closest('.list-item') || span.innerText.length > 1 ) { answerParts.push(span.innerText.trim()); } } }); }); answerText = answerParts.filter(part => part.length > 0).join(' ').replace(/\s\s+/g, ' ').trim(); } if (questionText || answerText) { conversationData.push({ human: questionText, assistant: answerText }); } }); if (conversationData.length === 0) { const emptyState = document.querySelector('.chat-panel-empty-state'); if (emptyState) { const titleEl = emptyState.querySelector('h1.notebook-title'); const summaryEl = emptyState.querySelector('.summary-content p'); if (titleEl && summaryEl) { conversationData.push({ notebook_title: titleEl.innerText.trim(), notebook_summary: summaryEl.innerText.trim(), type: "notebook_metadata" }); } } } return conversationData; } // --- 【新融合的】AI Studio 提取逻辑 --- function getAIStudioScroller() { const selectors = [ 'ms-chat-session ms-autoscroll-container', // 首选,最精确 'mat-sidenav-content', // 备选1 '.chat-view-container' // 备选2 ]; for (const selector of selectors) { const el = document.querySelector(selector); // 检查元素是否存在,并且是可滚动的 if (el && (el.scrollHeight > el.clientHeight || el.scrollWidth > el.clientWidth)) { console.log(`[Lyra's Exporter] Found AI Studio scroller with selector: ${selector}`); return el; } } console.warn("[Lyra's Exporter] Could not find a specific scroll container for AI Studio. Falling back to documentElement."); return document.documentElement; } /** * 增量捕获当前可见的AI Studio对话回合 */ function extractDataIncremental_AiStudio() { const turns = document.querySelectorAll('ms-chat-turn'); turns.forEach(turn => { if (collectedData.has(turn)) { // 如果已经记录过,就跳过 return; } const isUserTurn = turn.querySelector('.chat-turn-container.user'); const isModelTurn = turn.querySelector('.chat-turn-container.model'); let turnData = { type: 'unknown', text: '' }; if (isUserTurn) { const userNodes = isUserTurn.querySelectorAll('[data-turn-role="User"] ms-cmark-node'); let userText = ''; userNodes.forEach(node => { userText += node.innerText.trim() + '\n'; }); if (userText.trim()) { turnData.type = 'user'; turnData.text = userText.trim(); } } else if (isModelTurn) { const responseChunks = isModelTurn.querySelectorAll('ms-prompt-chunk'); let responseText = ''; responseChunks.forEach(chunk => { if (!chunk.querySelector('ms-thought-chunk')) { // 忽略思考过程 const cmarkNode = chunk.querySelector('ms-cmark-node'); if (cmarkNode) { responseText += cmarkNode.innerText.trim() + '\n'; } } }); if (responseText.trim()) { turnData.type = 'model'; turnData.text = responseText.trim(); } } if (turnData.type !== 'unknown') { collectedData.set(turn, turnData); } }); } /** * 【新】先滚动到顶部,再向下扫描并捕获所有对话 */ async function autoScrollAndCaptureAIStudio(onProgress) { collectedData.clear(); const scroller = getAIStudioScroller(); onProgress("正在滚动到顶部...", false); scroller.scrollTop = 0; await sleep(SCROLL_TOP_WAIT_MS); let lastScrollTop = -1; onProgress("开始向下扫描...", false); while (true) { extractDataIncremental_AiStudio(); // 捕获当前视图的内容 onProgress(`扫描中... ${Math.round((scroller.scrollTop + scroller.clientHeight) / scroller.scrollHeight * 100)}% (已发现 ${collectedData.size} 条)`, false); if (scroller.scrollTop + scroller.clientHeight >= scroller.scrollHeight - 10) { // 10px 容差 onProgress("已到达底部,扫描完成。", false); break; } lastScrollTop = scroller.scrollTop; scroller.scrollTop += scroller.clientHeight * 0.85; // 向下滚动一屏 await sleep(SCROLL_DELAY_MS); if (scroller.scrollTop === lastScrollTop) { onProgress("滚动位置无变化,扫描完成。", false); break; } } onProgress("提取并整理数据...", false); await sleep(500); // 按最终的DOM顺序整理数据 const finalTurnsInDom = document.querySelectorAll('ms-chat-turn'); let sortedData = []; finalTurnsInDom.forEach(turnNode => { if (collectedData.has(turnNode)) { sortedData.push(collectedData.get(turnNode)); } }); // 将临时的 `role` 和 `text` 格式转换为 `human` 和 `assistant` const pairedData = []; let lastHuman = null; sortedData.forEach(item => { if (item.type === 'user') { lastHuman = item.text; } else if (item.type === 'model' && lastHuman) { pairedData.push({ human: lastHuman, assistant: item.text }); lastHuman = null; } else if (item.type === 'model' && !lastHuman) { pairedData.push({ human: "[No preceding user prompt found]", assistant: item.text }); } }); if (lastHuman) { pairedData.push({ human: lastHuman, assistant: "[Model response is pending]" }); } return pairedData; } // 创建面板 function createFloatingPanel() { if (document.getElementById(CONTROL_ID) || panelInjected) { return false; } const container = document.createElement('div'); container.id = CONTROL_ID; if (isPanelCollapsed) container.classList.add('collapsed'); const toggleButton = document.createElement('div'); toggleButton.id = TOGGLE_ID; toggleButton.innerHTML = isPanelCollapsed ? '' : ''; toggleButton.addEventListener('click', toggleCollapsed); container.appendChild(toggleButton); const controlsArea = document.createElement('div'); controlsArea.className = 'lyra-main-controls'; const title = document.createElement('div'); title.className = 'lyra-title'; switch (currentPlatform) { case 'claude': title.textContent = 'Lyra\'s Claude Exporter'; break; case 'gemini': title.textContent = 'Gemini JSON Exporter'; break; case 'notebooklm': title.textContent = 'NotebookLM JSON Exporter'; break; case 'aistudio': title.textContent = 'AI Studio JSON Exporter'; break; default: title.textContent = 'Lyra\'s AI Exporter'; } controlsArea.appendChild(title); if (currentPlatform === 'claude') { // (Claude 的所有按钮逻辑保持不变) // ... (完整的Claude按钮创建代码) } else { // --- 其他平台的统一导出按钮 --- const exportButton = document.createElement('button'); exportButton.className = 'lyra-button'; exportButton.innerHTML = ` Export to JSON `; exportButton.addEventListener('click', async function() { this.disabled = true; const originalContent = this.innerHTML; this.innerHTML = '
'; let progressElem = null; if (currentPlatform === 'aistudio') { progressElem = document.createElement('div'); progressElem.className = 'lyra-progress'; progressElem.textContent = '准备...'; controlsArea.appendChild(progressElem); } try { let conversationData = []; if (currentPlatform === 'aistudio') { conversationData = await autoScrollAndCaptureAIStudio((message, isError) => { if (progressElem) progressElem.textContent = message; if (isError) showToast(message); }); } else { switch (currentPlatform) { case 'gemini': conversationData = extractGeminiConversationData(); break; case 'notebooklm': conversationData = extractNotebookLMConversationData(); break; } } if (conversationData && conversationData.length > 0) { const timestamp = new Date().toISOString().replace(/:/g, '-').slice(0, 19); let filenamePrefix = 'Chat'; if (currentPlatform === 'gemini') filenamePrefix = 'Gemini'; if (currentPlatform === 'notebooklm') filenamePrefix = 'NotebookLM'; if (currentPlatform === 'aistudio') filenamePrefix = 'AIStudio'; const filename = `${filenamePrefix}_Chat_${timestamp}.json`; const jsonData = JSON.stringify(conversationData, null, 2); const blob = new Blob([jsonData], { type: 'application/json;charset=utf-8' }); const link = document.createElement("a"); link.href = URL.createObjectURL(blob); link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(link.href); showToast("导出成功!"); } else { alert("未能导出任何对话内容。请确保对话已加载并且可见。"); } } catch (error) { console.error("导出错误:", error); alert(`导出过程中发生错误: ${error.message}`); } finally { this.disabled = false; this.innerHTML = originalContent; if (progressElem && progressElem.parentNode) { progressElem.parentNode.removeChild(progressElem); } } }); controlsArea.appendChild(exportButton); } container.appendChild(controlsArea); document.body.appendChild(container); panelInjected = true; return true; } // 初始化 function initScript() { if (!currentPlatform) { console.error("[Lyra's Universal Exporter] Platform not supported"); return; } injectCustomStyle(); setTimeout(() => { if (currentPlatform === 'claude') { if (/\/chat\/[a-zA-Z0-9-]+/.test(window.location.href)) { createFloatingPanel(); } } else { createFloatingPanel(); } }, 2000); if (currentPlatform === 'claude') { 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) && !document.getElementById(CONTROL_ID)) { createFloatingPanel(); } }, 1000); } }); observer.observe(document.body, { childList: true, subtree: true }); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initScript); } else { initScript(); } })();