ChatGPT对话Markdown导出

导出ChatGPT和Grok网站上的对话为Markdown格式.

  1. // ==UserScript==
  2. // @name ChatGPT对话Markdown导出
  3. // @name:en ChatGPT to Markdown Exporter
  4. // @version 1.2
  5. // @description 导出ChatGPT和Grok网站上的对话为Markdown格式.
  6. // @description:en Export chat history from ChatGPT and Grok websites to Markdown format.
  7. // @author ChingyuanCheng //origin from: Marverlises
  8. // @license MIT
  9. // @match https://chatgpt.com/*
  10. // @match https://*.openai.com/*
  11. // @match https://grok.com/*
  12. // @grant none
  13. // @namespace https://gf.qytechs.cn/users/1435416
  14. // ==/UserScript==
  15.  
  16. (function() {
  17. // Select chat elements based on the website
  18. function getConversationElements() {
  19. const currentUrl = window.location.href;
  20. if (currentUrl.includes("openai.com") || currentUrl.includes("chatgpt.com")) {
  21. return document.querySelectorAll('div.flex.flex-grow.flex-col.max-w-full');
  22. } else if (currentUrl.includes("grok.com")) {
  23. return document.querySelectorAll('div.message-bubble');
  24. }
  25. return [];
  26. }
  27.  
  28. // Convert HTML to Markdown
  29. function htmlToMarkdown(html) {
  30. const parser = new DOMParser();
  31. const doc = parser.parseFromString(html, 'text/html');
  32.  
  33. // Handle formulas
  34. doc.querySelectorAll('span.katex-html').forEach(element => element.remove());
  35. doc.querySelectorAll('mrow').forEach(mrow => mrow.remove());
  36. doc.querySelectorAll('annotation[encoding="application/x-tex"]').forEach(element => {
  37. if (element.closest('.katex-display')) {
  38. const latex = element.textContent;
  39. element.replaceWith(`\n$$\n${latex}\n$$\n`);
  40. } else {
  41. const latex = element.textContent;
  42. element.replaceWith(`$${latex}$`);
  43. }
  44. });
  45.  
  46. // Bold text
  47. doc.querySelectorAll('strong, b').forEach(bold => {
  48. bold.parentNode.replaceChild(document.createTextNode(`**${bold.textContent}**`), bold);
  49. });
  50.  
  51. // Italic text
  52. doc.querySelectorAll('em, i').forEach(italic => {
  53. italic.parentNode.replaceChild(document.createTextNode(`*${italic.textContent}*`), italic);
  54. });
  55.  
  56. // Inline code
  57. doc.querySelectorAll('p code').forEach(code => {
  58. code.parentNode.replaceChild(document.createTextNode(`\`${code.textContent}\``), code);
  59. });
  60.  
  61. // Links
  62. doc.querySelectorAll('a').forEach(link => {
  63. link.parentNode.replaceChild(document.createTextNode(`[${link.textContent}](${link.href})`), link);
  64. });
  65.  
  66. // Images
  67. doc.querySelectorAll('img').forEach(img => {
  68. img.parentNode.replaceChild(document.createTextNode(`![${img.alt}](${img.src})`), img);
  69. });
  70.  
  71. // Code blocks
  72. doc.querySelectorAll('pre').forEach(pre => {
  73. const codeType = pre.querySelector('div > div:first-child')?.textContent || '';
  74. const markdownCode = pre.querySelector('div > div:nth-child(3) > code')?.textContent || pre.textContent;
  75. pre.innerHTML = `\n\`\`\`${codeType}\n${markdownCode}\`\`\`\n`;
  76. });
  77.  
  78. // Unordered lists
  79. doc.querySelectorAll('ul').forEach(ul => {
  80. let markdown = '';
  81. ul.querySelectorAll(':scope > li').forEach(li => {
  82. markdown += `- ${li.textContent.trim()}\n`;
  83. });
  84. ul.parentNode.replaceChild(document.createTextNode('\n' + markdown.trim()), ul);
  85. });
  86.  
  87. // Ordered lists
  88. doc.querySelectorAll('ol').forEach(ol => {
  89. let markdown = '';
  90. ol.querySelectorAll(':scope > li').forEach((li, index) => {
  91. markdown += `${index + 1}. ${li.textContent.trim()}\n`;
  92. });
  93. ol.parentNode.replaceChild(document.createTextNode('\n' + markdown.trim()), ol);
  94. });
  95.  
  96. // Headers
  97. for (let i = 1; i <= 6; i++) {
  98. doc.querySelectorAll(`h${i}`).forEach(header => {
  99. header.parentNode.replaceChild(document.createTextNode('\n' + `${'#'.repeat(i)} ${header.textContent}\n`), header);
  100. });
  101. }
  102.  
  103. // Paragraphs
  104. doc.querySelectorAll('p').forEach(p => {
  105. p.parentNode.replaceChild(document.createTextNode('\n' + p.textContent + '\n'), p);
  106. });
  107.  
  108. // Tables
  109. doc.querySelectorAll('table').forEach(table => {
  110. let markdown = '';
  111. table.querySelectorAll('thead tr').forEach(tr => {
  112. tr.querySelectorAll('th').forEach(th => {
  113. markdown += `| ${th.textContent} `;
  114. });
  115. markdown += '|\n';
  116. tr.querySelectorAll('th').forEach(() => {
  117. markdown += '| ---- ';
  118. });
  119. markdown += '|\n';
  120. });
  121. table.querySelectorAll('tbody tr').forEach(tr => {
  122. tr.querySelectorAll('td').forEach(td => {
  123. markdown += `| ${td.textContent} `;
  124. });
  125. markdown += '|\n';
  126. });
  127. table.parentNode.replaceChild(document.createTextNode('\n' + markdown.trim() + '\n'), table);
  128. });
  129.  
  130. let markdown = doc.body.innerHTML.replace(/<[^>]*>/g, '');
  131. markdown = markdown.replaceAll(/- &gt;/g, '- $\\gt$')
  132. .replaceAll(/>/g, '>')
  133. .replaceAll(/</g, '<')
  134. .replaceAll(/≥/g, '>=')
  135. .replaceAll(/≤/g, '<=')
  136. .replaceAll(/≠/g, '\\neq');
  137. return markdown.trim();
  138. }
  139.  
  140. // Download content as a file
  141. function download(data, filename, type) {
  142. const file = new Blob([data], { type: type });
  143. const a = document.createElement('a');
  144. const url = URL.createObjectURL(file);
  145. a.href = url;
  146. a.download = filename;
  147. document.body.appendChild(a);
  148. a.click();
  149. setTimeout(() => {
  150. document.body.removeChild(a);
  151. window.URL.revokeObjectURL(url);
  152. }, 0);
  153. }
  154.  
  155. // Show export modal with Markdown content
  156. function showExportModal() {
  157. let markdownContent = "";
  158. const allElements = getConversationElements();
  159.  
  160. for (let i = 0; i < allElements.length; i += 2) {
  161. if (!allElements[i + 1]) break;
  162. let userText = allElements[i].textContent.trim();
  163. let answerHtml = allElements[i + 1].innerHTML.trim();
  164.  
  165. userText = htmlToMarkdown(userText);
  166. answerHtml = htmlToMarkdown(answerHtml);
  167.  
  168. const isGrok = window.location.href.includes("grok.com");
  169. markdownContent += `\n# User Question\n${userText}\n# ${isGrok ? 'Grok' : 'ChatGPT'}\n${answerHtml}`;
  170. }
  171. markdownContent = markdownContent.replace(/&amp;/g, '&');
  172.  
  173. if (!markdownContent) {
  174. alert("No conversation content found.");
  175. return;
  176. }
  177.  
  178. // Create modal
  179. const modal = document.createElement('div');
  180. modal.id = 'markdown-modal';
  181. Object.assign(modal.style, {
  182. position: 'fixed',
  183. top: '0',
  184. left: '0',
  185. width: '100%',
  186. height: '100%',
  187. backgroundColor: 'rgba(0, 0, 0, 0.5)',
  188. display: 'flex',
  189. alignItems: 'center',
  190. justifyContent: 'center',
  191. zIndex: '1000'
  192. });
  193.  
  194. const modalContent = document.createElement('div');
  195. Object.assign(modalContent.style, {
  196. backgroundColor: '#fff',
  197. color: '#000',
  198. padding: '20px',
  199. borderRadius: '8px',
  200. width: '50%',
  201. height: '80%',
  202. display: 'flex',
  203. flexDirection: 'column',
  204. boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
  205. overflow: 'hidden'
  206. });
  207.  
  208. const textarea = document.createElement('textarea');
  209. textarea.value = markdownContent;
  210. Object.assign(textarea.style, {
  211. flex: '1',
  212. resize: 'none',
  213. width: '100%',
  214. padding: '10px',
  215. fontSize: '14px',
  216. fontFamily: 'monospace',
  217. marginBottom: '10px',
  218. boxSizing: 'border-box',
  219. color: '#000',
  220. backgroundColor: '#f9f9f9',
  221. border: '1px solid #ccc',
  222. borderRadius: '4px'
  223. });
  224. textarea.setAttribute('readonly', true);
  225.  
  226. const buttonContainer = document.createElement('div');
  227. Object.assign(buttonContainer.style, {
  228. display: 'flex',
  229. justifyContent: 'flex-end'
  230. });
  231.  
  232. const copyButton = document.createElement('button');
  233. copyButton.textContent = 'Copy';
  234. Object.assign(copyButton.style, {
  235. padding: '8px 16px',
  236. fontSize: '14px',
  237. cursor: 'pointer',
  238. backgroundColor: '#28A745',
  239. color: '#fff',
  240. border: 'none',
  241. borderRadius: '4px',
  242. marginRight: '10px'
  243. });
  244.  
  245. const downloadButton = document.createElement('button');
  246. downloadButton.textContent = 'Download';
  247. Object.assign(downloadButton.style, {
  248. padding: '8px 16px',
  249. fontSize: '14px',
  250. cursor: 'pointer',
  251. backgroundColor: '#007BFF',
  252. color: '#fff',
  253. border: 'none',
  254. borderRadius: '4px',
  255. marginRight: '10px'
  256. });
  257.  
  258. const closeButton = document.createElement('button');
  259. closeButton.textContent = 'Close';
  260. Object.assign(closeButton.style, {
  261. padding: '8px 16px',
  262. fontSize: '14px',
  263. cursor: 'pointer',
  264. backgroundColor: '#DC3545',
  265. color: '#fff',
  266. border: 'none',
  267. borderRadius: '4px'
  268. });
  269.  
  270. buttonContainer.appendChild(copyButton);
  271. buttonContainer.appendChild(downloadButton);
  272. buttonContainer.appendChild(closeButton);
  273. modalContent.appendChild(textarea);
  274. modalContent.appendChild(buttonContainer);
  275. modal.appendChild(modalContent);
  276. document.body.appendChild(modal);
  277.  
  278. // Event listeners for buttons
  279. copyButton.addEventListener('click', () => {
  280. textarea.select();
  281. navigator.clipboard.writeText(textarea.value)
  282. .then(() => {
  283. copyButton.textContent = 'Copied';
  284. setTimeout(() => copyButton.textContent = 'Copy', 2000);
  285. })
  286. .catch(err => console.error('Copy failed', err));
  287. });
  288.  
  289. downloadButton.addEventListener('click', () => {
  290. download(markdownContent, 'chat-export.md', 'text/markdown');
  291. });
  292.  
  293. closeButton.addEventListener('click', () => {
  294. document.body.removeChild(modal);
  295. });
  296.  
  297. // Close modal with Escape key or click outside
  298. const escListener = (e) => {
  299. if (e.key === 'Escape' && document.getElementById('markdown-modal')) {
  300. document.body.removeChild(modal);
  301. document.removeEventListener('keydown', escListener);
  302. }
  303. };
  304. document.addEventListener('keydown', escListener);
  305.  
  306. modal.addEventListener('click', (e) => {
  307. if (e.target === modal) {
  308. document.body.removeChild(modal);
  309. document.removeEventListener('keydown', escListener);
  310. }
  311. });
  312. }
  313.  
  314. // Create the export button on the page
  315. function createExportButton() {
  316. const exportButton = document.createElement('button');
  317. exportButton.textContent = 'Export Chat';
  318. exportButton.id = 'export-chat';
  319. const styles = {
  320. position: 'fixed',
  321. height: '36px',
  322. top: '10px',
  323. right: '35%',
  324. zIndex: '10000',
  325. padding: '10px',
  326. backgroundColor: '#000000',
  327. color: 'white',
  328. border: 'none',
  329. borderRadius: '5px',
  330. cursor: 'pointer',
  331. textAlign: 'center',
  332. lineHeight: '16px'
  333. };
  334. Object.assign(exportButton.style, styles);
  335. document.body.appendChild(exportButton);
  336. exportButton.addEventListener('click', showExportModal);
  337. }
  338.  
  339. // Initialize button and periodically check its presence
  340. createExportButton();
  341. setInterval(() => {
  342. if (!document.getElementById('export-chat')) {
  343. createExportButton();
  344. }
  345. }, 1000);
  346. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址