// ==UserScript== // @name YouTube 到 Gemini 自动摘要生成器 // @namespace http://tampermonkey.net/ // @version 0.6 // @description 在YouTube视频中添加按钮,点击后跳转到Gemini并自动输入提示词总结视频 // @author hengyu (Optimized by Assistant) // @match *://www.youtube.com/* // @match *://youtube.com/* // @match *://gemini.google.com/* // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @run-at document-end // @license MIT // @downloadURL none // ==/UserScript== (function() { 'use strict'; // --- 配置 --- const CHECK_INTERVAL_MS = 100; // 检查元素的频率(毫秒) const YOUTUBE_ELEMENT_TIMEOUT_MS = 10000; // 等待YouTube元素的最大时间(毫秒) const GEMINI_ELEMENT_TIMEOUT_MS = 15000; // 等待Gemini元素的最大时间(毫秒) const GEMINI_PROMPT_EXPIRY_MS = 300000; // 提示词传输有效期5分钟 const URL_CHECK_INTERVAL_MS = 500; // URL变化检查频率 // --- 调试日志 --- function debugLog(message) { console.log(`[YouTube to Gemini] ${message}`); } // --- 辅助函数 --- function waitForElement(selector, timeoutMs, parent = document) { return new Promise((resolve, reject) => { let element = parent.querySelector(selector); if (element && element.offsetWidth > 0 && element.offsetHeight > 0) { return resolve(element); } const intervalId = setInterval(() => { element = parent.querySelector(selector); if (element && element.offsetWidth > 0 && element.offsetHeight > 0) { clearInterval(intervalId); clearTimeout(timeoutId); resolve(element); } }, CHECK_INTERVAL_MS); const timeoutId = setTimeout(() => { clearInterval(intervalId); debugLog(`Element not found or not visible after ${timeoutMs}ms: ${selector}`); reject(new Error(`Element not found or not visible: ${selector}`)); }, timeoutMs); }); } function waitForElements(selectors, timeoutMs, parent = document) { return new Promise((resolve, reject) => { let foundElement = null; const startTime = Date.now(); function checkElements() { for (const selector of selectors) { const elements = parent.querySelectorAll(selector); for (const el of elements) { if (el.offsetWidth > 0 || el.offsetHeight > 0 || el.getClientRects().length > 0) { if (selectors.some(s => s.includes('button')) && el.disabled) { continue; // 跳过禁用的按钮 } foundElement = el; break; } } if (foundElement) break; } if (foundElement) { clearInterval(intervalId); clearTimeout(timeoutId); resolve(foundElement); } else if (Date.now() - startTime > timeoutMs) { clearInterval(intervalId); debugLog(`Elements not found or not visible after ${timeoutMs}ms: ${selectors.join(', ')}`); reject(new Error(`Elements not found or not visible: ${selectors.join(', ')}`)); } } const intervalId = setInterval(checkElements, CHECK_INTERVAL_MS); const timeoutId = setTimeout(() => { clearInterval(intervalId); if (!foundElement) { debugLog(`Elements not found or not visible after ${timeoutMs}ms: ${selectors.join(', ')}`); reject(new Error(`Elements not found or not visible: ${selectors.join(', ')}`)); } }, timeoutMs); // 初始检查 checkElements(); }); } function copyToClipboard(text) { try { // 尝试使用现代剪贴板API if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text) .catch(err => { debugLog(`Clipboard API失败: ${err},使用后备方法`); legacyClipboardCopy(text); }); } else { legacyClipboardCopy(text); } } catch (e) { debugLog(`复制到剪贴板时出错: ${e}`); legacyClipboardCopy(text); } } function legacyClipboardCopy(text) { const textarea = document.createElement('textarea'); textarea.value = text; document.body.appendChild(textarea); textarea.select(); try { document.execCommand('copy'); } catch (err) { debugLog('Failed to copy to clipboard using execCommand.'); } document.body.removeChild(textarea); } function showNotification(elementId, message, styles, duration = 15000) { // 移除已存在的通知 let existingNotification = document.getElementById(elementId); if (existingNotification) { document.body.removeChild(existingNotification); } const notification = document.createElement('div'); notification.id = elementId; notification.innerText = message; Object.assign(notification.style, styles); // 应用基础样式 document.body.appendChild(notification); // 添加关闭按钮 const closeButton = document.createElement('button'); closeButton.innerText = '✕'; closeButton.style.position = 'absolute'; closeButton.style.top = '5px'; closeButton.style.right = '10px'; closeButton.style.background = 'transparent'; closeButton.style.border = 'none'; closeButton.style.color = 'inherit'; closeButton.style.fontSize = '16px'; closeButton.style.cursor = 'pointer'; closeButton.onclick = function() { if (document.body.contains(notification)) { document.body.removeChild(notification); } }; notification.appendChild(closeButton); // 自动移除 const timeoutId = setTimeout(() => { if (document.body.contains(notification)) { document.body.removeChild(notification); } }, duration); notification.dataset.timeoutId = timeoutId; } // --- YouTube 相关函数 --- const YOUTUBE_NOTIFICATION_ID = 'gemini-yt-notification'; const YOUTUBE_NOTIFICATION_STYLE = { position: 'fixed', bottom: '20px', left: '50%', transform: 'translateX(-50%)', backgroundColor: 'rgba(0,0,0,0.8)', color: 'white', padding: '20px', borderRadius: '8px', zIndex: '9999', maxWidth: '80%', textAlign: 'left', whiteSpace: 'pre-line' }; // 检查是否为视频页面 function isVideoPage() { return window.location.pathname === '/watch' && window.location.search.includes('?v='); } function addSummarizeButton() { // 检查是否是视频页面 if (!isVideoPage()) { debugLog("不是视频页面,不添加按钮"); return; } // 防止重复添加按钮 if (document.getElementById('gemini-summarize-btn')) { debugLog("摘要按钮已存在"); return; } debugLog("尝试添加摘要按钮..."); // 尝试多个可能的容器选择器 const containerSelectors = [ '#masthead #end', '#top-row ytd-video-owner-renderer', '#above-the-fold #top-row', '#owner', 'ytd-watch-metadata' ]; // 尝试找到容器并添加按钮 (async function() { for (const selector of containerSelectors) { try { const container = await waitForElement(selector, 2000); if (container) { // 再次检查按钮是否存在 if (document.getElementById('gemini-summarize-btn')) { return; } const button = document.createElement('button'); button.id = 'gemini-summarize-btn'; button.innerText = '📝 Gemini摘要'; // 应用样式 Object.assign(button.style, { backgroundColor: '#2F80ED', color: 'white', border: 'none', borderRadius: '4px', padding: '8px 16px', margin: '0 16px', cursor: 'pointer', fontWeight: 'bold', height: '36px', display: 'flex', alignItems: 'center', zIndex: '9999' }); // 添加点击事件 button.addEventListener('click', function() { try { const youtubeUrl = window.location.href; const videoTitle = document.querySelector('h1.ytd-watch-metadata')?.textContent?.trim() || document.title.replace(' - YouTube', ''); const prompt = `请分析这个YouTube视频: ${youtubeUrl}\n\n提供一个全面的摘要,包括主要观点、关键见解和视频中讨论的重要细节,以结构化的方式分解内容,并包括任何重要的结论或要点。`; // 存储数据到GM变量 GM_setValue('geminiPrompt', prompt); GM_setValue('videoTitle', videoTitle); GM_setValue('timestamp', Date.now()); // 在新标签页打开Gemini window.open('https://gemini.google.com/', '_blank'); // 显示通知 showNotification(YOUTUBE_NOTIFICATION_ID, ` 已跳转到Gemini! 系统将尝试自动输入并发送提示。 如果自动操作失败,提示词已复制到剪贴板,您可以手动粘贴。 视频: "${videoTitle}" `.trim(), YOUTUBE_NOTIFICATION_STYLE); // 复制到剪贴板作为备份 copyToClipboard(prompt); } catch (error) { console.error("Button click error:", error); alert("摘要功能出错: " + error.message); } }); // 添加按钮到容器 container.insertBefore(button, container.firstChild); debugLog("摘要按钮成功添加!"); return; // 成功添加后退出 } } catch (e) { // 继续尝试下一个选择器 } } debugLog("所有选择器都尝试失败,无法添加按钮"); })(); } // --- Gemini 相关函数 --- const GEMINI_NOTIFICATION_ID = 'gemini-auto-notification'; const GEMINI_NOTIFICATION_STYLES = { info: { backgroundColor: '#e8f4fd', color: '#0866c2', border: '1px solid #b8daff' }, warning: { backgroundColor: '#fff3e0', color: '#b35d00', border: '1px solid #ffe0b2' }, error: { backgroundColor: '#fdecea', color: '#c62828', border: '1px solid #ffcdd2' } }; const BASE_GEMINI_NOTIFICATION_STYLE = { position: 'fixed', bottom: '20px', right: '20px', padding: '15px 20px', borderRadius: '8px', zIndex: '9999', maxWidth: '350px', textAlign: 'left', boxShadow: '0 4px 12px rgba(0,0,0,0.15)', whiteSpace: 'pre-line' }; function showGeminiNotification(message, type = "info") { const style = { ...BASE_GEMINI_NOTIFICATION_STYLE, ...(GEMINI_NOTIFICATION_STYLES[type] || GEMINI_NOTIFICATION_STYLES.info) }; showNotification(GEMINI_NOTIFICATION_ID, message, style, 10000); } async function handleGemini() { debugLog("Gemini页面检测到。检查提示词..."); const prompt = GM_getValue('geminiPrompt', ''); const timestamp = GM_getValue('timestamp', 0); const videoTitle = GM_getValue('videoTitle', 'N/A'); // 检查提示词是否存在且未过期 if (!prompt || Date.now() - timestamp > GEMINI_PROMPT_EXPIRY_MS) { debugLog("未找到有效提示词或已过期"); GM_deleteValue('geminiPrompt'); GM_deleteValue('timestamp'); GM_deleteValue('videoTitle'); return; } debugLog("找到有效提示词。等待Gemini输入区域..."); // 使用多个选择器尝试找到输入区 const textareaSelectors = [ 'div[class*="text-input-field"][class*="with-toolbox-drawer"]', 'div[class*="input-area"]', 'div[contenteditable="true"]', 'div[class*="textarea-wrapper"]', 'textarea', 'div[role="textbox"]' ]; try { // 等待输入区域出现 const textarea = await waitForElements(textareaSelectors, GEMINI_ELEMENT_TIMEOUT_MS); debugLog("找到输入区域。尝试输入提示词。"); // 尝试输入文本 let inputSuccess = false; try { textarea.focus(); if (textarea.isContentEditable) { textarea.innerText = prompt; } else if (textarea.tagName.toLowerCase() === 'textarea') { textarea.value = prompt; } else { document.execCommand('insertText', false, prompt); } // 触发事件以确保框架检测到变化 textarea.dispatchEvent(new Event('input', { bubbles: true, cancelable: true })); textarea.dispatchEvent(new Event('change', { bubbles: true, cancelable: true })); inputSuccess = true; debugLog("提示词已插入到输入区域。"); } catch (inputError) { debugLog(`插入文本时出错: ${inputError}。尝试剪贴板方法。`); showGeminiNotification("无法自动填入提示词。请手动粘贴。\n提示词已复制到剪贴板。", "error"); copyToClipboard(prompt); GM_deleteValue('geminiPrompt'); GM_deleteValue('timestamp'); GM_deleteValue('videoTitle'); return; } if (inputSuccess) { // 短暂延迟,等待UI更新 await new Promise(resolve => setTimeout(resolve, 100)); debugLog("等待发送按钮..."); // 使用多个选择器尝试找到发送按钮 const sendButtonSelectors = [ 'button:has(mat-icon[data-mat-icon-name="send"])', 'mat-icon[data-mat-icon-name="send"]', 'button:has(span.mat-mdc-button-touch-target)', 'button.mat-mdc-icon-button', 'button[id*="submit"]', 'button[aria-label="Run"]', 'button[aria-label="Send"]', 'button[aria-label="Submit"]', 'button[aria-label="发送"]' ]; try { // 等待发送按钮出现 let sendButtonElement = await waitForElements(sendButtonSelectors, GEMINI_ELEMENT_TIMEOUT_MS); debugLog("找到发送按钮。"); // 如果找到的是图标,获取父按钮 if (sendButtonElement.tagName.toLowerCase() === 'mat-icon') { const parentButton = sendButtonElement.closest('button'); if (parentButton && !parentButton.disabled) { sendButtonElement = parentButton; } else { throw new Error("找到发送图标,但父按钮缺失或禁用。"); } } // 检查按钮是否启用 if (sendButtonElement.disabled) { debugLog("发送按钮已禁用。稍等片刻..."); await new Promise(resolve => setTimeout(resolve, 500)); if (sendButtonElement.disabled) { throw new Error("发送按钮仍然禁用。"); } } // 点击按钮 sendButtonElement.click(); debugLog("成功点击发送按钮。"); // 成功通知 const successMessage = ` 已自动发送视频摘要请求! 正在分析视频: "${videoTitle}" 请稍候,Gemini正在处理您的请求... `; showGeminiNotification(successMessage.trim(), "info"); // 清理存储 GM_deleteValue('geminiPrompt'); GM_deleteValue('timestamp'); GM_deleteValue('videoTitle'); } catch (buttonError) { debugLog(`发送按钮错误: ${buttonError.message}`); showGeminiNotification("找不到或无法点击发送按钮。\n提示词已填入,请手动点击发送。", "warning"); } } } catch (textareaError) { debugLog(`输入区域错误: ${textareaError.message}`); showGeminiNotification("无法找到Gemini输入框。\n请手动粘贴提示词。\n提示词已复制到剪贴板。", "error"); copyToClipboard(prompt); GM_deleteValue('geminiPrompt'); GM_deleteValue('timestamp'); GM_deleteValue('videoTitle'); } } // --- 主执行逻辑 --- // 检测当前网站 const isYouTube = window.location.hostname.includes('youtube.com'); const isGemini = window.location.hostname.includes('gemini.google.com'); if (isYouTube) { debugLog("YouTube页面检测到。初始化按钮添加器。"); // 初始尝试添加按钮 if (isVideoPage()) { addSummarizeButton(); } // 设置URL变化检测 const originalPushState = history.pushState; history.pushState = function() { originalPushState.apply(this, arguments); debugLog("检测到pushState URL变化"); setTimeout(() => { if (isVideoPage()) { addSummarizeButton(); } }, 500); }; // 监听popstate事件(浏览器前进/后退按钮) window.addEventListener('popstate', function() { debugLog("检测到popstate URL变化"); setTimeout(() => { if (isVideoPage()) { addSummarizeButton(); } }, 500); }); // 定期检查URL变化 let lastUrl = location.href; setInterval(() => { const currentUrl = location.href; if (currentUrl !== lastUrl) { lastUrl = currentUrl; debugLog(`通过轮询检测到URL变化: ${currentUrl}`); if (isVideoPage()) { setTimeout(addSummarizeButton, 500); } } }, URL_CHECK_INTERVAL_MS); } else if (isGemini) { // 等待页面加载后处理Gemini if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => setTimeout(handleGemini, 500)); } else { setTimeout(handleGemini, 500); } } })();