导出 ChatGPT 对话为 Markdown,支持代码块、列表、表格等格式化
// ==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+="";
}
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}
);
})();