// ==UserScript== // @name ChatGPT体验增强插件/全局与单条导出Markdown/美化对话列表面板 // @namespace https://fcai.me/afdian // @version 1.1 // @description ChatGPT官网 时间线/预览/跳转/长按标记/搜索/公式复制/全局与单条导出Markdown。增强稳定性与兼容性(修复长对话虚拟化节点消失,优化导出无冗余引用胶囊,完美修复公式多余转义字符,统一行内/行间公式嗅探复制交互,美化对话列表面板) // @author Kongkon // @match https://chatgpt.com/* // @match https://chat.openai.com/* // @match https://new.oaifree.com/** // @match https://share.github.cn.com/** // @match https://chatgpt.com/** // @match https://*.oaifree.com/** // @match https://cc.plusai.me/** // @match https://chat.chatgptplus.cn/** // @match https://chat.rawchat.cc/** // @match https://chat.sharedchat.cn/** // @match https://chat.gptdsb.com/** // @match https://chat.freegpts.org/** // @match https://gpt.github.cn.com/** // @match https://chat.aicnn.xyz/** // @match https://*.xyhelper.com.cn/** // @match https://oai.aitopk.com/** // @match https://www.opkfc.com/** // @match https://bus.hematown.com/** // @match https://chatgpt.dairoot.cn/** // @match https://plus.aivvm.com/** // @match https://sharechat.aischat.xyz/** // @match https://web.tu-zi.com/** // @match https://chat1.2233.ai/** // @match https://2233.ai/** // @match https://yesiamai.com/** // @match https://www.azs.ai/** // @match https://sass-node6.chatshare.biz/** // @match https://sass-node1.chatshare.biz/** // @match https://sass-node2.chatshare.biz/** // @match https://sass-node3.chatshare.biz/** // @match https://sass-node4.chatshare.biz/** // @match https://sass-node5.chatshare.biz/** // @match https://oai.253282.xyz/** // @match https://gpt.universalbus.cn/** // @match https://go.gptdsb.com/** // @match https://*.azs.ai/** // @match https://azs.ai/** // @match https://*.azs.ai/c/** // @license CC-BY-NC-4.0 // @grant GM_addStyle // @require https://unpkg.com/turndown/lib/turndown.browser.umd.js // @require https://unpkg.com/turndown-plugin-gfm/dist/turndown-plugin-gfm.js // @run-at document-idle // @noframes // ==/UserScript== (function() { 'use strict'; // ========================================== // 1. 注入 CSS 样式(对话列表面板已全面美化) // ========================================== GM_addStyle(` :root { --tl-capsule-bg: #f7f9fa; --tl-capsule-border: #e5e8eb; --tl-capsule-shadow: rgba(0, 0, 0, 0.05); --tl-dot-color: #c5cbd5; --tl-dot-active-ring: #508cf3; --tl-tooltip-bg: rgba(28, 28, 30, 0.95); --tl-tooltip-text: #ffffff; --tl-tooltip-shadow: rgba(0, 0, 0, 0.15); --tl-panel-bg: rgba(255, 255, 255, 0.90); --tl-panel-text: #1c1c1e; --tl-input-bg: rgba(0, 0, 0, 0.05); --tl-item-hover: rgba(0, 0, 0, 0.04); --tl-icon-color: #565869; --tl-highlight-bg: rgba(255, 193, 7, 0.12); --tl-index-bg: rgba(0, 0, 0, 0.06); } html.dark { --tl-capsule-bg: rgba(45, 45, 45, 0.85); --tl-capsule-border: rgba(255, 255, 255, 0.08); --tl-capsule-shadow: rgba(0, 0, 0, 0.3); --tl-dot-color: #565869; --tl-dot-active-ring: #508cf3; --tl-tooltip-bg: rgba(236, 236, 236, 0.95); --tl-tooltip-text: #1c1c1e; --tl-tooltip-shadow: rgba(0, 0, 0, 0.3); --tl-panel-bg: rgba(32, 33, 35, 0.92); --tl-panel-text: #ececec; --tl-input-bg: rgba(255, 255, 255, 0.1); --tl-item-hover: rgba(255, 255, 255, 0.06); --tl-icon-color: #c5cbd5; --tl-highlight-bg: rgba(255, 193, 7, 0.18); --tl-index-bg: rgba(255, 255, 255, 0.08); } #chatgpt-dot-timeline { position: fixed; top: 50%; right: 20px; transform: translateY(-50%); width: 36px; background: var(--tl-capsule-bg); border: 1px solid var(--tl-capsule-border); border-radius: 20px; padding: 18px 0; display: none; flex-direction: column; align-items: center; gap: 20px; z-index: 99998; box-shadow: 0 4px 12px var(--tl-capsule-shadow); max-height: 80vh; overflow-y: auto; backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); } #chatgpt-dot-timeline::-webkit-scrollbar { display: none; } #chatgpt-timeline-indicator { position: absolute; width: 14px; height: 14px; border-radius: 50%; box-shadow: 0 0 0 3px var(--tl-dot-active-ring); pointer-events: none; z-index: 10; top: 0; left: 10px; opacity: 0; transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s ease, opacity 0.3s ease; } .timeline-dot { width: 14px; height: 14px; border-radius: 50%; background-color: var(--tl-dot-color); cursor: pointer; position: relative; flex-shrink: 0; transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); -webkit-user-select: none; user-select: none; } .timeline-dot.highlighted { background-color: #FFC107 !important; } .timeline-dot.highlighted:hover { box-shadow: 0 0 0 3px rgba(255, 193, 7, 0.5); } #chatgpt-timeline-tooltip { position: fixed; background: var(--tl-tooltip-bg); color: var(--tl-tooltip-text); padding: 10px 14px; border-radius: 8px; font-size: 13px; line-height: 1.6; width: max-content; max-width: 300px; word-break: break-word; white-space: pre-wrap; pointer-events: none; z-index: 99999; opacity: 0; transform: translateY(-50%) translateX(8px); transition: opacity 0.2s ease, transform 0.2s ease; box-shadow: 0 4px 16px var(--tl-tooltip-shadow); backdrop-filter: blur(8px); } #chatgpt-timeline-tooltip.visible { opacity: 1; transform: translateY(-50%) translateX(0); } #chatgpt-timeline-tooltip::after { content: ''; position: absolute; right: -6px; top: 50%; transform: translateY(-50%); border-width: 6px 0 6px 6px; border-style: solid; border-color: transparent transparent transparent var(--tl-tooltip-bg); } /* 工具栏配置 */ #chatgpt-timeline-tools { position: fixed; top: 50%; right: 66px; transform: translateY(-50%); display: flex; flex-direction: column; gap: 12px; z-index: 99998; } .tool-btn { width: 36px; height: 36px; border-radius: 50%; background: var(--tl-capsule-bg); border: 1px solid var(--tl-capsule-border); box-shadow: 0 4px 12px var(--tl-capsule-shadow); display: flex; align-items: center; justify-content: center; cursor: pointer; color: var(--tl-icon-color); transition: all 0.3s; backdrop-filter: blur(10px); } .tool-btn:hover { color: var(--tl-dot-active-ring); } #menu-btn { display: none; } /* ================= 对话列表面板(全面美化) ================= */ #chatgpt-timeline-panel { position: fixed; top: 50%; right: 112px; transform: translateY(-50%) translateX(20px); width: 320px; max-height: 75vh; background: var(--tl-panel-bg); border: 1px solid var(--tl-capsule-border); border-radius: 16px; box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0,0,0,0.04); z-index: 99997; display: flex; flex-direction: column; opacity: 0; pointer-events: none; visibility: hidden; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); } #chatgpt-timeline-panel.visible { opacity: 1; visibility: visible; pointer-events: auto; transform: translateY(-50%) translateX(0); } #chatgpt-panel-header { padding: 14px 16px 12px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--tl-capsule-border); user-select: none; } #chatgpt-panel-header h3 { margin: 0; font-size: 15px; font-weight: 600; color: var(--tl-panel-text); letter-spacing: -0.01em; } #chatgpt-panel-close { background: none; border: none; color: var(--tl-icon-color); cursor: pointer; padding: 4px; border-radius: 6px; display: flex; align-items: center; justify-content: center; transition: background 0.2s, color 0.2s; width: 28px; height: 28px; } #chatgpt-panel-close:hover { background: rgba(235, 87, 87, 0.15); color: #eb5757; } #chatgpt-panel-search-wrapper { padding: 10px 14px; position: relative; border-bottom: 1px solid var(--tl-capsule-border); } .search-icon { position: absolute; left: 24px; top: 50%; transform: translateY(-50%); color: var(--tl-icon-color); pointer-events: none; opacity: 0.6; } #chatgpt-panel-search { width: 100%; background: var(--tl-input-bg); border: 1px solid transparent; border-radius: 10px; padding: 9px 14px 9px 38px; color: var(--tl-panel-text); font-size: 13px; outline: none; box-sizing: border-box; transition: border-color 0.25s, box-shadow 0.25s; } #chatgpt-panel-search::placeholder { color: var(--tl-icon-color); opacity: 0.7; } #chatgpt-panel-search:focus { border-color: var(--tl-dot-active-ring); box-shadow: 0 0 0 3px rgba(80, 140, 243, 0.15); } #chatgpt-panel-list { flex-grow: 1; overflow-y: auto; padding: 8px 6px; display: flex; flex-direction: column; gap: 4px; } #chatgpt-panel-list::-webkit-scrollbar { width: 5px; } #chatgpt-panel-list::-webkit-scrollbar-thumb { background: var(--tl-input-bg); border-radius: 3px; } #chatgpt-panel-list::-webkit-scrollbar-track { background: transparent; } .panel-list-item { padding: 10px 12px; border-radius: 12px; cursor: pointer; color: var(--tl-panel-text); font-size: 13px; line-height: 1.5; display: flex; align-items: flex-start; gap: 12px; transition: background 0.2s, box-shadow 0.2s; margin: 0 2px; position: relative; } .panel-list-item:hover { background: var(--tl-item-hover); } .panel-list-item.active { background: var(--tl-item-hover); box-shadow: inset 3px 0 0 var(--tl-dot-active-ring); } .panel-list-item.highlighted { background: var(--tl-highlight-bg); } .panel-list-index { display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: 50%; background: var(--tl-index-bg); color: var(--tl-icon-color); font-weight: 700; font-size: 12px; flex-shrink: 0; margin-top: 1px; transition: background 0.2s, color 0.2s, transform 0.2s; } .panel-list-item.active .panel-list-index { background: var(--tl-dot-active-ring); color: #fff; transform: scale(1.05); } .panel-list-item.highlighted .panel-list-index { background: #FFC107; color: #000; } .panel-list-text { flex: 1; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; word-break: break-word; min-width: 0; } /* 行内与行间公式交互样式统一 */ .katex { cursor: pointer; transition: opacity 0.2s ease, background-color 0.2s ease; border-radius: 4px; } .katex:hover { opacity: 0.85; } .katex:active { opacity: 0.6; } /* 复制成功Toast样式 */ #formula-copy-toast { position: fixed; background: rgba(16, 163, 127, 0.95); color: #fff; padding: 6px 12px; border-radius: 6px; font-size: 13px; font-weight: 500; pointer-events: none; z-index: 100000; opacity: 0; transform: translate(-50%, 0); transition: opacity 0.2s ease, transform 0.2s ease; box-shadow: 0 4px 12px rgba(0,0,0,0.15); display: flex; align-items: center; gap: 4px; backdrop-filter: blur(4px); } #formula-copy-toast.show { opacity: 1; transform: translate(-50%, -20px); } `); // ========================================== // 2. 构建 DOM 与 状态缓存 // ========================================== const container = document.createElement('div'); container.id = 'chatgpt-dot-timeline'; document.body.appendChild(container); const activeIndicator = document.createElement('div'); activeIndicator.id = 'chatgpt-timeline-indicator'; container.appendChild(activeIndicator); const globalTooltip = document.createElement('div'); globalTooltip.id = 'chatgpt-timeline-tooltip'; document.body.appendChild(globalTooltip); const toolsContainer = document.createElement('div'); toolsContainer.id = 'chatgpt-timeline-tools'; document.body.appendChild(toolsContainer); const menuBtn = document.createElement('div'); menuBtn.id = 'menu-btn'; menuBtn.className = 'tool-btn'; menuBtn.title = '打开对话列表'; menuBtn.innerHTML = ``; toolsContainer.appendChild(menuBtn); const thinkBtn = document.createElement('div'); thinkBtn.id = 'think-btn'; thinkBtn.className = 'tool-btn'; thinkBtn.title = '开启深度思考 (/think)'; thinkBtn.innerHTML = ``; toolsContainer.appendChild(thinkBtn); // 新增:全局聊天导出按钮 const exportGlobalBtn = document.createElement('div'); exportGlobalBtn.id = 'export-global-btn'; exportGlobalBtn.className = 'tool-btn'; exportGlobalBtn.title = '全局聊天导出 (/export)'; exportGlobalBtn.innerHTML = ``; toolsContainer.appendChild(exportGlobalBtn); exportGlobalBtn.addEventListener('click', async (e) => { e.stopPropagation(); await exportGlobalChat(); }); // ====== 带有标题栏和关闭按钮的面板 ====== const panel = document.createElement('div'); panel.id = 'chatgpt-timeline-panel'; panel.innerHTML = `

对话列表

`; document.body.appendChild(panel); const searchInput = document.getElementById('chatgpt-panel-search'); const panelList = document.getElementById('chatgpt-panel-list'); const closeBtn = document.getElementById('chatgpt-panel-close'); // 全局复制Toast提示框 const formulaToast = document.createElement('div'); formulaToast.id = 'formula-copy-toast'; formulaToast.innerHTML = `复制成功`; document.body.appendChild(formulaToast); // 全局文本缓存 const turnTextCache = new Map(); // ========================================== // 3. 逻辑引擎与Markdown导出集成 // ========================================== let isAutoScrolling = false; let scrollEndTimer = null; let tdService = null; // 初始化Turndown引擎 function getTurndownService() { if (tdService) return tdService; tdService = new TurndownService({ headingStyle: 'atx', codeBlockStyle: 'fenced', emDelimiter: '*' }); if (typeof turndownPluginGfm !== 'undefined') { tdService.use(turndownPluginGfm.gfm); } tdService.addRule('strikethrough-fix', { filter:['del', 's', 'strike'], replacement: function (content) { return '~~' + content + '~~'; } }); // 自定义解析ChatGPT特有代码块 tdService.addRule('chatgptCodeBlock', { filter: 'pre', replacement: function (content, node) { let lang = ''; const langDiv = node.querySelector('.text-token-text-primary'); if (langDiv) lang = langDiv.textContent.trim(); let codeText = ''; const contentDiv = node.querySelector('.cm-content'); if (contentDiv) { const clone = contentDiv.cloneNode(true); clone.querySelectorAll('br').forEach(br => br.replaceWith('\n')); codeText = clone.textContent; } else { codeText = node.textContent; } return '\n```' + lang + '\n' + codeText + '\n```\n'; } }); // 完美解析公式,防止多余转义符 tdService.addRule('chatgptMath', { filter: function(node) { if (!node.classList) return false; if (node.classList.contains('katex-display')) return true; if (node.classList.contains('katex') && (!node.parentElement || !node.parentElement.classList.contains('katex-display'))) return true; return false; }, replacement: function(content, node) { const annotation = node.querySelector('annotation[encoding="application/x-tex"]'); if (!annotation) return content; const tex = annotation.textContent.trim(); if (node.classList.contains('katex-display')) { return '\n$$\n' + tex + '\n$$\n'; } else { return '$' + tex + '$'; } } }); return tdService; } // 统一复用的核心提取逻辑(抽取自原单个回答提取逻辑) function getAssistantTurnMarkdown(turnNode) { const contentContainer = turnNode.querySelector('.flex.max-w-full.flex-col.gap-4.grow'); // 兼容不同的DOM层级嵌套 if (!contentContainer) { const mdNode = turnNode.querySelector('.markdown'); if (mdNode) { const turndown = getTurndownService(); const clone = mdNode.cloneNode(true); clone.querySelectorAll('svg').forEach(svg => svg.remove()); return turndown.turndown(clone).trim(); } return ''; } let mdContent = ""; const children = contentContainer.children; const turndown = getTurndownService(); for (let i = 0; i < children.length; i++) { const child = children[i]; if (child.getAttribute('data-message-author-role') === 'assistant') { const markdownDiv = child.querySelector('.markdown'); if (markdownDiv) { const nextChild = children[i+1]; let isThought = false; let thoughtTime = "Thought process"; if (nextChild && nextChild.classList.contains('justify-between')) { const btn = nextChild.querySelector('button'); if (btn && btn.textContent.includes('Thought')) { isThought = true; thoughtTime = btn.textContent.trim(); } } const clone = markdownDiv.cloneNode(true); // 擦除SVG与引用胶囊等冗余节点 clone.querySelectorAll('svg').forEach(svg => svg.remove()); clone.querySelectorAll('[data-testid="webpage-citation-pill"]').forEach(pill => { const parent = pill.parentElement; if (parent && parent.tagName.toLowerCase() === 'span' && parent.childElementCount === 1) { parent.remove(); } else { pill.remove(); } }); clone.querySelectorAll('[aria-haspopup="dialog"]').forEach(el => el.remove()); let md = turndown.turndown(clone); if (isThought) { mdContent += `> **${thoughtTime}**\n>\n> ` + md.split('\n').join('\n> ') + `\n\n`; } else { mdContent += md + `\n\n`; } } } } return mdContent.trim(); } // 提取 User Turn 内容 function extractUserContent(turnNode) { const userDiv = turnNode.querySelector('[data-message-author-role="user"]'); if (userDiv) { return userDiv.innerText.trim(); } return ''; } // 格式化花边构造器 function formatTurn(turnIndex, userText, assistantText) { return `╔═══════\n║ TURN #${turnIndex}\n╚═══════\nUser:\n${userText || '[未检测到文本或仅包含多媒体/附件]'}\n---\nChatGPT:\n${assistantText || ''}\n---\n`; } // ================== 全局导出新功能 ================== async function exportGlobalChat() { // 1. 创建毛玻璃全屏遮罩层防止用户操作 const mask = document.createElement('div'); mask.style.cssText = ` position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0, 0, 0, 0.4); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); z-index: 999999; display: flex; flex-direction: column; align-items: center; justify-content: center; color: #ffffff; font-size: 16px; font-weight: 500; font-family: sans-serif; pointer-events: auto; user-select: none; `; mask.innerHTML = `
正在准备拉取历史记录...
`; document.body.appendChild(mask); const statusEl = document.getElementById('global-export-status'); // 2. 动态寻找滚动容器并主动滑动到最顶端 let scrollRetries = 0; const maxRetries = 40; // 设定重试阈值,足够应对上百轮的超级长对话 while (scrollRetries < maxRetries) { const turns = document.querySelectorAll('section[data-turn]'); if (!turns.length) break; const firstTurn = turns[0]; // 【核心优化】动态向上探测当前生效的滚动包裹层,不依赖固定类名 let scroller = firstTurn.parentElement; let scrollContainer = window; let isWindow = true; while (scroller && scroller !== document.documentElement && scroller !== document.body) { const style = window.getComputedStyle(scroller); if ((style.overflowY === 'auto' || style.overflowY === 'scroll' || style.overflowY === 'overlay') && scroller.scrollHeight > scroller.clientHeight) { scrollContainer = scroller; isWindow = false; break; } scroller = scroller.parentElement; } const prevFirstTurn = firstTurn; // 【三重触发机制】确保万无一失拉到最顶 if (!isWindow) { scrollContainer.scrollTop = 0; } window.scrollTo(0, 0); firstTurn.scrollIntoView({ behavior: 'instant', block: 'start', inline: 'nearest' }); statusEl.textContent = `正在向上追溯历史记录... (第 ${scrollRetries + 1} 次翻页)`; // 给网络请求和 React 渲染留出缓冲时间 await new Promise(r => setTimeout(r, 800)); const newTurns = document.querySelectorAll('section[data-turn]'); const newFirstTurn = newTurns.length ? newTurns[0] : null; // 如果向上翻页后,顶层第一个对话节点仍是之前那个,说明很可能到底(顶)了 if (prevFirstTurn === newFirstTurn) { // 防抖:网络波动可能导致加载延迟,再等一下进行二次验证 await new Promise(r => setTimeout(r, 1000)); const finalTurns = document.querySelectorAll('section[data-turn]'); if (prevFirstTurn === (finalTurns.length ? finalTurns[0] : null)) { break; // 确实顶端了,跳出循环 } } scrollRetries++; } statusEl.textContent = "所有历史记录加载完毕,正在极速提取与排版..."; await new Promise(r => setTimeout(r, 300)); // 让界面重绘给用户直观反馈 // 3. 开始按序抓取DOM中呈现的对话组序列并匹配 const turnsList = document.querySelectorAll('section[data-turn]'); let markdownChunks =[]; let currentTurnNum = 1; let currentUserText = ''; let currentAssistantText = ''; let hasUser = false; for (let i = 0; i < turnsList.length; i++) { const node = turnsList[i]; const role = node.getAttribute('data-turn'); if (role === 'user') { // 如果已经搜集完了整整一回合却碰到了新的User,那么封包推入栈中再开始下一轮 if (hasUser || currentAssistantText) { markdownChunks.push(formatTurn(currentTurnNum, currentUserText, currentAssistantText)); currentTurnNum++; currentUserText = ''; currentAssistantText = ''; } currentUserText = extractUserContent(node); hasUser = true; } else if (role === 'assistant') { const astText = getAssistantTurnMarkdown(node); // 处理Assistant长文本或是连续生成分叉文本的聚合情况 if (currentAssistantText) { currentAssistantText += '\n\n' + astText; } else { currentAssistantText = astText; } } } // 将循环结束时遗留的最后一块压入最终组合栈 if (hasUser || currentAssistantText) { markdownChunks.push(formatTurn(currentTurnNum, currentUserText, currentAssistantText)); } // 4. 以双换行符拼接后导出流 const finalMarkdown = markdownChunks.join('\n'); if (finalMarkdown.trim().length > 0) { const blob = new Blob([finalMarkdown], { type: 'text/markdown;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `ChatGPT_Global_Export_${new Date().toISOString().replace(/[:.]/g, '-')}.md`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } else { alert('当前页面未能提取到任何合法对话内容!'); } // 5. 关闭遮罩层 mask.remove(); } // 面板打开关闭交互 menuBtn.addEventListener('click', (e) => { e.stopPropagation(); panel.classList.toggle('visible'); if (panel.classList.contains('visible')) searchInput.focus(); }); closeBtn.addEventListener('click', (e) => { e.stopPropagation(); panel.classList.remove('visible'); }); document.addEventListener('click', (e) => { if (panel.classList.contains('visible') && !panel.contains(e.target) && !menuBtn.contains(e.target)) { panel.classList.remove('visible'); } }); searchInput.addEventListener('input', (e) => { const keyword = e.target.value.toLowerCase().trim(); panelList.querySelectorAll('.panel-list-item').forEach(item => { const text = item.querySelector('.panel-list-text').textContent.toLowerCase(); const indexStr = (parseInt(item.getAttribute('data-index'), 10) + 1).toString(); const match = keyword === '' || text.includes(keyword) || indexStr.includes(keyword); item.style.display = match ? 'flex' : 'none'; }); }); thinkBtn.addEventListener('click', async (e) => { e.stopPropagation(); const editor = document.querySelector('#prompt-textarea'); if (!editor) return; editor.dispatchEvent(new MouseEvent('mousedown', { bubbles: true })); editor.dispatchEvent(new MouseEvent('mouseup', { bubbles: true })); editor.dispatchEvent(new MouseEvent('click', { bubbles: true })); editor.focus(); const selection = window.getSelection(); const range = document.createRange(); range.selectNodeContents(editor); range.collapse(false); selection.removeAllRanges(); selection.addRange(range); await new Promise(r => setTimeout(r, 50)); document.execCommand('insertText', false, '/think'); await new Promise(r => setTimeout(r, 150)); editor.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true })); await new Promise(r => setTimeout(r, 100)); editor.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true })); const originalColor = thinkBtn.style.color; thinkBtn.style.color = '#10a37f'; setTimeout(() => thinkBtn.style.color = originalColor, 1000); }); function updateIndicator(targetDot) { if (!targetDot) { activeIndicator.style.opacity = '0'; return; } activeIndicator.style.opacity = '1'; activeIndicator.style.transform = `translateY(${targetDot.offsetTop}px)`; activeIndicator.style.boxShadow = targetDot.classList.contains('highlighted') ? '0 0 0 3px rgba(255, 193, 7, 0.5)' : '0 0 0 3px var(--tl-dot-active-ring)'; } function jumpToTurn(index, turnNode, sourceDot) { globalTooltip.classList.remove('visible'); isAutoScrolling = true; turnNode.scrollIntoView({ behavior: 'smooth', block: 'center' }); document.querySelectorAll('.timeline-dot').forEach(d => d.classList.remove('active')); const targetDot = sourceDot || document.querySelector(`.timeline-dot[data-index="${index}"]`); if (targetDot) { targetDot.classList.add('active'); updateIndicator(targetDot); } document.querySelectorAll('.panel-list-item').forEach(item => item.classList.remove('active')); const targetListItem = document.querySelector(`.panel-list-item[data-index="${index}"]`); if (targetListItem) targetListItem.classList.add('active'); } window.addEventListener('scroll', () => { if (isAutoScrolling) { clearTimeout(scrollEndTimer); scrollEndTimer = setTimeout(() => { isAutoScrolling = false; }, 150); } }, { passive: true }); const scrollObserver = new IntersectionObserver((entries) => { if (isAutoScrolling) return; entries.forEach(entry => { if (entry.isIntersecting) { const index = entry.target.getAttribute('data-timeline-index'); if (index === null) return; document.querySelectorAll('.timeline-dot').forEach(dot => dot.classList.remove('active')); const activeDot = document.querySelector(`.timeline-dot[data-index="${index}"]`); if (activeDot) { activeDot.classList.add('active'); updateIndicator(activeDot); } document.querySelectorAll('.panel-list-item').forEach(item => item.classList.remove('active')); const targetListItem = document.querySelector(`.panel-list-item[data-index="${index}"]`); if (targetListItem) targetListItem.classList.add('active'); } }); }, { rootMargin: '-20% 0px -60% 0px', threshold: 0 }); // 统一公式点击嗅探交互 function enhanceFormulas() { document.querySelectorAll('.katex-display:not(.enhanced-formula), .katex:not(.katex-display):not(.enhanced-formula)').forEach(mathNode => { if (!mathNode.classList.contains('katex-display') && mathNode.closest('.katex-display')) { return; } const annotation = mathNode.querySelector('annotation[encoding="application/x-tex"]'); if (!annotation) return; mathNode.classList.add('enhanced-formula'); const isBlock = mathNode.classList.contains('katex-display'); const tex = annotation.textContent.trim(); mathNode.title = isBlock ? '点击复制行间公式' : '点击复制行内公式'; mathNode.addEventListener('click', (e) => { e.stopPropagation(); e.preventDefault(); const textToCopy = isBlock ? `$$\n${tex}\n$$` : `$${tex}$`; navigator.clipboard.writeText(textToCopy).then(() => { formulaToast.style.left = `${e.clientX}px`; formulaToast.style.top = `${e.clientY}px`; formulaToast.classList.add('show'); if (formulaToast.timeoutId) clearTimeout(formulaToast.timeoutId); formulaToast.timeoutId = setTimeout(() => { formulaToast.classList.remove('show'); }, 1500); }); }); }); } // 提取并导出单条回答为Markdown function exportTurnToMarkdown(actionContainer) { const turnNode = actionContainer.closest('[data-testid^="conversation-turn-"], [data-turn="assistant"]'); if (!turnNode) return; let mdContent = getAssistantTurnMarkdown(turnNode); if (!mdContent || mdContent.trim() === '') return; const blob = new Blob([mdContent], { type: 'text/markdown;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `ChatGPT_Response_${new Date().toISOString().replace(/[:.]/g, '-')}.md`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } // 挂载导出Markdown按钮 function addExportButtons() { document.querySelectorAll('[data-testid="copy-turn-action-button"]').forEach(copyBtn => { const container = copyBtn.parentElement; if (!container || container.querySelector('.export-md-btn')) return; const exportBtn = document.createElement('button'); exportBtn.className = 'export-md-btn text-token-text-secondary hover:bg-token-bg-secondary rounded-lg'; exportBtn.setAttribute('aria-label', '导出为Markdown'); exportBtn.setAttribute('title', '导出为Markdown'); exportBtn.innerHTML = ``; exportBtn.addEventListener('click', (e) => { e.stopPropagation(); exportTurnToMarkdown(container); }); container.appendChild(exportBtn); }); } function updateTimeline() { enhanceFormulas(); addExportButtons(); const userTurns = document.querySelectorAll('section[data-turn="user"]'); const existingDots = document.querySelectorAll('.timeline-dot'); const highlightedIndexes = new Set(); document.querySelectorAll('.timeline-dot.highlighted').forEach(d => highlightedIndexes.add(d.getAttribute('data-index'))); if (userTurns.length === 0) { container.style.display = 'none'; menuBtn.style.display = 'none'; panel.classList.remove('visible'); return; } else { container.style.display = 'flex'; menuBtn.style.display = 'flex'; } let needsRedraw = userTurns.length !== existingDots.length; userTurns.forEach((turn, index) => { const turnId = turn.getAttribute('data-turn-id') || `turn-${index}`; const messageDiv = turn.querySelector('[data-message-author-role="user"]'); if (messageDiv) { let rawText = messageDiv.innerText.replace(/\s+/g, ' ').trim(); let previewText = rawText.length > 120 ? rawText.substring(0, 120) + '...' : rawText; if (previewText.length === 0) previewText = '[图片或文件]'; if (turnTextCache.get(turnId) !== previewText) { turnTextCache.set(turnId, previewText); if (!needsRedraw) { const listItemText = panelList.querySelector(`.panel-list-item[data-index="${index}"] .panel-list-text`); if (listItemText) { listItemText.textContent = previewText; } } } } else if (!turnTextCache.has(turnId)) { turnTextCache.set(turnId, '[已折叠的历史消息]'); } const observeTarget = turn.closest('[data-turn-id-container]') || turn; observeTarget.setAttribute('data-timeline-index', index); scrollObserver.observe(observeTarget); }); if (!needsRedraw) return; container.querySelectorAll('.timeline-dot').forEach(d => d.remove()); scrollObserver.disconnect(); panelList.innerHTML = ''; userTurns.forEach((turn, index) => { const turnId = turn.getAttribute('data-turn-id') || `turn-${index}`; const previewText = turnTextCache.get(turnId); const observeTarget = turn.closest('[data-turn-id-container]') || turn; observeTarget.setAttribute('data-timeline-index', index); scrollObserver.observe(observeTarget); const dot = document.createElement('div'); dot.className = 'timeline-dot'; dot.setAttribute('data-index', index); if (highlightedIndexes.has(index.toString())) dot.classList.add('highlighted'); const listItem = document.createElement('div'); listItem.className = 'panel-list-item'; listItem.setAttribute('data-index', index); // 全新的序号徽章 + 文本布局 listItem.innerHTML = `
${index + 1}
${previewText}
`; if (highlightedIndexes.has(index.toString())) listItem.classList.add('highlighted'); if (index === userTurns.length - 1) { dot.classList.add('active'); listItem.classList.add('active'); requestAnimationFrame(() => updateIndicator(dot)); } dot.addEventListener('mouseenter', () => { const rect = dot.getBoundingClientRect(); globalTooltip.innerText = turnTextCache.get(turnId); globalTooltip.style.top = `${rect.top + rect.height / 2}px`; globalTooltip.style.right = `${window.innerWidth - rect.left + 14}px`; globalTooltip.classList.add('visible'); }); dot.addEventListener('mouseleave', () => globalTooltip.classList.remove('visible')); let pressTimer; let isLongPress = false; dot.addEventListener('mousedown', (e) => { if (e.button !== 0) return; isLongPress = false; pressTimer = setTimeout(() => { isLongPress = true; dot.classList.toggle('highlighted'); listItem.classList.toggle('highlighted'); if (dot.classList.contains('active')) updateIndicator(dot); }, 450); }); dot.addEventListener('mouseup', () => clearTimeout(pressTimer)); dot.addEventListener('mouseleave', () => clearTimeout(pressTimer)); dot.addEventListener('click', (e) => { if (!isLongPress) jumpToTurn(index, turn, dot); }); listItem.addEventListener('click', () => jumpToTurn(index, turn, dot)); container.appendChild(dot); panelList.appendChild(listItem); }); } let debounceTimer; const domObserver = new MutationObserver(() => { clearTimeout(debounceTimer); debounceTimer = setTimeout(updateTimeline, 600); }); domObserver.observe(document.body, { childList: true, subtree: true }); updateTimeline(); })();