您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
一键导出 ChatGPT 聊天记录为 HTML 或 Markdown(按钮放在了油猴菜单栏) (适配2025年7月新版UI,支持DeepResearch)
// ==UserScript== // @name ChatGPT对话导出(2025年7月新版UI) // @namespace http://tampermonkey.net/ // @version 0.4.0 // @description 一键导出 ChatGPT 聊天记录为 HTML 或 Markdown(按钮放在了油猴菜单栏) (适配2025年7月新版UI,支持DeepResearch) // @author Marx (updated by schweigen) // @license MIT // @match https://chatgpt.com/ // @match https://chatgpt.com/c/* // @match https://chatgpt.com/g/* // @match https://chatgpt.com/share/* // @grant GM_xmlhttpRequest // @grant GM.xmlHttpRequest // @grant GM_registerMenuCommand // @icon https://www.chatgpt.com/apple-touch-icon.png // @run-at document-end // ==/UserScript== (function() { 'use strict'; console.log('ChatGPT导出脚本初始化中...'); // 确保在页面加载完成后运行 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initScript); } else { initScript(); } function initScript() { console.log('初始化导出脚本...'); registerMenuCommands(); } function registerMenuCommands() { // 注册油猴脚本菜单命令 GM_registerMenuCommand('导出为 Markdown', async () => { await exportChatAsMarkdown(); }); GM_registerMenuCommand('导出为 HTML', async () => { await exportChatAsHTML(); }); } function createExportButton() { // 如果按钮已存在,先移除 const existingButton = document.getElementById('export-chat'); if (existingButton) { existingButton.remove(); } const exportButton = document.createElement('button'); exportButton.id = 'export-chat'; exportButton.setAttribute('data-testid', 'export-chat-button'); function updateButtonText() { if (window.innerWidth <= 768) { exportButton.textContent = '导'; exportButton.style.fontSize = '10px'; exportButton.style.width = '24px'; exportButton.style.height = '24px'; } else { exportButton.textContent = '导出聊天'; exportButton.style.fontSize = '12px'; } } window.addEventListener('resize', updateButtonText); updateButtonText(); // 添加内联样式,确保按钮可见 Object.assign(exportButton.style, { position: 'fixed', bottom: '15px', right: '15px', padding: '6px 12px', background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', color: 'white', border: 'none', borderRadius: '8px', cursor: 'pointer', textAlign: 'center', lineHeight: '1.2', fontSize: '12px', fontWeight: '500', boxShadow: '0 2px 8px rgba(102, 126, 234, 0.3)', zIndex: '99999', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', transition: 'all 0.2s ease', border: '1px solid rgba(255, 255, 255, 0.2)' }); // 悬停效果 exportButton.addEventListener('mouseenter', () => { exportButton.style.transform = 'translateY(-1px)'; exportButton.style.boxShadow = '0 4px 12px rgba(102, 126, 234, 0.5)'; }); exportButton.addEventListener('mouseleave', () => { exportButton.style.transform = 'translateY(0)'; exportButton.style.boxShadow = '0 2px 8px rgba(102, 126, 234, 0.3)'; }); // 移动设备样式调整 if (window.innerWidth <= 768) { Object.assign(exportButton.style, { width: '40px', height: '40px', right: '15px', bottom: '65px', padding: '0', fontSize: '10px', lineHeight: '40px', textAlign: 'center', borderRadius: '50%', background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' }); } // 全局样式 const style = document.createElement('style'); style.textContent = ` #export-options { position: fixed; bottom: 75px; right: 15px; background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(10px); border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 16px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); padding: 16px; z-index: 100000; min-width: 160px; animation: slideIn 0.3s ease-out; } @keyframes slideIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } #export-options button { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; border-radius: 8px; padding: 10px 16px; margin: 4px 0; cursor: pointer; font-size: 14px; font-weight: 500; width: 100%; transition: all 0.2s ease; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } #export-options button:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); } #export-options span { color: #666; font-size: 16px; font-weight: bold; cursor: pointer; transition: color 0.2s ease; } #export-options span:hover { color: #333; } @media (max-width: 768px) { #export-options { bottom: 115px; right: 15px; width: 140px; padding: 12px; } #export-options button { padding: 8px 12px; font-size: 13px; } } `; document.head.appendChild(style); document.body.appendChild(exportButton); console.log('导出按钮已创建并添加到页面'); exportButton.addEventListener('click', showExportOptions); } function showExportOptions() { console.log('显示导出选项...'); const existingOptions = document.getElementById('export-options'); if (existingOptions) { document.body.removeChild(existingOptions); return; } const optionsContainer = document.createElement('div'); optionsContainer.id = 'export-options'; const buttonsContainer = document.createElement('div'); Object.assign(buttonsContainer.style, { display: 'flex', flexDirection: 'column', alignItems: 'stretch', gap: '5px' }); const mdButton = document.createElement('button'); mdButton.textContent = '导出为 Markdown'; mdButton.addEventListener('click', async () => { await exportChatAsMarkdown(); document.body.removeChild(optionsContainer); }); const htmlButton = document.createElement('button'); htmlButton.textContent = '导出为 HTML'; htmlButton.addEventListener('click', async () => { await exportChatAsHTML(); document.body.removeChild(optionsContainer); }); const closeButton = document.createElement('span'); closeButton.textContent = '✖'; closeButton.addEventListener('click', () => { document.body.removeChild(optionsContainer); }); buttonsContainer.appendChild(mdButton); buttonsContainer.appendChild(htmlButton); optionsContainer.appendChild(closeButton); optionsContainer.appendChild(buttonsContainer); document.body.appendChild(optionsContainer); } async function exportChatAsMarkdown() { console.log('开始导出Markdown...'); let date = new Date(); let year = date.getFullYear(); let month = date.getMonth() + 1; let day = date.getDate(); let formattedDate = `${year}年${month}月${day}日`; let markdownContent = `# ${formattedDate} ChatGPT对话记录\n\n`; // 2025年新版UI选择器 const conversationTurns = document.querySelectorAll('article[data-testid^="conversation-turn-"]'); console.log(`找到 ${conversationTurns.length} 个对话轮次`); if (conversationTurns.length === 0) { alert("未找到任何对话内容。请确保您已经在ChatGPT页面并且有对话历史。"); return; } let userIndex = 1; let assistantIndex = 1; for (let turn of conversationTurns) { // 确定是用户消息还是AI回复 const isUser = turn.querySelector('[data-message-author-role="user"]'); const isAssistant = turn.querySelector('[data-message-author-role="assistant"]'); if (isUser) { // 用户消息 const messageDiv = isUser.querySelector('.whitespace-pre-wrap'); if (!messageDiv) continue; let userText = messageDiv.innerHTML.trim(); // 清理用户消息 let tempDivUser = document.createElement('div'); tempDivUser.innerHTML = userText; removeEditButtons(tempDivUser); await convertImagesToBase64(tempDivUser); userText = tempDivUser.innerHTML; userText = await htmlToMarkdown(userText); markdownContent += `## User ${userIndex}\n\n${userText}\n\n`; userIndex++; } else if (isAssistant) { // AI回复 const messageDiv = isAssistant.querySelector('.markdown'); if (!messageDiv) continue; let answerText = ''; let researchInfo = ''; let thinkingTime = ''; // 检查是否有思考时间 const thinkingTimeElement = turn.querySelector('span.text-token-text-secondary'); if (thinkingTimeElement && thinkingTimeElement.textContent.includes('Thought for')) { thinkingTime = thinkingTimeElement.textContent.trim(); console.log('找到思考时间:', thinkingTime); } // 检查是否是深度研究结果 const deepResearchContainer = turn.querySelector('.border-token-border-sharp .markdown'); const deepResearchResult = turn.querySelector('.deep-research-result'); if (deepResearchResult) { // 深度研究结果 answerText = deepResearchResult.innerHTML.trim(); console.log('找到深度研究结果'); } else if (deepResearchContainer) { // 新的深度研究容器结构 answerText = deepResearchContainer.innerHTML.trim(); console.log('找到新的深度研究容器'); } else { // 普通回复 answerText = messageDiv.innerHTML.trim(); } // 检查是否有研究状态信息 const researchButton = turn.querySelector('button.text-token-text-tertiary'); if (researchButton) { const researchText = researchButton.textContent.trim(); if (researchText.includes('Research completed') || researchText.includes('sources')) { console.log('找到研究状态信息'); researchInfo = `*${researchText}*\n\n`; } } // 清理回复内容 let tempDivAnswer = document.createElement('div'); tempDivAnswer.innerHTML = answerText; removeEditButtons(tempDivAnswer); await convertImagesToBase64(tempDivAnswer); answerText = tempDivAnswer.innerHTML; // 获取模型信息 const modelSlug = isAssistant.getAttribute('data-message-model-slug') || ''; const roleName = getRoleName(modelSlug); answerText = await htmlToMarkdown(answerText); markdownContent += `## ${roleName} ${assistantIndex}\n\n`; if (thinkingTime) { markdownContent += `*${thinkingTime}*\n\n`; } if (researchInfo) { markdownContent += researchInfo; } markdownContent += `${answerText}\n\n`; assistantIndex++; } } markdownContent = markdownContent.replace(/&/g, '&'); if (markdownContent.length > 0) { download(markdownContent, 'chat-export.md', 'text/markdown'); console.log('Markdown导出完成'); } else { alert("未找到任何问题或答案。"); } } async function exportChatAsHTML() { console.log('开始导出HTML...'); let htmlContent = `<!DOCTYPE html> <html><head> <meta charset='UTF-8'> <title>ChatGPT对话导出</title> <style> body { font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif; line-height: 1.5; max-width: 800px; margin: 0 auto; padding: 20px; background: #f5f5f5; } .conversation-container { background: white; border-radius: 10px; padding: 20px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } .message { margin-bottom: 20px; padding: 15px; border-radius: 8px; } .user-message { background: #e3f2fd; border-left: 4px solid #2196f3; } .assistant-message { background: #f3e5f5; border-left: 4px solid #9c27b0; } .research-info { background: #fff3cd; border: 1px solid #ffeaa7; border-radius: 5px; padding: 10px; margin: 10px 0; font-style: italic; } .thinking-time { color: #666; font-size: 12px; font-style: italic; margin-bottom: 10px; } .timestamp { color: #666; font-size: 12px; margin-top: 5px; } pre { background: #f5f5f5; padding: 10px; border-radius: 5px; overflow-x: auto; } code { font-family: monospace; } h1 { color: #333; text-align: center; } h2 { color: #444; border-bottom: 1px solid #ddd; padding-bottom: 5px; } </style> </head><body> <div class="conversation-container"> <h1>ChatGPT对话导出</h1>`; // 2025年新版UI选择器 const conversationTurns = document.querySelectorAll('article[data-testid^="conversation-turn-"]'); console.log(`找到 ${conversationTurns.length} 个对话轮次`); if (conversationTurns.length === 0) { alert("未找到任何对话内容。请确保您已经在ChatGPT页面并且有对话历史。"); return; } let userIndex = 1; let assistantIndex = 1; for (let turn of conversationTurns) { // 确定是用户消息还是AI回复 const isUser = turn.querySelector('[data-message-author-role="user"]'); const isAssistant = turn.querySelector('[data-message-author-role="assistant"]'); if (isUser) { // 用户消息 const messageDiv = isUser.querySelector('.whitespace-pre-wrap'); if (!messageDiv) continue; let userText = messageDiv.innerHTML.trim(); // 清理用户消息 let tempDivUser = document.createElement('div'); tempDivUser.innerHTML = userText; removeEditButtons(tempDivUser); await convertImagesToBase64(tempDivUser); userText = tempDivUser.innerHTML; // 获取时间戳 const timestamp = turn.querySelector('time[datetime]'); const timeText = timestamp ? timestamp.getAttribute('title') || timestamp.textContent : ''; htmlContent += ` <div class="message user-message"> <h2>User ${userIndex}</h2> <div>${userText}</div> ${timeText ? `<div class="timestamp">${timeText}</div>` : ''} </div>`; userIndex++; } else if (isAssistant) { // AI回复 const messageDiv = isAssistant.querySelector('.markdown'); if (!messageDiv) continue; let answerText = ''; let researchInfo = ''; let thinkingTime = ''; // 检查是否有思考时间 const thinkingTimeElement = turn.querySelector('span.text-token-text-secondary'); if (thinkingTimeElement && thinkingTimeElement.textContent.includes('Thought for')) { thinkingTime = thinkingTimeElement.textContent.trim(); console.log('找到思考时间:', thinkingTime); } // 检查是否是深度研究结果 const deepResearchContainer = turn.querySelector('.border-token-border-sharp .markdown'); const deepResearchResult = turn.querySelector('.deep-research-result'); if (deepResearchResult) { // 深度研究结果 answerText = deepResearchResult.innerHTML.trim(); console.log('找到深度研究结果'); } else if (deepResearchContainer) { // 新的深度研究容器结构 answerText = deepResearchContainer.innerHTML.trim(); console.log('找到新的深度研究容器'); } else { // 普通回复 answerText = messageDiv.innerHTML.trim(); } // 检查是否有研究状态信息 const researchButton = turn.querySelector('button.text-token-text-tertiary'); if (researchButton) { const researchText = researchButton.textContent.trim(); if (researchText.includes('Research completed') || researchText.includes('sources')) { console.log('找到研究状态信息'); researchInfo = `<div class="research-info">${researchText}</div>`; } } // 清理回复内容 let tempDivAnswer = document.createElement('div'); tempDivAnswer.innerHTML = answerText; removeEditButtons(tempDivAnswer); await convertImagesToBase64(tempDivAnswer); answerText = tempDivAnswer.innerHTML; // 获取模型信息 const modelSlug = isAssistant.getAttribute('data-message-model-slug') || ''; const roleName = getRoleName(modelSlug); // 获取时间戳 const timestamp = turn.querySelector('time[datetime]'); const timeText = timestamp ? timestamp.getAttribute('title') || timestamp.textContent : ''; htmlContent += ` <div class="message assistant-message"> <h2>${roleName} ${assistantIndex}</h2> ${thinkingTime ? `<div class="thinking-time">${thinkingTime}</div>` : ''} ${researchInfo} <div>${answerText}</div> ${timeText ? `<div class="timestamp">${timeText}</div>` : ''} </div>`; assistantIndex++; } } htmlContent += ` </div> </body></html>`; if (htmlContent.length > 100) { download(htmlContent, 'chat-export.html', 'text/html'); console.log('HTML导出完成'); } else { alert("未找到任何问题或答案。"); } } function removeEditButtons(container) { // 移除编辑按钮和其他UI元素 container.querySelectorAll('button[aria-label*="编辑"], button[aria-label*="Edit"], button[data-testid*="copy"]').forEach(button => { let parentDiv = button.closest('div'); if (parentDiv && parentDiv.children.length <= 2) { parentDiv.remove(); } else { button.remove(); } }); // 移除脚注按钮和其他交互元素 container.querySelectorAll('button[class*="footnote"], div[class*="sources"]').forEach(element => { element.remove(); }); } function getRoleName(modelSlug) { // 根据模型slug返回角色名称 if (modelSlug.includes('research')) { return 'DeepResearch'; } else { return 'GPT'; } } function download(data, filename, type) { const blob = new Blob([data], {type: type}); if (window.navigator.msSaveOrOpenBlob) { window.navigator.msSaveOrOpenBlob(blob, filename); } else { const a = document.createElement('a'); const url = URL.createObjectURL(blob); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); setTimeout(() => { document.body.removeChild(a); window.URL.revokeObjectURL(url); }, 0); } } async function convertImagesToBase64(container) { const imgElements = container.querySelectorAll('img'); for (let img of imgElements) { let src = img.src; try { let base64 = await getBase64FromImageUrl(src); img.src = base64; } catch (error) { console.error(`图片转换为Base64失败,使用原始链接: ${src}`, error); } } } function getBase64FromImageUrl(url) { return new Promise((resolve, reject) => { try { // 使用GM_xmlhttpRequest来避免跨域问题 if (typeof GM_xmlhttpRequest !== 'undefined') { GM_xmlhttpRequest({ method: 'GET', url: url, responseType: 'blob', onload: function(response) { let reader = new FileReader(); reader.onloadend = function() { resolve(reader.result); }; reader.onerror = function(error) { reject(error); }; reader.readAsDataURL(response.response); }, onerror: function(error) { reject(error); } }); } else if (typeof GM !== 'undefined' && typeof GM.xmlHttpRequest !== 'undefined') { // 备选GM API GM.xmlHttpRequest({ method: 'GET', url: url, responseType: 'blob', onload: function(response) { let reader = new FileReader(); reader.onloadend = function() { resolve(reader.result); }; reader.onerror = function(error) { reject(error); }; reader.readAsDataURL(response.response); }, onerror: function(error) { reject(error); } }); } else { // 如果GM API不可用,尝试使用普通的fetch (可能会受跨域限制) fetch(url) .then(response => response.blob()) .then(blob => { let reader = new FileReader(); reader.onloadend = function() { resolve(reader.result); }; reader.onerror = function(error) { reject(error); }; reader.readAsDataURL(blob); }) .catch(error => reject(error)); } } catch (error) { reject(error); } }); } async function htmlToMarkdown(html) { const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); removeEditButtons(doc.body); // 保留引用链接,不转换成普通链接 doc.querySelectorAll('a[class*="inline-flex"]').forEach(a => { a.setAttribute('data-preserve', 'true'); }); // 处理数学公式 doc.querySelectorAll('span.katex-html').forEach(element => element.remove()); doc.querySelectorAll('mrow').forEach(mrow => mrow.remove()); doc.querySelectorAll('annotation[encoding="application/x-tex"]').forEach(element => { if (element.closest('.katex-display')) { const latex = element.textContent; element.replaceWith(`\n$$\n${latex}\n$$\n`); } else { const latex = element.textContent; element.replaceWith(`$${latex}$`); } }); // 处理代码块 doc.querySelectorAll('pre').forEach(pre => { const codeContainer = pre.querySelector('code'); if (!codeContainer) return; let codeType = ''; const languageDiv = pre.querySelector('[data-testid="code-block-language"]'); if (languageDiv) { codeType = languageDiv.textContent.trim(); } const codeContent = codeContainer.textContent || ''; pre.innerHTML = `\n\`\`\`${codeType}\n${codeContent}\n\`\`\`\n`; }); // 处理行内代码 doc.querySelectorAll('p code, li code, span code').forEach(code => { const markdownCode = `\`${code.textContent}\``; const codeTextNode = document.createTextNode(markdownCode); code.parentNode.replaceChild(codeTextNode, code); }); // 处理粗体文本 doc.querySelectorAll('strong, b').forEach(bold => { if (bold.closest('[data-preserve="true"]')) return; const markdownBold = `**${bold.textContent}**`; const boldTextNode = document.createTextNode(markdownBold); bold.parentNode.replaceChild(boldTextNode, bold); }); // 处理斜体文本 doc.querySelectorAll('em, i').forEach(italic => { if (italic.closest('[data-preserve="true"]')) return; const markdownItalic = `*${italic.textContent}*`; const italicTextNode = document.createTextNode(markdownItalic); italic.parentNode.replaceChild(italicTextNode, italic); }); // 处理链接,但保留需要保留的链接 doc.querySelectorAll('a').forEach(link => { if (link.hasAttribute('data-preserve')) return; const markdownLink = `[${link.textContent}](${link.href})`; const linkTextNode = document.createTextNode(markdownLink); link.parentNode.replaceChild(linkTextNode, link); }); // 处理图片 doc.querySelectorAll('img').forEach(img => { if (img.closest('[data-preserve="true"]')) return; const markdownImage = ``; const imgTextNode = document.createTextNode(markdownImage); img.parentNode.replaceChild(imgTextNode, img); }); // 处理无序列表 doc.querySelectorAll('ul').forEach(ul => { if (ul.closest('[data-preserve="true"]')) return; let markdown = ''; ul.querySelectorAll(':scope > li').forEach(li => { markdown += `- ${li.textContent.trim()}\n`; }); const markdownTextNode = document.createTextNode('\n' + markdown.trim()); ul.parentNode.replaceChild(markdownTextNode, ul); }); // 处理有序列表 doc.querySelectorAll('ol').forEach(ol => { if (ol.closest('[data-preserve="true"]')) return; let markdown = ''; ol.querySelectorAll(':scope > li').forEach((li, index) => { markdown += `${index + 1}. ${li.textContent.trim()}\n`; }); const markdownTextNode = document.createTextNode('\n' + markdown.trim()); ol.parentNode.replaceChild(markdownTextNode, ol); }); // 处理标题 for (let i = 1; i <= 6; i++) { doc.querySelectorAll(`h${i}`).forEach(header => { if (header.closest('[data-preserve="true"]')) return; const markdownHeader = `\n${'#'.repeat(i)} ${header.textContent}\n`; const headerTextNode = document.createTextNode(markdownHeader); header.parentNode.replaceChild(headerTextNode, header); }); } // 处理段落 doc.querySelectorAll('p').forEach(p => { if (p.closest('[data-preserve="true"]')) return; const markdownParagraph = `\n${p.textContent}\n`; const paragraphTextNode = document.createTextNode(markdownParagraph); p.parentNode.replaceChild(paragraphTextNode, p); }); // 处理表格 doc.querySelectorAll('table').forEach(table => { if (table.closest('[data-preserve="true"]')) return; let markdown = ''; table.querySelectorAll('thead tr').forEach(tr => { tr.querySelectorAll('th').forEach(th => { markdown += `| ${th.textContent} `; }); markdown += '|\n'; tr.querySelectorAll('th').forEach(() => { markdown += '| ---- '; }); markdown += '|\n'; }); table.querySelectorAll('tbody tr').forEach(tr => { tr.querySelectorAll('td').forEach(td => { markdown += `| ${td.textContent} `; }); markdown += '|\n'; }); const markdownTextNode = document.createTextNode('\n' + markdown.trim() + '\n'); table.parentNode.replaceChild(markdownTextNode, table); }); // 移除所有剩余HTML标签 let markdown = doc.body.innerHTML.replace(/<[^>]*>/g, ''); // 清理额外的空白行和空格 markdown = markdown.replace(/\n{3,}/g, '\n\n'); return markdown.trim(); } })();