// ==UserScript== // @name Gemini Deep Research Exporter // @namespace http://tampermonkey.net/ // @version 1.4 // @description Deep Research 导出 // @author Eddy // @match https://gemini.google.com/* // @grant GM_registerMenuCommand // @run-at document-end // @downloadURL https://update.greasyfork.icu/scripts/563106/Gemini%20Deep%20Research%20Exporter.user.js // @updateURL https://update.greasyfork.icu/scripts/563106/Gemini%20Deep%20Research%20Exporter.meta.js // ==/UserScript== (function() { 'use strict'; console.log("🚀 Gemini Exporter: 脚本已启动"); // --- 1. 注册菜单命令(保底方案) --- // 如果实在看不到按钮,点击油猴插件图标,菜单里一定有这个选项 GM_registerMenuCommand("⚡️ 手动导出 Markdown", executeExport); GM_registerMenuCommand("🔄 强制刷新按钮", initButton); // --- 2. 暴力强显逻辑 --- function initButton() { // 如果按钮已存在,直接返回,避免重复 if (document.getElementById('gemini-export-btn-v3')) return; console.log("🔧 Gemini Exporter: 正在创建按钮..."); const btn = document.createElement('div'); btn.id = 'gemini-export-btn-v3'; btn.innerText = "MD"; btn.title = "点击导出 Deep Research"; // 强力样式 - 确保层级最高,位置显眼 Object.assign(btn.style, { position: 'fixed', top: '150px', // 距离顶部 150px,避开顶部栏 right: '20px', // 距离右侧 20px width: '50px', height: '50px', lineHeight: '50px', textAlign: 'center', backgroundColor: '#B22222', // 改为深红色,确保显眼 color: 'white', borderRadius: '50%', fontWeight: 'bold', fontSize: '14px', boxShadow: '0 0 15px rgba(0,0,0,0.5)', // 强阴影 zIndex: '2147483647', // CSS 允许的最大层级 cursor: 'pointer', userSelect: 'none', fontFamily: 'sans-serif', border: '2px solid white' }); // 悬浮变色 btn.onmouseenter = () => btn.style.backgroundColor = '#FF0000'; btn.onmouseleave = () => btn.style.backgroundColor = '#B22222'; // 点击事件 btn.onclick = (e) => { e.stopPropagation(); e.preventDefault(); console.log("👆 点击了导出按钮"); executeExport(); }; // 插入到 body document.body.appendChild(btn); console.log("✅ Gemini Exporter: 按钮已插入页面"); } // --- 3. 监控页面变化 (MutationObserver) --- // 只要 Gemini 刷新页面把按钮删了,这个监听器就会立刻把它加回来 const observer = new MutationObserver((mutations) => { if (!document.getElementById('gemini-export-btn-v3')) { initButton(); } }); // 开始监控整个 body observer.observe(document.body, { childList: true, subtree: true }); // 启动时的双重保险 window.addEventListener('load', initButton); setTimeout(initButton, 1000); setTimeout(initButton, 3000); // ============================================================ // 以下为核心导出逻辑 (保持不变) // ============================================================ let globalCitations = []; const UI_BLACKLIST = ["Export to Sheets", "Export to Gmail", "Show drafts", "Regenerate", "Modify response", "share", "more_vert", "volume_up", "thumb_up", "thumb_down", "google_lens", "Sources", "View other drafts", "expand_more"]; async function executeExport() { globalCitations = []; showToast('⚡️ 正在扫描引用 (多源模式)...', 0); try { const contentNodes = getResponseNodes(); if (!contentNodes.length) throw new Error("页面未就绪"); // --- 阶段 1: 抓取引用 --- for (const node of contentNodes) { await processCitationsInNode(node); } // --- 阶段 1.5: 抓取思考过程 --- let thoughtsMarkdown = ""; if (contentNodes.length > 0) { thoughtsMarkdown = await processThoughts(contentNodes[contentNodes.length - 1], globalCitations.length); } // --- 阶段 2: 文本解析 --- showToast('📝 正在解析文本内容...', 0); let fullMarkdown = ""; contentNodes.forEach((node) => { fullMarkdown += parseNode(node, { listDepth: -1, inTable: false }); fullMarkdown += "\n\n---\n\n"; }); // --- 阶段 3: 清洗与组合 --- fullMarkdown = cleanMarkdown(fullMarkdown); // 添加正文参考文献 if (globalCitations.length > 0) { fullMarkdown += "\n\n## 🔗 正文参考文献 (References)\n\n"; globalCitations.sort((a,b) => a.id - b.id).forEach(cite => { fullMarkdown += `[^${cite.id}]: [${cite.title}](${cite.url})\n\n`; }); } // 添加思考过程 if (thoughtsMarkdown) { fullMarkdown += thoughtsMarkdown; } downloadMD(fullMarkdown); showToast(`✅ 导出成功!引用源: ${globalCitations.length} | 思考过程已处理`, 4000); } catch (e) { console.error(e); showToast(`❌ 错误: ${e.message}`, 5000); } } // --- 思考过程处理 --- async function processThoughts(anchorNode, startCitationIndex) { try { const container = anchorNode.closest('[data-test-id="scroll-container"]') || document.body; const btn = container.querySelector('.collapsible-thinking-button') || Array.from(container.querySelectorAll('div, button')).find(el => el.innerText === "Thoughts" && el.classList.contains('gds-title-m'))?.parentElement; if (!btn) return ""; showToast('🧠 正在提取并格式化思考过程...', 0); let isExpanded = btn.getAttribute('aria-expanded') === 'true'; if (!isExpanded) { btn.click(); await new Promise(r => setTimeout(r, 1200)); } const panel = container.querySelector('.thinking-panel'); if (!panel) return ""; let mdContent = "\n\n---\n\n## 🧠 思考过程 (Thoughts)\n\n"; let thoughtRefs = []; let currentRefId = startCitationIndex; let steps = Array.from(panel.children); steps = steps.filter(s => s.innerText.trim().length > 0 && !s.classList.contains('mat-progress-spinner')); steps.forEach((step, index) => { let clone = step.cloneNode(true); removeTextFromNode(clone, "Researching websites"); removeTextFromNode(clone, "Analysis"); // 提取链接 const links = clone.querySelectorAll('a'); let stepRefIds = []; links.forEach(link => { const url = link.href; if (!url || url.startsWith('javascript')) return; currentRefId++; const title = link.innerText.trim() || "Source"; thoughtRefs.push({ id: currentRefId, title: title, url: url }); stepRefIds.push(currentRefId); link.remove(); // 移除链接节点,防止它留在正文中 }); // 提取标题 let titleText = ""; const titleNode = clone.querySelector('strong, b, h3, .title'); if (titleNode) { titleText = titleNode.innerText.trim(); titleNode.remove(); } else { const fullText = clone.innerText; const splitIdx = fullText.indexOf('\n'); if (splitIdx > 0 && splitIdx < 50) { titleText = fullText.substring(0, splitIdx).trim(); removeTextFromNode(clone, titleText); } } // 解析内容 let bodyMarkdown = parseNode(clone, { listDepth: 0, inTable: false }).trim(); titleText = titleText.replace(/:$/, '').trim(); // --- 修改点:仅加粗 Step 和标题,内容不加粗 --- let stepHeader = ""; if (titleText) { stepHeader = `Step ${index + 1} ${titleText}:`; } else { stepHeader = `Step ${index + 1}:`; } // 组合:**标题** 内容 mdContent += `**${stepHeader}** ${bodyMarkdown}`; // --- 修改点:角标紧跟内容,不换行 --- if (stepRefIds.length > 0) { const refString = stepRefIds.map(id => `[^${id}]`).join(''); mdContent += ` ${refString}\n\n`; } else { mdContent += `\n\n`; } }); // 思考过程的参考文献 if (thoughtRefs.length > 0) { mdContent += "\n## 🔗 思考过程参考文献\n\n"; thoughtRefs.forEach(ref => { mdContent += `[^${ref.id}]: [${ref.title}](${ref.url})\n\n`; }); } if (!isExpanded) { setTimeout(() => { try { btn.click(); } catch(e) {} }, 200); } return mdContent; } catch (err) { console.warn("思考过程提取失败", err); return ""; } } // --- 辅助工具 --- function removeTextFromNode(element, textToRemove) { const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false); let node; while(node = walker.nextNode()) { if (node.nodeValue.includes(textToRemove)) { node.nodeValue = node.nodeValue.replace(textToRemove, ""); } } } async function processCitationsInNode(rootNode) { const container = rootNode.closest('[data-test-id="scroll-container"]') || rootNode; const buttons = Array.from(container.querySelectorAll('button[aria-label="Learn More"]:not([data-citation-scanned="true"])')); if (buttons.length === 0) return; showToast(`🔍 发现 ${buttons.length} 个引用组,正在极速处理...`, 0); for (let i = 0; i < buttons.length; i++) { const btn = buttons[i]; try { btn.click(); const foundLinks = await waitForLinksToAppear(btn); let assignedIds = []; if (foundLinks && foundLinks.length > 0) { for (const link of foundLinks) { const url = link.href; const title = link.innerText.trim() || link.textContent.trim() || url; const existingCite = globalCitations.find(c => c.url === url); let finalId = -1; if (existingCite) { finalId = existingCite.id; } else { finalId = globalCitations.length + 1; globalCitations.push({ id: finalId, title: title, url: url }); } assignedIds.push(finalId); } } else { const finalId = globalCitations.length + 1; globalCitations.push({ id: finalId, title: "Unknown Source", url: "#" }); assignedIds.push(finalId); } btn.setAttribute('data-citation-scanned', 'true'); btn.setAttribute('data-citation-id', assignedIds.join(',')); setTimeout(() => { try { btn.click(); } catch(e) {} }, 50); } catch (err) { console.warn("引用处理异常", err); } await new Promise(r => setTimeout(r, 80)); } } function waitForLinksToAppear(btn) { return new Promise((resolve) => { let links = findLinksNearButton(btn); if (links.length > 0) return resolve(links); const startTime = Date.now(); const intervalId = setInterval(() => { links = findLinksNearButton(btn); if (links.length > 0) { clearInterval(intervalId); resolve(links); } if (Date.now() - startTime > 2500) { clearInterval(intervalId); resolve([]); } }, 50); }); } function findLinksNearButton(btn) { if (btn.tagName === 'A' && btn.href) return [btn]; let parent = btn.parentElement; for (let k = 0; k < 4; k++) { if (!parent) break; const candidates = Array.from(parent.querySelectorAll('a[href]')); const validLinks = []; const seenUrls = new Set(); for (let link of candidates) { if (link.href && !link.href.startsWith('javascript') && !link.href.startsWith('#') && link !== btn && link.offsetParent !== null) { if (!seenUrls.has(link.href)) { seenUrls.add(link.href); validLinks.push(link); } } } if (validLinks.length > 0) return validLinks; parent = parent.parentElement; } return []; } // --- Markdown 解析 --- function parseNode(node, context) { if (!node) return ''; if (node.nodeType === Node.TEXT_NODE) { let text = node.textContent.replace(/\s+/g, ' '); if (context.inTable) return text.replace(/\|/g, '\\|').trim(); return text; } if (node.nodeType === Node.ELEMENT_NODE) { if (shouldSkipNode(node)) return ''; if (node.hasAttribute && node.hasAttribute('data-citation-id')) { const idAttr = node.getAttribute('data-citation-id'); const ids = idAttr.split(','); return ids.map(id => `[^${id}]`).join(''); } const tag = node.tagName.toLowerCase(); const classList = (node.getAttribute('class') || ""); // LaTeX 处理 const isMath = classList.includes('katex') || classList.includes('math-block') || tag === 'math' || tag === 'math-renderer' || node.hasAttribute('data-tex') || node.hasAttribute('data-math') || classList.includes('math-display'); if (isMath) { let latex = extractLatex(node); if (latex) return latex; if (classList.includes('katex-html')) return ''; const label = node.getAttribute('aria-label'); if (label) return `$${label}$`; return node.innerText.trim(); } if (tag === 'table') return parseTable(node); if (tag === 'pre') { const codeDiv = node.querySelector('div[data-language]'); const lang = codeDiv ? codeDiv.getAttribute('data-language') : ''; const codeContent = node.querySelector('code')?.innerText || node.innerText; const cleanCode = codeContent.replace(/Copy code|content_copy/g, '').trim(); return `\n\`\`\`${lang}\n${cleanCode}\n\`\`\`\n`; } if (tag === 'ul' || tag === 'ol') { let inner = ''; for (const child of node.childNodes) inner += parseNode(child, { ...context, listDepth: context.listDepth + 1 }); return `\n${inner}\n`; } if (tag === 'li') { const indent = ' '.repeat(Math.max(0, context.listDepth)); return `\n${indent}- ${parseChildren(node, context).trim()}`; } if (/^h[1-6]$/.test(tag)) return `\n\n${'#'.repeat(parseInt(tag[1]))} ${parseChildren(node, context).trim()}\n\n`; if (tag === 'strong' || tag === 'b') return `**${parseChildren(node, context)}**`; if (tag === 'em' || tag === 'i') return `*${parseChildren(node, context)}*`; if (tag === 'a') { const inner = parseChildren(node, context); const href = node.getAttribute('href'); if (/^\[\d+\]$/.test(inner.trim()) || /^\[\^\d+\]$/.test(inner.trim())) return inner; if (href && !href.startsWith('javascript')) return `[${inner}](${href})`; return inner; } let result = parseChildren(node, context); if ((tag === 'p' || tag === 'div') && result.trim().length > 0) return `\n${result}\n`; return result; } return ''; } function parseChildren(node, context) { let inner = ''; for (const child of node.childNodes) inner += parseNode(child, context); return inner; } function extractLatex(node) { let tex = null; let isDisplay = false; if (node.classList.contains('katex-display') || node.classList.contains('math-block') || node.getAttribute('display') === 'block' || node.tagName === 'DIV' || node.querySelector('.katex-display')) isDisplay = true; if (node.hasAttribute('data-math')) tex = node.getAttribute('data-math'); else { const dm = node.querySelector('[data-math]'); if (dm) tex = dm.getAttribute('data-math'); } if (!tex) { const annotations = node.querySelectorAll('annotation'); for (let ann of annotations) if (ann.getAttribute('encoding') === 'application/x-tex') { tex = ann.textContent; break; } } if (!tex && node.hasAttribute('data-tex')) tex = node.getAttribute('data-tex'); if (!tex) { const dt = node.querySelector('[data-tex]'); if (dt) tex = dt.getAttribute('data-tex'); } if (tex) { tex = tex.trim().replace(/^LaTeX:\s*/i, '').replace(/^\$+|\$+$/g, ''); if (tex.startsWith('<') && tex.includes('>')) return null; if (tex.endsWith('\\')) tex += ' '; if (tex.length > 50 || tex.includes('\\sum') || tex.includes('\\int') || tex.includes('\\frac')) isDisplay = true; return isDisplay ? `\n$$\n${tex}\n$$\n` : `$${tex}$`; } return null; } function shouldSkipNode(node) { const label = (node.getAttribute('aria-label') || "") + (node.className || ""); if (UI_BLACKLIST.some(b => label.includes(b) || node.innerText === b)) return true; return false; } function parseTable(tableNode) { const rows = Array.from(tableNode.querySelectorAll('tr')); if (!rows.length) return ''; let md = '\n'; const matrix = rows.map(r => Array.from(r.querySelectorAll('td, th')).map(c => { return parseChildren(c, { listDepth: -1, inTable: true }).replace(/\s+/g, ' ').replace(/\|/g, '\\|').trim(); })); if (matrix.length) { const headers = matrix[0]; md += `| ${headers.join(' | ')} |\n| ${headers.map(()=>'---').join(' | ')} |\n`; for(let i=1; i 0) toastTimeout = setTimeout(() => { toast.style.opacity = '0'; }, duration); } })();