您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Export current ChatGPT chat to JSON (AI-friendly) and RTF (Unicode, links, tables). Adds timestamps and message IDs.
// ==UserScript== // @name Chat Transcript Exporter (JSON+RTF) // @namespace TamperMonkey // @version 0.4.5 // @description Export current ChatGPT chat to JSON (AI-friendly) and RTF (Unicode, links, tables). Adds timestamps and message IDs. // @author Jeroen Bazuin // @match https://chat.openai.com/* // @match https://chatgpt.com/* // @run-at document-start // @license MIT // @grant GM_registerMenuCommand // @grant GM_addStyle // ==/UserScript== (function () { 'use strict'; console.info('[Chat Transcript Exporter] Boot v0.4.5'); // ---------- Settings ---------- const FILL_SYNTHETIC_TIMES = true; // fill missing timestamps (+1s monotonic) const SYNTHETIC_STEP_MS = 1000; // ---------- Styles (two blue buttons, top-right) ---------- GM_addStyle(` .jb-toolbar{position:fixed;top:84px;right:16px;z-index:2147483647;display:flex;gap:6px;align-items:center} .jb-btn{background:#2563eb;color:#fff;border:1px solid #1d4ed8;border-radius:8px;padding:6px 10px; font:600 12px/1 system-ui,-apple-system,Segoe UI,Roboto,Arial;cursor:pointer;box-shadow:0 4px 12px rgba(0,0,0,.12)} .jb-btn:hover{filter:brightness(1.05)} .jb-toast{position:fixed;left:50%;transform:translateX(-50%);top:12px;background:#111827;color:#fff;padding:8px 12px; border-radius:8px;border:1px solid #e5e7eb;z-index:2147483647;font:500 12px/1 system-ui,-apple-system,Segoe UI,Roboto,Arial;pointer-events:none} `); // ---------- Utils ---------- const sleep = (ms) => new Promise(r => setTimeout(r, ms)); const pad = (n) => String(n).padStart(2, '0'); const dateStamp = () => { const d = new Date(); return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}_${pad(d.getHours())}-${pad(d.getMinutes())}-${pad(d.getSeconds())}`; }; function toast(msg, ms = 1200) { if (!document.body) return; const el = document.createElement('div'); el.className = 'jb-toast'; el.textContent = msg; document.body.appendChild(el); setTimeout(() => el.remove(), ms); } function download(filename, data, type = 'application/octet-stream') { const blob = new Blob([data], { type }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; (document.body || document.documentElement).appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(url), 1500); } // ---------- Load all messages ---------- async function ensureAllMessagesLoaded(maxLoops = 28) { let last = 0; for (let i = 0; i < maxLoops; i++) { const c = document.querySelectorAll('[data-message-author-role]').length; if (c > last) { last = c; window.scrollTo({ top: 0, behavior: 'auto' }); await sleep(650); } else break; } } const getMessageNodes = () => Array.from(document.querySelectorAll('[data-message-author-role]')); // ---------- Extractors ---------- function sanitizeClone(node) { const clone = node.cloneNode(true); clone.querySelectorAll('*').forEach(el => { [...el.attributes].forEach(a => { if (/^on/i.test(a.name)) el.removeAttribute(a.name); }); }); return clone; } function stripUI(root) { [ 'button','svg','textarea','input','[role="menu"]', '[data-testid="copy-code-button"]','[data-testid="followup-actions"]', '[data-testid="toolbar"]','nav','header','footer', 'figure:has(img[alt*="avatar"])','[aria-label="Actions"]' ].forEach(sel => root.querySelectorAll(sel).forEach(n => n.remove())); } function pickContentEl(node) { return node.querySelector('article') || node.querySelector('.markdown, .prose, [data-testid="conversation-turn-content"]') || node; } function extractCodeBlocks(root) { const blocks = []; root.querySelectorAll('pre').forEach(pre => { const code = pre.querySelector('code'); const text = (code ? code.textContent : pre.textContent) || ''; let lang = ''; if (code) { const ds = code.getAttribute('data-language'); if (ds) lang = ds.toLowerCase(); if (!lang && code.className) { const m = code.className.match(/language-([a-z0-9]+)/i); if (m) lang = m[1].toLowerCase(); } } blocks.push({ lang, text }); }); return blocks; } function htmlToPlainText(html) { const c = document.createElement('div'); c.innerHTML = html; c.querySelectorAll('pre').forEach(pre => { pre.replaceWith(document.createTextNode('\n' + pre.textContent + '\n')); }); c.querySelectorAll('br').forEach(br => br.replaceWith(document.createTextNode('\n'))); c.querySelectorAll('li').forEach(li => li.insertAdjacentText('afterbegin', '• ')); return c.innerText.replace(/\n{3,}/g, '\n\n').trim(); } function extractTimestamp(node) { const t = node.querySelector('time[datetime]'); if (t && t.getAttribute('datetime')) return t.getAttribute('datetime'); for (const a of ['data-message-timestamp','data-timestamp','data-created','data-created-at']) { const v = node.getAttribute(a) || (node.dataset && node.dataset[a.replace('data-','').replace(/-([a-z])/g,(_,c)=>c.toUpperCase())]); if (v) return v; } const t2 = node.querySelector('time'); if (t2 && t2.textContent) { const d = new Date(t2.textContent.trim()); if (!isNaN(d.getTime())) return d.toISOString(); } return null; } function extractMessageId(node) { const self = node.getAttribute('data-message-id'); if (self) return self; const desc = node.querySelector('[data-message-id]'); if (desc) return desc.getAttribute('data-message-id'); const anc = node.closest('[data-message-id]'); if (anc) return anc.getAttribute('data-message-id'); const idLike = node.id || node.getAttribute('id'); if (idLike && /message|msg|node/i.test(idLike)) return idLike; return null; } // AI-friendly fragments function extractFragments(root) { const out = []; const el = root.cloneNode(true); stripUI(el); function push(f) { if (!f) return; if (f.text && !f.text.trim()) return; out.push(f); } function getText(n) { return (n.innerText || '').replace(/\s+\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim(); } function readList(listEl, ordered) { const items = Array.from(listEl.children).map(li => getText(li)).filter(Boolean); return { type: ordered ? 'ordered_list' : 'unordered_list', items }; } function readTable(tbl) { const rows = Array.from(tbl.querySelectorAll('tr')).map(tr => Array.from(tr.children).map(td => getText(td)) ); const hasHeader = rows.length && tbl.querySelector('th') != null; return { type: 'table', header: hasHeader ? rows[0] : null, rows: hasHeader ? rows.slice(1) : rows }; } const blocks = Array.from(el.children.length ? el.children : [el]); blocks.forEach(node => { if (node.nodeType !== 1) return; const tag = node.tagName; if (/^H[1-6]$/.test(tag)) push({ type: 'heading', level: +tag[1], text: getText(node) }); else if (tag === 'P') push({ type: 'paragraph', text: getText(node) }); else if (tag === 'UL') push(readList(node, false)); else if (tag === 'OL') push(readList(node, true)); else if (tag === 'PRE') { const code = node.querySelector('code'); let lang = ''; if (code) { const ds = code.getAttribute('data-language'); if (ds) lang = ds.toLowerCase(); if (!lang && code.className) { const m = code.className.match(/language-([a-z0-9]+)/i); if (m) lang = m[1].toLowerCase(); } } push({ type: 'code_block', lang, code: code ? code.textContent : node.textContent }); } else if (tag === 'BLOCKQUOTE') push({ type: 'blockquote', text: getText(node) }); else if (tag === 'TABLE') push(readTable(node)); }); if (!out.length) out.push({ type: 'paragraph', text: getText(el) }); return out; } function extractMessage(node) { const role = node.getAttribute('data-message-author-role') || 'unknown'; const message_id = extractMessageId(node); const created_at_raw = extractTimestamp(node); const contentEl = pickContentEl(node); const clean = sanitizeClone(contentEl); stripUI(clean); const html = clean.innerHTML; const text = htmlToPlainText(html); const code_blocks = extractCodeBlocks(clean); const fragments = extractFragments(clean); return { role, message_id, created_at: created_at_raw || null, html, text, code_blocks, fragments }; } async function collectTranscript() { await ensureAllMessagesLoaded(); const nodes = getMessageNodes(); let messages = nodes.map((n, i) => ({ index: i + 1, ...extractMessage(n) })).filter(m => m.text || m.html); if (messages.some(m => !m.created_at) && FILL_SYNTHETIC_TIMES) { const start = Date.now() - (messages.length * SYNTHETIC_STEP_MS); messages = messages.map((m, i) => m.created_at ? { ...m, created_at_synthetic: false } : { ...m, created_at: new Date(start + i * SYNTHETIC_STEP_MS).toISOString(), created_at_synthetic: true } ); } else { messages = messages.map(m => ({ ...m, created_at_synthetic: false })); } return { source_url: location.href, title: document.title || 'ChatGPT chat', exported_at: new Date().toISOString(), messages }; } // ---------- RTF helpers (Unicode, links, lists, tables) ---------- function escapeRtfU(str) { let out = ''; for (let i = 0; i < str.length; i++) { let cp = str.codePointAt(i); if (cp > 0xFFFF) i++; const ch = String.fromCodePoint(cp); if (ch === '\\'){ out += '\\\\'; continue; } if (ch === '{'){ out += '\\{'; continue; } if (ch === '}'){ out += '\\}'; continue; } if (ch === '\r'){ continue; } if (ch === '\n'){ out += '\\line '; continue; } if (cp > 127){ out += `\\u${cp}?`; continue; } out += ch; } return out; } function fldinstEscape(s) { return String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"'); } function rtfTableFromDOM(table) { // Word-friendly: per-cell \\pard\\intbl ... \\cell, then \\row const rows = Array.from(table.querySelectorAll('tr')).map(tr => Array.from(tr.children)); if (!rows.length) return ''; const maxCols = Math.max(...rows.map(r => r.length)); const cellW = Math.round(9000 / Math.max(1, maxCols)); // twips let r = ''; rows.forEach(tr => { r += '\\trowd\\trgaph108\\trleft0 '; let x = 0; for (let i = 0; i < maxCols; i++) { x += cellW; r += `\\cellx${x} `; } tr.forEach((cell) => { const isHead = cell.tagName === 'TH'; const txt = (cell.innerText || '').trim(); if (isHead) r += '\\b '; r += `\\pard\\intbl ${escapeRtfU(txt)}\\cell `; if (isHead) r += '\\b0 '; }); for (let k = tr.length; k < maxCols; k++) r += '\\pard\\intbl \\cell '; r += '\\row '; }); return r; } function richFragmentToRtf(html) { const root = document.createElement('div'); root.innerHTML = html; let out = ''; function walk(node, ctx = { inList: false }) { if (node.nodeType === 3) { out += escapeRtfU(node.nodeValue); return; } if (node.nodeType !== 1) return; const tag = node.tagName; switch (tag) { case 'BR': out += '\\line '; break; case 'P': Array.from(node.childNodes).forEach(n => walk(n, ctx)); if (!ctx.inList) out += '\\par '; break; case 'H1': case 'H2': case 'H3': out += '\\b '; if (tag === 'H1') out += '\\fs36 '; if (tag === 'H2') out += '\\fs32 '; if (tag === 'H3') out += '\\fs28 '; Array.from(node.childNodes).forEach(n => walk(n, ctx)); out += '\\b0 \\fs24 \\par '; break; case 'STRONG': case 'B': out += '\\b '; Array.from(node.childNodes).forEach(n => walk(n, ctx)); out += ' \\b0 '; break; case 'EM': case 'I': out += '\\i '; Array.from(node.childNodes).forEach(n => walk(n, ctx)); out += ' \\i0 '; break; case 'UL': Array.from(node.children).forEach(li => { out += '\\pard\\tx720\\li720\\fi-360 \\bullet\\tab '; Array.from(li.childNodes).forEach(n => { if (n.nodeType === 1 && n.tagName === 'P') Array.from(n.childNodes).forEach(nn => walk(nn, { inList: true })); else walk(n, { inList: true }); }); out += '\\par '; }); out += '\\pard '; break; case 'OL': { let counter = 1; Array.from(node.children).forEach(li => { out += `\\pard\\tx720\\li720\\fi-360 ${escapeRtfU(String(counter) + '.')}\\tab `; Array.from(li.childNodes).forEach(n => { if (n.nodeType === 1 && n.tagName === 'P') Array.from(n.childNodes).forEach(nn => walk(nn, { inList: true })); else walk(n, { inList: true }); }); out += '\\par '; counter++; }); out += '\\pard '; break; } case 'BLOCKQUOTE': out += '\\li360 '; Array.from(node.childNodes).forEach(n => walk(n, ctx)); out += '\\li0 \\par '; break; case 'PRE': { const code = node.querySelector('code'); const text = code ? code.textContent : node.textContent; out += '{\\pard\\f1\\cb2 ' + escapeRtfU(text) + ' \\par} '; break; } case 'CODE': { if (node.parentElement && node.parentElement.tagName === 'PRE') break; out += '{\\f1 ' + escapeRtfU(node.textContent) + '} '; break; } case 'A': { const href = node.getAttribute('href') || ''; const label = node.textContent || href; out += `{\\field{\\*\\fldinst HYPERLINK "${fldinstEscape(href)}"}{\\fldrslt ${escapeRtfU(label)}}}`; break; } case 'TABLE': { out += rtfTableFromDOM(node); break; } default: Array.from(node.childNodes).forEach(n => walk(n, ctx)); } } Array.from(root.childNodes).forEach(n => walk(n)); return out; } function buildRTF(doc) { let body = ''; doc.messages.forEach(m => { const roleLabel = m.role === 'user' ? 'User' : m.role === 'assistant' ? 'ChatGPT' : m.role === 'system' ? 'System' : m.role; const approx = m.created_at_synthetic ? ' ~' : ''; const metaParts = []; if (m.created_at) metaParts.push(`${m.created_at}${approx}`); if (m.message_id) metaParts.push(`id: ${m.message_id}`); const meta = metaParts.join('; '); // Role + small inline meta in parentheses body += `\\b ${escapeRtfU(roleLabel)} \\b0`; if (meta) body += ` {\\fs18 (${escapeRtfU(meta)})}\\par `; else body += ' \\par '; body += richFragmentToRtf(m.html || m.text || ''); body += '\\par\\par '; // blank line between messages }); return '{\\rtf1\\ansi\\ansicpg1252\\deff0\\uc1' + '{\\fonttbl{\\f0 Segoe UI;}{\\f1 Courier New;}}' + '{\\colortbl;\\red0\\green0\\blue0;\\red243\\green244\\blue246;}' // \cb2 for code + '\\fs24 ' + body + '}'; } // ---------- Export actions ---------- async function exportJSON() { try { toast('Collecting transcript (JSON)…'); const data = await collectTranscript(); const titleSlug = (document.title || 'chat_transcript').replace(/[^\p{L}\p{N} _-]+/gu,'').replace(/\s+/g,'_').slice(0,70) || 'chat_transcript'; const fname = `${titleSlug}_${dateStamp()}.json`; download(fname, JSON.stringify(data, null, 2), 'application/json;charset=utf-8'); toast('JSON downloaded ✔'); } catch (e) { console.error('[Chat Transcript Exporter] JSON error', e); toast('JSON export failed (see console).'); } } async function exportRTF() { try { toast('Collecting transcript (RTF)…'); const data = await collectTranscript(); const rtf = buildRTF(data); const titleSlug = (document.title || 'chat_transcript').replace(/[^\p{L}\p{N} _-]+/gu,'').replace(/\s+/g,'_').slice(0,70) || 'chat_transcript'; const fname = `${titleSlug}_${dateStamp()}.rtf`; download(fname, rtf, 'application/rtf;charset=utf-8'); toast('RTF downloaded ✔'); } catch (e) { console.error('[Chat Transcript Exporter] RTF error', e); toast('RTF export failed (see console).'); } } // ---------- UI (top-right) ---------- function topbarBottomY() { for (const sel of ['header[role="banner"]','[data-testid="top-nav"]','nav[role="navigation"]','header']) { const el = document.querySelector(sel); if (el) { const r = el.getBoundingClientRect(); if (r.bottom > 0) return Math.max(r.bottom + 12, 60); } } return 84; } function ensureToolbar() { if (!document.body) return; let bar = document.querySelector('.jb-toolbar'); const top = topbarBottomY(); if (!bar) { bar = document.createElement('div'); bar.className = 'jb-toolbar'; const btnJson = document.createElement('button'); btnJson.className = 'jb-btn'; btnJson.textContent = 'Export JSON'; btnJson.title = 'Download transcript as JSON'; btnJson.addEventListener('click', exportJSON); const btnRtf = document.createElement('button'); btnRtf.className = 'jb-btn'; btnRtf.textContent = 'Export RTF'; btnRtf.title = 'Download transcript as RTF'; btnRtf.addEventListener('click', exportRTF); bar.append(btnJson, btnRtf); document.body.appendChild(bar); toast('Exporter active'); console.info('[Chat Transcript Exporter] Toolbar ready'); } bar.style.top = `${top}px`; } // Tampermonkey menu if (typeof GM_registerMenuCommand === 'function') { GM_registerMenuCommand('Export JSON', exportJSON); GM_registerMenuCommand('Export RTF', exportRTF); } // Lifecycle const boot = () => { const readyInt = setInterval(() => { if (document.body) { ensureToolbar(); clearInterval(readyInt); } }, 200); const mo = new MutationObserver(() => ensureToolbar()); mo.observe(document.documentElement, { childList: true, subtree: true }); window.addEventListener('resize', ensureToolbar); window.addEventListener('scroll', ensureToolbar); }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', boot, { once: true }); } else { boot(); } })();