// ==UserScript== // @name AiMsgExport // @namespace https://github.com/QingJ01/AiMsgExport // @version 0.1.0 // @description Export selected AI chat messages to PDF/Markdown/Image/TXT // @author QingJ // @license MIT // @icon data:image/svg+xml,%3Csvg%20width%3D%22128%22%20height%3D%22128%22%20viewBox%3D%220%200%20128%20128%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cdefs%3E%3ClinearGradient%20id%3D%22bg%22%20x1%3D%220%22%20y1%3D%220%22%20x2%3D%22128%22%20y2%3D%22128%22%20gradientUnits%3D%22userSpaceOnUse%22%3E%3Cstop%20offset%3D%220%25%22%20stop-color%3D%22%233B82F6%22%2F%3E%3Cstop%20offset%3D%22100%25%22%20stop-color%3D%22%236366F1%22%2F%3E%3C%2FlinearGradient%3E%3ClinearGradient%20id%3D%22arrow%22%20x1%3D%2264%22%20y1%3D%2230%22%20x2%3D%2264%22%20y2%3D%2298%22%20gradientUnits%3D%22userSpaceOnUse%22%3E%3Cstop%20offset%3D%220%25%22%20stop-color%3D%22%23FFFFFF%22%2F%3E%3Cstop%20offset%3D%22100%25%22%20stop-color%3D%22%23E0E7FF%22%2F%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3Crect%20width%3D%22128%22%20height%3D%22128%22%20rx%3D%2228%22%20fill%3D%22url(%23bg)%22%2F%3E%3Cpath%20d%3D%22M30%2038C30%2033.5817%2033.5817%2030%2038%2030H90C94.4183%2030%2098%2033.5817%2098%2038V74C98%2078.4183%2094.4183%2082%2090%2082H72L64%2092L56%2082H38C33.5817%2082%2030%2078.4183%2030%2074V38Z%22%20fill%3D%22rgba(255%2C255%2C255%2C0.15)%22%2F%3E%3Cpath%20d%3D%22M64%2036V72%22%20stroke%3D%22url(%23arrow)%22%20stroke-width%3D%227%22%20stroke-linecap%3D%22round%22%2F%3E%3Cpath%20d%3D%22M48%2060L64%2076L80%2060%22%20stroke%3D%22url(%23arrow)%22%20stroke-width%3D%227%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%3Cpath%20d%3D%22M40%2088H88%22%20stroke%3D%22url(%23arrow)%22%20stroke-width%3D%226%22%20stroke-linecap%3D%22round%22%2F%3E%3Cpath%20d%3D%22M96%2022L97.5%2027L102.5%2028.5L97.5%2030L96%2035L94.5%2030L89.5%2028.5L94.5%2027L96%2022Z%22%20fill%3D%22%23FDE68A%22%2F%3E%3Cpath%20d%3D%22M36%2020L37%2023L40%2024L37%2025L36%2028L35%2025L32%2024L35%2023L36%2020Z%22%20fill%3D%22%23FDE68A%22%20opacity%3D%220.7%22%2F%3E%3C%2Fsvg%3E // @match https://chatgpt.com/* // @match https://gemini.google.com/* // @match https://claude.ai/* // @match https://grok.com/* // @match https://chat.deepseek.com/* // @match https://www.doubao.com/chat/* // @match https://chat.qwen.ai/* // @match https://aistudio.google.com/* // @match https://copilot.cloud.microsoft/* // @match https://m365.cloud.microsoft/* // @match https://www.perplexity.ai/* // @grant GM_addStyle // @run-at document-idle // @require https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js // @require https://cdn.jsdelivr.net/npm/jspdf@2.5.1/dist/jspdf.umd.min.js // @require https://cdn.jsdelivr.net/npm/turndown@7.1.2/dist/turndown.js // ==/UserScript== (function () { "use strict"; const SITE_SELECTORS = { "chatgpt.com": [ "[data-testid^='conversation-turn-']", "article[data-testid='conversation-turn']" ], "gemini.google.com": [ "message-content", "user-query", "[data-user-text]", ".markdown-main-panel" ], "claude.ai": [ "div[data-testid='user-message']", "div.font-claude-response" ], "grok.com": [ "div[id^='response-']", "div[data-testid='message']" ], "chat.deepseek.com": [ "div[class*='chat-message']", ".ds-chat-message" ], "www.doubao.com": [ "[data-testid='send_message']", "[data-testid='receive_message']" ], "chat.qwen.ai": [ ".qwen-chat-message", "div[class*='chat-message']:not(.chat-messages)" ], "aistudio.google.com": [ "ms-cmark-node.cmark-node.v3-font-body.user-chunk", "ms-chat-turn .virtual-scroll-container.model-prompt-container ms-text-chunk ms-cmark-node.cmark-node.v3-font-body" ], "copilot.cloud.microsoft": [ "article[role='article']", "div[class*='conversation']" ], "m365.cloud.microsoft": [ "article[role='article']", "div[class*='conversation']" ], "www.perplexity.ai": [ "h1.group\\/query", "[id^='markdown-content-']", "#markdown-content-0" ] }; const SITE_CONTAINERS = { "chatgpt.com": ["main", "div[role='main']"], "gemini.google.com": ["main", "div[role='main']"], "claude.ai": ["main", "div[role='main']"], "grok.com": ["main", "div[role='main']"], "chat.deepseek.com": ["main", "[role='main']"], "www.doubao.com": ["main"], "chat.qwen.ai": ["main"], "aistudio.google.com": ["ms-chat-session", "main"], "copilot.cloud.microsoft": ["main", "div[role='main']"], "m365.cloud.microsoft": ["main", "div[role='main']"], "www.perplexity.ai": ["main", "div[role='main']"] }; const MESSAGE_ROOTS = { "www.doubao.com": ["[data-testid='message-list']"], "chatgpt.com": ["main"], "gemini.google.com": ["main"], "claude.ai": ["main"], "grok.com": ["main"], "chat.deepseek.com": ["main"], "chat.qwen.ai": ["main"], "aistudio.google.com": ["main"], "copilot.cloud.microsoft": ["main"], "m365.cloud.microsoft": ["main"], "www.perplexity.ai": ["main"] }; const ROLE_SELECTORS = { "chatgpt.com": { user: ["[data-message-author-role='user']"], assistant: ["[data-message-author-role='assistant']"] }, "claude.ai": { user: ["[data-testid='user-message']", "[data-author='user']"], assistant: [".font-claude-response", "[data-author='assistant']"] }, "gemini.google.com": { user: ["user-query", "[data-user-text]"], assistant: ["message-content", ".markdown-main-panel"] }, "aistudio.google.com": { user: ["ms-cmark-node.user-chunk", ".virtual-scroll-container[data-turn-role='User']"], assistant: [".virtual-scroll-container[data-turn-role='Model']"] }, "grok.com": { user: ["[data-role='user']"], assistant: ["[data-role='assistant']"] }, "chat.deepseek.com": { user: [".ds-chat-user-message", "[data-role='user']"], assistant: [".ds-markdown-paragraph"] }, "www.doubao.com": { user: ["[data-testid='send_message']"], assistant: ["[data-testid='receive_message']"] }, "www.perplexity.ai": { user: ["h1.group\\/query"], assistant: ["[id^='markdown-content-']", "#markdown-content-0"] }, "chat.qwen.ai": { user: [".qwen-chat-message-user", ".chat-user-message-container-wrapper"], assistant: [".qwen-chat-message-assistant", ".qwen-chat-message-bot", ".chat-assistant-message-container-wrapper"] } }; const SITE_BRAND = { "chatgpt.com": { name: "ChatGPT", color: "#10a37f", icon: "CGPT" }, "gemini.google.com": { name: "Gemini", color: "#1a73e8", icon: "GEM" }, "claude.ai": { name: "Claude", color: "#111111", icon: "CLD" }, "grok.com": { name: "Grok", color: "#000000", icon: "GRK" }, "chat.deepseek.com": { name: "DeepSeek", color: "#1d4ed8", icon: "DS" }, "www.doubao.com": { name: "Doubao", color: "#2563eb", icon: "DB", imgUrl: "https://lf-flow-web-cdn.doubao.com/obj/flow-doubao/samantha/logo-icon-white-bg.png" }, "chat.qwen.ai": { name: "Qwen", color: "#0f766e", icon: "QW" }, "aistudio.google.com": { name: "AI Studio", color: "#6b7280", icon: "AI" }, "copilot.cloud.microsoft": { name: "Copilot", color: "#0f172a", icon: "CP" }, "m365.cloud.microsoft": { name: "Copilot", color: "#0f172a", icon: "CP" }, "www.perplexity.ai": { name: "Perplexity", color: "#334155", icon: "PP" } }; const SITE_ICON_SVGS = { "chatgpt.com": `OpenAI`, "gemini.google.com": `Gemini`, "claude.ai": `Claude`, "grok.com": `Grok`, "chat.deepseek.com": ``, "chat.qwen.ai": `Qwen`, "copilot.cloud.microsoft": `Copilot`, "www.perplexity.ai": `Perplexity` }; const state = { selectionMode: false, selectedIds: new Set(), startId: null, endId: null, observer: null, processedElements: new WeakSet(), observerQueued: false, renderQueue: [], rendering: false }; const host = window.location.host; const styles = ` :root { /* 现代化色彩系统 - 基于 HSL 以获得更好的调和感 */ --tm-primary: hsl(221, 100%, 60%); --tm-primary-hover: hsl(221, 100%, 55%); --tm-primary-active: hsl(221, 100%, 45%); --tm-surface: rgba(255, 255, 255, 0.75); --tm-surface-solid: #ffffff; --tm-text: #0f172a; --tm-text-muted: #64748b; --tm-border: rgba(226, 232, 240, 0.6); --tm-shadow: 0 8px 32px rgba(15, 23, 42, 0.12); --tm-radius: 16px; --tm-radius-sm: 8px; } /* 全局过渡效果 */ .tm-export-panel, .tm-export-panel button { transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); } .tm-export-panel { position: fixed; top: 100px; right: 30px; width: 320px; z-index: 10000; background: var(--tm-surface); backdrop-filter: blur(16px) saturate(180%); -webkit-backdrop-filter: blur(16px) saturate(180%); border: 1px solid var(--tm-border); border-radius: var(--tm-radius); box-shadow: var(--tm-shadow); padding: 20px; font-family: 'Outfit', 'Inter', system-ui, -apple-system, sans-serif; color: var(--tm-text); opacity: 0; transform: translateY(10px) scale(0.98); pointer-events: none; visibility: hidden; } .tm-export-panel:not(.tm-export-hidden) { opacity: 1; transform: translateY(0) scale(1); pointer-events: auto; visibility: visible; } .tm-export-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; cursor: grab; user-select: none; } .tm-export-header:active { cursor: grabbing; } .tm-export-header h4 { margin: 0; font-size: 16px; font-weight: 700; letter-spacing: -0.02em; background: linear-gradient(135deg, var(--tm-primary), #4f46e5); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } .tm-export-header .tm-export-close { width: 28px !important; height: 28px !important; min-width: 28px !important; display: flex; align-items: center; justify-content: center; border: none !important; background: rgba(0, 0, 0, 0.05) !important; border-radius: 50% !important; cursor: pointer; color: var(--tm-text-muted) !important; font-size: 18px; padding: 0 !important; margin: 0 !important; box-shadow: none !important; transform: none !important; } .tm-export-header .tm-export-close:hover { background: rgba(239, 68, 68, 0.1) !important; color: #ef4444 !important; transform: rotate(90deg) !important; } #tm-export-count { display: block; font-size: 24px; font-weight: 800; margin-bottom: 4px; color: var(--tm-primary); } .tm-export-help { font-size: 12px; color: var(--tm-text-muted); margin-bottom: 20px; line-height: 1.5; } .tm-export-section { margin-bottom: 16px; } .tm-export-section-title { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--tm-text-muted); margin-bottom: 8px; display: block; } .tm-export-panel button { width: 100%; padding: 10px 14px; border-radius: var(--tm-radius-sm); border: 1px solid var(--tm-border); background: #fff; cursor: pointer; font-size: 13px; font-weight: 500; display: flex; align-items: center; justify-content: center; gap: 8px; margin: 4px 0; } .tm-export-panel button:hover { border-color: var(--tm-primary); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); transform: translateY(-1px); } .tm-export-panel button:active { transform: translateY(0); } .tm-export-panel .tm-primary { background: linear-gradient(135deg, var(--tm-primary), #4f46e5); color: #fff !important; border: none; font-weight: 600; box-shadow: 0 4px 14px rgba(37, 99, 235, 0.3); } .tm-export-panel .tm-primary:hover { box-shadow: 0 6px 20px rgba(37, 99, 235, 0.4); transform: translateY(-2px); } .tm-export-panel .tm-row { display: flex; gap: 8px; margin-bottom: 8px; } .tm-export-panel .tm-row button { flex: 1; margin-bottom: 0; } .tm-export-range { display: flex; align-items: center; justify-content: center; gap: 12px; margin: 8px 0; padding: 8px; border-radius: var(--tm-radius-sm); background: rgba(37, 99, 235, 0.05); font-size: 12px; } .tm-export-selected { outline: 3px solid var(--tm-primary); outline-offset: 4px; border-radius: 12px !important; transition: outline 0.2s ease; } .tm-timeline { display: none !important; } .tm-export-range button { font-size: 11px; padding: 4px 10px; background: #fff; border: 1px solid var(--tm-border); border-radius: 6px; } .tm-export-range button.tm-range-active { background: var(--tm-primary); color: #fff; border-color: var(--tm-primary); } .tm-export-hidden { display: none !important; } .tm-export-btn { display: flex; align-items: center; gap: 10px; padding: 10px 16px; margin: 4px 12px; border-radius: 12px; background: transparent; color: inherit; font-size: 14px; cursor: pointer; border: none; transition: background 0.2s; } .tm-export-btn:hover { background: rgba(0,0,0,0.06); } `; GM_addStyle(styles); function uniqueElements(elements) { const seen = new Set(); const result = []; elements.forEach((el) => { if (!seen.has(el)) { seen.add(el); result.push(el); } }); return result; } function getChatRoot() { const selectors = SITE_CONTAINERS[host] || []; for (const sel of selectors) { const node = document.querySelector(sel); if (node) return node; } return document.querySelector("main") || document.body; } function getMessageRoot() { const selectors = MESSAGE_ROOTS[host] || []; for (const sel of selectors) { const node = document.querySelector(sel); if (node) return node; } return getChatRoot(); } function filterTopLevel(elements) { const set = new Set(elements); return elements.filter((el) => { let parent = el.parentElement; let depth = 0; while (parent && depth < 20) { if (set.has(parent)) return false; parent = parent.parentElement; depth++; } return true; }); } function sortByDocumentOrder(elements) { return elements.slice().sort((a, b) => { if (a === b) return 0; const pos = a.compareDocumentPosition(b); if (pos & Node.DOCUMENT_POSITION_FOLLOWING) return -1; if (pos & Node.DOCUMENT_POSITION_PRECEDING) return 1; return 0; }); } function getClaudeMessageWrapper(el) { if (!(el instanceof Element)) return el; return el.closest("div.group.relative.inline-flex") || el.closest("div.group.relative.pb-3") || el; } let _messageElementsCache = null; function invalidateMessageCache() { _messageElementsCache = null; } function getMessageElements() { if (_messageElementsCache) return _messageElementsCache; const selectors = SITE_SELECTORS[host] || []; const elements = []; const scopeRoot = getMessageRoot(); if (selectors.length > 0) { const combined = selectors.join(","); try { scopeRoot.querySelectorAll(combined).forEach((el) => elements.push(el)); } catch { selectors.forEach((sel) => { scopeRoot.querySelectorAll(sel).forEach((el) => elements.push(el)); }); } } if (elements.length === 0) { scopeRoot .querySelectorAll("article, [data-testid*='message'], div[class*='message']") .forEach((el) => elements.push(el)); } let filtered = uniqueElements(elements).filter((el) => { const text = el.textContent || ""; if (text.trim().length === 0) return false; if (host === "aistudio.google.com" && el.closest("ms-thought-chunk")) return false; if (host === "aistudio.google.com" && el.closest(".thought-panel")) return false; if (host === "aistudio.google.com" && el.closest(".author-label")) return false; if (host === "gemini.google.com" && el.classList.contains("conversation-container")) return false; if (host === "gemini.google.com" && el.classList.contains("cdk-describedby-message-container")) return false; if (host === "chat.qwen.ai" && el.classList.contains("chat-messages")) return false; if (el.closest(".tm-export-panel")) return false; if (el.classList.contains("tm-export-range")) return false; return true; }); if (host === "claude.ai") { filtered = uniqueElements(filtered.map((el) => getClaudeMessageWrapper(el))); } const result = sortByDocumentOrder(filterTopLevel(filtered)); _messageElementsCache = result; return result; } function simpleHash(str) { let hash = 0; for (let i = 0; i < str.length; i++) { hash = ((hash << 5) - hash) + str.charCodeAt(i); hash |= 0; } return Math.abs(hash).toString(36); } function ensureMessageIds(messageElements) { messageElements.forEach((el, idx) => { if (!el.dataset.tmExportId) { const text = (el.textContent || "").slice(0, 200); el.dataset.tmExportId = `msg-${idx}-${simpleHash(text)}`; } }); } /* ── Per-site sidebar insertion configs ── */ const SIDEBAR_CONFIGS = { "www.doubao.com": { // 豆包左栏:主侧栏容器 containerSel: "#flow_chat_sidebar", itemSel: "[data-testid='create_conversation_button']", position: "after-create" }, "chat.deepseek.com": { containerSel: 'div.ds-scroll-area, nav, aside', itemSel: 'a, div[class*="item"]', position: "prepend" }, "chatgpt.com": { containerSel: 'nav', itemSel: 'a', position: "prepend" }, "claude.ai": { containerSel: 'nav, aside, [role="navigation"]', itemSel: 'a, button', position: "prepend" } }; const EXPORT_ICON_SVG = ` `; /* ── Shared click handler for sidebar buttons ── */ function attachExportClickHandler(el) { el.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); if (!document.querySelector(".tm-export-panel")) createPanel(); togglePanel(); }); } /* ── Per-platform sidebar entry creators ── */ function createDoubaoSidebarEntry() { if (document.querySelector("[data-testid='skill-page-item-export']")) return; const sidebar = document.querySelector("#flow_chat_sidebar"); const createBtn = document.querySelector("[data-testid='create_conversation_button']"); if (!sidebar || !createBtn) return; const wrapper = document.createElement("div"); wrapper.className = "section-zyMRVh"; wrapper.setAttribute("data-testid", "skill-page-item-export"); const item = document.createElement("a"); item.href = "#"; item.className = "group/sidebar_nav_item cursor-pointer section-item-pcVecT nav-link-IkIer0"; attachExportClickHandler(item); const icon = document.createElement("svg"); icon.setAttribute("width", "24"); icon.setAttribute("height", "24"); icon.setAttribute("viewBox", "0 0 24 24"); icon.setAttribute("fill", "currentColor"); icon.setAttribute("xmlns", "http://www.w3.org/2000/svg"); icon.className = "size-16 block text-16 box-content p-2 flex-shrink-0"; icon.innerHTML = EXPORT_ICON_SVG; const content = document.createElement("div"); content.className = "flex w-full items-center justify-between"; const label = document.createElement("div"); label.className = "section-item-title-K023pw title-kfNY6R"; label.setAttribute("title", "AiMsgExport"); label.textContent = "AiMsgExport"; content.appendChild(label); item.appendChild(icon); item.appendChild(content); wrapper.appendChild(item); createBtn.insertAdjacentElement("afterend", wrapper); } function createQwenSidebarEntry() { if (document.querySelector("[data-tm-export-entry='qwen']")) return; const items = Array.from(document.querySelectorAll(".sidebar-entry-list-content")); const communityItem = items.find((item) => { const label = item.querySelector(".sidebar-entry-list-text") || item; const text = (label.textContent || "").trim(); return text === "社区"; }); if (!communityItem) return; const entry = communityItem.cloneNode(true); entry.setAttribute("data-tm-export-entry", "qwen"); attachExportClickHandler(entry); const label = entry.querySelector(".sidebar-entry-list-text") || entry; if (label) label.textContent = "AiMsgExport"; const icon = entry.querySelector(".sidebar-entry-list-icon"); if (icon) { icon.innerHTML = EXPORT_ICON_SVG.replace( " { const label = Array.from(item.querySelectorAll("span")).find((span) => { return (span.textContent || "").trim() === "项目"; }); return Boolean(label); }); if (!projectItem) return; const entry = projectItem.cloneNode(true); entry.setAttribute("data-tm-export-entry", "grok"); if (entry.tagName.toLowerCase() === "a") { entry.setAttribute("href", "#"); } attachExportClickHandler(entry); const label = Array.from(entry.querySelectorAll("span")).find((span) => { return (span.textContent || "").trim() === "项目"; }); if (label) label.textContent = "AiMsgExport"; const icon = entry.querySelector("[data-sidebar='icon']"); if (icon) { icon.innerHTML = EXPORT_ICON_SVG.replace( " el.remove()); const entry = entryHost.querySelector("[data-test-id='expanded-button']") || entryHost.querySelector("a, button"); if (!entry) return; if (entry.tagName.toLowerCase() === "a") { entry.setAttribute("href", "#"); } attachExportClickHandler(entry); const label = entry.querySelector("[data-test-id='side-nav-action-button-content']") || entry.querySelector(".mdc-list-item__primary-text span") || entry.querySelector("span"); if (label) { label.textContent = "AiMsgExport"; label.style.display = "inline"; label.style.visibility = "visible"; label.style.opacity = "1"; label.style.setProperty("color", "currentColor"); label.style.setProperty("-webkit-text-fill-color", "currentColor"); } const icon = entry.querySelector("[data-test-id='side-nav-action-button-icon']") || entry.querySelector(".mat-mdc-list-item-icon") || entry.querySelector("mat-icon"); if (icon) { icon.innerHTML = EXPORT_ICON_SVG.replace( " { const label = (item.getAttribute("aria-label") || "").trim(); return item.getAttribute("id") === "OneNoteOnline" || item.getAttribute("value") === "OneNoteOnline" || label === "OneNote" || label === "OneNote Online"; }); const chatItem = navItems.find((item) => { const label = (item.getAttribute("aria-label") || "").trim(); return item.getAttribute("value") === "chat" || item.getAttribute("id") === "chat" || label === "聊天"; }); const anchorItem = oneNoteItem || chatItem; if (!anchorItem) return; const entry = anchorItem.cloneNode(true); entry.setAttribute("data-tm-export-entry", "copilot"); entry.removeAttribute("aria-current"); entry.removeAttribute("id"); entry.removeAttribute("value"); attachExportClickHandler(entry); const labelNode = Array.from(entry.querySelectorAll("span")).find((span) => { const text = (span.textContent || "").trim(); return text && text !== "搜索"; }); if (labelNode) labelNode.textContent = "AiMsgExport"; const icon = entry.querySelector(".fui-NavItem__icon") || entry.querySelector("svg"); if (icon) { icon.innerHTML = EXPORT_ICON_SVG.replace( " { const text = (item.textContent || "").trim(); return text === "更多"; }); if (!moreItem) return; const entry = moreItem.cloneNode(true); entry.setAttribute("data-tm-export-entry", "perplexity"); attachExportClickHandler(entry); const labelCandidates = Array.from(entry.querySelectorAll("div, span")) .filter((el) => el.childElementCount === 0) .filter((el) => (el.textContent || "").trim().length > 0); labelCandidates.forEach((el) => { const text = (el.textContent || "").trim(); if (text === "更多") { el.textContent = "AiMsgExport"; } }); if (labelCandidates.length > 0 && !labelCandidates.some((el) => (el.textContent || "").trim() === "AiMsgExport")) { labelCandidates[0].textContent = "AiMsgExport"; } const icon = entry.querySelector("svg") || entry.querySelector(".size-6"); if (icon) { icon.outerHTML = EXPORT_ICON_SVG.replace( " { const text = (link.textContent || "").trim(); return text === "Documentation"; }); if (!docLink) return; const entry = docLink.cloneNode(true); entry.setAttribute("data-tm-export-entry", "aistudio"); entry.setAttribute("href", "#"); entry.removeAttribute("target"); attachExportClickHandler(entry); const label = entry.querySelector(".nav-item-v3-main-text") || entry.querySelector("span:not(.material-symbols-outlined)"); if (label) label.textContent = "AiMsgExport"; const icon = entry.querySelector(".material-symbols-outlined") || entry.querySelector("svg"); if (icon) { icon.innerHTML = EXPORT_ICON_SVG.replace( " { const label = item.querySelector("span") || item; const text = (label.textContent || "").trim(); return text === "开启新对话"; }); const sidebarRoot = (newChatButton && newChatButton.parentElement) || document.querySelector("div.ds-scroll-area, nav, aside"); if (!sidebarRoot) return; const navItems = Array.from(sidebarRoot.querySelectorAll("a")); const refItem = newChatButton || navItems[0]; if (!refItem) return; const entry = refItem.cloneNode(true); entry.setAttribute("data-tm-export-entry", "deepseek"); entry.style.marginTop = "8px"; if (entry.tagName.toLowerCase() === "a") { entry.setAttribute("href", "#"); } attachExportClickHandler(entry); entry.querySelectorAll("svg, .ds-icon").forEach((node) => node.remove()); const iconWrapper = document.createElement("div"); iconWrapper.className = "ds-icon"; iconWrapper.style.marginRight = "8px"; iconWrapper.style.display = "flex"; iconWrapper.style.alignItems = "center"; iconWrapper.style.justifyContent = "center"; iconWrapper.style.width = "17px"; iconWrapper.style.height = "17px"; iconWrapper.style.flexShrink = "0"; iconWrapper.innerHTML = EXPORT_ICON_SVG; entry.prepend(iconWrapper); const labelNode = entry.querySelector("span") || entry; if (labelNode) { labelNode.textContent = "AiMsgExport"; labelNode.style.marginLeft = "0px"; } entry.querySelectorAll("*").forEach((node) => { const text = (node.textContent || "").trim(); if (/ctrl\+j/i.test(text)) node.remove(); }); const actions = entry.querySelector("[class*='actions']"); if (actions) actions.remove(); if (newChatButton && newChatButton.parentElement) { newChatButton.insertAdjacentElement("afterend", entry); } else { sidebarRoot.prepend(entry); } } function createClaudeSidebarEntry() { if (document.querySelector("[data-tm-export-entry='claude']")) return; const sidebarRoot = document.querySelector('nav[aria-label="Sidebar"]'); if (!sidebarRoot) return; // 使用 aria-label 精确匹配侧边栏项目,避免 textContent 包含快捷键文本导致匹配失败 const refLink = sidebarRoot.querySelector('a[aria-label="Customize"]') || sidebarRoot.querySelector('a[aria-label="Search"]') || sidebarRoot.querySelector('a[aria-label="New chat"]'); if (!refLink) return; // 每个侧边栏项目都被 div.relative.group 包裹,需要克隆整个包裹层 const refWrapper = refLink.closest('div.relative.group') || refLink.parentElement; if (!refWrapper) return; const entryWrapper = refWrapper.cloneNode(true); entryWrapper.setAttribute("data-tm-export-entry", "claude"); const entryLink = entryWrapper.querySelector("a"); if (!entryLink) return; entryLink.setAttribute("href", "#"); entryLink.setAttribute("aria-label", "AiMsgExport"); entryLink.removeAttribute("data-dd-action-name"); attachExportClickHandler(entryLink); // 替换图标和文字 const innerWrapper = entryLink.querySelector('div[class*="translate-x"]'); if (innerWrapper) { // 替换图标容器内容 const iconContainer = innerWrapper.querySelector('.flex.items-center.justify-center.text-text-100'); if (iconContainer) { iconContainer.innerHTML = `
${EXPORT_ICON_SVG}
`; } // 更新文字标签 const labelSpan = innerWrapper.querySelector("span.truncate"); if (labelSpan) { labelSpan.innerHTML = '
AiMsgExport
'; } // 移除快捷键提示 innerWrapper.querySelectorAll("span.flex-shrink-0").forEach(n => n.remove()); } // 移除克隆出来的悬浮操作按钮 entryWrapper.querySelectorAll('.absolute').forEach(n => n.remove()); refWrapper.insertAdjacentElement("afterend", entryWrapper); } function createChatgptSidebarEntry() { if (document.querySelector("[data-tm-export-entry='chatgpt']")) return; const sidebarRoot = document.querySelector("nav[aria-label], nav, aside"); if (!sidebarRoot) return; const menuItems = Array.from(sidebarRoot.querySelectorAll("[data-sidebar-item='true']")); const healthItem = menuItems.find((item) => { const label = item.querySelector(".truncate") || item; const text = (label.textContent || "").trim(); return text === "Health"; }); const refItem = healthItem || menuItems[0]; if (!refItem) return; const entry = refItem.cloneNode(true); entry.setAttribute("data-tm-export-entry", "chatgpt"); if (entry.tagName.toLowerCase() === "a") { entry.setAttribute("href", "#"); } attachExportClickHandler(entry); entry.querySelectorAll(".icon, svg, [class*='trailing'], kbd").forEach((node) => node.remove()); const iconWrapper = document.createElement("div"); iconWrapper.className = "flex items-center justify-center mr-3 text-token-text-secondary flex-shrink-0"; iconWrapper.style.width = "16px"; iconWrapper.style.height = "16px"; iconWrapper.innerHTML = EXPORT_ICON_SVG; entry.prepend(iconWrapper); const labelNode = entry.querySelector(".truncate") || entry.querySelector("span") || entry; if (labelNode) labelNode.textContent = "AiMsgExport"; if (healthItem && healthItem.parentElement) { healthItem.insertAdjacentElement("afterend", entry); } else { sidebarRoot.appendChild(entry); } } function createGenericSidebarEntry() { if (document.querySelector("[data-tm-export-entry='generic']")) return; const button = document.createElement("div"); button.setAttribute("data-tm-export-entry", "generic"); button.className = "tm-export-btn"; button.innerHTML = `
${EXPORT_ICON_SVG}
AiMsgExport`; attachExportClickHandler(button); const config = SIDEBAR_CONFIGS[host]; let inserted = false; if (config) { const container = document.querySelector(config.containerSel); if (container) { const refItem = container.querySelector(config.itemSel); if (refItem) { const refStyle = window.getComputedStyle(refItem); button.style.padding = refStyle.padding; button.style.fontSize = refStyle.fontSize; button.style.color = refStyle.color; button.style.borderRadius = refStyle.borderRadius; button.style.fontFamily = refStyle.fontFamily; button.style.lineHeight = refStyle.lineHeight; if (refStyle.height && refStyle.height !== "auto") { button.style.height = refStyle.height; } } if (config.position === "append") { container.appendChild(button); } else { container.prepend(button); } inserted = true; } } if (!inserted) { const sidebar = document.querySelector("aside, nav, [role='navigation']"); if (sidebar) { const refItem = sidebar.querySelector("a, button, li, div[class*='item']"); if (refItem) { const refStyle = window.getComputedStyle(refItem); button.style.padding = refStyle.padding; button.style.fontSize = refStyle.fontSize; button.style.color = refStyle.color; button.style.borderRadius = refStyle.borderRadius; button.style.fontFamily = refStyle.fontFamily; } sidebar.appendChild(button); inserted = true; } } } /* ── Sidebar dispatch table ── */ const SIDEBAR_CREATORS = { "www.doubao.com": createDoubaoSidebarEntry, "chat.qwen.ai": createQwenSidebarEntry, "grok.com": createGrokSidebarEntry, "gemini.google.com": createGeminiSidebarEntry, "copilot.cloud.microsoft": createCopilotSidebarEntry, "m365.cloud.microsoft": createCopilotSidebarEntry, "www.perplexity.ai": createPerplexitySidebarEntry, "aistudio.google.com": createAiStudioSidebarEntry, "chat.deepseek.com": createDeepseekSidebarEntry, "claude.ai": createClaudeSidebarEntry, "chatgpt.com": createChatgptSidebarEntry, }; function createSidebarButton() { const creator = SIDEBAR_CREATORS[host]; if (creator) { creator(); return; } createGenericSidebarEntry(); } function ensureSidebarButton() { if (host === "www.doubao.com") { if (document.querySelector("[data-testid='skill-page-item-export']")) return; } createSidebarButton(); } function cleanupDoubaoButtons() { const buttons = document.querySelectorAll("[data-testid='skill-page-item-export']"); for (let i = 1; i < buttons.length; i++) buttons[i].remove(); } /* ── Sidebar observer configs & factory ── */ const SIDEBAR_OBSERVER_CONFIGS = { "www.doubao.com": { selector: "#flow_chat_sidebar", beforeEnsure: cleanupDoubaoButtons }, "chatgpt.com": { selector: "nav[aria-label], nav, aside" }, "chat.qwen.ai": { selector: ".sidebar-entry-list" }, "claude.ai": { selector: '.z-sidebar' }, "grok.com": { selector: "[data-sidebar='sidebar']" }, "gemini.google.com": { selector: "side-navigation-content" }, "copilot.cloud.microsoft": { selector: ".fui-NavDrawerBody" }, "m365.cloud.microsoft": { selector: ".fui-NavDrawerBody" }, "www.perplexity.ai": { selector: ".group\\/sidebar" }, "aistudio.google.com": { selector: ".nav-content" }, "chat.deepseek.com": { selector: "div.ds-scroll-area, nav, aside" }, }; function connectSidebarObserverFor(hostKey) { const config = SIDEBAR_OBSERVER_CONFIGS[hostKey || host]; if (!config) return; // Observe document.body as ultimate fallback — immune to SPA sidebar replacement const target = document.querySelector(config.selector) || document.body; let debounceTimer = null; const observer = new MutationObserver(() => { clearTimeout(debounceTimer); debounceTimer = setTimeout(() => { if (config.beforeEnsure) config.beforeEnsure(); ensureSidebarButton(); }, 300); }); observer.observe(target, { childList: true, subtree: true }); // No timeout — observer stays active for the entire page lifetime } function createPanel() { if (document.querySelector(".tm-export-panel")) return; const panel = document.createElement("div"); panel.className = "tm-export-panel tm-export-hidden"; panel.innerHTML = `

AiMsgExport

0
已选消息数量。您可以直接勾选消息,或使用下方的范围选择工具。
消息选择
导出格式
范围工具
`; document.body.appendChild(panel); panel.querySelector("#tm-selection-toggle").addEventListener("click", toggleSelectionMode); panel.querySelector("#tm-select-all").addEventListener("click", selectAll); panel.querySelector("#tm-clear-selection").addEventListener("click", clearSelection); panel.querySelector("#tm-apply-range").addEventListener("click", applyRangeSelection); panel.querySelector("#tm-clear-range").addEventListener("click", clearRangeSelection); panel.querySelector("#tm-export-pdf").addEventListener("click", (e) => withLoadingState(e.currentTarget, exportPdf)); panel.querySelector("#tm-export-md").addEventListener("click", (e) => withLoadingState(e.currentTarget, exportMarkdown)); panel.querySelector("#tm-export-img").addEventListener("click", (e) => withLoadingState(e.currentTarget, exportImage)); panel.querySelector("#tm-export-txt").addEventListener("click", (e) => withLoadingState(e.currentTarget, exportTxt)); panel.querySelector(".tm-export-close").addEventListener("click", () => { togglePanel(); }); setupPanelDrag(panel); } function togglePanel() { const panel = document.querySelector(".tm-export-panel"); if (!panel) return; panel.classList.toggle("tm-export-hidden"); if (!panel.classList.contains("tm-export-hidden")) { positionPanelNearButton(panel); } } function positionPanelNearButton(panel) { if (panel.dataset.tmMoved === "true") return; const exportNode = document.querySelector("[data-testid='skill-page-item-export']") || document.querySelector("[data-tm-export-entry='deepseek']") || document.querySelector(".tm-export-btn"); if (!exportNode) return; const rect = exportNode.getBoundingClientRect(); const top = Math.max(20, rect.top + window.scrollY - 10); const left = Math.max(20, rect.right + window.scrollX + 12); panel.style.top = `${top}px`; panel.style.left = `${left}px`; panel.style.right = "auto"; } function setupPanelDrag(panel) { const header = panel.querySelector(".tm-export-header"); if (!header) return; let dragging = false; let offsetX = 0; let offsetY = 0; function startDrag(clientX, clientY) { dragging = true; panel.dataset.tmMoved = "true"; panel.style.transition = "none"; const rect = panel.getBoundingClientRect(); offsetX = clientX - rect.left; offsetY = clientY - rect.top; } function moveDrag(clientX, clientY) { if (!dragging) return; const left = Math.max(10, clientX - offsetX); const top = Math.max(10, clientY - offsetY); panel.style.left = `${left}px`; panel.style.top = `${top}px`; panel.style.right = "auto"; } function stopDrag() { if (!dragging) return; dragging = false; panel.style.transition = ""; } header.addEventListener("mousedown", (e) => { startDrag(e.clientX, e.clientY); e.preventDefault(); }); document.addEventListener("mousemove", (e) => moveDrag(e.clientX, e.clientY)); document.addEventListener("mouseup", stopDrag); header.addEventListener("touchstart", (e) => { const t = e.touches[0]; startDrag(t.clientX, t.clientY); e.preventDefault(); }, { passive: false }); document.addEventListener("touchmove", (e) => { if (!dragging) return; const t = e.touches[0]; moveDrag(t.clientX, t.clientY); }, { passive: true }); document.addEventListener("touchend", stopDrag); } function toggleSelectionMode() { state.selectionMode = !state.selectionMode; invalidateMessageCache(); const btn = document.querySelector("#tm-selection-toggle"); if (btn) btn.textContent = state.selectionMode ? "退出选择" : "开启选择"; if (state.selectionMode) { scheduleRenderSelectionControls(); connectObserver(); } else { removeSelectionControls(); disconnectObserver(); } } function scheduleRenderSelectionControls(newElements) { if (!state.selectionMode) return; if (state.rendering) return; const messageElements = Array.isArray(newElements) && newElements.length > 0 ? newElements : getMessageElements(); ensureMessageIds(messageElements); const queue = []; messageElements.forEach((el) => { if (state.processedElements.has(el)) return; if (el.nextElementSibling && el.nextElementSibling.classList.contains("tm-export-range")) { state.processedElements.add(el); return; } queue.push(el); }); if (queue.length === 0) { updateCount(); return; } if (state.selectionMode) { insertStartBeforeFirstMessage(messageElements); } state.renderQueue = queue; state.rendering = true; processRenderQueue(); } function insertStartBeforeFirstMessage(messageElements) { if (document.querySelector(".tm-export-range-start")) return; const first = messageElements[0]; if (!first || !first.parentElement) return; const range = document.createElement("div"); range.className = "tm-export-range tm-export-range-start"; const startBtn = document.createElement("button"); startBtn.dataset.action = "start"; startBtn.textContent = "从这里开始"; startBtn.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); document.querySelectorAll(".tm-export-range button[data-action='start']").forEach((btn) => btn.classList.remove("tm-range-active") ); state.startId = first.dataset.tmExportId || null; startBtn.classList.add("tm-range-active"); if (state.endId) applyRangeSelection(); updateCount(); }); range.appendChild(startBtn); first.parentElement.insertBefore(range, first); } function processRenderQueue() { if (!state.selectionMode) { state.renderQueue = []; state.rendering = false; return; } const start = performance.now(); while (state.renderQueue.length > 0 && performance.now() - start < 8) { const el = state.renderQueue.shift(); if (!el || state.processedElements.has(el)) continue; if (el.nextElementSibling && el.nextElementSibling.classList.contains("tm-export-range")) { state.processedElements.add(el); continue; } const range = document.createElement("div"); range.className = "tm-export-range"; const startBtn = document.createElement("button"); startBtn.dataset.action = "start"; startBtn.textContent = "从这里开始"; const endBtn = document.createElement("button"); endBtn.dataset.action = "end"; endBtn.textContent = "到这里结束"; range.appendChild(startBtn); range.appendChild(endBtn); const id = el.dataset.tmExportId; if (state.selectedIds.has(id)) { el.classList.add("tm-export-selected"); } if (!el.dataset.tmSelectable) { el.dataset.tmSelectable = "true"; el.addEventListener("click", (e) => { if (!state.selectionMode) return; if (e.target.closest(".tm-export-range")) return; if (e.target.closest("a[href]")) return; if (state.selectedIds.has(id)) { state.selectedIds.delete(id); el.classList.remove("tm-export-selected"); } else { state.selectedIds.add(id); el.classList.add("tm-export-selected"); } updateCount(); }); } startBtn.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); document.querySelectorAll(".tm-export-range button[data-action='start']").forEach((btn) => btn.classList.remove("tm-range-active") ); state.startId = id; startBtn.classList.add("tm-range-active"); if (state.endId) applyRangeSelection(); updateCount(); }); endBtn.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); document.querySelectorAll(".tm-export-range button[data-action='end']").forEach((btn) => btn.classList.remove("tm-range-active") ); state.endId = id; endBtn.classList.add("tm-range-active"); if (state.startId) applyRangeSelection(); updateCount(); }); el.insertAdjacentElement("afterend", range); state.processedElements.add(el); } if (state.renderQueue.length > 0) { requestAnimationFrame(processRenderQueue); } else { state.rendering = false; updateCount(); } } function removeSelectionControls() { document.querySelectorAll(".tm-export-range").forEach((el) => el.remove()); state.processedElements = new WeakSet(); state.renderQueue = []; state.rendering = false; } function applyRangeSelection() { const messageElements = getMessageElements(); ensureMessageIds(messageElements); if (!state.startId || !state.endId) return; const startIndex = messageElements.findIndex((el) => el.dataset.tmExportId === state.startId); const endIndex = messageElements.findIndex((el) => el.dataset.tmExportId === state.endId); if (startIndex === -1 || endIndex === -1) return; const start = Math.min(startIndex, endIndex); const end = Math.max(startIndex, endIndex); state.selectedIds.clear(); messageElements.forEach((el, idx) => { if (idx >= start && idx <= end) { state.selectedIds.add(el.dataset.tmExportId); } }); if (state.selectionMode) { messageElements.forEach((el) => { const selected = state.selectedIds.has(el.dataset.tmExportId); if (selected) { el.classList.add("tm-export-selected"); } else { el.classList.remove("tm-export-selected"); } }); } updateCount(); } function clearRangeSelection() { state.startId = null; state.endId = null; document.querySelectorAll(".tm-export-range button").forEach((btn) => btn.classList.remove("tm-range-active") ); updateCount(); } function clearSelection() { state.selectedIds.clear(); if (state.selectionMode) { document.querySelectorAll(".tm-export-selected").forEach((el) => el.classList.remove("tm-export-selected")); } updateCount(); } function selectAll() { const messageElements = getMessageElements(); ensureMessageIds(messageElements); if (!state.selectionMode) { toggleSelectionMode(); } messageElements.forEach((el) => { state.selectedIds.add(el.dataset.tmExportId); el.classList.add("tm-export-selected"); }); updateCount(); } function showToast(message) { let toast = document.querySelector(".tm-export-toast"); if (!toast) { toast = document.createElement("div"); toast.className = "tm-export-toast"; toast.style.cssText = "position:fixed;bottom:24px;left:50%;transform:translateX(-50%);padding:10px 20px;background:#1e293b;color:#fff;border-radius:8px;font-size:13px;z-index:10001;opacity:0;transition:opacity 0.3s;pointer-events:none;"; document.body.appendChild(toast); } toast.textContent = message; toast.style.opacity = "1"; clearTimeout(toast._tid); toast._tid = setTimeout(() => { toast.style.opacity = "0"; }, 3000); } async function withLoadingState(btn, fn) { const allBtns = document.querySelectorAll(".tm-export-panel button[id^='tm-export-']"); const origText = btn.textContent; allBtns.forEach(b => { b.disabled = true; }); btn.textContent = "导出中..."; try { await fn(); showToast("导出完成"); } catch (err) { showToast("导出失败: " + (err.message || err)); } finally { allBtns.forEach(b => { b.disabled = false; }); btn.textContent = origText; } } function updateCount() { const countEl = document.querySelector("#tm-export-count"); if (!countEl) return; countEl.textContent = `已选:${state.selectedIds.size}`; } function getSelectedMessages() { const messageElements = getMessageElements(); ensureMessageIds(messageElements); return messageElements.filter((el) => state.selectedIds.has(el.dataset.tmExportId)); } function ensureInPageHost() { let hostEl = document.querySelector(".tm-export-host"); if (hostEl) return hostEl; hostEl = document.createElement("div"); hostEl.className = "tm-export-host"; if (host === "www.doubao.com") { const sidebar = document.querySelector("#flow_chat_sidebar"); if (sidebar) { sidebar.appendChild(hostEl); return hostEl; } } const root = getChatRoot(); root.appendChild(hostEl); return hostEl; } function elementMatchesAny(el, selectors) { for (const sel of selectors) { try { if (el.matches(sel) || el.querySelector(sel)) return true; } catch { // ignore invalid selector } } return false; } function guessRole(el) { const roleAttr = el.getAttribute("data-message-author-role") || el.getAttribute("data-author"); if (roleAttr) return roleAttr; const hostSelectors = ROLE_SELECTORS[host]; if (hostSelectors) { if (elementMatchesAny(el, hostSelectors.user)) return "user"; if (elementMatchesAny(el, hostSelectors.assistant)) return "assistant"; } const roleNode = el.querySelector("[data-message-author-role], [data-author]"); if (roleNode) { return roleNode.getAttribute("data-message-author-role") || roleNode.getAttribute("data-author") || "message"; } const className = (el.className || "").toString().toLowerCase(); if (className.includes("user")) return "user"; if (className.includes("assistant") || className.includes("model")) return "assistant"; return "message"; } function roleLabel(role) { if (role === "user" || role === "human") return "用户"; if (role === "assistant" || role === "model") return "AI"; return "AI"; } function cloneMessageForExport(el) { const clone = el.cloneNode(true); clone.querySelectorAll( ".tm-export-range, button, textarea, input, .sr-only, [role='button']" ).forEach((node) => node.remove()); if (host === "aistudio.google.com") { clone.querySelectorAll( "ms-thought-chunk, .thought-panel, .thought-collapsed-text, .thinking-progress-icon, .mat-accordion, .mat-expansion-panel, .author-label, .timestamp, .model-run-time-pill, .turn-footer, .turn-information, .actions-container, ms-chat-turn-options" ).forEach((node) => node.remove()); } sanitizeCloneForExport(clone); return clone; } function normalizeExportText(text) { if (!text) return ""; return text.replace(/已经完成思考\s*/g, "").trim(); } /** * Sanitize a cloned DOM tree for export: * - Strip all classes, IDs, data attributes (prevents host CSS from applying) * - Remove all inline styles (prevents host-injected layout from interfering) * - Our EXPORT_CONTENT_CSS provides clean, consistent styling via tag selectors */ function sanitizeCloneForExport(clone) { // Sanitize root clone.removeAttribute('style'); clone.removeAttribute('id'); for (const attr of Array.from(clone.attributes)) { if (attr.name.startsWith('data-') && !attr.name.startsWith('data-tm')) { clone.removeAttribute(attr.name); } } // Sanitize all descendants clone.querySelectorAll('*').forEach(node => { // Strip all classes (host CSS won't match) if (node.classList && node.classList.length > 0) { node.className = ''; } // Strip IDs and data attributes node.removeAttribute('id'); for (const attr of Array.from(node.attributes)) { if (attr.name.startsWith('data-')) { node.removeAttribute(attr.name); } } // Remove ALL inline styles — let export CSS handle everything node.removeAttribute('style'); }); } /* ── Export container styles for markdown/rich content ── */ const EXPORT_CONTENT_CSS = ` /* Hard reset: revert ALL host-page CSS for export isolation */ .tm-ec *, .tm-ec *::before, .tm-ec *::after { all: revert; box-sizing: border-box; max-width: 100%; } /* Force safe layout on message content — override any surviving host rules */ .tm-ec-msg, .tm-ec-msg * { position: static !important; float: none !important; transform: none !important; -webkit-transform: none !important; z-index: auto !important; visibility: visible !important; opacity: 1 !important; } .tm-ec { line-height: 1.6; word-break: break-word; overflow-wrap: break-word; color: #0f172a; } /* Headings */ .tm-ec h1, .tm-ec h2, .tm-ec h3, .tm-ec h4, .tm-ec h5, .tm-ec h6 { margin: 16px 0 8px; font-weight: 600; line-height: 1.35; color: #0f172a; } .tm-ec h1 { font-size: 1.5em; } .tm-ec h2 { font-size: 1.3em; } .tm-ec h3 { font-size: 1.15em; } .tm-ec h4, .tm-ec h5, .tm-ec h6 { font-size: 1em; } /* Paragraphs */ .tm-ec p { margin: 8px 0; line-height: 1.7; } /* Links */ .tm-ec a { color: #2563eb; text-decoration: underline; } /* Lists */ .tm-ec ul, .tm-ec ol { margin: 8px 0; padding-left: 24px; } .tm-ec li { margin: 4px 0; line-height: 1.6; } .tm-ec ul { list-style-type: disc; } .tm-ec ol { list-style-type: decimal; } .tm-ec li > ul, .tm-ec li > ol { margin: 2px 0; } /* Inline code */ .tm-ec code { font-family: "SF Mono", "Fira Code", "Cascadia Code", Consolas, monospace; font-size: 0.875em; background: #f1f5f9; color: #dc2626; padding: 2px 6px; border-radius: 4px; white-space: pre-wrap; word-break: break-all; } /* Code blocks */ .tm-ec pre { margin: 12px 0; padding: 14px 16px; border-radius: 8px; background: #1e293b !important; color: #e2e8f0 !important; overflow-x: auto; font-size: 13px; line-height: 1.55; white-space: pre-wrap; word-break: break-all; } .tm-ec pre code { background: none !important; color: inherit !important; padding: 0; border-radius: 0; font-size: inherit; white-space: pre-wrap; word-break: break-all; } /* Blockquotes */ .tm-ec blockquote { margin: 12px 0; padding: 10px 16px; border-left: 4px solid #3b82f6; background: #eff6ff; color: #1e40af; border-radius: 0 6px 6px 0; } .tm-ec blockquote p { margin: 4px 0; } /* Tables */ .tm-ec table { width: 100%; border-collapse: collapse; margin: 12px 0; font-size: 13px; table-layout: fixed; } .tm-ec th, .tm-ec td { border: 1px solid #cbd5e1; padding: 8px 12px; text-align: left; word-break: break-word; overflow-wrap: break-word; } .tm-ec th { background: #f1f5f9; font-weight: 600; color: #334155; } .tm-ec tr:nth-child(even) td { background: #f8fafc; } /* Horizontal rule */ .tm-ec hr { border: none; border-top: 1px solid #e2e8f0; margin: 16px 0; } /* Images */ .tm-ec img { max-width: 100%; height: auto; border-radius: 8px; margin: 8px 0; } /* Strong / emphasis */ .tm-ec strong, .tm-ec b { font-weight: 600; } .tm-ec em, .tm-ec i { font-style: italic; } /* Definition lists / details */ .tm-ec details { margin: 8px 0; } .tm-ec summary { cursor: pointer; font-weight: 500; } /* Math / KaTeX containers */ .tm-ec .katex-display, .tm-ec .MathJax_Display { overflow-x: auto; margin: 8px 0; } .tm-ec .katex { font-size: 1em; } /* Overflow protection */ .tm-ec-msg { overflow: visible; } .tm-ec-msg > * { max-width: 100%; } /* SVG / media */ .tm-ec svg { max-width: 100%; height: auto; } .tm-ec video, .tm-ec audio { max-width: 100%; } `; function injectExportStyles(container) { container.classList.add("tm-ec"); const style = document.createElement("style"); style.textContent = EXPORT_CONTENT_CSS; container.prepend(style); } function svgToDataUrl(svg) { return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`; } function createExportHeader() { const header = document.createElement("div"); header.style.display = "flex"; header.style.justifyContent = "space-between"; header.style.alignItems = "center"; header.style.marginBottom = "18px"; header.style.paddingBottom = "12px"; header.style.borderBottom = "1px solid #e2e8f0"; const brand = SITE_BRAND[host] || { name: "AI Chat", color: "#2563eb", icon: "AI" }; const left = document.createElement("div"); left.style.display = "flex"; left.style.alignItems = "center"; left.style.gap = "10px"; const icon = document.createElement("img"); icon.style.width = "36px"; icon.style.height = "36px"; icon.style.borderRadius = "10px"; icon.style.objectFit = "contain"; icon.style.background = "#ffffff"; icon.style.border = "1px solid #e2e8f0"; const svg = SITE_ICON_SVGS[host]; if (svg) { icon.src = svgToDataUrl(svg); } else if (brand.imgUrl) { icon.src = brand.imgUrl; } else { const fallback = document.createElement("div"); fallback.textContent = brand.icon; fallback.style.width = "36px"; fallback.style.height = "36px"; fallback.style.display = "inline-flex"; fallback.style.alignItems = "center"; fallback.style.justifyContent = "center"; fallback.style.borderRadius = "10px"; fallback.style.fontSize = "11px"; fallback.style.fontWeight = "700"; fallback.style.letterSpacing = "0.6px"; fallback.style.background = brand.color; fallback.style.color = "#ffffff"; left.appendChild(fallback); } const title = document.createElement("div"); title.textContent = `${brand.name} Export`; title.style.fontSize = "18px"; title.style.fontWeight = "700"; if (icon.src) left.appendChild(icon); left.appendChild(title); const meta = document.createElement("div"); meta.textContent = new Date().toLocaleString(); meta.style.fontSize = "12px"; meta.style.color = "#64748b"; header.appendChild(left); header.appendChild(meta); return header; } function createMessageWrapper(el, index) { const wrapper = document.createElement("div"); wrapper.style.border = "none"; wrapper.style.borderRadius = "12px"; wrapper.style.padding = "14px 16px"; wrapper.style.marginBottom = "14px"; wrapper.style.background = "#ffffff"; wrapper.style.boxShadow = "0 6px 16px rgba(15,23,42,0.06)"; wrapper.style.overflow = "visible"; const role = roleLabel(guessRole(el)); const isUser = role === "用户"; const heading = document.createElement("div"); heading.textContent = role; heading.style.fontWeight = "600"; heading.style.marginBottom = "8px"; heading.style.fontSize = "12px"; heading.style.color = isUser ? "#1d4ed8" : "#0f766e"; heading.style.textTransform = "uppercase"; heading.style.letterSpacing = "0.8px"; const clone = cloneMessageForExport(el); clone.className = "tm-ec-msg"; clone.style.fontSize = "14px"; clone.style.lineHeight = "1.6"; clone.style.maxWidth = "100%"; clone.style.overflowWrap = "break-word"; if (role) wrapper.appendChild(heading); wrapper.appendChild(clone); wrapper.setAttribute("data-index", index + 1); return wrapper; } function createExportContainer(messages) { const container = document.createElement("div"); container.style.width = "820px"; container.style.padding = "28px"; container.style.background = "#f8fafc"; container.style.color = "#0f172a"; container.style.fontFamily = "'Segoe UI', 'Helvetica Neue', Arial, sans-serif"; injectExportStyles(container); container.appendChild(createExportHeader()); messages.forEach((el, index) => { container.appendChild(createMessageWrapper(el, index)); }); return container; } /** Calculate the maximum safe scale for html2canvas to avoid exceeding browser canvas limits */ function getSafeScale(cssWidth, cssHeight, desiredScale) { // Browser limits: Chrome/Safari ~16384 per dimension, ~268M total pixels // Use conservative limits to be safe across browsers const MAX_DIM = 16384; const MAX_AREA = 200_000_000; let scale = desiredScale; if (cssWidth * scale > MAX_DIM) scale = Math.floor(MAX_DIM / cssWidth * 100) / 100; if (cssHeight * scale > MAX_DIM) scale = Math.floor(MAX_DIM / cssHeight * 100) / 100; if (cssWidth * scale * cssHeight * scale > MAX_AREA) { scale = Math.floor(Math.sqrt(MAX_AREA / (cssWidth * cssHeight)) * 100) / 100; } return Math.max(1, scale); } function downloadBlob(blob, filename) { const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); } function exportFilename(ext) { const brand = SITE_BRAND[host]; const platform = brand ? brand.name.toLowerCase().replace(/\s+/g, "-") : "chat"; const date = new Date().toISOString().slice(0, 10); return `${platform}-export-${date}.${ext}`; } function exportMarkdown() { const messages = getSelectedMessages(); if (messages.length === 0) return; const turndownService = new TurndownService(); turndownService.addRule("fencedCodeBlock", { filter: (node) => node.nodeName === "PRE" && node.querySelector("code"), replacement: (content, node) => { const code = node.querySelector("code"); const cls = code.getAttribute("class") || ""; const langMatch = cls.match(/language-(\S+)/); const lang = langMatch ? langMatch[1] : ""; const text = code.textContent || ""; return `\n\n\`\`\`${lang}\n${text}\n\`\`\`\n\n`; } }); const content = messages .map((el, idx) => { const role = roleLabel(guessRole(el)); const clone = cloneMessageForExport(el); const md = normalizeExportText(turndownService.turndown(clone.innerHTML)); return `### ${role}\n\n${md}`; }) .join("\n\n---\n\n"); const blob = new Blob([content], { type: "text/markdown" }); downloadBlob(blob, exportFilename("md")); } function exportTxt() { const messages = getSelectedMessages(); if (messages.length === 0) return; const content = messages.map((el, idx) => { const role = roleLabel(guessRole(el)); const clone = cloneMessageForExport(el); const text = normalizeExportText(clone.textContent || ""); return `${role}\n${text}`; }) .join("\n\n---\n\n"); const blob = new Blob([content], { type: "text/plain" }); downloadBlob(blob, exportFilename("txt")); } async function exportImage() { const messages = getSelectedMessages(); if (messages.length === 0) return; const container = createExportContainer(messages); container.style.position = "fixed"; container.style.left = "-9999px"; document.body.appendChild(container); await new Promise(r => requestAnimationFrame(r)); const w = container.offsetWidth; const h = container.offsetHeight; const scale = getSafeScale(w, h, 2); const canvas = await html2canvas(container, { scale, useCORS: true, backgroundColor: "#f8fafc", logging: false, imageTimeout: 5000, width: w, height: h }); container.remove(); canvas.toBlob((blob) => { if (blob) downloadBlob(blob, exportFilename("png")); }); } /** Create a styled off-screen container for export rendering */ function createOffscreenExportContainer(width, padding) { const c = document.createElement("div"); c.style.cssText = `width:${width}px;padding:${padding}px;background:#f8fafc;color:#0f172a;font-family:'Segoe UI','Helvetica Neue',Arial,sans-serif;position:fixed;left:-9999px;`; injectExportStyles(c); return c; } async function exportPdf() { const messages = getSelectedMessages(); if (messages.length === 0) return; const { jsPDF } = window.jspdf; const pdf = new jsPDF("p", "pt", "a4"); const pageWidth = pdf.internal.pageSize.getWidth(); const pageHeight = pdf.internal.pageSize.getHeight(); const contentBoxWidth = 820; const containerPadding = 28; const containerTotalWidth = contentBoxWidth + containerPadding * 2; const maxCssHeight = pageHeight * containerTotalWidth / pageWidth; const usableHeight = maxCssHeight - containerPadding * 2; // Phase 1: Create all wrappers ONCE and measure const measurer = createOffscreenExportContainer(contentBoxWidth, containerPadding); document.body.appendChild(measurer); const headerEl = createExportHeader(); measurer.appendChild(headerEl); const wrappers = messages.map((el, idx) => { const w = createMessageWrapper(el, idx); measurer.appendChild(w); return w; }); await new Promise(r => requestAnimationFrame(r)); const headerHeight = headerEl.offsetHeight + 18; const msgHeights = wrappers.map(w => w.offsetHeight + 14); measurer.remove(); // Phase 2: Group into pages const pages = []; let currentPage = []; let currentHeight = headerHeight; for (let i = 0; i < messages.length; i++) { if (currentPage.length > 0 && currentHeight + msgHeights[i] > usableHeight) { pages.push(currentPage); currentPage = []; currentHeight = 0; } currentPage.push(i); currentHeight += msgHeights[i]; } if (currentPage.length > 0) pages.push(currentPage); // Phase 3: Render each page by MOVING existing wrappers (not re-creating) for (let p = 0; p < pages.length; p++) { const container = createOffscreenExportContainer(contentBoxWidth, containerPadding); document.body.appendChild(container); if (p === 0) container.appendChild(headerEl); pages[p].forEach(idx => container.appendChild(wrappers[idx])); await new Promise(r => requestAnimationFrame(r)); const scale = getSafeScale(container.offsetWidth, container.offsetHeight, 2); const canvas = await html2canvas(container, { scale, useCORS: true, backgroundColor: "#f8fafc", logging: false, imageTimeout: 5000, width: container.offsetWidth, height: container.offsetHeight }); container.remove(); const imgData = canvas.toDataURL("image/jpeg", 0.92); const imgWidth = pageWidth; const imgHeight = (canvas.height * imgWidth) / canvas.width; if (p > 0) pdf.addPage(); pdf.addImage(imgData, "JPEG", 0, 0, imgWidth, imgHeight); } pdf.save(exportFilename("pdf")); } function elementMatchesMessage(el) { if (!(el instanceof Element)) return false; const selectors = SITE_SELECTORS[host] || []; return selectors.some((sel) => { try { return el.matches(sel); } catch { return false; } }); } function collectMessageElementsFromNodes(nodes) { const collected = []; nodes.forEach((node) => { if (!(node instanceof Element)) return; if (elementMatchesMessage(node)) collected.push(node); const selectors = SITE_SELECTORS[host] || []; selectors.forEach((sel) => { node.querySelectorAll(sel).forEach((el) => collected.push(el)); }); }); return uniqueElements(collected); } function connectObserver() { if (state.observer) return; state.observer = new MutationObserver((mutations) => { if (!state.selectionMode || state.observerQueued) return; invalidateMessageCache(); const added = []; mutations.forEach((m) => { if (m.addedNodes && m.addedNodes.length) { added.push(...collectMessageElementsFromNodes(m.addedNodes)); } }); if (added.length === 0) return; state.observerQueued = true; requestAnimationFrame(() => { state.observerQueued = false; scheduleRenderSelectionControls(added); }); }); state.observer.observe(getChatRoot(), { childList: true, subtree: true }); } function disconnectObserver() { if (!state.observer) return; state.observer.disconnect(); state.observer = null; state.observerQueued = false; } /* ── Init retry configs ── */ const INIT_CONFIGS = { "www.doubao.com": { checkSel: "[data-testid='skill-page-item-export']", interval: 500, timeout: 15000, observer: true }, "chat.deepseek.com": { checkSel: "[data-tm-export-entry='deepseek']", interval: 700, timeout: 20000, observer: true }, "chatgpt.com": { checkSel: "[data-tm-export-entry='chatgpt']", interval: 700, timeout: 20000, observer: true }, "chat.qwen.ai": { checkSel: "[data-tm-export-entry='qwen']", interval: 700, timeout: 20000, observer: true }, "grok.com": { checkSel: "[data-tm-export-entry='grok']", interval: 700, timeout: 20000, observer: true }, "gemini.google.com": { checkSel: "[data-tm-export-entry='gemini']", interval: 700, timeout: 20000, observer: true }, "copilot.cloud.microsoft": { checkSel: "[data-tm-export-entry='copilot']", interval: 700, timeout: 20000, observer: true }, "m365.cloud.microsoft": { checkSel: "[data-tm-export-entry='copilot']", interval: 700, timeout: 20000, observer: true }, "www.perplexity.ai": { checkSel: "[data-tm-export-entry='perplexity']", interval: 700, timeout: 20000, observer: true }, "aistudio.google.com": { checkSel: "[data-tm-export-entry='aistudio']", interval: 700, timeout: 20000, observer: true }, "claude.ai": { checkSel: "[data-tm-export-entry='claude']", interval: 700, timeout: 20000, observer: true }, }; function init() { createSidebarButton(); createPanel(); const cfg = INIT_CONFIGS[host]; if (cfg) { // Phase 1: Fast retries during initial page load const retryId = setInterval(() => { ensureSidebarButton(); if (document.querySelector(cfg.checkSel)) clearInterval(retryId); }, cfg.interval); setTimeout(() => clearInterval(retryId), cfg.timeout); // Phase 2: Persistent slow check — survives SPA navigation setInterval(() => { if (!document.querySelector(cfg.checkSel)) { ensureSidebarButton(); } }, 3000); if (cfg.observer) connectSidebarObserverFor(host); } } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); } })();