Skip to content

Instantly share code, notes, and snippets.

@bczhc
Created May 30, 2026 08:23
Show Gist options
  • Select an option

  • Save bczhc/c9406492ee7e24d1d60b276185b65fe2 to your computer and use it in GitHub Desktop.

Select an option

Save bczhc/c9406492ee7e24d1d60b276185b65fe2 to your computer and use it in GitHub Desktop.
Gemini Save to TXT Tampermonkey
// ==UserScript==
// @name Gemini Save to TXT
// @namespace https://github.com/bczhc
// @version 2.0
// @description Extract current Gemini conversation and download as .txt
// @author bczhc
// @match https://gemini.google.com/app/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=gemini.google.com
// @grant GM_registerMenuCommand
// @grant GM_notification
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
// =========================================================================
// Markdown 提取核心
// =========================================================================
function collapseWs(s) {
return s.replace(/\s+/g, ' ').trim();
}
function childrenToMarkdown(node) {
let result = '';
for (let child = node.firstChild; child; child = child.nextSibling) {
result += nodeToMarkdown(child);
}
return result;
}
function nodeToMarkdown(node) {
if (node.nodeType === Node.TEXT_NODE) return node.textContent;
if (node.nodeType !== Node.ELEMENT_NODE) return '';
const tag = node.tagName;
const cls = node.className || '';
if (cls.includes('cdk-visually-hidden') || cls.includes('screen-reader')) return '';
const skipTags = ['RESPONSE-ELEMENT', 'LINK-BLOCK', 'THINKING-OVERLAY',
'STRUCTURED-CONTENT-CONTAINER', 'MESSAGE-CONTENT',
'GEM-ICON-BUTTON', 'GEM-ICON', 'MAT-ICON',
'THUMB-UP-BUTTON', 'THUMB-DOWN-BUTTON', 'REGENERATE-BUTTON',
'COPY-BUTTON', 'MESSAGE-ACTIONS', 'SOURCES-LIST',
'RESPONSE-CONTAINER', 'SENSITIVE-MEMORIES-BANNER'];
if (skipTags.includes(tag)) return childrenToMarkdown(node);
if (tag === 'SPAN' && cls.includes('math-inline')) {
const math = node.getAttribute('data-math') || '';
return math ? '$' + math + '$' : node.textContent;
}
if (tag === 'SPAN' && cls.includes('math-display')) {
const math = node.getAttribute('data-math') || '';
return math ? '\n$$\n' + math + '\n$$\n' : node.textContent;
}
if (tag === 'CODE-BLOCK') {
const langSpan = node.querySelector('.code-block-decoration > span');
const lang = langSpan ? langSpan.textContent.trim() : '';
const codeEl = node.querySelector('code.code-container.formatted');
const code = codeEl ? extractCodeText(codeEl) : node.textContent;
return '\n```' + (lang || '') + '\n' + code.replace(/\n{3,}/g, '\n\n').trim() + '\n```\n';
}
if (/^H[1-6]$/.test(tag)) {
const level = parseInt(tag[1]);
return '\n' + '#'.repeat(level) + ' ' + collapseWs(childrenToMarkdown(node)) + '\n';
}
if (tag === 'P') {
const content = childrenToMarkdown(node).trim();
return content ? '\n' + content + '\n' : '';
}
if (tag === 'B' || tag === 'STRONG') return '**' + childrenToMarkdown(node) + '**';
if (tag === 'I' || tag === 'EM') return '*' + childrenToMarkdown(node) + '*';
if (tag === 'S' || tag === 'DEL' || tag === 'STRIKE') return '~~' + childrenToMarkdown(node) + '~~';
if (tag === 'CODE' && !cls.includes('code-container')) {
if (node.closest('code-block') || node.closest('pre')) return childrenToMarkdown(node);
return '`' + node.textContent + '`';
}
if (tag === 'A') {
const href = node.getAttribute('href') || '';
const text = collapseWs(childrenToMarkdown(node));
if (!text) return '';
if (href.startsWith('/app/') || href.startsWith('https://gemini.google.com/app/')) return text;
return '[' + text + '](' + href + ')';
}
if (tag === 'IMG') {
const alt = node.getAttribute('alt') || '';
const src = node.getAttribute('src') || '';
return '![' + alt + '](' + src + ')';
}
if (tag === 'HR') return '\n---\n';
if (tag === 'BR') return '\n';
if (tag === 'UL') {
const items = [];
const lis = Array.from(node.children).filter(c => c.tagName === 'LI');
for (const li of lis) items.push(processListItem(li, '-', 0));
return '\n' + items.join('') + '\n';
}
if (tag === 'OL') {
const items = [];
const start = parseInt(node.getAttribute('start')) || 1;
const lis = Array.from(node.children).filter(c => c.tagName === 'LI');
lis.forEach((li, i) => items.push(processListItem(li, (start + i) + '.', 0)));
return '\n' + items.join('') + '\n';
}
if (tag === 'BLOCKQUOTE') {
const content = childrenToMarkdown(node).trim();
const lines = content.split('\n').filter(l => l.trim());
return '\n' + lines.map(l => '> ' + l).join('\n') + '\n';
}
if (tag === 'TABLE') return '\n' + tableToMarkdown(node) + '\n';
if (tag === 'DIV' && cls.includes('horizontal-scroll-wrapper')) return childrenToMarkdown(node);
if (tag === 'DIV' && cls.includes('markdown-main-panel')) return childrenToMarkdown(node);
if (tag === 'PRE' && !node.closest('code-block')) {
const code = node.querySelector('code');
const text = code ? code.textContent : node.textContent;
return '\n```\n' + text.trim() + '\n```\n';
}
return childrenToMarkdown(node);
}
function extractCodeText(codeEl) {
const lines = [];
let currentLine = '';
function walk(n) {
if (n.nodeType === Node.TEXT_NODE) {
currentLine += n.textContent;
} else if (n.nodeType === Node.ELEMENT_NODE && n.tagName === 'BR') {
lines.push(currentLine);
currentLine = '';
} else if (n.nodeType === Node.ELEMENT_NODE) {
for (let c = n.firstChild; c; c = c.nextSibling) walk(c);
}
}
walk(codeEl);
if (currentLine) lines.push(currentLine);
return lines.join('\n');
}
function processListItem(li, prefix, depth) {
const indent = ' '.repeat(depth);
let text = '';
let nested = '';
for (let c = li.firstChild; c; c = c.nextSibling) {
if (c.nodeType === Node.ELEMENT_NODE && (c.tagName === 'UL' || c.tagName === 'OL')) {
const subItems = [];
const subLis = Array.from(c.children).filter(ch => ch.tagName === 'LI');
if (c.tagName === 'UL') {
subLis.forEach(sub => subItems.push(processListItem(sub, '-', depth + 1)));
} else {
const start = parseInt(c.getAttribute('start')) || 1;
subLis.forEach((sub, i) => subItems.push(processListItem(sub, (start + i) + '.', depth + 1)));
}
nested = subItems.join('');
} else {
text += nodeToMarkdown(c);
}
}
return indent + prefix + ' ' + collapseWs(text) + '\n' + nested;
}
function tableToMarkdown(table) {
const rows = [];
const thead = table.querySelector('thead');
if (thead) thead.querySelectorAll('tr').forEach(tr => rows.push(getRowCells(tr)));
const tbody = table.querySelector('tbody');
if (tbody) tbody.querySelectorAll('tr').forEach(tr => rows.push(getRowCells(tr)));
if (rows.length === 0) table.querySelectorAll('tr').forEach(tr => rows.push(getRowCells(tr)));
if (rows.length < 2) return '';
const maxCols = Math.max(...rows.map(r => r.length));
rows.forEach(r => { while (r.length < maxCols) r.push(''); });
const lines = [];
lines.push('| ' + rows[0].map(c => collapseWs(c)).join(' | ') + ' |');
lines.push('| ' + rows[0].map(() => '---').join(' | ') + ' |');
for (let i = 1; i < rows.length; i++) {
lines.push('| ' + rows[i].map(c => collapseWs(c)).join(' | ') + ' |');
}
return lines.join('\n');
}
function getRowCells(tr) {
return Array.from(tr.querySelectorAll('td, th')).map(cell => cell.textContent.trim());
}
function cleanMarkdown(md) {
md = md.replace(/\n{4,}/g, '\n\n\n').trim();
return md.split('\n').map(l => l.trimEnd()).join('\n');
}
function getCurrentTitle() {
const current = document.querySelector('[aria-current]');
if (current) {
const txt = current.textContent.trim();
if (txt && txt.length > 0 && txt.length < 200) return txt;
}
const t = document.title || '';
return t.replace(/\s*[-|]\s*Google\s*Gemini\s*$/i, '').trim() || 'Gemini Chat';
}
// =========================================================================
// 提取 & 下载
// =========================================================================
function extractAndDownload() {
const chatHistory = document.querySelector('infinite-scroller[data-test-id="chat-history-container"]');
if (!chatHistory) {
alert('[Gemini Save] 未找到聊天历史。');
return;
}
const turns = chatHistory.querySelectorAll('.conversation-container');
if (turns.length === 0) {
alert('[Gemini Save] 未找到任何对话。');
return;
}
const outputParts = [];
turns.forEach(turn => {
const queryText = turn.querySelector('.query-text');
if (queryText) {
const lines = queryText.querySelectorAll('p.query-text-line');
const userText = Array.from(lines)
.map(p => {
if (p.querySelector('br') && !p.textContent.trim()) return '';
return p.textContent.trim();
})
.join('\n');
if (userText) outputParts.push('[User]\n' + userText);
}
const responseContainers = turn.querySelectorAll('response-container');
const allDrafts = [];
responseContainers.forEach(rc => {
const containerDiv = rc.querySelector('structured-content-container > div.container');
if (!containerDiv) return;
const mdParts = [];
for (const child of containerDiv.children) {
if (child.tagName === 'MESSAGE-CONTENT') {
const md = nodeToMarkdown(child);
if (md.trim()) mdParts.push(md);
} else if (child.tagName === 'CODE-BLOCK') {
const md = nodeToMarkdown(child);
if (md.trim()) mdParts.push(md);
}
}
const draftText = cleanMarkdown(mdParts.join('').trim());
if (draftText) allDrafts.push(draftText);
});
if (allDrafts.length > 1) {
allDrafts.forEach((draft, di) => {
outputParts.push('[Gemini#' + (di + 1) + ']\n' + draft);
});
} else if (allDrafts.length === 1) {
outputParts.push('[Gemini]\n' + allDrafts[0]);
}
});
const title = getCurrentTitle();
const fullText = '# ' + title + '\n\n' + outputParts.join('\n\n---\n\n');
const blob = new Blob([fullText], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = title.replace(/[/\\?%*:|"<>]/g, '_') + '.txt';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
// =========================================================================
// 注册到 Tampermonkey 菜单
// =========================================================================
GM_registerMenuCommand('Save to txt \u{1F4BE}', extractAndDownload);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment