您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Export ChatGPT conversations (incl. thoughts, tool calls & custom instructions) to clean Markdown or raw JSON.
// ==UserScript== // @name Easy ChatGPT Markdown & JSON Exporter // @namespace https://github.com/NoahTheGinger/Userscripts/ // @version 1.6.0 // @description Export ChatGPT conversations (incl. thoughts, tool calls & custom instructions) to clean Markdown or raw JSON. // @author NoahTheGinger // @note Original development assistance from Gemini 2.5 Pro in AI Studio, and a large logic fix for tool calls by o3 (high reasoning effort) in OpenAI's Chat Playground, and button logic fixed by Grok 4 via API. JSON export feature added by Claude 4 Sonnet as the Cursor Agent. // @match https://chat.openai.com/* // @match https://chatgpt.com/* // @grant none // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/sentinel.min.js // @license MIT // ==/UserScript== (function () { "use strict"; /* ---------- 1. authentication & fetch ---------- */ async function getAccessToken() { const r = await fetch("/api/auth/session"); if (!r.ok) throw new Error("Not authorised – log-in again"); const j = await r.json(); if (!j.accessToken) throw new Error("No access token"); return j.accessToken; } function getChatIdFromUrl() { const m = location.pathname.match(/\/c\/([a-zA-Z0-9-]+)/); return m ? m[1] : null; } async function fetchConversation(id) { const token = await getAccessToken(); const resp = await fetch(`${location.origin}/backend-api/conversation/${id}`, { headers: { Authorization: `Bearer ${token}` } }); if (!resp.ok) throw new Error(resp.statusText); return resp.json(); } /* ---------- 2. processing & markdown ---------- */ function processConversation(raw) { const title = raw.title || "ChatGPT Conversation"; const nodes = []; let cur = raw.current_node; while (cur) { const n = raw.mapping[cur]; if (n && n.message && n.message.author?.role !== "system") nodes.unshift(n); cur = n?.parent; } return { title, nodes }; } /* message --> markdown */ function transformMessage(msg) { if (!msg || !msg.content) return ""; const { content, metadata, author } = msg; switch (content.content_type) { case "text": return content.parts?.join("\n") || ""; case "code": { // tool-call or normal snippet const raw = content.text || ""; const looksJson = raw.trim().startsWith("{") && raw.trim().endsWith("}"); const lang = content.language || metadata?.language || (looksJson ? "json" : "") || "txt"; const header = looksJson ? "**Tool Call:**\n" : ""; return `${header}\`\`\`${lang}\n${raw}\n\`\`\``; } case "thoughts": return content.thoughts .map( t => `**${t.summary}**\n\n> ${t.content.replace(/\n/g, "\n> ")}` ) .join("\n\n"); case "multimodal_text": return ( content.parts ?.map(p => { if (typeof p === "string") return p; if (p.content_type === "image_asset_pointer") return "![Image]"; if (p.content_type === "code") return `\`\`\`\n${p.text || ""}\n\`\`\``; return `[Unsupported: ${p.content_type}]`; }) .join("\n") || "" ); /* noise we always skip */ case "model_editable_context": case "reasoning_recap": return ""; default: return `[Unsupported content type: ${content.content_type}]`; } } /* whole conversation --> markdown */ function conversationToMarkdown({ title, nodes }) { let md = `# ${title}\n\n`; /* prepend custom instructions (user_editable_context) --------- */ const idx = nodes.findIndex( n => n.message?.content?.content_type === "user_editable_context" ); if (idx > -1) { const ctx = nodes[idx].message.content; md += "### User Editable Context:\n\n"; if (ctx.user_profile) md += `**About User:**\n\`\`\`\n${ctx.user_profile}\n\`\`\`\n\n`; if (ctx.user_instructions) md += `**About GPT:**\n\`\`\`\n${ctx.user_instructions}\n\`\`\`\n\n`; md += "---\n\n"; nodes.splice(idx, 1); // remove so we don’t re-process it } /* main loop --------------------------------------------------- */ for (let i = 0; i < nodes.length; ) { const n = nodes[i]; const m = n.message; if (!m || m.recipient !== "all") { i++; continue; } if (m.author.role === "user") { md += `### User:\n\n${transformMessage(m)}\n\n---\n\n`; i++; continue; } if (m.author.role === "assistant") { /* gather reasoning (thoughts & tool-call code) ------------- */ if (m.content.content_type !== "text") { md += "### Thoughts:\n\n"; while ( i < nodes.length && ["assistant", "tool"].includes(nodes[i].message.author.role) && nodes[i].message.content.content_type !== "text" ) { const chunk = transformMessage(nodes[i].message); if (chunk) md += `${chunk}\n\n`; i++; } md += "---\n\n"; continue; } /* final assistant reply ------------------------------------ */ md += `### ChatGPT:\n\n${transformMessage(m)}\n\n---\n\n`; i++; continue; } /* tool messages that slipped through and weren’t handled */ if (m.author.role === "tool") { const chunk = transformMessage(m); if (chunk) md += `### Thoughts:\n\n${chunk}\n\n---\n\n`; } i++; } return md.trimEnd(); } /* ---------- 3. UI / download ---------- */ const sanitizeFilename = s => s.replace(/[\/\\?<>:*|"]/g, "-"); function downloadFile(name, data, contentType = "text/markdown") { const url = URL.createObjectURL( new Blob([data], { type: `${contentType};charset=utf-8` }) ); const a = Object.assign(document.createElement("a"), { href: url, download: name }); document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); } /* export actions */ async function exportToMarkdown() { const btn = document.getElementById("simplified-markdown-exporter-button"); if (btn) { btn.textContent = "Exporting..."; btn.disabled = true; } try { const id = getChatIdFromUrl(); if (!id) return alert("No conversation ID found."); const raw = await fetchConversation(id); const md = conversationToMarkdown(processConversation(raw)); downloadFile(`${sanitizeFilename(raw.title)}.md`, md, "text/markdown"); } catch (e) { console.error(e); alert("Markdown export failed – see console."); } finally { if (btn) { btn.textContent = "Export"; btn.disabled = false; } } } async function exportToJSON() { const btn = document.getElementById("simplified-markdown-exporter-button"); if (btn) { btn.textContent = "Exporting..."; btn.disabled = true; } try { const id = getChatIdFromUrl(); if (!id) return alert("No conversation ID found."); const raw = await fetchConversation(id); const jsonContent = JSON.stringify(raw, null, 2); downloadFile(`${sanitizeFilename(raw.title)}.json`, jsonContent, "application/json"); } catch (e) { console.error(e); alert("JSON export failed – see console."); } finally { if (btn) { btn.textContent = "Export"; btn.disabled = false; } } } function showExportDialog() { // Create modal dialog const modal = document.createElement("div"); modal.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 10000; display: flex; align-items: center; justify-content: center; `; const dialog = document.createElement("div"); dialog.style.cssText = ` background: var(--surface-primary, white); border-radius: 8px; padding: 24px; min-width: 300px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); color: var(--text-primary, #333); border: 1px solid var(--border-light, #e5e7eb); `; dialog.innerHTML = ` <h3 style="margin: 0 0 16px 0; color: var(--text-primary, #333); font-size: 18px;">Choose Export Format</h3> <p style="margin: 0 0 20px 0; color: var(--text-secondary, #666); font-size: 14px;">Select the format you'd like to export this conversation in:</p> <div style="display: flex; gap: 12px; justify-content: flex-end;"> <button id="export-markdown-btn" style=" background: var(--accent-primary, #10a37f); color: white; border: none; border-radius: 6px; padding: 8px 16px; font-size: 14px; cursor: pointer; transition: background-color 0.2s; ">Markdown (.md)</button> <button id="export-json-btn" style=" background: var(--accent-secondary, #2563eb); color: white; border: none; border-radius: 6px; padding: 8px 16px; font-size: 14px; cursor: pointer; transition: background-color 0.2s; ">JSON (.json)</button> <button id="export-cancel-btn" style=" background: var(--bg-secondary, #6b7280); color: white; border: none; border-radius: 6px; padding: 8px 16px; font-size: 14px; cursor: pointer; transition: background-color 0.2s; ">Cancel</button> </div> `; modal.appendChild(dialog); document.body.appendChild(modal); // Add event listeners const markdownBtn = dialog.querySelector("#export-markdown-btn"); const jsonBtn = dialog.querySelector("#export-json-btn"); const cancelBtn = dialog.querySelector("#export-cancel-btn"); markdownBtn.addEventListener("click", () => { document.body.removeChild(modal); exportToMarkdown(); }); jsonBtn.addEventListener("click", () => { document.body.removeChild(modal); exportToJSON(); }); cancelBtn.addEventListener("click", () => { document.body.removeChild(modal); }); // Close on background click modal.addEventListener("click", (e) => { if (e.target === modal) { document.body.removeChild(modal); } }); // Close on Escape key const handleEscape = (e) => { if (e.key === "Escape") { document.body.removeChild(modal); document.removeEventListener("keydown", handleEscape); } }; document.addEventListener("keydown", handleEscape); } /* button */ function createButton() { const b = document.createElement("button"); b.id = "simplified-markdown-exporter-button"; b.textContent = "Export"; b.className = "btn relative btn-neutral rounded-md px-3 py-1.5 text-sm font-medium transition-colors duration-150"; b.style.backgroundColor = "var(--bg-elevated-secondary)"; b.style.border = "1px solid var(--border-light)"; b.style.cursor = "pointer"; b.style.display = "inline-flex"; b.style.alignItems = "center"; b.style.justifyContent = "center"; b.style.lineHeight = "1.5"; b.addEventListener("click", showExportDialog); return b; } function init() { // This selector targets the container for the buttons on the right side of the composer. sentinel.on("div[data-testid='composer-trailing-actions'] > .ms-auto", (buttonContainer) => { if (document.getElementById("simplified-markdown-exporter-button")) { return; } const newButton = createButton(); // The first element in this container is the dictate button's span. const referenceNode = buttonContainer.firstChild; if (referenceNode) { // Insert our button before the first existing button. buttonContainer.insertBefore(newButton, referenceNode); } else { // Fallback if the container is somehow empty when found. buttonContainer.appendChild(newButton); } }); } init(); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址