您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds “Export Chat” button to ChatGPT threads (JSON / TXT)
// ==UserScript== // @name ChatGPT Chat Exporter (beta) // @namespace https://github.com/merlinm/claude-exporter // @version 0.9 // @description Adds “Export Chat” button to ChatGPT threads (JSON / TXT) // @author Merlin McKean // @license MIT // @match https://chat.openai.com/* // @grant GM_xmlhttpRequest // @grant GM_download // ==/UserScript== (function () { 'use strict'; /* ---------- 1. Helpers ---------- */ // Generic XHR wrapper (same pattern as Claude script) function apiRequest(method, url, headers = {}, data = null) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method, url, headers, data: data ? JSON.stringify(data) : null, responseType: 'json', withCredentials: true, // *include cookies* onload: ({ status, response }) => status >= 200 && status < 300 ? resolve(response) : reject(Error(`HTTP ${status}`)), onerror: reject, }); }); } // Get JWT access token (valid ~30 days) async function getAccessToken() { const { accessToken } = await apiRequest('GET', 'https://chat.openai.com/api/auth/session'); if (!accessToken) throw Error('No access token'); return accessToken; } // Fetch full conversation JSON async function getConversation(chatId) { const token = await getAccessToken(); return await apiRequest( 'GET', `https://chat.openai.com/backend-api/conversation/${chatId}`, { Authorization: `Bearer ${token}` } ); } /* ---------- 2. Formatting ---------- */ function mappingToOrderedArray(mapping) { return Object.values(mapping) .filter(m => m.message?.content?.parts?.length) // skip system/meta msgs .map(m => ({ role: m.message.author.role, // 'user' | 'assistant' text: m.message.content.parts.join('\n'), time: m.message.create_time, })) .sort((a, b) => a.time - b.time); } function toTxt(name, msgs) { let out = `Chat Title: ${name}\nDate: ${new Date().toISOString()}\n\n`; msgs.forEach(m => { const who = m.role === 'user' ? 'User' : 'ChatGPT'; out += `${who}:\n${m.text}\n\n`; }); return out.trim(); } function download(str, filename) { const blob = new Blob([str], { type: 'text/plain' }); GM_download({ url: URL.createObjectURL(blob), name: filename }); } /* ---------- 3. UI ---------- */ function addButton() { // Avoid duplicates when navigating SPA-style if (document.getElementById('m_export_btn')) return; const btn = document.createElement('button'); btn.id = 'm_export_btn'; btn.textContent = 'Export Chat'; Object.assign(btn.style, { position: 'fixed', bottom: '90px', right: '20px', padding: '10px 18px', fontSize: '14px', background: '#202123', color: '#fff', border: 'none', borderRadius: '6px', cursor: 'pointer', zIndex: 9_999, }); btn.onclick = async () => { const format = prompt('json or txt?', 'json'); if (!/^(json|txt)$/i.test(format)) return alert('Invalid format'); try { const chatId = getChatIdFromUrl(); const data = await getConversation(chatId); const msgs = mappingToOrderedArray(data.mapping); const file = `chatgpt_${chatId}_${Date.now()}.${format}`; if (format === 'json') { download(JSON.stringify(data, null, 2), file); } else { download(toTxt(data.title, msgs), file); } alert('Export complete!'); } catch (e) { console.error(e); alert('Export failed—see console.'); } }; document.body.appendChild(btn); } /* ---------- 4. Routing ---------- */ function getChatIdFromUrl() { // Handles both /c/<id> and /share/<id> const m = location.pathname.match(/\/(?:c|share)\/([\w-]+)/); if (!m) throw Error('Cannot find conversation ID in URL'); return m[1]; } // Observe URL changes in the SPA let lastUrl = ''; new MutationObserver(() => { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(addButton, 600); // wait for DOM paint } }).observe(document, { childList: true, subtree: true }); // Run once on load addButton(); })();