您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Mobile-first script which exports the full contents of a chat-gpt conversation as JSON, plaintext, or a zip file containing both.
// ==UserScript== // @name [mobile-optimized] ChatGPT Conversation Exporter // @namespace http://tampermonkey.net/ // @version 1.0 // @description Mobile-first script which exports the full contents of a chat-gpt conversation as JSON, plaintext, or a zip file containing both. // @author rASTROco Labs, with input/assistance from OpenAI via ChatGPT // @match https://*.chatgpt.com/* // @grant download // @license MIT // ==/UserScript== (function() { 'use strict'; // ----------------------------- // Wait for a selector // ----------------------------- function waitForSelector(selector, timeout = 10000) { return new Promise((resolve, reject) => { const start = Date.now(); const interval = setInterval(() => { const el = document.querySelector(selector); if (el) { clearInterval(interval); resolve(el); } else if (Date.now() - start > timeout) { clearInterval(interval); reject(`Selector ${selector} not found`); } }, 300); }); } // ----------------------------- // Create mobile export button // ----------------------------- function createExportButton() { const exportBtn = document.createElement("button"); exportBtn.id = "chat-export-btn"; exportBtn.innerHTML = ` <div style=" width: 20px; height: 16px; display: flex; flex-direction: column; justify-content: space-between; margin: auto; "> <span style="display:block;height:2px;width:100%;background:white;border-radius:1px;"></span> <span style="display:block;height:2px;width:100%;background:white;border-radius:1px;"></span> <span style="display:block;height:2px;width:100%;background:white;border-radius:1px;"></span> </div> `; exportBtn.style.cssText = ` background-color: #2b2b2b; /* dark grey */ border: none; border-radius: 50%; width: 50px; height: 50px; z-index: 9999; position: fixed; bottom: 20px; right: 20px; display: flex; align-items: center; justify-content: center; padding: 0; box-shadow: 0 2px 8px rgba(0,0,0,0.4); transition: opacity 0.5s ease, transform 0.2s ease; opacity: 1; touch-action: none; `; document.body.appendChild(exportBtn); makeDraggable(exportBtn); addFadeEffect(exportBtn); exportBtn.addEventListener('click', exportChatSequence, { passive: true }); } // ----------------------------- // Mobile drag with corner snapping, viewport clamping, haptics // ----------------------------- function makeDraggable(el) { let dragging = false; let touchOffsetX = 0; let touchOffsetY = 0; // Initialize at bottom-right corner el.style.position = "fixed"; el.style.left = (window.innerWidth - el.offsetWidth - 20) + "px"; el.style.top = (window.innerHeight - el.offsetHeight - 20) + "px"; el.addEventListener('touchstart', function(e) { const touch = e.touches[0]; const rect = el.getBoundingClientRect(); touchOffsetX = touch.clientX - rect.left; touchOffsetY = touch.clientY - rect.top; dragging = false; el.style.transition = "none"; }, { passive: false }); el.addEventListener('touchmove', function(e) { e.preventDefault(); const touch = e.touches[0]; let newX = touch.clientX - touchOffsetX; let newY = touch.clientY - touchOffsetY; // Clamp to viewport const maxX = window.innerWidth - el.offsetWidth - 5; const maxY = window.innerHeight - el.offsetHeight - 5; newX = Math.min(Math.max(5, newX), maxX); newY = Math.min(Math.max(5, newY), maxY); el.style.left = newX + "px"; el.style.top = newY + "px"; dragging = true; el.style.opacity = "1"; }, { passive: false }); el.addEventListener('touchend', function(e) { if (!dragging) return; const rect = el.getBoundingClientRect(); const screenWidth = window.innerWidth; const screenHeight = window.innerHeight; const centerX = rect.left + rect.width / 2; const centerY = rect.top + rect.height / 2; // Decide nearest corner const snapLeft = (centerX < screenWidth / 2); const snapTop = (centerY < screenHeight / 2); const targetX = snapLeft ? 10: (screenWidth - rect.width - 10); const targetY = snapTop ? 10: (screenHeight - rect.height - 10); el.style.transition = "left 0.2s ease, top 0.2s ease"; el.style.left = targetX + "px"; el.style.top = targetY + "px"; // Haptic feedback if (navigator.vibrate) { navigator.vibrate(20); } setTimeout(() => { el.style.transition = "opacity 0.5s ease, left 0.2s ease, top 0.2s ease"; }, 250); }, { passive: false }); } // ----------------------------- // Fade effect for idle // ----------------------------- function addFadeEffect(el, idleTime = 3000) { let fadeTimer; const setFade = () => { el.style.opacity = "0.3"; }; const resetFade = () => { el.style.opacity = "1"; clearTimeout(fadeTimer); fadeTimer = setTimeout(setFade, idleTime); }; ['touchstart', 'touchend', 'touchmove'].forEach(evt => { el.addEventListener(evt, resetFade, { passive: true }); }); resetFade(); } // ----------------------------- // Toolbar observer // ----------------------------- async function observeToolbar() { const container = await waitForSelector("div[class*='flex'][class*='gap']"); const observer = new MutationObserver(() => { if (!document.querySelector("#chat-export-btn")) { createExportButton(); } }); observer.observe(container, { childList: true, subtree: true }); } // ----------------------------- // Scroll and extract messages // ----------------------------- async function scrollToTopDynamic(delay = 500) { const scrollable = document.querySelector("main"); if (!scrollable) return; let previousHeight = -1, stableCount = 0; while (stableCount < 3) { const currentHeight = scrollable.scrollHeight; scrollable.scrollTop = 0; await new Promise(r => setTimeout(r, delay)); stableCount = (currentHeight === previousHeight) ? stableCount+1: 0; previousHeight = currentHeight; } } function extractMessages() { const messages = []; const messageDivs = document.querySelectorAll("div[class*='group']"); messageDivs.forEach((div, idx) => { let content = Array.from(div.childNodes).map(n => { if (n.nodeType === Node.TEXT_NODE) return n.nodeValue; if (n.nodeType === Node.ELEMENT_NODE) { if (n.tagName === 'PRE') { const codeLang = n.getAttribute('data-lang') || ''; return `\`\`\`${codeLang}\n${n.innerText}\n\`\`\``; } return n.innerText; } return ''; }).join('\n').trim(); if (!content) return; let role = /^(you|user):/i.test(content)?'user': (/^(chatgpt|assistant):/i.test(content)?'assistant': idx%2 === 0?'user': 'assistant'); const timeEl = div.querySelector("time"); const timestamp = timeEl ? (timeEl.getAttribute("datetime") || timeEl.innerText): null; content = content.replace(/([^\n])\n{3,}/g, '$1\n\n'); messages.push({ role, content, ...(timestamp && { timestamp }) }); }); return messages; } // ----------------------------- // Download helpers // ----------------------------- function downloadJSON(data, filename = "chat_export.json") { const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" }); const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = filename; a.click(); URL.revokeObjectURL(a.href); } function downloadPlaintext(data, filename = "chat_export.txt") { let text = ''; data.forEach(msg => { const ts = msg.timestamp ? ` [${msg.timestamp}]`: ''; text += `${msg.role.toUpperCase()}${ts}:\n${msg.content}\n\n`; }); const blob = new Blob([text], { type: "text/plain" }); const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = filename; a.click(); URL.revokeObjectURL(a.href); } async function downloadZip(data, filename = "chat_export.zip") { if (typeof JSZip === 'undefined') { await new Promise(resolve => { const s = document.createElement("script"); s.src = "https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"; s.onload = resolve; document.head.appendChild(s); }); } const zip = new JSZip(); zip.file("chat_export.json", JSON.stringify(data, null, 2)); let text = ''; data.forEach(msg => { const ts = msg.timestamp?` [${msg.timestamp}]`: ''; text += `${msg.role.toUpperCase()}${ts}:\n${msg.content}\n\n`; }); zip.file("chat_export.txt", text); const blob = await zip.generateAsync({ type: "blob" }); const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = filename; a.click(); URL.revokeObjectURL(a.href); } // ----------------------------- // Export sequence // ----------------------------- async function exportChatSequence() { try { if (!confirm("This will load the entire conversation for export. Continue?")) return; await scrollToTopDynamic(); const messages = extractMessages(); const choice = prompt("Export options:\n1=JSON (default)\n2=Plaintext\n3=ZIP (JSON+Plaintext)\n\nEnter choice number:", "1"); switch (choice) { case "2": downloadPlaintext(messages); alert(`Plaintext export complete: ${messages.length} messages saved.`); break; case "3": await downloadZip(messages); alert(`ZIP export complete: ${messages.length} messages saved.`); break; default: downloadJSON(messages); alert(`JSON export complete: ${messages.length} messages saved.`); } } catch(err) { console.error("Error during export:", err); alert("Failed to export chat. See console for details."); } } // ----------------------------- // Initialize // ----------------------------- createExportButton(); observeToolbar(); })();