✨ Gemini Key:一鍵網頁文章傳送與智慧火花

為您的瀏覽體驗注入智慧魔法。按住 Ctrl+中鍵或 Alt+G 鍵,即可在背景抓取網頁內容並自動切換模型傳送至 Gemini 進行深度分析。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name               ✨ Gemini Key: One-Click Web Intelligence & Spark Magic
// @name:en            ✨ Gemini Key: One-Click Web Intelligence & Spark Magic
// @name:zh-TW         ✨ Gemini Key:一鍵網頁文章傳送與智慧火花
// @name:zh-CN         ✨ Gemini Key:一键网页文章传送与智慧火花
// @name:ja            ✨ Gemini Key: ワンクリックウェブ記事転送&スパークマジック
// @name:ko            ✨ Gemini Key: 원클릭 웹 문서 전송 및 스파크 매직
// @name:es            ✨ Gemini Key: Inteligencia web con un clic y magia Spark
// @author             WellsTsai
// @namespace          wellstsai.com
// @version            2026.5.29.1
// @description        Add a spark of magic to your browsing. Background fetch page content with Ctrl+Middle Click or Alt+G, and seamlessly send it to Gemini with automatic model switching.
// @description:en     Add a spark of magic to your browsing. Background fetch page content with Ctrl+Middle Click or Alt+G, and seamlessly send it to Gemini with automatic model switching.
// @description:zh-TW 為您的瀏覽體驗注入智慧魔法。按住 Ctrl+中鍵或 Alt+G 鍵,即可在背景抓取網頁內容並自動切換模型傳送至 Gemini 進行深度分析。
// @description:zh-CN 为您的浏览体验注入智慧魔法。按住 Ctrl+中键或 Alt+G 键,即可在背景抓取网页内容并自动切换模型传送至 Gemini 进行深度分析。
// @description:ja    ネットサーフィンにスパークマジックを。Ctrl+中クリックまたはAlt+Gキーでコンテンツを抽出し、自動モデル切り替え機能でGeminiに送信して要約・分析します。
// @description:ko    브라우징에 스파크 매직을 더해보세요. Ctrl+휠 클릭 또는 Alt+G로 본문을 백그라운드에서 추출하여 모델 자동 전환과 함께 Gemini로 전송합니다.
// @description:es    Añade un toque de magia Spark a tu navegación. Extrae contenido en segundo plano con Ctrl+Clic Central o Alt+G, y envíalo a Gemini con cambio de modelo automático.
// @match             *://*/*
// @grant             GM_openInTab
// @grant             GM_xmlhttpRequest
// @grant             GM_setValue
// @grant             GM_getValue
// @grant             GM_deleteValue
// @grant             GM_registerMenuCommand
// @connect           *
// @license           MIT
// ==/UserScript==

(function() {
    'use strict';

    if (window.top !== window.self) return;

    const LANG_MAP = {
        'zh-TW': '臺灣正體中文 (Traditional Chinese, Taiwan)',
        'zh-CN': '简体中文 (Simplified Chinese)',
        'en': 'English',
        'ja': '日本語 (Japanese)',
        'ko': '한국어 (Korean)',
        'es': 'Español (Spanish)'
    };

    // 完整的 i18n 字典,包含 Prompt 標題、UI 介面與系統通知
    const I18N = {
        'zh-TW': {
            sum: '一句話總結', highlights: '重點解析', conclusion: '結論與後續',
            errTextTooShort: '⚠️ 抓取到的文字過少,請直接進入網頁使用 Alt+G。',
            errFetchFailed: '⚠️ 無法取得文章內容。',
            alertSaved: '✅ 設定已儲存!',
            alertLang: '語系', alertAuth: '帳號', alertDefault: '未指定 (預設)',
            menuSend: '✨ 摘要網頁並傳送 (Alt+G)',
            menuSettings: '⚙️ 設定輸出語系與帳號',
            uiTitle: '✨ Gemini Key 設定',
            uiLangLabel: '選擇輸出語系:',
            uiAuthLabel: '指定 Google 帳號 (authuser):',
            uiAuthPlaceholder: '填寫信箱或數字 (留空則不指定)',
            uiBtnCancel: '取消', uiBtnSave: '儲存設定'
        },
        'zh-CN': {
            sum: '一句话总结', highlights: '重点解析', conclusion: '结论与后续',
            errTextTooShort: '⚠️ 抓取到的文字过少,请直接进入网页使用 Alt+G。',
            errFetchFailed: '⚠️ 无法获取文章内容。',
            alertSaved: '✅ 设置已保存!',
            alertLang: '语言', alertAuth: '账号', alertDefault: '未指定 (默认)',
            menuSend: '✨ 摘要网页并传送 (Alt+G)',
            menuSettings: '⚙️ 设置输出语言与账号',
            uiTitle: '✨ Gemini Key 设置',
            uiLangLabel: '选择输出语言:',
            uiAuthLabel: '指定 Google 账号 (authuser):',
            uiAuthPlaceholder: '填写邮箱或数字 (留空则不指定)',
            uiBtnCancel: '取消', uiBtnSave: '保存设置'
        },
        'en': {
            sum: 'One-Sentence Summary', highlights: 'Key Highlights', conclusion: 'Conclusion & Takeaways',
            errTextTooShort: '⚠️ Extracted text is too short. Please visit the page directly and use Alt+G.',
            errFetchFailed: '⚠️ Failed to retrieve article content.',
            alertSaved: '✅ Settings saved!',
            alertLang: 'Language', alertAuth: 'Account', alertDefault: 'Not specified (Default)',
            menuSend: '✨ Summarize & Send (Alt+G)',
            menuSettings: '⚙️ Settings (Language & Account)',
            uiTitle: '✨ Gemini Key Settings',
            uiLangLabel: 'Select Output Language:',
            uiAuthLabel: 'Specify Google Account (authuser):',
            uiAuthPlaceholder: 'Email or number (leave blank for default)',
            uiBtnCancel: 'Cancel', uiBtnSave: 'Save Settings'
        },
        'ja': {
            sum: '一文要約', highlights: '重要なポイント', conclusion: '結論と今後の展開',
            errTextTooShort: '⚠️ 抽出されたテキストが少なすぎます。ページに直接アクセスして Alt+G を使用してください。',
            errFetchFailed: '⚠️ 記事コンテンツの取得に失敗しました。',
            alertSaved: '✅ 設定を保存しました!',
            alertLang: '言語', alertAuth: 'アカウント', alertDefault: '未指定 (デフォルト)',
            menuSend: '✨ ページを要約して送信 (Alt+G)',
            menuSettings: '⚙️ 言語とアカウントの設定',
            uiTitle: '✨ Gemini Key 設定',
            uiLangLabel: '出力言語の選択:',
            uiAuthLabel: 'Google アカウント (authuser) の指定:',
            uiAuthPlaceholder: 'メールアドレスまたは数字 (空白でデフォルト)',
            uiBtnCancel: 'キャンセル', uiBtnSave: '設定を保存'
        },
        'ko': {
            sum: '한 줄 요약', highlights: '핵심 하이라이트', conclusion: '결론 및 시사점',
            errTextTooShort: '⚠️ 추출된 텍스트가 너무 짧습니다. 페이지에 직접 접속하여 Alt+G를 사용하세요.',
            errFetchFailed: '⚠️ 기사 콘텐츠를 가져오는 데 실패했습니다.',
            alertSaved: '✅ 설정이 저장되었습니다!',
            alertLang: '언어', alertAuth: '계정', alertDefault: '지정되지 않음 (기본값)',
            menuSend: '✨ 웹페이지 요약 및 전송 (Alt+G)',
            menuSettings: '⚙️ 언어 및 계정 설정',
            uiTitle: '✨ Gemini Key 설정',
            uiLangLabel: '출력 언어 선택:',
            uiAuthLabel: 'Google 계정 (authuser) 지정:',
            uiAuthPlaceholder: '이메일 또는 숫자 (비워두면 기본값)',
            uiBtnCancel: '취소', uiBtnSave: '설정 저장'
        },
        'es': {
            sum: 'Resumen en una frase', highlights: 'Puntos clave', conclusion: 'Conclusión y próximos pasos',
            errTextTooShort: '⚠️ El texto extraído es demasiado corto. Visita la página directamente y usa Alt+G.',
            errFetchFailed: '⚠️ Error al obtener el contenido del artículo.',
            alertSaved: '✅ ¡Configuración guardada!',
            alertLang: 'Idioma', alertAuth: 'Cuenta', alertDefault: 'No especificado (Predeterminado)',
            menuSend: '✨ Resumir y enviar (Alt+G)',
            menuSettings: '⚙️ Configuración (Idioma y cuenta)',
            uiTitle: '✨ Configuración de Gemini Key',
            uiLangLabel: 'Seleccionar idioma de salida:',
            uiAuthLabel: 'Especificar cuenta de Google (authuser):',
            uiAuthPlaceholder: 'Correo electrónico o número (dejar en blanco para predeterminado)',
            uiBtnCancel: 'Cancelar', uiBtnSave: 'Guardar configuración'
        }
    };

    const getBrowserLang = () => {
        const lang = (navigator.language || navigator.userLanguage || 'en').toLowerCase();
        if (lang.includes('zh-tw') || lang.includes('zh-hk')) return 'zh-TW';
        if (lang.includes('zh-cn') || lang.includes('zh-sg') || lang === 'zh') return 'zh-CN';
        if (lang.startsWith('ja')) return 'ja';
        if (lang.startsWith('ko')) return 'ko';
        if (lang.startsWith('es')) return 'es';
        return 'en';
    };

    const uiLang = getBrowserLang();
    const t = I18N[uiLang] || I18N['en'];

    const LOCAL_CONFIG = {
        get outputLang() { return GM_getValue('gemini_key_lang', uiLang); },
        set outputLang(val) { GM_setValue('gemini_key_lang', val); },

        get authUser() { return GM_getValue('gemini_key_authuser', ''); },
        set authUser(val) { GM_setValue('gemini_key_authuser', val); },

        getPrompt: function(title, url, text) {
            const langKey = this.outputLang;
            const langFull = LANG_MAP[langKey] || LANG_MAP['en'];
            const outT = I18N[langKey] || I18N['en'];

            return `Please bypass all pleasantries and introductions. This is a web article. Please extract the core insights, but [absolutely do not oversimplify to the point of breaking the author's original logic and flow].\n` +
                `⚠️ STRICT REQUIREMENT: Please output your entire response and formatting in "${langFull}", providing a rich and detailed explanation.\n\n` +
                `Please provide the following three pieces of information in a clear and highly scannable layout:\n` +
                `1. 🎯 [${outT.sum}]: Accurately pinpoint what this article is about or what problem it solves.\n` +
                `2. 📝 [${outT.highlights}]: Bullet-point all crucial information from the article.\n` +
                `   - For News/Tech: List new features, specifications, impacts, or key metrics.\n` +
                `   - For Forums/Discussions: List the original poster's pain points, general consensus, or opposing views.\n` +
                `   - For Reviews/Food Blogs: List pros & cons, unique features, or red flags.\n` +
                `3. 💡 [${outT.conclusion}]: The article's final conclusion, proposed solutions, or points worthy of the reader's attention.\n\n` +
                `[Article Title]: ${title}\n[Source URL]: ${url}\n[Original Content]:\n${text}`;
        }
    };

    const CONFIG = {
        MAX_LEN: 25000,
        MIN_LEN: 100,
        TARGET_IDX: 1,
        KEY: 'pending_gemini_prompt',
        GEMINI_URL: 'https://gemini.google.com'
    };

    const SELECTORS = {
        READY: 'h1[data-test-id="message"], .zero-state-container',
        MENU_BTN: 'button[data-test-id="bard-mode-menu-button"]',
        MENU_ITEM: 'gem-menu-item',
        EDITOR: '.ql-editor[contenteditable="true"], div[contenteditable="true"]',
        SEND_BTN: 'button:has(mat-icon[data-mat-icon-name="arrow_upward"]), button:has(mat-icon[data-mat-icon-name="send"])'
    };

    // ==========================================
    // [ Utility Helpers ]
    // ==========================================
    const sleep = ms => new Promise(r => setTimeout(r, ms));

    const waitEl = (sel, timeout = 10000) => new Promise((res, rej) => {
        let el = document.querySelector(sel);
        if (el) return res(el);
        const obs = new MutationObserver(() => {
            if (el = document.querySelector(sel)) { obs.disconnect(); res(el); }
        });
        obs.observe(document, { childList: true, subtree: true });
        setTimeout(() => { obs.disconnect(); rej(new Error(`Timeout: ${sel}`)); }, timeout);
    });

    const click = el => {
        if (!el) return;
        el.focus?.();
        const win = el.ownerDocument?.defaultView || window;
        for (const type of ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click']) {
            const Cls = type.startsWith('pointer') ? PointerEvent : MouseEvent;
            const init = { bubbles: true, cancelable: true, composed: true, detail: 1, buttons: type.includes('down') ? 1 : 0 };
            try {
                el.dispatchEvent(new Cls(type, { ...init, view: win }));
            } catch {
                el.dispatchEvent(new Cls(type, init));
            }
        }
    };

    const cleanText = root => {
        const clone = root.cloneNode(true);
        clone.querySelectorAll('script, style, noscript, nav, header, footer, aside, iframe, .ads, .menu').forEach(el => el.remove());
        return clone.innerText.replace(/\n\s*\n/g, '\n').replace(/\s{2,}/g, ' ').trim();
    };

    // ==========================================
    // [ GUI Settings Panel ]
    // ==========================================
    const showSettings = () => {
        if (document.getElementById('gemini-key-settings-modal')) return;

        const optionsHtml = Object.entries(LANG_MAP)
            .map(([val, name]) => `<option value="${val}">${name}</option>`)
            .join('');

        const modal = document.createElement('div');
        modal.id = 'gemini-key-settings-modal';
        modal.innerHTML = `
            <div style="position:fixed; top:0; left:0; width:100vw; height:100vh; background:rgba(0,0,0,0.5); z-index:2147483646;"></div>
            <div style="position:fixed; top:50%; left:50%; transform:translate(-50%, -50%); background:#fff; padding:24px; border-radius:12px; box-shadow:0 10px 30px rgba(0,0,0,0.2); z-index:2147483647; width:320px; font-family:sans-serif; color:#333; box-sizing:border-box;">
                <h3 style="margin:0 0 16px 0; font-size:18px; border-bottom:1px solid #eee; padding-bottom:10px;">${t.uiTitle}</h3>

                <label style="display:block; margin-bottom:8px; font-size:14px; font-weight:bold;">${t.uiLangLabel}</label>
                <select id="gemini-key-lang-select" style="width:100%; padding:8px; border-radius:6px; border:1px solid #ccc; font-size:14px; margin-bottom:16px; outline:none; box-sizing:border-box;">
                    ${optionsHtml}
                </select>

                <label style="display:block; margin-bottom:8px; font-size:14px; font-weight:bold;">${t.uiAuthLabel}</label>
                <input type="text" id="gemini-key-authuser-input" placeholder="${t.uiAuthPlaceholder}" style="width:100%; padding:8px; border-radius:6px; border:1px solid #ccc; font-size:14px; margin-bottom:24px; outline:none; box-sizing:border-box;">

                <div style="display:flex; justify-content:flex-end; gap:10px;">
                    <button id="gemini-key-btn-cancel" style="padding:8px 16px; border:none; background:#f1f3f4; color:#333; border-radius:6px; cursor:pointer; font-size:14px;">${t.uiBtnCancel}</button>
                    <button id="gemini-key-btn-save" style="padding:8px 16px; border:none; background:#1a73e8; color:#fff; border-radius:6px; cursor:pointer; font-size:14px; font-weight:bold;">${t.uiBtnSave}</button>
                </div>
            </div>
        `;
        document.body.appendChild(modal);

        const select = document.getElementById('gemini-key-lang-select');
        const authInput = document.getElementById('gemini-key-authuser-input');

        select.value = LOCAL_CONFIG.outputLang;
        authInput.value = LOCAL_CONFIG.authUser;

        document.getElementById('gemini-key-btn-cancel').onclick = () => modal.remove();
        document.getElementById('gemini-key-btn-save').onclick = () => {
            LOCAL_CONFIG.outputLang = select.value;
            LOCAL_CONFIG.authUser = authInput.value.trim();
            modal.remove();
            alert(`${t.alertSaved}\n${t.alertLang}: ${LANG_MAP[select.value]}\n${t.alertAuth}: ${LOCAL_CONFIG.authUser || t.alertDefault}`);
        };
    };

    // ==========================================
    // [ Phase 1 ] Source Page Logic
    // ==========================================
    const initSource = () => {
        const fetchAndSend = (title, url) => {
            if (!url || url.startsWith('javascript:')) return;
            document.body.style.cursor = 'wait';

            GM_xmlhttpRequest({
                method: "GET", url,
                onload: ({ responseText }) => {
                    document.body.style.cursor = 'default';
                    const doc = new DOMParser().parseFromString(responseText, "text/html");
                    const text = cleanText(doc.body);
                    if (text.length < CONFIG.MIN_LEN) return alert(t.errTextTooShort);
                    sendToGemini(title || doc.title || 'Untitled Article', url, text);
                },
                onerror: () => {
                    document.body.style.cursor = 'default';
                    alert(t.errFetchFailed);
                }
            });
        };

        const sendToGemini = (title, url, text) => {
            let promptText = text;
            if (promptText.length > CONFIG.MAX_LEN) {
                promptText = promptText.substring(0, CONFIG.MAX_LEN) + '\n\n...(文章過長,已自動截斷 / Article truncated due to length limits)';
            }

            const finalPrompt = LOCAL_CONFIG.getPrompt(title, url, promptText);
            GM_setValue(CONFIG.KEY, finalPrompt);

            let targetUrl = `${CONFIG.GEMINI_URL}/app`;
            const authUser = LOCAL_CONFIG.authUser;
            if (authUser !== '') {
                targetUrl += `?authuser=${encodeURIComponent(authUser)}`;
            }

            GM_openInTab(targetUrl, { active: false, insert: true, setParent: true });
            console.log('[Universal->Gemini] Task prepared. New Gemini tab spawned.');
        };

        const sendCurrent = () => sendToGemini(document.title, location.href, cleanText(document.body));

        GM_registerMenuCommand(t.menuSend, sendCurrent);
        GM_registerMenuCommand(t.menuSettings, showSettings);

        window.addEventListener('keydown', e => {
            if (e.altKey && e.key.toLowerCase() === 'g') {
                e.preventDefault(); sendCurrent();
            }
        });

        const onMiddleClick = e => {
            if (e.button === 1 && e.ctrlKey) {
                const a = e.target.closest('a');
                if (a?.href) {
                    e.preventDefault(); e.stopPropagation();
                    if (e.type === 'auxclick') fetchAndSend(a.innerText.trim() || a.title || 'Untitled Link', a.href);
                }
            }
        };

        window.addEventListener('mousedown', onMiddleClick);
        window.addEventListener('auxclick', onMiddleClick);
    };

    // ==========================================
    // [ Phase 2 ] Gemini Page Logic
    // ==========================================
    const initGemini = async () => {
        const prompt = GM_getValue(CONFIG.KEY);
        if (!prompt) return;

        GM_deleteValue(CONFIG.KEY);
        console.log('[Universal->Gemini] Pending task detected. Waiting for page to load...');

        try {
            await waitEl(SELECTORS.READY);
            await sleep(300);
        } catch (e) {
            console.warn('[Universal->Gemini] Welcome UI timeout. Proceeding with execution...', e);
        }

        try {
            const trigger = await waitEl(SELECTORS.MENU_BTN);
            click(trigger);
            await sleep(200);

            await waitEl(SELECTORS.MENU_ITEM);
            const items = document.querySelectorAll(SELECTORS.MENU_ITEM);
            const target = items[CONFIG.TARGET_IDX - 1];

            if (target) {
                click(target);
                console.log(`[Universal->Gemini] Model switched successfully.`);
            } else {
                click(document.body);
            }
            await sleep(500);
        } catch (e) {
            console.warn("[Universal->Gemini] Model switching process failed or timed out", e);
        }

        try {
            const editor = await waitEl(SELECTORS.EDITOR);
            editor.focus();
            document.execCommand('insertText', false, prompt);
            editor.dispatchEvent(new Event('input', { bubbles: true }));

            await sleep(800);
            const sendBtn = await waitEl(SELECTORS.SEND_BTN);
            click(sendBtn);
            console.log('[Universal->Gemini] Message dispatched automatically.');
        } catch (e) {
            console.error("[Universal->Gemini] Text injection or dispatch process failed", e);
        }
    };

    if (location.origin === CONFIG.GEMINI_URL) {
        initGemini();
    } else {
        initSource();
    }
})();