ChatGPT Markdown Export

导出 ChatGPT 对话为 Markdown,支持代码块、列表、表格等格式化

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name         ChatGPT Markdown Export
// @namespace    http://tampermonkey.net/
// @version      4.0
// @description  导出 ChatGPT 对话为 Markdown,支持代码块、列表、表格等格式化
// @author       Kiracle
// @match        https://chatgpt.com/*
// @match        https://chat.openai.com/*
// @grant        none
// @license MIT
// ==/UserScript==

(function(){

"use strict";

////////////////////////////////////////////////
//// 标题
////////////////////////////////////////////////

function getTitle(){

let t=document.title
.replace(" - ChatGPT","")
.trim();

if(!t) t="chatgpt-chat";

return t;

}

////////////////////////////////////////////////
//// DOM → Markdown
////////////////////////////////////////////////

function parse(node){

let md="";

node.childNodes.forEach(child=>{

if(child.nodeType===3){

md+=child.textContent;

return;

}

let tag=child.nodeName;

if(tag==="P"){

md+=parse(child)+"\n";

}

else if(tag==="BR"){

md+="\n";

}

else if(tag==="STRONG"||tag==="B"){

md+="**"+parse(child)+"**";

}

else if(tag==="EM"||tag==="I"){

md+="*"+parse(child)+"*";

}

else if(tag==="CODE" && child.parentNode.nodeName!=="PRE"){

md+="`"+child.textContent+"`";

}

else if(tag==="PRE"){

let code=child.innerText.trim();

let lang="";

let c=child.querySelector("code");

if(c){

let m=c.className.match(/language-(\w+)/);

if(m) lang=m[1];

}

md+="\n```"+lang+"\n"+
code+
"\n```\n";

}

else if(tag==="UL"){

child.querySelectorAll(":scope > li")
.forEach(li=>{

md+="- "+parse(li).trim()+"\n";

});

}

else if(tag==="OL"){

let i=1;

child.querySelectorAll(":scope > li")
.forEach(li=>{

md+=i+". "+
parse(li).trim()+"\n";

i++;

});

}

else if(tag==="BLOCKQUOTE"){

md+="> "+
parse(child).trim()+
"\n";

}

else if(tag==="A"){

md+="["+
parse(child)+
"]("+
child.href+
")";

}

else if(tag==="IMG"){

md+="![]("+
child.src+
")";

}

else if(tag==="TABLE"){

md+=table(child);

}

else{

md+=parse(child);

}

});

return md;

}

////////////////////////////////////////////////
//// 表格
////////////////////////////////////////////////

function table(t){

let md="\n";

let rows=t.querySelectorAll("tr");

rows.forEach((row,i)=>{

let cols=row.querySelectorAll("th,td");

let line="|";

cols.forEach(col=>{

line+=parse(col).trim()+"|";

});

md+=line+"\n";

if(i===0){

let sep="|";

cols.forEach(()=>{

sep+="---|";

});

md+=sep+"\n";

}

});

return md+"\n";

}

////////////////////////////////////////////////
//// 获取消息
////////////////////////////////////////////////

function getMessages(){

let nodes=document.querySelectorAll(
"[data-message-author-role]"
);

let arr=[];

nodes.forEach(n=>{

let role=n.getAttribute(
"data-message-author-role"
);

let content=n.querySelector(
".markdown,.prose"
)||n;

let text=parse(content).trim();

if(text){

arr.push({

role:role,
text:text

});

}

});

return arr;

}

////////////////////////////////////////////////
//// Markdown优化(核心)
////////////////////////////////////////////////

function clean(md){

md=md.replace(/\r\n/g,"\n");

// 超过2空行 → 1空行
md=md.replace(/\n{3,}/g,"\n\n");

// 列表紧凑
md=md.replace(/\n\n- /g,"\n- ");

md=md.replace(/\n\n\d+\. /g,
m=>"\n"+m.trim()
);

// 标题规范
md=md.replace(
/(#+ .*?)\n{2,}/g,
"$1\n"
);

// 代码块规范
md=md.replace(
/\n{2,}```/g,
"\n```"
);

md=md.replace(
/```\n{2,}/g,
"```\n"
);

// 引用规范
md=md.replace(
/\n\n> /g,
"\n> "
);

// 去头尾空白
md=md.trim();

return md+"\n";

}

////////////////////////////////////////////////
//// 生成markdown
////////////////////////////////////////////////

function build(messages){

let md="# "+getTitle()+"\n\n";

md+="Export time: "+
new Date().toLocaleString()+
"\n\n---\n\n";

messages.forEach(m=>{

if(m.role==="user"){

md+="## 🧑 User\n";

}else{

md+="## 🤖 ChatGPT\n";

}

md+=m.text+"\n\n";

});

return clean(md);

}

////////////////////////////////////////////////
//// 下载
////////////////////////////////////////////////

function download(md){

let blob=new Blob(
[md],
{type:"text/markdown"}
);

let url=URL.createObjectURL(blob);

let a=document.createElement("a");

let name=getTitle()
.replace(/[\\/:*?"<>|]/g,"");

let time=new Date()
.toISOString()
.slice(0,19)
.replace(/:/g,"-");

a.download=
name+"-"+time+".md";

a.href=url;

document.body.appendChild(a);

a.click();

a.remove();

URL.revokeObjectURL(url);

}

////////////////////////////////////////////////
//// 导出
////////////////////////////////////////////////

function exportMD(){

let msgs=getMessages();

if(!msgs.length){

alert("No messages");

return;

}

download(build(msgs));

}

////////////////////////////////////////////////
//// UI
////////////////////////////////////////////////

function addBtn(){

if(
document.getElementById(
"md-export"
)
)return;

let btn=document.createElement(
"button"
);

btn.id="md-export";

btn.innerText="MD";

btn.style.margin="6px";

btn.style.padding=
"6px 12px";

btn.style.borderRadius=
"8px";

btn.style.border="none";

btn.style.background=
"#10a37f";

btn.style.color="white";

btn.style.cursor="pointer";

btn.onclick=exportMD;

let nav=
document.querySelector("nav");

if(nav){

nav.appendChild(btn);

}

}

new MutationObserver(addBtn)
.observe(

document.body,

{childList:true,subtree:true}

);

})();