為您的瀏覽體驗注入智慧魔法。按住 Ctrl+中鍵或 Alt+G 鍵,即可在背景抓取網頁內容並自動切換模型傳送至 Gemini 進行深度分析。
// ==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();
}
})();