您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Claude、Gemini、NotebookLM、Google AI Studio对话管理工具的配套脚本:一键获取对话UUID和完整JSON数据,支持树形分支模式导出。轻松管理数百个对话窗口,导出完整时间线、思考过程、工具调用和Artifacts。Gemini和AI Studio支持保留富文本格式(标题、粗体、引用、代码块等)。配合Lyra's Exporter使用,让每次AI对话都成为您的数字资产!
当前为
// ==UserScript== // @name Lyra's Fetch // @namespace userscript://lyra-universal-ai-exporter // @version 1.2.2 // @description Claude、Gemini、NotebookLM、Google AI Studio对话管理工具的配套脚本:一键获取对话UUID和完整JSON数据,支持树形分支模式导出。轻松管理数百个对话窗口,导出完整时间线、思考过程、工具调用和Artifacts。Gemini和AI Studio支持保留富文本格式(标题、粗体、引用、代码块等)。配合Lyra's Exporter使用,让每次AI对话都成为您的数字资产! // @description:en Claude, Gemini, NotebookLM, Google AI Studio conversation management companion script: One-click UUID extraction and complete JSON export with tree-branch mode support. Designed for power 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://yalums.github.io/lyra-exporter/ // @supportURL https://github.com/Yalums/lyra-exporter/issues // @author Yalums // @match https://pro.easychat.top/* // @match https://claude.ai/* // @match https://gemini.google.com/app/* // @match https://notebooklm.google.com/* // @match https://aistudio.google.com/* // @include *://gemini.google.com/* // @include *://notebooklm.google.com/* // @include *://aistudio.google.com/* // @run-at document-idle // @grant GM_addStyle // @grant GM_xmlhttpRequest // @license GNU General Public License v3.0 // ==/UserScript== (function() { 'use strict'; if (window.lyraFetchInitialized) { return; } window.lyraFetchInitialized = true; let trustedPolicy; if (typeof window.trustedTypes !== 'undefined' && window.trustedTypes.createPolicy) { try { trustedPolicy = window.trustedTypes.createPolicy('lyra-exporter-policy', { createHTML: (input) => input }); } catch (e) { try { trustedPolicy = window.trustedTypes.defaultPolicy || window.trustedTypes.createPolicy('default', { createHTML: (input) => input }); } catch (e2) { } } } function safeSetInnerHTML(element, html) { if (trustedPolicy) { element.innerHTML = trustedPolicy.createHTML(html); } else { element.innerHTML = html; } } let currentPlatform = ''; const hostname = window.location.hostname; if (hostname.includes('easychat.top') || 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'; } const LYRA_EXPORTER_URL = 'https://yalums.github.io/lyra-exporter/'; const LYRA_EXPORTER_ORIGIN = 'https://yalums.github.io'; let capturedUserId = ''; let isPanelCollapsed = localStorage.getItem('lyraExporterCollapsed') === 'true'; let includeImages = localStorage.getItem('lyraIncludeImages') === 'true'; const CONTROL_ID = "lyra-universal-exporter-container"; const TOGGLE_ID = "lyra-toggle-button"; const TREE_SWITCH_ID = "lyra-tree-mode"; const IMAGE_SWITCH_ID = "lyra-image-mode"; let panelInjected = false; const SCROLL_DELAY_MS = 250; const SCROLL_TOP_WAIT_MS = 1000; let collectedData = new Map(); 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]; } 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]; } } return originalFetch.apply(this, arguments); }; } function injectCustomStyle() { let primaryColor = '#1a73e8'; let buttonColor = '#1a73e8'; let buttonHoverColor = '#1765c2'; switch(currentPlatform) { case 'claude': primaryColor = '#d97706'; buttonColor = '#EA580C'; buttonHoverColor = '#DC2626'; break; case 'notebooklm': primaryColor = '#374151'; buttonColor = '#4B5563'; buttonHoverColor = '#1F2937'; break; case 'gemini': case 'aistudio': default: break; } const styleContent = ` #${CONTROL_ID} { position: fixed !important; right: 15px !important; bottom: 75px !important; display: flex !important; flex-direction: column !important; gap: 8px !important; z-index: 2147483647 !important; transition: all 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 ${currentPlatform === 'notebooklm' ? '#9CA3AF' : currentPlatform === 'claude' ? 'rgba(217, 119, 6, 0.3)' : '#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: #2C84DB !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: ${buttonColor} !important; color: white !important; border: none !important; transition: all 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: ${buttonHoverColor} !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: ${primaryColor} !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: ${buttonColor} !important; } input:checked + .lyra-slider:before { transform: translateX(16px) !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%; } `; if (typeof GM_addStyle === 'function') { GM_addStyle(styleContent); } else { const existingStyle = document.querySelector('style[data-lyra-styles]'); if (!existingStyle) { const style = document.createElement('style'); style.textContent = styleContent; style.setAttribute('data-lyra-styles', 'true'); document.head.appendChild(style); } } } function htmlToMarkdown(element) { if (!element) return ''; let result = ''; function processNode(node) { if (node.nodeType === Node.TEXT_NODE) { return node.textContent; } if (node.nodeType !== Node.ELEMENT_NODE) { return ''; } const tagName = node.tagName.toLowerCase(); const children = Array.from(node.childNodes).map(processNode).join(''); switch(tagName) { case 'h1': return `\n# ${children}\n`; case 'h2': return `\n## ${children}\n`; case 'h3': return `\n### ${children}\n`; case 'h4': return `\n#### ${children}\n`; case 'h5': return `\n##### ${children}\n`; case 'h6': return `\n###### ${children}\n`; case 'strong': case 'b': return `**${children}**`; case 'em': case 'i': return `*${children}*`; case 'code': if (children.includes('\n')) { return `\n\`\`\`\n${children}\n\`\`\`\n`; } return `\`${children}\``; case 'pre': const codeChild = node.querySelector('code'); if (codeChild) { const lang = codeChild.className.match(/language-(\w+)/)?.[1] || ''; const codeContent = codeChild.textContent; return `\n\`\`\`${lang}\n${codeContent}\n\`\`\`\n`; } return `\n\`\`\`\n${children}\n\`\`\`\n`; case 'hr': return '\n---\n'; case 'br': return '\n'; case 'p': return `\n${children}\n`; case 'div': return `${children}\n`; case 'a': const href = node.getAttribute('href'); if (href) { return `[${children}](${href})`; } return children; case 'ul': case 'ol': return `\n${children}\n`; case 'li': const parent = node.parentElement; if (parent && parent.tagName.toLowerCase() === 'ol') { const index = Array.from(parent.children).indexOf(node) + 1; return `${index}. ${children}\n`; } return `- ${children}\n`; case 'blockquote': return `\n> ${children.split('\n').join('\n> ')}\n`; case 'table': return `\n${children}\n`; case 'thead': return `${children}`; case 'tbody': return `${children}`; case 'tr': return `${children}|\n`; case 'th': return `| **${children}** `; case 'td': return `| ${children} `; default: return children; } } result = processNode(element); result = result.replace(/\n{3,}/g, '\n\n'); result = result.trim(); return result; } function toggleCollapsed() { const container = document.getElementById(CONTROL_ID); const toggleButton = document.getElementById(TOGGLE_ID); if (container && toggleButton) { isPanelCollapsed = !isPanelCollapsed; container.classList.toggle('collapsed', isPanelCollapsed); safeSetInnerHTML(toggleButton, isPanelCollapsed ? '<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"><polyline points="15 18 9 12 15 6"></polyline></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"><polyline points="9 18 15 12 9 6"></polyline></svg>'); localStorage.setItem('lyraExporterCollapsed', isPanelCollapsed); } } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function blobToBase64(blob) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onloadend = () => resolve(reader.result.split(',')[1]); reader.onerror = reject; reader.readAsDataURL(blob); }); } async function openLyraExporterWithData(jsonData, filename) { try { const exporterWindow = window.open(LYRA_EXPORTER_URL, '_blank'); if (!exporterWindow) { console.error("无法打开 Lyra Exporter,请检查弹窗拦截器"); return false; } const checkInterval = setInterval(() => { try { exporterWindow.postMessage({ type: 'LYRA_HANDSHAKE', source: 'lyra-fetch-script' }, LYRA_EXPORTER_ORIGIN); } catch (e) { } }, 1000); const handleMessage = (event) => { if (event.origin !== LYRA_EXPORTER_ORIGIN) { return; } if (event.data && event.data.type === 'LYRA_READY') { clearInterval(checkInterval); const dataToSend = { type: 'LYRA_LOAD_DATA', source: 'lyra-fetch-script', data: { content: jsonData, filename: filename || `${currentPlatform}_export_${new Date().toISOString().slice(0,10)}.json` } }; exporterWindow.postMessage(dataToSend, LYRA_EXPORTER_ORIGIN); window.removeEventListener('message', handleMessage); } }; window.addEventListener('message', handleMessage); setTimeout(() => { clearInterval(checkInterval); window.removeEventListener('message', handleMessage); console.error("连接超时,请检查 Lyra Exporter 是否正常运行"); }, 45000); return true; } catch (error) { console.error("打开 Lyra Exporter 失败: " + error.message); return false; } } 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) { console.error("未能获取用户ID,请刷新页面"); return null; } try { const baseUrl = window.location.hostname.includes('claude.ai') ? 'https://claude.ai' : 'https://pro.easychat.top'; const apiUrl = `${baseUrl}/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.message); return null; } } async function processImageAttachment(imageUrl, debugInfo = '') { try { if (!imageUrl.startsWith('http')) { const baseUrl = window.location.hostname.includes('claude.ai') ? 'https://claude.ai' : 'https://pro.easychat.top'; imageUrl = baseUrl + imageUrl; } const response = await fetch(imageUrl); if (!response.ok) { throw new Error(`Failed to fetch image: ${response.status}`); } const blob = await response.blob(); const base64 = await blobToBase64(blob); return { type: 'image', format: blob.type, size: blob.size, data: base64, original_url: imageUrl }; } catch (error) { return null; } } async function getConversationDetailsWithImages(uuid, includeImagesParam) { if (!capturedUserId) { return null; } try { const baseUrl = window.location.hostname.includes('claude.ai') ? 'https://claude.ai' : 'https://pro.easychat.top'; const treeMode = document.getElementById(TREE_SWITCH_ID) ? document.getElementById(TREE_SWITCH_ID).checked : false; const apiUrl = `${baseUrl}/api/organizations/${capturedUserId}/chat_conversations/${uuid}${treeMode ? '?tree=True&rendering_mode=messages&render_all_tools=true' : ''}`; const response = await fetch(apiUrl); if (!response.ok) { throw new Error(`请求失败: ${response.status}`); } const data = await response.json(); if (!includeImagesParam) { return data; } let processedImageCount = 0; if (data.chat_messages && Array.isArray(data.chat_messages)) { for (let msgIndex = 0; msgIndex < data.chat_messages.length; msgIndex++) { const message = data.chat_messages[msgIndex]; const fileArrays = ['files', 'files_v2', 'attachments']; for (const key of fileArrays) { if (message[key] && Array.isArray(message[key])) { for (let i = 0; i < message[key].length; i++) { const file = message[key][i]; let imageUrl = null; let isImage = false; if (file.file_kind === 'image' || (file.file_type && file.file_type.startsWith('image/'))) { isImage = true; imageUrl = file.preview_url || file.thumbnail_url || file.file_url; } if (isImage && imageUrl && !file.embedded_image) { const imageData = await processImageAttachment(imageUrl, `消息${msgIndex + 1}-${key}-${i + 1}`); if (imageData) { message[key][i].embedded_image = imageData; processedImageCount++; } } } } } } } data._debug_info = { images_processed: processedImageCount, processing_time: new Date().toISOString() }; return data; } catch (error) { return null; } } async function exportCurrentConversationWithImages() { const uuid = getCurrentChatUUID(); if (!uuid) { console.error("未找到对话UUID!"); return; } if (!capturedUserId) { console.error("未能获取用户ID,请刷新页面"); return; } try { const shouldIncludeImages = document.getElementById(IMAGE_SWITCH_ID).checked; const data = await getConversationDetailsWithImages(uuid, shouldIncludeImages); if (!data) { throw new Error("无法获取对话数据"); } 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)}_${shouldIncludeImages ? 'with_images_' : ''}${new Date().toISOString().slice(0,10)}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } catch (error) { console.error("导出失败: " + error.message); } } function getConversationTitle() { let title = ''; try { switch (currentPlatform) { case 'gemini': const defaultTitle = `Gemini Chat ${new Date().toISOString().slice(0,10)}`; title = prompt("请输入对话标题:", defaultTitle); if (title === null) { return null; } return title.trim() || defaultTitle; case 'notebooklm': const nblmTitleInput = document.querySelector('input.title-input.mat-title-large') || document.querySelector('.title-input.mat-title-large.ng-pristine.ng-valid.ng-touched') || document.querySelector('h1.notebook-title'); if (nblmTitleInput) { title = nblmTitleInput.value || nblmTitleInput.innerText || nblmTitleInput.textContent; title = title.trim(); } return title || 'Untitled NotebookLM Chat'; case 'aistudio': const studioTitleEl = document.querySelector('div.page-title h1'); title = studioTitleEl ? studioTitleEl.innerText.trim() : 'Untitled AI Studio Chat'; return title; default: return `Chat Export ${new Date().toISOString().slice(0,10)}`; } } catch (error) { console.warn("获取标题失败,将使用默认标题。"); return `Untitled Chat ${new Date().toISOString().slice(0,10)}`; } } function fetchViaGM(url) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: url, responseType: "blob", onload: function(response) { if (response.status >= 200 && response.status < 300) { resolve(response.response); } else { reject(new Error(`GM_xmlhttpRequest 失败,状态码: ${response.status}`)); } }, onerror: function(error) { reject(new Error(`GM_xmlhttpRequest 网络错误: ${error.statusText}`)); } }); }); } async function processImageElement(imgElement) { if (!imgElement) return null; let imageUrlToFetch = null; const previewContainer = imgElement.closest('user-query-file-preview'); if (previewContainer) { const lensLinkElement = previewContainer.querySelector('a[href*="lens.google.com"]'); if (lensLinkElement && lensLinkElement.href) { try { const urlObject = new URL(lensLinkElement.href); const realImageUrl = urlObject.searchParams.get('url'); if (realImageUrl) { imageUrlToFetch = realImageUrl; } } catch (e) { } } } if (!imageUrlToFetch) { const fallbackSrc = imgElement.src; if (fallbackSrc && !fallbackSrc.startsWith('data:')) { imageUrlToFetch = fallbackSrc; } } if (!imageUrlToFetch) { return null; } try { const blob = await fetchViaGM(imageUrlToFetch); const base64 = await blobToBase64(blob); return { type: 'image', format: blob.type, size: blob.size, data: base64, original_src: imageUrlToFetch }; } catch (error) { return null; } } async function processGeminiContainer(container) { 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() : ""; let answerText = ""; if (modelResponseElement) { if (typeof htmlToMarkdown === 'function') { answerText = htmlToMarkdown(modelResponseElement); } else { answerText = modelResponseElement.innerHTML .replace(/<h1[^>]*>(.*?)<\/h1>/gi, '\n# $1\n') .replace(/<h2[^>]*>(.*?)<\/h2>/gi, '\n## $1\n') .replace(/<h3[^>]*>(.*?)<\/h3>/gi, '\n### $1\n') .replace(/<h4[^>]*>(.*?)<\/h4>/gi, '\n#### $1\n') .replace(/<h5[^>]*>(.*?)<\/h5>/gi, '\n##### $1\n') .replace(/<h6[^>]*>(.*?)<\/h6>/gi, '\n###### $1\n') .replace(/<hr[^>]*>/gi, '\n---\n') .replace(/<strong[^>]*>(.*?)<\/strong>/gi, '**$1**') .replace(/<b[^>]*>(.*?)<\/b>/gi, '**$1**') .replace(/<em[^>]*>(.*?)<\/em>/gi, '*$1*') .replace(/<i[^>]*>(.*?)<\/i>/gi, '*$1*') .replace(/<code[^>]*>(.*?)<\/code>/gi, '`$1`') .replace(/<blockquote[^>]*>(.*?)<\/blockquote>/gi, '\n> $1\n') .replace(/<br[^>]*>/gi, '\n') .replace(/<p[^>]*>/gi, '\n') .replace(/<\/p>/gi, '\n') .replace(/<[^>]+>/g, '') .replace(/\n{3,}/g, '\n\n') .trim(); } } const userImageElements = container.querySelectorAll("user-query img"); const modelImageElements = modelResponseContainer.querySelectorAll("model-response img"); const userImagesPromises = Array.from(userImageElements).map(img => processImageElement(img)); const modelImagesPromises = Array.from(modelImageElements).map(img => processImageElement(img)); const userImages = (await Promise.all(userImagesPromises)).filter(Boolean); const modelImages = (await Promise.all(modelImagesPromises)).filter(Boolean); if (questionText || answerText || userImages.length > 0 || modelImages.length > 0) { return { human: { text: questionText, images: userImages }, assistant: { text: answerText, images: modelImages } }; } return null; } async function extractGeminiConversationData() { const conversationTurns = document.querySelectorAll("div.conversation-turn"); let conversationData = []; if (conversationTurns.length > 0) { for (const turn of conversationTurns) { const data = await processGeminiContainer(turn); if (data) conversationData.push(data); } } else { const legacyContainers = document.querySelectorAll("div.single-turn, div.conversation-container"); for (const container of legacyContainers) { const data = await processGeminiContainer(container); if (data) conversationData.push(data); } } return conversationData; } 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"); if (userQueryEl) { questionText = userQueryEl.innerText.trim(); if (questionText.startsWith('[Preamble] ')) { questionText = questionText.substring('[Preamble] '.length).trim(); } } let answerText = ""; const modelResponseContent = turnContainer.querySelector("chat-message .to-user-container .message-text-content"); if (modelResponseContent) { const answerParts = []; const structuralElements = modelResponseContent.querySelectorAll('labs-tailwind-structural-element-view-v2'); structuralElements.forEach(structEl => { const bulletEl = structEl.querySelector('.bullet'); const paragraphEl = structEl.querySelector('.paragraph'); let lineText = ''; if (bulletEl) { lineText += bulletEl.innerText.trim() + ' '; } if (paragraphEl) { let paragraphText = ''; paragraphEl.childNodes.forEach(node => { if (node.nodeType === Node.TEXT_NODE) { paragraphText += node.textContent; } else if (node.nodeType === Node.ELEMENT_NODE) { if (node.querySelector && node.querySelector('button.citation-marker')) { return; } if (node.tagName === 'SPAN' && node.classList.contains('bold')) { paragraphText += `**${node.innerText}**`; } else { paragraphText += node.innerText || node.textContent || ''; } } }); lineText += paragraphText; } if (lineText.trim()) { answerParts.push(lineText.trim()); } }); answerText = answerParts.join('\n\n'); } if (questionText || answerText) { conversationData.push({ human: questionText, assistant: answerText }); } }); return conversationData; } function getAIStudioScroller() { const selectors = [ 'ms-chat-session ms-autoscroll-container', 'mat-sidenav-content', '.chat-view-container' ]; for (const selector of selectors) { const el = document.querySelector(selector); if (el && (el.scrollHeight > el.clientHeight || el.scrollWidth > el.clientWidth)) { return el; } } return document.documentElement; } 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 userPromptNode = isUserTurn.querySelector('.user-prompt-container .turn-content'); if (userPromptNode) { const userText = userPromptNode.innerText.trim(); if (userText) { turnData.type = 'user'; turnData.text = userText; } } } else if (isModelTurn) { const responseChunks = isModelTurn.querySelectorAll('ms-prompt-chunk'); let responseTexts = []; responseChunks.forEach(chunk => { if (!chunk.querySelector('ms-thought-chunk')) { const cmarkNode = chunk.querySelector('ms-cmark-node'); if (cmarkNode) { const markdownText = htmlToMarkdown(cmarkNode); if (markdownText) { responseTexts.push(markdownText); } } } }); const responseText = responseTexts.join('\n\n').trim(); if (responseText) { turnData.type = 'model'; turnData.text = responseText; } } 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) { break; } lastScrollTop = scroller.scrollTop; scroller.scrollTop += scroller.clientHeight * 0.85; await sleep(SCROLL_DELAY_MS); if (scroller.scrollTop === lastScrollTop) { break; } } onProgress("扫描完成,正在提取...", false); extractDataIncremental_AiStudio(); await sleep(500); const finalTurnsInDom = document.querySelectorAll('ms-chat-turn'); let sortedData = []; finalTurnsInDom.forEach(turnNode => { if (collectedData.has(turnNode)) { sortedData.push(collectedData.get(turnNode)); } }); 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; safeSetInnerHTML(toggleButton, isPanelCollapsed ? '<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"><polyline points="15 18 9 12 15 6"></polyline></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"><polyline points="9 18 15 12 9 6"></polyline></svg>'); 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 = 'Claude Chat Fetch'; break; case 'gemini': title.textContent = 'Gemini Chat Fetch'; break; case 'notebooklm': title.textContent = 'NotebookLM Fetch'; break; case 'aistudio': title.textContent = 'AI Studio Fetch'; break; default: title.textContent = 'Lyra\'s Exporter'; } controlsArea.appendChild(title); if (currentPlatform === 'claude') { const toggleContainer = document.createElement('div'); toggleContainer.className = 'lyra-toggle'; safeSetInnerHTML(toggleContainer, ` <span>多分支模式</span> <label class="lyra-switch"> <input type="checkbox" id="${TREE_SWITCH_ID}" ${checkUrlForTreeMode() ? 'checked' : ''}> <span class="lyra-slider"></span> </label> `); controlsArea.appendChild(toggleContainer); const imageToggleContainer = document.createElement('div'); imageToggleContainer.className = 'lyra-toggle'; safeSetInnerHTML(imageToggleContainer, ` <span>包含图片</span> <label class="lyra-switch"> <input type="checkbox" id="${IMAGE_SWITCH_ID}" ${includeImages ? 'checked' : ''}> <span class="lyra-slider"></span> </label> `); controlsArea.appendChild(imageToggleContainer); document.addEventListener('change', function(e) { if (e.target.id === IMAGE_SWITCH_ID) { includeImages = e.target.checked; localStorage.setItem('lyraIncludeImages', includeImages); } }); const uuidButton = document.createElement('button'); uuidButton.className = 'lyra-button'; safeSetInnerHTML(uuidButton, ` <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <circle cx="11" cy="11" r="8"></circle> <path d="m21 21-4.35-4.35"></path> </svg> 在线查看 `); uuidButton.addEventListener('click', async () => { const uuid = getCurrentChatUUID(); if (!uuid) { console.error("未找到UUID!"); return; } if (!capturedUserId) { console.error("未能获取用户ID,请刷新页面"); return; } const originalContent = uuidButton.innerHTML; safeSetInnerHTML(uuidButton, '<div class="lyra-loading"></div><span>加载中...</span>'); uuidButton.disabled = true; try { const shouldIncludeImages = document.getElementById(IMAGE_SWITCH_ID).checked; const data = await getConversationDetailsWithImages(uuid, shouldIncludeImages); if (!data) { throw new Error("无法获取对话数据"); } const jsonString = JSON.stringify(data, null, 2); const filename = `claude_${uuid.substring(0, 8)}_${shouldIncludeImages ? 'with_images_' : ''}${new Date().toISOString().slice(0,10)}.json`; await openLyraExporterWithData(jsonString, filename); } catch (error) { console.error("加载失败: " + error.message); } finally { safeSetInnerHTML(uuidButton, originalContent); uuidButton.disabled = false; } }); controlsArea.appendChild(uuidButton); const exportButton = document.createElement('button'); exportButton.className = 'lyra-button'; safeSetInnerHTML(exportButton, ` <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path> <polyline points="7 10 12 15 17 10"></polyline> <line x1="12" y1="15" x2="12" y2="3"></line> </svg> 导出对话JSON `); exportButton.addEventListener('click', exportCurrentConversationWithImages); controlsArea.appendChild(exportButton); const exportAllButton = document.createElement('button'); exportAllButton.className = 'lyra-button'; safeSetInnerHTML(exportAllButton, ` <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <rect x="2" y="7" width="20" height="14" rx="2" ry="2"></rect> <path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"></path> </svg> 导出所有对话 `); exportAllButton.addEventListener('click', async function(event) { if (!capturedUserId) { console.error("未能获取用户ID,请刷新页面"); return; } const shouldIncludeImages = document.getElementById(IMAGE_SWITCH_ID).checked; const progressElem = document.createElement('div'); progressElem.className = 'lyra-progress'; progressElem.textContent = '准备中...'; controlsArea.appendChild(progressElem); const originalContent = this.innerHTML; safeSetInnerHTML(this, '<div class="lyra-loading"></div><span>导出中...</span>'); this.disabled = true; try { const allConversations = await getAllConversations(); if (!allConversations || !Array.isArray(allConversations)) { throw new Error("无法获取对话列表"); } const result = { exportedAt: new Date().toISOString(), totalConversations: allConversations.length, conversations: [] }; for (let i = 0; i < allConversations.length; i++) { const conversation = allConversations[i]; progressElem.textContent = `获取对话 ${i + 1}/${allConversations.length}${shouldIncludeImages ? ' (处理图片中...)' : ''}`; if (i > 0) await sleep(500); const details = await getConversationDetailsWithImages(conversation.uuid, shouldIncludeImages); if (details) result.conversations.push(details); } const blob = new Blob([JSON.stringify(result, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `claude_all_conversations_${shouldIncludeImages ? 'with_images_' : ''}${new Date().toISOString().slice(0,10)}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); console.log(`成功导出 ${result.conversations.length} 个对话!`); } catch (error) { console.error("导出失败: " + error.message); } finally { safeSetInnerHTML(this, originalContent); this.disabled = false; if (progressElem.parentNode) progressElem.parentNode.removeChild(progressElem); } }); controlsArea.appendChild(exportAllButton); } else { const exportButton = document.createElement('button'); exportButton.className = 'lyra-button'; safeSetInnerHTML(exportButton, ` <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> <path d="M14 2H6C4.9 2 4 2.9 4 4V20C4 21.1 4.9 22 6 22H18C19.1 22 20 21.1 20 20V8L14 2ZM18 20H6V4H13V9H18V20ZM8 15.01L9.41 16.42L11 14.84V19H13V14.84L14.59 16.43L16 15.01L12.01 11L8 15.01Z"/> </svg> <span>保存对话</span> `); exportButton.addEventListener('click', async function() { const title = getConversationTitle(); if (title === null) { console.log("导出已取消。"); return; } this.disabled = true; const originalContent = this.innerHTML; safeSetInnerHTML(this, '<div class="lyra-loading"></div><span>导出中...</span>'); let progressElem = null; if (currentPlatform === 'aistudio') { progressElem = document.createElement('div'); progressElem.className = 'lyra-progress'; controlsArea.appendChild(progressElem); } try { let conversationData = []; if (currentPlatform === 'aistudio') { conversationData = await autoScrollAndCaptureAIStudio((message) => { if (progressElem) progressElem.textContent = message; }); } else if (currentPlatform === 'gemini') { conversationData = await extractGeminiConversationData(); } else if (currentPlatform === 'notebooklm') { conversationData = extractNotebookLMConversationData(); } if (conversationData.length > 0) { const finalJson = { title: title, platform: currentPlatform, exportedAt: new Date().toISOString(), conversation: conversationData }; const timestamp = new Date().toISOString().replace(/:/g, '-').slice(0, 19); const sanitizedTitle = title.replace(/[^a-z0-9\u4e00-\u9fa5]/gi, '_'); const filename = `${currentPlatform}_${sanitizedTitle}_${timestamp}.json`; const jsonData = JSON.stringify(finalJson, 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); console.log("导出成功!"); } else { console.warn("没有可导出的对话内容。"); } } catch (error) { console.error(`导出过程中发生错误: ${error.message}`); } finally { this.disabled = false; safeSetInnerHTML(this, originalContent); if (progressElem && progressElem.parentNode) { progressElem.parentNode.removeChild(progressElem); } } }); controlsArea.appendChild(exportButton); if (currentPlatform !== 'notebooklm') { const onlineViewButton = document.createElement('button'); onlineViewButton.className = 'lyra-button'; safeSetInnerHTML(onlineViewButton, ` <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/> </svg> <span>在线查看</span> `); onlineViewButton.addEventListener('click', async function() { const title = getConversationTitle(); if (title === null) { console.log("操作已取消。"); return; } this.disabled = true; const originalContent = this.innerHTML; safeSetInnerHTML(this, '<div class="lyra-loading"></div><span>加载中...</span>'); let progressElem = null; if (currentPlatform === 'aistudio') { progressElem = document.createElement('div'); progressElem.className = 'lyra-progress'; controlsArea.appendChild(progressElem); } try { let conversationData = []; if (currentPlatform === 'aistudio') { conversationData = await autoScrollAndCaptureAIStudio((message) => { if (progressElem) progressElem.textContent = message; }); } else if (currentPlatform === 'gemini') { conversationData = await extractGeminiConversationData(); } else if (currentPlatform === 'notebooklm') { conversationData = extractNotebookLMConversationData(); } if (conversationData.length > 0) { const finalJson = { title: title, platform: currentPlatform, exportedAt: new Date().toISOString(), conversation: conversationData }; const timestamp = new Date().toISOString().replace(/:/g, '-').slice(0, 19); const sanitizedTitle = title.replace(/[^a-z0-9\u4e00-\u9fa5]/gi, '_'); const filename = `${currentPlatform}_${sanitizedTitle}_${timestamp}.json`; const jsonString = JSON.stringify(finalJson, null, 2); await openLyraExporterWithData(jsonString, filename); } else { console.warn("没有可导出的对话内容。"); } } catch (error) { console.error(`在线查看过程中发生错误: ${error.message}`); } finally { this.disabled = false; safeSetInnerHTML(this, originalContent); if (progressElem && progressElem.parentNode) { progressElem.parentNode.removeChild(progressElem); } } }); controlsArea.appendChild(onlineViewButton); } } container.appendChild(controlsArea); document.body.appendChild(container); panelInjected = true; return true; } function initScript() { if (!currentPlatform) { return; } injectCustomStyle(); let initDelay = 1500; let maxRetries = 10; let retryCount = 0; switch(currentPlatform) { case 'claude': initDelay = 1000; maxRetries = 10; break; case 'gemini': initDelay = 1500; maxRetries = 30; break; case 'notebooklm': initDelay = 1500; maxRetries = 20; break; case 'aistudio': initDelay = 1500; maxRetries = 20; break; } function tryCreatePanel() { retryCount++; if (document.getElementById(CONTROL_ID)) { return; } if (currentPlatform === 'claude') { if (/\/chat\/[a-zA-Z0-9-]+/.test(window.location.href)) { if (createFloatingPanel()) { return; } } } else { if (createFloatingPanel()) { return; } } if (retryCount < maxRetries && !document.getElementById(CONTROL_ID)) { setTimeout(tryCreatePanel, initDelay); } } setTimeout(tryCreatePanel, initDelay); 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 }); } } initScript(); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址