Gemini to Notion Exporter

Gemini 导出:智能图片去水印后归位 (支持 PicList/PicGo)+隐私开关+单个对话导出+多代码块列表修复

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Gemini to Notion Exporter
// @namespace    http://tampermonkey.net/
// @version      13.4
// @license      MIT
// @description  Gemini 导出:智能图片去水印后归位 (支持 PicList/PicGo)+隐私开关+单个对话导出+多代码块列表修复
// @author       Wyih with Gemini Thought Partner
// @match        https://gemini.google.com/*
// @connect      api.notion.com
// @connect      127.0.0.1
// @connect      *
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        GM_addStyle
// ==/UserScript==

(function () {
    'use strict';

    // --- 基础配置 ---
    const PICLIST_URL = "http://127.0.0.1:36677/upload";
    const ASSET_PLACEHOLDER_PREFIX = "PICLIST_WAITING::";
    const MAX_TEXT_LENGTH = 2000;
    // =========================================================================
    // 🧱 资源处理与去水印增强版 (内嵌资源版)
    // =========================================================================

    // --- 1. 内嵌遮罩资源 (Base64) ---
    const MASK_48_DATA = "data:image/png;base64,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";    
    const MASK_96_DATA = "data:image/png;base64,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";
    
    let maskCache = { 48: null, 96: null };

    // --- 2. 加载遮罩 (从 Base64 读取) ---
    function loadMask(size) {
        return new Promise((resolve, reject) => {
            if (maskCache[size]) return resolve(maskCache[size]);
            
            // 直接读取常量,不再发起网络请求
            const dataUri = size === 96 ? MASK_96_DATA : MASK_48_DATA;

            // fetch 支持 data: 协议,可以直接把 Base64 转为 Blob
            fetch(dataUri)
                .then(res => res.blob())
                .then(blob => createImageBitmap(blob))
                .then(bmp => {
                    const canvas = document.createElement('canvas');
                    canvas.width = size; canvas.height = size;
                    const ctx = canvas.getContext('2d');
                    ctx.drawImage(bmp, 0, 0);
                    
                    const data = ctx.getImageData(0, 0, size, size).data;
                    // 预处理 Alpha Map
                    const alphaMap = new Float32Array(data.length / 4);
                    for (let i = 0; i < alphaMap.length; i++) {
                        const r = data[i * 4];
                        const g = data[i * 4 + 1];
                        const b = data[i * 4 + 2];
                        alphaMap[i] = Math.max(r, g, b) / 255.0;
                    }
                    maskCache[size] = alphaMap;
                    resolve(alphaMap);
                })
                .catch(e => {
                    console.error("Mask Load Error:", e);
                    reject(e);
                });
        });
    }

    // ⚡️ 极速版:不再进行像素安全检查,默认传入的都是 Gemini 图片
    async function cleanGeminiWatermark(bufferObj) {
        if (!bufferObj.type || !bufferObj.type.startsWith('image/')) return bufferObj;
        try {
            const blob = new Blob([bufferObj.buffer], { type: bufferObj.type });
            const bitmap = await createImageBitmap(blob);
            const { width, height } = bitmap;

            if (width < 100 || height < 100) return bufferObj; // 太小的不可能有水印

            // 判断规格
            const isLarge = width > 1024 && height > 1024;
            const maskSize = isLarge ? 96 : 48;
            const margin = isLarge ? 64 : 32;

            const alphaMap = await loadMask(maskSize);
            const canvas = document.createElement('canvas');
            canvas.width = width; canvas.height = height;
            const ctx = canvas.getContext('2d');
            ctx.drawImage(bitmap, 0, 0);

            const startX = width - maskSize - margin;
            const startY = height - maskSize - margin;
            if (startX < 0 || startY < 0) return bufferObj;

            const imageData = ctx.getImageData(startX, startY, maskSize, maskSize);
            const pixels = imageData.data;
            const LOGO_VALUE = 255;
            const MAX_ALPHA = 0.99;
            const ALPHA_THRESHOLD = 0.002;

            // ⚠️ Fix: 严格对齐参考脚本的反向混合逻辑
            for (let i = 0; i < alphaMap.length; i++) {
                let alpha = alphaMap[i];
                if (alpha < ALPHA_THRESHOLD) continue;
                
                alpha = Math.min(alpha, MAX_ALPHA);
                const oneMinusAlpha = 1 - alpha;
                const idx = i * 4;

                for (let c = 0; c < 3; c++) {
                    const watermarked = pixels[idx + c];
                    const original = (watermarked - alpha * LOGO_VALUE) / oneMinusAlpha;
                    // 限制范围 + 四舍五入 (Math.round 也是参考脚本的关键点)
                    pixels[idx + c] = Math.max(0, Math.min(255, Math.round(original)));
                }
            }
            ctx.putImageData(imageData, startX, startY);
            
            const newBlob = await new Promise(r => canvas.toBlob(r, bufferObj.type));
            return { buffer: await newBlob.arrayBuffer(), type: bufferObj.type };
        } catch (e) {
            console.error("Clean error:", e);
            return bufferObj;
        }
    }

    // ------------------- 0. 环境自检 -------------------
    function checkPicListConnection() {
        GM_xmlhttpRequest({
            method: "GET", url: "http://127.0.0.1:36677/heartbeat", timeout: 2000,
            onload: (res) => { if (res.status === 200) console.log("✅ PicList 连接正常"); },
            onerror: () => console.error("❌ 无法连接到 PicList")
        });
    }
    setTimeout(checkPicListConnection, 3000);

    // ------------------- 1. 配置管理 -------------------
    function getConfig() { return { token: GM_getValue('notion_token', ''), dbId: GM_getValue('notion_db_id', '') }; }
    function promptConfig() {
        const token = prompt('请输入 Notion Integration Secret:', GM_getValue('notion_token', ''));
        if (token) {
            const dbId = prompt('请输入 Notion Database ID:', GM_getValue('notion_db_id', ''));
            if (dbId) { GM_setValue('notion_token', token); GM_setValue('notion_db_id', dbId); alert('配置已保存'); }
        }
    }
    GM_registerMenuCommand("⚙️ 设置 Notion Token", promptConfig);

    // ------------------- 2. UI 样式 (全员 Sticky 版) -------------------
    GM_addStyle(`
        /* 全量导出按钮 */
        #gemini-saver-btn {
            position: fixed; bottom: 20px; right: 20px; z-index: 9999;
            background-color: #0066CC; color: white; border: none; border-radius: 6px;
            padding: 10px 16px; cursor: pointer; box-shadow: 0 4px 12px rgba(0,0,0,0.15);
            font-family: sans-serif; font-weight: 600; font-size: 14px; transition: all 0.2s;
        }
        #gemini-saver-btn:hover { background-color: #0052a3; transform: translateY(-2px); }
        #gemini-saver-btn.loading { background-color: #666; cursor: wait; }

        /* --- 视觉边界 --- */
        user-query:hover, model-response:hover {
            box-shadow: 0 0 0 2px rgba(66, 133, 244, 0.3);
            border-radius: 8px;
            background-color: rgba(66, 133, 244, 0.02);
        }

        /* --- 工具栏基础样式 --- */
        .gemini-tool-group {
            z-index: 9500;
            display: flex; gap: 6px;
            opacity: 0;
            transition: opacity 0.2s ease-in-out;
            background: white;
            padding: 4px 6px; border-radius: 20px;
            box-shadow: 0 2px 5px rgba(0,0,0,0.15);
            border: 1px solid #e0e0e0;
        }
        user-query:hover .gemini-tool-group, model-response:hover .gemini-tool-group { opacity: 1; }
        .gemini-tool-group:has(.gemini-privacy-toggle[data-skip="true"]) { opacity: 1 !important; border-color: #fce8e6; background: #fff8f8; }

        /* =============================================
           🔥 全员 Sticky:双轨制解决方案
           ============================================= */

        /* 方案 A: AI 回复 (Model) - Block 布局 */
        model-response .gemini-tool-group {
            position: sticky;
            top: 14px;
            float: right;
            margin-left: 10px;
            margin-bottom: 10px;
        }

        /* 方案 B: 用户提问 (User) - Flex 布局 */
        user-query .gemini-tool-group {
            position: sticky;
            top: 14px;
            align-self: flex-start;
            margin-left: auto;
            margin-right: 10px;
            order: 100;
        }

        /* 图标样式 */
        .gemini-icon-btn {
            cursor: pointer; font-size: 16px; line-height: 24px; user-select: none;
            width: 26px; height: 26px; text-align: center;
            border-radius: 50%; transition: background 0.2s;
            display: flex; align-items: center; justify-content: center; color: #555;
        }
        .gemini-icon-btn:hover { background: rgba(0,0,0,0.08); color: #000; }
        .gemini-privacy-toggle[data-skip="true"] { color: #d93025; background: #fce8e6; }

        @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
        .gemini-icon-btn.processing { cursor: wait; color: #1a73e8; background: #e8f0fe; }
        .gemini-icon-btn.processing span { display: block; animation: spin 1s linear infinite; }
        .gemini-icon-btn.success { color: #188038 !important; background: #e6f4ea; }
        .gemini-icon-btn.error { color: #d93025 !important; background: #fce8e6; }
    `);

    // ------------------- 3. UI 注入 -------------------
    function injectPageControls() {
        const bubbles = document.querySelectorAll('user-query, model-response');
        bubbles.forEach(bubble => {
            if (bubble.querySelector('.gemini-tool-group')) return;

            if (getComputedStyle(bubble).position === 'static') bubble.style.position = 'relative';

            const group = document.createElement('div');
            group.className = 'gemini-tool-group';

            // --- 隐私按钮 ---
            const privacyBtn = document.createElement('div');
            privacyBtn.className = 'gemini-icon-btn gemini-privacy-toggle';
            privacyBtn.title = "点击切换:是否导出此条内容";
            privacyBtn.setAttribute('data-skip', 'false');

            const privacyIcon = document.createElement('span');
            privacyIcon.textContent = '👁️';
            privacyBtn.appendChild(privacyIcon);

            privacyBtn.onclick = (e) => {
                e.stopPropagation();
                const isSkipping = privacyBtn.getAttribute('data-skip') === 'true';
                if (isSkipping) {
                    privacyBtn.setAttribute('data-skip', 'false'); privacyIcon.textContent = '👁️'; bubble.setAttribute('data-privacy-skip', 'false');
                } else {
                    privacyBtn.setAttribute('data-skip', 'true'); privacyIcon.textContent = '🚫'; bubble.setAttribute('data-privacy-skip', 'true');
                }
            };

            // --- 单条导出按钮 ---
            const singleExportBtn = document.createElement('div');
            singleExportBtn.className = 'gemini-icon-btn';
            singleExportBtn.title = "仅导出此条对话";

            const exportIcon = document.createElement('span');
            exportIcon.textContent = '📤';
            singleExportBtn.appendChild(exportIcon);

            singleExportBtn.onclick = (e) => {
                e.stopPropagation();
                handleSingleExport(bubble, singleExportBtn, exportIcon);
            };
            group.appendChild(privacyBtn);
            group.appendChild(singleExportBtn);

            if (bubble.tagName.toLowerCase() === 'user-query') {
                bubble.appendChild(group);
            } else {
                bubble.prepend(group);
            }
        });
    }

    // ------------------- 4. 资源处理 -------------------
    function convertBlobImageToBuffer(blobUrl) {
        return new Promise((resolve, reject) => {
            const img = document.querySelector(`img[src="${blobUrl}"]`);
            if (!img || !img.complete || img.naturalWidth === 0) return reject("图片加载失败");
            try {
                const canvas = document.createElement('canvas');
                canvas.width = img.naturalWidth; canvas.height = img.naturalHeight;
                canvas.getContext('2d').drawImage(img, 0, 0);
                canvas.toBlob(b => b ? b.arrayBuffer().then(buf => resolve({ buffer: buf, type: b.type })) : reject("Canvas失败"), 'image/png');
            } catch (e) { reject(e.message); }
        });
    }

    function fetchAssetAsArrayBuffer(url) {
        return new Promise((resolve, reject) => {
            if (url.startsWith('blob:')) {
                convertBlobImageToBuffer(url).then(resolve).catch(() => {
                    GM_xmlhttpRequest({ method: "GET", url, responseType: 'arraybuffer', onload: r => r.status === 200 ? resolve({ buffer: r.response, type: 'application/octet-stream' }) : reject() });
                });
            } else {
                GM_xmlhttpRequest({
                    method: "GET", url, responseType: 'arraybuffer',
                    onload: r => {
                        if (r.status === 200) {
                            const m = r.responseHeaders.match(/content-type:\s*(.*)/i);
                            resolve({ buffer: r.response, type: m ? m[1] : undefined });
                        } else reject();
                    }
                });
            }
        });
    }

    function uploadToPicList(arrayBufferObj, filename) {
        return new Promise((resolve, reject) => {
            if (!arrayBufferObj.buffer) return reject("空文件");
            let finalFilename = filename.split('?')[0];
            const mime = (arrayBufferObj.type || '').split(';')[0].trim().toLowerCase();
            if (!finalFilename.includes('.') || finalFilename.length - finalFilename.lastIndexOf('.') > 6) {
                const mimeMap = {
                    'application/pdf': '.pdf',
                    'application/msword': '.doc',
                    'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx',
                    'image/png': '.png',
                    'image/jpeg': '.jpg',
                    'image/webp': '.webp'
                };
                if (mimeMap[mime]) finalFilename += mimeMap[mime];
            }
            const boundary = "----GeminiSaverBoundary" + Math.random().toString(36).substring(2);
            const preData =
                `--${boundary}\r\n` +
                `Content-Disposition: form-data; name="file"; filename="${finalFilename.replace(/"/g, '')}"\r\n` +
                `Content-Type: ${mime || 'application/octet-stream'}\r\n\r\n`;
            const combinedBlob = new Blob([preData, arrayBufferObj.buffer, `\r\n--${boundary}--\r\n`]);

            GM_xmlhttpRequest({
                method: "POST", url: PICLIST_URL,
                headers: { "Content-Type": `multipart/form-data; boundary=${boundary}` },
                data: combinedBlob,
                onload: (res) => {
                    try {
                        const r = JSON.parse(res.responseText);
                        r.success ? resolve(r.result[0]) : reject(r.message);
                    } catch (e) { reject(e.message); }
                },
                onerror: () => reject("网络错误")
            });
        });
    }

    async function processAssets(blocks, statusCallback) {
        const tasks = []; 
        const map = new Map();

        blocks.forEach((b, i) => {
            let urlObj = null;
            if (b.type === 'image' && b.image?.external?.url?.startsWith(ASSET_PLACEHOLDER_PREFIX)) {
                urlObj = b.image.external;
            } else if (b.type === 'file' && b.file?.external?.url?.startsWith(ASSET_PLACEHOLDER_PREFIX)) {
                urlObj = b.file.external;
            }

            if (urlObj) {
                const parts = urlObj.url.split('::');
                let isGeminiImg = false, name, realUrl;

                if (parts[1].startsWith('G=')) {
                    isGeminiImg = parts[1] === 'G=1';
                    name = parts[2];
                    realUrl = parts.slice(3).join('::'); 
                } else {
                    name = parts[1];
                    realUrl = parts.slice(2).join('::');
                }

                if (realUrl.startsWith('blob:') && b.type === 'file') {
                    b.type = "paragraph";
                    b.paragraph = { rich_text: [{ type: "text", text: { content: `📄 [本地文件未上传] ${name}` }, annotations: { color: "gray", italic: true } }] };
                    delete b.file; 
                    return;
                }


                let downloadUrl = realUrl;
                if (isGeminiImg && downloadUrl.match(/=s\d+/)) {
                    downloadUrl = downloadUrl.replace(/=s\d+(?:-c)?/g, '=s0');
                }


                // 使用替换后的 downloadUrl 来获取图片
                const task = fetchAssetAsArrayBuffer(downloadUrl)
                    .then(async (bufObj) => {
                        if (isGeminiImg && (b.type === 'image' || /\.(png|jpg|webp)$/i.test(name))) {
                            if (statusCallback) statusCallback(`🚿 Cleaning Gemini Img...`);
                            return await cleanGeminiWatermark(bufObj);
                        }
                        return bufObj;
                    })
                    .then(bufObj => {
                        return uploadToPicList(bufObj, name);
                    })
                    .then(u => ({ i, url: u, name, ok: true }))
                    .catch(e => ({ i, err: e, name, ok: false }));

                tasks.push(task); 
                map.set(i, b);
            }
        });

        if (tasks.length) {
            statusCallback(`⏳ Processing ${tasks.length} assets...`);
            const res = await Promise.all(tasks);
            
            res.forEach(r => {
                const blk = map.get(r.i);
                if (r.ok) {
                    if (blk.type === 'image') {
                        blk.image.external.url = r.url;
                    } else {
                        blk.file.external.url = r.url;
                        blk.file.name = r.name || "File";
                    }
                } else {
                    console.error(`Upload Fail: ${r.name}`, r.err);
                    blk.type = "paragraph";
                    blk.paragraph = {
                        rich_text: [{
                            type: "text",
                            text: { content: `⚠️ Upload Failed: ${r.name}` },
                            annotations: { color: "red" }
                        }]
                    };
                    delete blk.file; 
                    delete blk.image;
                }
            });
        }
        return blocks;
    }



    // ------------------- 5. DOM 解析 -------------------
    const NOTION_LANGUAGES = new Set([
        "bash", "c", "c++", "css", "go", "html", "java", "javascript",
        "json", "kotlin", "markdown", "php", "python", "ruby", "rust",
        "shell", "sql", "swift", "typescript", "yaml", "r", "plain text"
    ]);

    function mapLanguageToNotion(lang) {
        if (!lang) return "plain text";
        lang = lang.toLowerCase().trim();
        if (lang === "js") return "javascript";
        if (lang === "py") return "python";
        if (NOTION_LANGUAGES.has(lang)) return lang;
        return "plain text";
    }

    function detectLanguageRecursive(preNode) {
        let c = preNode;
        for (let i = 0; i < 3; i++) {
            if (!c) break;
            const h = c.previousElementSibling;
            if (h && NOTION_LANGUAGES.has(h.innerText.toLowerCase())) {
                return mapLanguageToNotion(h.innerText);
            }
            c = c.parentElement;
        }
        const code = preNode.querySelector('code');
        const m = code && code.className.match(/language-([\w-]+)/);
        return m ? mapLanguageToNotion(m[1]) : "plain text";
    }

    function splitCodeSafe(code) {
        const chunks = [];
        let remaining = code;
        while (remaining.length > 0) {
            if (remaining.length <= MAX_TEXT_LENGTH) {
                chunks.push(remaining);
                break;
            }
            let splitIndex = remaining.lastIndexOf('\n', MAX_TEXT_LENGTH - 1);
            if (splitIndex === -1) {
                splitIndex = MAX_TEXT_LENGTH;
            } else {
                splitIndex += 1;
            }
            chunks.push(remaining.slice(0, splitIndex));
            remaining = remaining.slice(splitIndex);
        }
        return chunks;
    }

    function parseInlineNodes(nodes) {
        const rt = [];
        function tr(n, s = {}) {
            // 文本节点
            if (n.nodeType === 3) {
                const fullText = n.textContent;
                if (!fullText) return;
                for (let i = 0; i < fullText.length; i += MAX_TEXT_LENGTH) {
                    rt.push({
                        type: "text",
                        text: { content: fullText.slice(i, i + MAX_TEXT_LENGTH), link: s.link },
                        annotations: {
                            bold: !!s.bold,
                            italic: !!s.italic,
                            code: !!s.code,
                            color: "default"
                        }
                    });
                }
            }
            // 元素节点
            else if (n.nodeType === 1) {
                // 行内公式:data-latex-source / data-math
                const latex = n.getAttribute('data-latex-source') || n.getAttribute('data-math');
                if (latex) {
                    rt.push({
                        type: "equation",
                        equation: { expression: latex.trim() }
                    });
                    return;
                }

                const ns = { ...s };
                if (['B', 'STRONG'].includes(n.tagName)) ns.bold = true;
                if (['I', 'EM'].includes(n.tagName)) ns.italic = true;
                if (n.tagName === 'CODE') ns.code = true;
                if (n.tagName === 'A') ns.link = { url: n.href };

                n.childNodes.forEach(c => tr(c, ns));
            }
        }
        nodes.forEach(n => tr(n));
        return rt;
    }

    // ------------------- 6. 核心:块级解析(包含 UL/OL 修复版) -------------------
    // ⚠️ 修改了函数签名,增加 isGeminiSource 参数
    function processNodesToBlocks(nodes, isGeminiSource = false) {
        const blocks = [], buf = [];
        const flush = () => {
            if (buf.length) {
                const rt = parseInlineNodes(buf);
                if (rt.length) blocks.push({ object: "block", type: "paragraph", paragraph: { rich_text: rt } });
                buf.length = 0;
            }
        };
        const fileExtRegex = /\.(pdf|zip|docx?|xlsx?|pptx?|csv|txt|md|html?|rar|7z|tar|gz|iso|exe|apk|dmg|json|xml|epub|R|Rmd|qmd)(\?|$)/i;

        Array.from(nodes).forEach(n => {
            if (['SCRIPT', 'STYLE', 'SVG'].includes(n.nodeName)) return;
            const isElement = n.nodeType === 1;

            // --- 公式块处理 (略,保持原样) ---
            if (isElement) {
                const isMathTag = n.hasAttribute('data-math') || n.hasAttribute('data-latex-source');
                const isBlockLayout = n.tagName === 'DIV' || n.classList.contains('math-block') || n.classList.contains('katex-display');
                if (isMathTag && isBlockLayout) {
                    const latex = n.getAttribute('data-latex-source') || n.getAttribute('data-math');
                    if (latex) {
                        flush();
                        blocks.push({ object: "block", type: "equation", equation: { expression: latex.trim() } });
                        return;
                    }
                }
            }

            // --- 行内元素缓冲 ---
            if (n.nodeType === 3 || ['B', 'I', 'CODE', 'SPAN', 'A', 'STRONG', 'EM', 'MAT-ICON'].includes(n.nodeName)) {
                // 文件下载链接处理
                if (n.nodeName === 'A' && isElement && (n.hasAttribute('download') || n.href.includes('blob:') || fileExtRegex.test(n.href))) {
                    flush();
                    const fn = (n.innerText || 'file').trim();
                    // 标记:这里通常是文件,暂不打 Gemini 标记或设为 0
                    blocks.push({
                        object: "block", type: "file",
                        file: { type: "external", name: fn.slice(0, 60), external: { url: `${ASSET_PLACEHOLDER_PREFIX}G=0::${fn}::${n.href}` } }
                    });
                    return;
                }
                buf.push(n);
                return;
            }

            if (isElement) {
                flush();
                const t = n.tagName;
                if (t === 'P') {
                    // 递归传递 isGeminiSource
                    blocks.push(...processNodesToBlocks(n.childNodes, isGeminiSource));
                }
                else if (t === 'IMG' && !n.className.includes('avatar')) {
                    // 🔥 关键修改:写入来源标记 G=1 (是Gemini) 或 G=0 (不是)
                    const flag = isGeminiSource ? "1" : "0";
                    blocks.push({
                        object: "block", type: "image",
                        image: { type: "external", external: { url: `${ASSET_PLACEHOLDER_PREFIX}G=${flag}::image.png::${n.src}` } }
                    });
                }
                else if (t === 'PRE') { /* ...代码块处理保持原样... */
                    const fullCode = n.textContent;
                    const lang = detectLanguageRecursive(n);
                    const rawChunks = splitCodeSafe(fullCode);
                    const codeRichText = rawChunks.map(chunk => ({ type: "text", text: { content: chunk } }));
                    blocks.push({ object: "block", type: "code", code: { rich_text: codeRichText, language: lang } });
                }
                else if (/^H[1-6]$/.test(t)) { /* ...标题处理保持原样... */
                    const level = t[1] < 4 ? t[1] : 3;
                    blocks.push({ object: "block", type: `heading_${level}`, [`heading_${level}`]: { rich_text: parseInlineNodes(n.childNodes) } });
                }
                else if (t === 'BLOCKQUOTE') {
                    blocks.push({ object: "block", type: "quote", quote: { rich_text: parseInlineNodes(n.childNodes) } });
                }
                else if (t === 'UL' || t === 'OL') {
                    const tp = t === 'UL' ? 'bulleted_list_item' : 'numbered_list_item';
                    Array.from(n.children).forEach(li => {
                        if (li.tagName !== 'LI') return;
                        // 递归传递 isGeminiSource
                        const liBlocks = processNodesToBlocks(li.childNodes, isGeminiSource); 
                        if (!liBlocks.length) return;
                        let richText, children = [];
                        const first = liBlocks[0];
                        if (first.type === 'paragraph' && first.paragraph?.rich_text?.length) {
                            richText = first.paragraph.rich_text;
                            children = liBlocks.slice(1);
                        } else {
                            richText = parseInlineNodes(li.childNodes);
                            children = liBlocks;
                        }
                        const listBlock = { object: "block", type: tp, [tp]: { rich_text: richText } };
                        if (children.length) listBlock[tp].children = children;
                        blocks.push(listBlock);
                    });
                }
                else if (t === 'TABLE') { /* ...表格处理保持原样... */
                    const rows = Array.from(n.querySelectorAll('tr'));
                    if (rows.length) {
                        const tb = { object: "block", type: "table", table: { table_width: 1, children: [] } };
                        let max = 0;
                        rows.forEach(r => {
                            const cs = Array.from(r.querySelectorAll('td,th'));
                            max = Math.max(max, cs.length);
                            tb.table.children.push({ object: "block", type: "table_row", table_row: { cells: cs.map(c => parseInlineNodes(c.childNodes)) } });
                        });
                        tb.table.table_width = max;
                        blocks.push(tb);
                    }
                }
                else {
                    // 递归默认
                    blocks.push(...processNodesToBlocks(n.childNodes, isGeminiSource));
                }
            }
        });
        flush();
        return blocks;
    }


    // ------------------- 7. 抓取逻辑 -------------------
    function buildUploadedImageMap() {
        const map = new Map();
        const imgs = document.querySelectorAll('img[data-test-id="uploaded-img"], img.preview-image');
        const bubbles = Array.from(document.querySelectorAll('user-query'));
        imgs.forEach(img => {
            let p = img.parentElement;
            while (p && p !== document.body) {
                if (p.tagName === 'USER-QUERY' || p.querySelector('user-query')) break;
                p = p.parentElement;
            }
            const owner = p && (p.tagName === 'USER-QUERY' ? p : p.querySelector('user-query')) || bubbles[bubbles.length - 1];
            if (owner) {
                if (!map.has(owner)) map.set(owner, []);
                map.get(owner).push(img);
            }
        });
        return map;
    }

    function getChatBlocks(targetBubbles = null) {
        const allBubbles = document.querySelectorAll('user-query, model-response');
        const bubblesToProcess = targetBubbles || Array.from(allBubbles);
        const children = [];
        const uploadMap = buildUploadedImageMap();

        if (bubblesToProcess.length > 0) {
            bubblesToProcess.forEach(bubble => {
                const isUser = bubble.tagName.toLowerCase() === 'user-query';
                const role = isUser ? "User" : "Gemini";
                
                // 🔥 关键标识:如果是 Gemini,则为 True
                const isGeminiSource = !isUser; 

                if (bubble.getAttribute('data-privacy-skip') === 'true') {
                    /* ... 隐私跳过逻辑保持原样 ... */
                    children.push({ object: "block", type: "callout", callout: { rich_text: [{ type: "text", text: { content: `🚫 此 ${role} 内容已标记为隐私,未导出。` }, annotations: { color: "gray", italic: true } }], icon: { emoji: "🔒" }, color: "gray_background" } });
                    return;
                }

                children.push({ object: "block", type: "heading_3", heading_3: { rich_text: [{ type: "text", text: { content: role } }], color: isUser ? "default" : "blue_background" } });

                const clone = bubble.cloneNode(true);
                ['.gemini-tool-group', 'mat-icon', '.response-footer', '.message-actions'].forEach(s => clone.querySelectorAll(s).forEach(e => e.remove()));

                if (isUser && uploadMap.has(bubble)) {
                    const d = document.createElement("div");
                    uploadMap.get(bubble).forEach(img => d.appendChild(img.cloneNode(true)));
                    clone.appendChild(d);
                }

                // 🔥 传入 isGeminiSource
                children.push(...processNodesToBlocks(clone.childNodes, isGeminiSource));
                children.push({ object: "block", type: "divider", divider: {} });
            });
        }
        return children;
    }


    // ------------------- 8. Notion 上传 -------------------
    function getChatTitle(specificBubble = null) {
        if (specificBubble)
            return specificBubble.innerText.replace(/\n/g, ' ').slice(0, 50) + "...";
        const q = document.querySelector('user-query');
        return q ? q.innerText.replace(/\n/g, ' ').slice(0, 60) : "Gemini Chat";
    }

    function appendBlocksBatch(pageId, blocks, token, statusCallback) {
        if (!blocks.length) {
            statusCallback('✅ Saved!');
            setTimeout(() => statusCallback(null), 3000);
            return;
        }
        GM_xmlhttpRequest({
            method: "PATCH",
            url: `https://api.notion.com/v1/blocks/${pageId}/children`,
            headers: {
                "Authorization": `Bearer ${token}`,
                "Content-Type": "application/json",
                "Notion-Version": "2022-06-28"
            },
            data: JSON.stringify({ children: blocks.slice(0, 90) }),
            onload: (res) => {
                if (res.status === 200) {
                    appendBlocksBatch(pageId, blocks.slice(90), token, statusCallback);
                } else {
                    console.error(res.responseText);
                    statusCallback('❌ Fail');
                }
            }
        });
    }

    function createPageAndUpload(title, blocks, token, dbId, statusCallback) {
        GM_xmlhttpRequest({
            method: "POST",
            url: "https://api.notion.com/v1/pages",
            headers: {
                "Authorization": `Bearer ${token}`,
                "Content-Type": "application/json",
                "Notion-Version": "2022-06-28"
            },
            data: JSON.stringify({
                parent: { database_id: dbId },
                properties: {
                    "Name": { title: [{ text: { content: title } }] },
                    "Date": { date: { start: new Date().toISOString() } },
                    "URL": { url: location.href }
                },
                children: blocks.slice(0, 90)
            }),
            onload: (res) => {
                if (res.status === 200) {
                    const pageId = JSON.parse(res.responseText).id;
                    appendBlocksBatch(pageId, blocks.slice(90), token, statusCallback);
                } else {
                    statusCallback('❌ Fail');
                    alert(res.responseText);
                }
            },
            onerror: () => statusCallback('❌ Net Error')
        });
    }

    // ------------------- 9. 主逻辑 & 状态控制 -------------------
    async function executeExport(blocks, title, btnOrLabelUpdater, iconElem) {
        const { token, dbId } = getConfig();
        if (!token) return promptConfig();

        const updateStatus = (msg) => {
            // 单条导出按钮
            if (btnOrLabelUpdater.classList && btnOrLabelUpdater.classList.contains('gemini-icon-btn') && iconElem) {
                if (msg && msg.includes('Saved')) {
                    btnOrLabelUpdater.classList.remove('processing');
                    btnOrLabelUpdater.classList.add('success');
                    iconElem.textContent = '✅';
                    setTimeout(() => {
                        btnOrLabelUpdater.classList.remove('success');
                        iconElem.textContent = '📤';
                    }, 2500);
                } else if (msg && (msg.includes('Fail') || msg.includes('Error'))) {
                    btnOrLabelUpdater.classList.remove('processing');
                    btnOrLabelUpdater.classList.add('error');
                    iconElem.textContent = '❌';
                } else if (msg) {
                    btnOrLabelUpdater.classList.add('processing');
                    btnOrLabelUpdater.classList.remove('success', 'error');
                    iconElem.textContent = '⏳';
                }
            }
            // 全局按钮
            else if (btnOrLabelUpdater.id === 'gemini-saver-btn') {
                if (msg === null) btnOrLabelUpdater.textContent = '📥 Save to Notion';
                else btnOrLabelUpdater.textContent = msg;
            }
        };

        if (btnOrLabelUpdater.id === 'gemini-saver-btn') {
            btnOrLabelUpdater.classList.add('loading');
            btnOrLabelUpdater.textContent = '🕵️ Processing...';
        } else {
            updateStatus('Processing...');
        }

        try {
            blocks = await processAssets(blocks, updateStatus);
            if (btnOrLabelUpdater.id === 'gemini-saver-btn') btnOrLabelUpdater.textContent = '💾 Saving...';
            createPageAndUpload(title, blocks, token, dbId, updateStatus);
        } catch (e) {
            console.error(e);
            if (btnOrLabelUpdater.id === 'gemini-saver-btn') btnOrLabelUpdater.textContent = '❌ Error';
            updateStatus('❌ Fail');
            alert(e.message);
        } finally {
            if (btnOrLabelUpdater.id === 'gemini-saver-btn') btnOrLabelUpdater.classList.remove('loading');
        }
    }

    function handleFullExport() {
        const btn = document.getElementById('gemini-saver-btn');
        const blocks = getChatBlocks(null);
        executeExport(blocks, getChatTitle(), btn);
    }

    function handleSingleExport(bubble, iconBtn, iconElem) {
        const targets = [bubble];
        if (bubble.tagName.toLowerCase() === 'user-query') {
            const next = bubble.nextElementSibling;
            if (next && next.tagName.toLowerCase() === 'model-response' && next.getAttribute('data-privacy-skip') !== 'true') {
                targets.push(next);
            }
        }
        const blocks = getChatBlocks(targets);
        const title = getChatTitle(bubble);
        executeExport(blocks, title, iconBtn, iconElem);
    }

    function tryInit() {
        if (!document.getElementById('gemini-saver-btn')) {
            const btn = document.createElement('button');
            btn.id = 'gemini-saver-btn';
            btn.textContent = '📥 Save to Notion';
            btn.onclick = handleFullExport;
            document.body.appendChild(btn);
        }
        injectPageControls();
    }
    setInterval(tryInit, 1500);

})();