您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Newest userscript to export ChatGPT chat as JSON or Markdown. 📚All formulas 🧮 and tables📊 are fully preserved ✔️
// ==UserScript== // @name 🚀Export ChatGPT Chat as JSON or Markdown🚀 // @namespace ChatGPT Mods // @version 1.0.3 // @author ChatGPT Mods // @description Newest userscript to export ChatGPT chat as JSON or Markdown. 📚All formulas 🧮 and tables📊 are fully preserved ✔️ // @license MIT // @icon data:image/svg+xml;base64,PHN2ZyBmaWxsPSJjdXJyZW50Q29sb3IiIGZpbGwtcnVsZT0iZXZlbm9kZCIgaGVpZ2h0PSIxZW0iIHN0eWxlPSJmbGV4Om5vbmU7bGluZS1oZWlnaHQ6MSIgdmlld0JveD0iMCAwIDI0IDI0IiB3aWR0aD0iMWVtIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjx0aXRsZT5PcGVuQUk8L3RpdGxlPjxwYXRoIGQ9Ik0yMS41NSAxMC4wMDRhNS40MTYgNS40MTYgMCAwMC0uNDc4LTQuNTAxYy0xLjIxNy0yLjA5LTMuNjYyLTMuMTY2LTYuMDUtMi42NkE1LjU5IDUuNTkgMCAwMDEwLjgzMSAxQzguMzkuOTk1IDYuMjI0IDIuNTQ2IDUuNDczIDQuODM4QTUuNTUzIDUuNTUzIDAgMDAxLjc2IDcuNDk2YTUuNDg3IDUuNDg3IDAgMDAuNjkxIDYuNSA1LjQxNiA1LjQxNiAwIDAwLjQ3NyA0LjUwMmMxLjIxNyAyLjA5IDMuNjYyIDMuMTY1IDYuMDUgMi42NkE1LjU4NiA1LjU4NiAwIDAwMTMuMTY4IDIzYzIuNDQzLjAwNiA0LjYxLTEuNTQ2IDUuMzYxLTMuODRhNS41NTMgNS41NTMgMCAwMDMuNzE1LTIuNjYgNS40ODggNS40ODggMCAwMC0uNjkzLTYuNDk3di4wMDF6bS04LjM4MSAxMS41NThhNC4xOTkgNC4xOTkgMCAwMS0yLjY3NS0uOTU0Yy4wMzQtLjAxOC4wOTMtLjA1LjEzMi0uMDc0bDQuNDQtMi41M2EuNzEuNzEgMCAwMC4zNjQtLjYyM3YtNi4xNzZsMS44NzcgMS4wNjljLjAyLjAxLjAzMy4wMjkuMDM2LjA1djUuMTE1Yy0uMDAzIDIuMjc0LTEuODcgNC4xMTgtNC4xNzQgNC4xMjN6TTQuMTkyIDE3Ljc4YTQuMDU5IDQuMDU5IDAgMDEtLjQ5OC0yLjc2M2MuMDMyLjAyLjA5LjA1NS4xMzEuMDc4bDQuNDQgMi41M2MuMjI1LjEzLjUwNC4xMy43MyAwbDUuNDItMy4wODh2Mi4xMzhhLjA2OC4wNjggMCAwMS0uMDI3LjA1N0w5LjkgMTkuMjg4Yy0xLjk5OSAxLjEzNi00LjU1Mi40Ni01LjcwNy0xLjUxaC0uMDAxek0zLjAyMyA4LjIxNkE0LjE1IDQuMTUgMCAwMTUuMTk4IDYuNDFsLS4wMDIuMTUxdjUuMDZhLjcxMS43MTEgMCAwMC4zNjQuNjI0bDUuNDIgMy4wODctMS44NzYgMS4wN2EuMDY3LjA2NyAwIDAxLS4wNjMuMDA1bC00LjQ4OS0yLjU1OWMtMS45OTUtMS4xNC0yLjY3OS0zLjY1OC0xLjUzLTUuNjNoLjAwMXptMTUuNDE3IDMuNTRsLTUuNDItMy4wODhMMTQuODk2IDcuNmEuMDY3LjA2NyAwIDAxLjA2My0uMDA2bDQuNDg5IDIuNTU3YzEuOTk4IDEuMTQgMi42ODMgMy42NjIgMS41MjkgNS42MzNhNC4xNjMgNC4xNjMgMCAwMS0yLjE3NCAxLjgwN1YxMi4zOGEuNzEuNzEgMCAwMC0uMzYzLS42MjN6bTEuODY3LTIuNzczYTYuMDQgNi4wNCAwIDAwLS4xMzItLjA3OGwtNC40NC0yLjUzYS43MzEuNzMxIDAgMDAtLjcyOSAwbC01LjQyIDMuMDg4VjcuMzI1YS4wNjguMDY4IDAgMDEuMDI3LS4wNTdMMTQuMSA0LjcxM2MyLTEuMTM3IDQuNTU1LS40NiA1LjcwNyAxLjUxMy40ODcuODMzLjY2NCAxLjgwOS40OTkgMi43NTdoLjAwMXptLTExLjc0MSAzLjgxbC0xLjg3Ny0xLjA2OGEuMDY1LjA2NSAwIDAxLS4wMzYtLjA1MVY2LjU1OWMuMDAxLTIuMjc3IDEuODczLTQuMTIyIDQuMTgxLTQuMTIuOTc2IDAgMS45Mi4zMzggMi42NzEuOTU0LS4wMzQuMDE4LS4wOTIuMDUtLjEzMS4wNzNsLTQuNDQgMi41M2EuNzEuNzEgMCAwMC0uMzY1LjYyM2wtLjAwMyA2LjE3M3YuMDAyem0xLjAyLTIuMTY4TDEyIDkuMjVsMi40MTQgMS4zNzV2Mi43NUwxMiAxNC43NWwtMi40MTUtMS4zNzV2LTIuNzV6Ij48L3BhdGg+PC9zdmc+ // @match https://chatgpt.com/* // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.global.prod.js // @grant GM_download // @grant unsafeWindow // @run-at document-start // ==/UserScript== (function (vue) { 'use strict'; const GLOBAL_MESSAGE_OPEN_AI_TOKEN_GOT = "GLOBAL_MESSAGE_OPEN_AI_TOKEN_GOT"; const DOM_ID_HEADER_TOOL = "DOM_ID_HEADER_TOOL"; const DOM_ID_GLOBAL_LAYER = "DOM_ID_GLOBAL_LAYER"; var _GM_download = (() => typeof GM_download != "undefined" ? GM_download : void 0)(); var _unsafeWindow = (() => typeof unsafeWindow != "undefined" ? unsafeWindow : void 0)(); class FetchInterceptorManager { requestInterceptors = []; responseInterceptors = []; originalFetch; constructor() { this.originalFetch = _unsafeWindow.fetch.bind(_unsafeWindow); this.patchFetch(); } patchFetch() { _unsafeWindow.fetch = async (input, init) => { const newInput = typeof input === "string" ? input : input instanceof URL ? input.toString() : input; let context = { input: newInput, init }; for (const interceptor of this.requestInterceptors) { context = await interceptor(context.input, context.init); } const response = await this.originalFetch(context.input, context.init); let finalResponse = response; for (const interceptor of this.responseInterceptors) { finalResponse = await interceptor(finalResponse, context); } return finalResponse; }; } useRequest(interceptor) { this.requestInterceptors.push(interceptor); } useResponse(interceptor) { this.responseInterceptors.push(interceptor); } ejectAll() { this.requestInterceptors = []; this.responseInterceptors = []; _unsafeWindow.fetch = this.originalFetch; } } const FetchInterceptor = new FetchInterceptorManager(); bootstrap$1(); function bootstrap$1() { FetchInterceptor.useRequest(async (input, init) => { try { const a = document.createElement("a"); a.href = typeof input === "string" ? input : input.url; const url = a.href; console.log(url); const regexp = "/backend-api/(?!edge$)"; if (regexp && new RegExp(regexp).test(url)) { let authorization = ""; if (input instanceof Request) { authorization = input.headers.get("Authorization") || ""; } else if (init?.headers) { authorization = init.headers.Authorization || ""; } if (authorization) { window.postMessage({ type: GLOBAL_MESSAGE_OPEN_AI_TOKEN_GOT, data: { authorization } }); } } } catch { } return { input, init }; }); } vue.ref(false); vue.ref(false); vue.ref(false); vue.ref(false); vue.ref(false); vue.ref(false); const overlayContainer = vue.ref(); function initGlobal(container) { overlayContainer.value = container; } const configStore = vue.ref({ data: { chatGpt: { conversationIdRegExp: "/c/(?!WEB:)([0-9a-zA-Z-]+)", authRelatedPathRegExp: "/backend-api/(?!edge$)", apiConversationContent: "/backend-api/conversation/{{id}}" }, injectTarget: { headerTool: '//*[@id="conversation-header-actions"]' } } }); const userInfoStore = vue.ref({ auth: { openaiAuth: "" } }); function simpleTemplate(template, data) { if (!data) { return template; } else { return template.replace(/\{\{(\w+)\}\}/g, (_, key) => { return key in data ? data[key] : ""; }); } } function getConversationId(url) { const pathname = location.pathname; const match = pathname.match(new RegExp(configStore.value.data.chatGpt.conversationIdRegExp)); if (match && match.length === 2) { return match[1]; } return null; } function getElementsByXPath(xpath, contextNode = document, rootNode = document) { const snapshots = rootNode.evaluate(xpath, contextNode, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); if (snapshots.snapshotLength > 0) { const elements = []; for (let i = 0; i < snapshots.snapshotLength; i++) { const item = snapshots.snapshotItem(i); if (item) { elements.push(item); } } return elements; } return null; } function getElementByXPath(xpath, contextNode = document, rootNode = document) { const elements = getElementsByXPath(xpath, contextNode, rootNode); if (elements) { return elements[0]; } return null; } function textToBase64Url(text, mimeType) { const encoder = new TextEncoder(); const encoded = encoder.encode(text); const base64 = btoa(String.fromCharCode(...encoded)); return `data:${mimeType};base64,${base64}`; } navigator.userAgent.toLocaleLowerCase().includes("mac"); const _export_sfc = (sfc, props) => { const target = sfc.__vccOpts || sfc; for (const [key, val] of props) { target[key] = val; } return target; }; const _sfc_main$1 = {}; function _sfc_render(_ctx, _cache) { return null; } const GlobalOverlay = _export_sfc(_sfc_main$1, [["render", _sfc_render]]); async function request(method, path, params, signal) { if (!userInfoStore.value.auth.openaiAuth) { throw new Error("openAI not login"); } let body = null; const headers = { authorization: userInfoStore.value.auth.openaiAuth }; const response = await fetch(path, { signal, headers, method, body, mode: "cors", credentials: "include" }); if (response.ok) { return response.json(); } else { throw new Error(response.statusText); } } async function getConversationContent(id) { const result = await request("GET", simpleTemplate(configStore.value.data.chatGpt.apiConversationContent, { id })); const mapping = {}; for (const [key, value] of Object.entries(result.mapping)) { if (value.message && value.message.content) { mapping[key] = { role: value.message.author?.role, type: value.message.content.content_type, content: value.message.content.parts?.filter((item) => typeof item === "string").join(""), recipient: value.message.recipient, parentId: value.parent }; } } return { title: result.title, mapping, currentNode: result.current_node }; } const _hoisted_1 = { key: 0, class: "flex gap-2 text-sm items-center h-full" }; const _hoisted_2 = { key: 0 }; const _hoisted_3 = { key: 0 }; const _hoisted_4 = { key: 1 }; const _hoisted_5 = { key: 0 }; const _hoisted_6 = { key: 1 }; const _hoisted_7 = { key: 1, class: "text-sm flex items-center h-full text-red-500" }; const _sfc_main = vue.defineComponent({ __name: "HeaderTool", setup(__props) { const isMac = navigator.userAgent.includes("Macintosh"); const downloadingJson = vue.ref(false); const downloadingMarkdown = vue.ref(false); const counter = vue.ref(JSON.parse(localStorage.getItem("oai/apps/counter") || JSON.stringify({ count: 0, date: ( new Date()).toDateString() }))); if (counter.value.date !== ( new Date()).toDateString()) { counter.value = { count: 0, date: ( new Date()).toDateString() }; localStorage.setItem("oai/apps/counter", JSON.stringify(counter.value)); } vue.watch(counter, (value) => { localStorage.setItem("oai/apps/counter", JSON.stringify(value)); }, { deep: true }); function isLimited() { const today = ( new Date()).toDateString(); if (counter.value.date !== today) { counter.value = { count: 0, date: today }; } if (counter.value.count > 1) { return true; } else { counter.value.count++; return false; } } async function saveAsJson() { if (isLimited()) { return; } downloadingJson.value = true; const conversationData = await getConversationData(); downloadingJson.value = false; if (conversationData) { const lastMessageId = conversationData.currentNode; let currentMessage = conversationData.mapping[lastMessageId]; const result = []; while (currentMessage) { if (currentMessage.role && currentMessage.type && currentMessage.content && ["user", "assistant"].includes(currentMessage.role) && ["text", "multimodal_text"].includes(currentMessage.type) && currentMessage.recipient === "all") { result.push({ role: currentMessage.role, content: currentMessage.content }); } currentMessage = conversationData.mapping[currentMessage.parentId || ""]; } result.reverse(); const base64Url = textToBase64Url(JSON.stringify({ title: conversationData.title, items: result }, null, 2), "application/json"); _GM_download(base64Url, `${conversationData.title}.json`); } } async function saveAsMarkdown() { if (isLimited()) { return; } downloadingMarkdown.value = true; const conversationData = await getConversationData(); downloadingMarkdown.value = false; if (conversationData) { const result = []; const lastMessageId = conversationData.currentNode; let currentMessage = conversationData.mapping[lastMessageId]; while (currentMessage) { if (currentMessage.role && currentMessage.type && currentMessage.content && ["user", "assistant"].includes(currentMessage.role) && ["text", "multimodal_text"].includes(currentMessage.type) && currentMessage.recipient === "all") { result.push(`# ${currentMessage.role}: ${currentMessage.content}`); } currentMessage = conversationData.mapping[currentMessage.parentId || ""]; } result.reverse(); const base64Url = textToBase64Url(result.join("\n\n"), "text/markdown"); _GM_download(base64Url, `${conversationData.title}.md.txt`); } } async function getConversationData() { const conversationId = getConversationId(); if (conversationId) { return await getConversationContent(conversationId); } } return (_ctx, _cache) => { return vue.unref(userInfoStore).auth.openaiAuth ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_1, [ counter.value.count > 1 ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_2, [..._cache[0] || (_cache[0] = [ vue.createTextVNode("You have reached today's limit. ", -1), vue.createElementVNode("a", { target: "_blank", class: "text-red-500 underline", href: "https://chromewebstore.google.com/detail/lmiigijnefpkjcenfbinhdpafehaddag?utm_source=userscript" }, [ vue.createTextVNode("Get more for "), vue.createElementVNode("strong", null, "free🎉") ], -1) ])])) : (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 1 }, [ vue.createElementVNode("button", { class: "rounded-2xl border border-gray-200 px-2 py-1 hover:bg-gray-100", onClick: saveAsJson }, [ downloadingJson.value ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_3, "Downloading...")) : (vue.openBlock(), vue.createElementBlock("span", _hoisted_4, "Export JSON")) ]), vue.createElementVNode("button", { class: "rounded-2xl border border-gray-200 px-2 py-1 hover:bg-gray-100", onClick: saveAsMarkdown }, [ downloadingMarkdown.value ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_5, "Downloading...")) : (vue.openBlock(), vue.createElementBlock("span", _hoisted_6, "Export Markdown")) ]) ], 64)) ])) : (vue.openBlock(), vue.createElementBlock("div", _hoisted_7, " Make a Hard Reload using " + vue.toDisplayString(vue.unref(isMac) ? "⇧⌘R" : "Ctrl+Shift+R") + " to activate the script ", 1)); }; } }); const observerHandlers = []; waitForDomReady().then(() => { const observer = new MutationObserver((mutations, observer2) => { for (const handler of observerHandlers) { handler(mutations, observer2); } }); observer.observe(document.body, { childList: true, subtree: true }); if (localStorage.getItem("oai/apps/auth")) { userInfoStore.value.auth.openaiAuth = localStorage.getItem("oai/apps/auth"); } bootstrap(); }); window.addEventListener("message", messageHandler); function messageHandler(e) { if (e.data.type === GLOBAL_MESSAGE_OPEN_AI_TOKEN_GOT) { userInfoStore.value.auth.openaiAuth = e.data.data.authorization; localStorage.setItem("oai/apps/auth", e.data.data.authorization); } } async function waitForDomReady() { return new Promise((resolve) => { console.log(document.readyState); if (document.readyState === "interactive" || document.readyState === "complete") { resolve(); } else { window.addEventListener("DOMContentLoaded", () => resolve()); } }); } function bootstrap() { createGlobalLayer(); const appBuilder = createAppBuilder(); bootstrapHeaderTool(appBuilder); } function createAppBuilder() { function buildApp(cmp, id, props) { const container = document.createElement("div"); container.id = id; const root = document.createElement("div"); root.id = `app`; container.appendChild(root); const app = vue.createApp(cmp, props); return { app, root, container }; } return buildApp; } function bootstrapHeaderTool(appBuilder) { const containerId = DOM_ID_HEADER_TOOL; let mounted = false; const { app, root, container } = appBuilder(_sfc_main, containerId); container.style.alignSelf = "stretch"; root.style.height = "100%"; function tryMount() { const pageHeader = getElementByXPath(configStore.value.data.injectTarget.headerTool); if (pageHeader instanceof HTMLElement) { if (!document.querySelector(`#${containerId}`) && getConversationId()) { pageHeader.prepend(container); if (!mounted) { mounted = true; app.mount(root); } } } } tryMount(); observerHandlers.push(tryMount); } function createGlobalLayer() { const container = document.createElement("div"); container.id = DOM_ID_GLOBAL_LAYER; const root = document.createElement("div"); root.style.position = "fixed"; root.style.left = "0px"; root.style.top = "0px"; root.id = `app`; const $globalLayer = document.createElement("div"); $globalLayer.style.position = "fixed"; $globalLayer.style.left = "0px"; $globalLayer.style.top = "0px"; $globalLayer.id = `container`; root.appendChild($globalLayer); initGlobal($globalLayer); container.appendChild(root); document.body.after(container); const globalLayerApp = vue.createApp(GlobalOverlay); globalLayerApp.mount($globalLayer); } })(Vue);