// ==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
// ==/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);
}
}
})();