您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
整合了“从此处删除”、“HTML预览(已修复)”和“自动中文输出”三大功能,全面提升 Google AI Studio 使用体验。
// ==UserScript== // @name Google AI Studio Helper // @namespace http://tampermonkey.net/ // @version 2.1 // @description 整合了“从此处删除”、“HTML预览(已修复)”和“自动中文输出”三大功能,全面提升 Google AI Studio 使用体验。 // @author Chris_C // @match *://aistudio.google.com/* // @grant none // @icon https://www.google.com/s2/favicons?sz=64&domain=aistudio.google.com // @license MIT // ==/UserScript== /** * 模块一:自动添加“中文输出”指令 */ (function() { 'use strict'; // --- 配置常量 --- // 聊天回合元素的选择器 const CHAT_TURN_SELECTOR = 'ms-chat-turn'; // 选项按钮的选择器(支持中英文) const OPTIONS_BUTTON_SELECTOR = 'button[aria-label="Open options"], button[aria-label="打开选项"]'; // 聊天会话容器的选择器 const CHAT_SESSION_SELECTOR = 'ms-chat-session'; // --- 配置结束 --- console.log('AI Studio Delete Script: Initializing (v2.0)'); /** * 快速删除函数 * @param {Element} turnElement - 要删除的回合元素 * @returns {Promise<boolean>} 删除是否成功 */ async function directDelete(turnElement) { if (!turnElement) { const turns = document.querySelectorAll(CHAT_TURN_SELECTOR); turnElement = turns[turns.length - 1]; } if (!turnElement) return false; const optionsBtn = turnElement.querySelector('button[aria-label="Open options"]'); if (!optionsBtn) return false; return new Promise(resolve => { // 监听菜单出现 const observer = new MutationObserver(() => { const menu = document.querySelector('.cdk-overlay-pane'); if (menu) { const deleteBtn = Array.from(menu.querySelectorAll('button')) .find(btn => btn.textContent?.toLowerCase().includes('delete')); if (deleteBtn) { deleteBtn.click(); observer.disconnect(); // 快速检查删除结果 setTimeout(() => resolve(!document.contains(turnElement)), 200); } } }); observer.observe(document.body, {childList: true, subtree: true}); optionsBtn.click(); // 超时处理 setTimeout(() => {observer.disconnect(); resolve(false);}, 1000); }); } /** * 执行删除操作的异步函数(原始方法作为备用) * @param {Element} turnElement - 要删除的对话回合元素 * @returns {Promise<Object>} 返回删除操作的结果 */ async function performDeleteAction(turnElement) { return new Promise(resolve => { // 查找当前回合的选项按钮 const optionsButton = turnElement.querySelector(OPTIONS_BUTTON_SELECTOR); if (!optionsButton) { console.warn('Delete action failed: Options button not found on a turn.', turnElement); resolve({ success: false }); return; } // 创建观察器来监听菜单的出现 const menuObserver = new MutationObserver((mutations, observer) => { // 查找弹出的菜单面板 const menuPanel = document.querySelector('.cdk-overlay-pane, [role="menu"]'); if (menuPanel) { // 等待菜单内容完全加载 setTimeout(() => { observer.disconnect(); // 停止观察 // 获取菜单中的所有可点击元素 const allButtons = Array.from(menuPanel.querySelectorAll('button, [role="menuitem"], .mat-menu-item')); console.log('Found menu items:', allButtons.map(btn => ({ text: btn.textContent?.trim(), ariaLabel: btn.getAttribute('aria-label'), className: btn.className }))); // 查找删除按钮(支持中英文,更宽泛的匹配) const deleteButton = allButtons.find(btn => { const text = (btn.textContent || '').trim().toLowerCase(); const ariaLabel = (btn.getAttribute('aria-label') || '').toLowerCase(); const title = (btn.getAttribute('title') || '').toLowerCase(); // 检查各种可能的删除相关文本 const deleteKeywords = ['delete', '删除', 'remove', '移除', 'trash', '垃圾', 'del']; return deleteKeywords.some(keyword => text.includes(keyword) || ariaLabel.includes(keyword) || title.includes(keyword) ) || btn.querySelector('svg, mat-icon, .material-icons')?.textContent?.includes('delete'); }); if (deleteButton) { console.log('Found delete button, clicking it.'); deleteButton.click(); // 点击后等待一段时间确保操作完成 setTimeout(() => resolve({ success: true }), 800); } else { console.warn('Could not find the "Delete" button in the menu.'); console.log('Available menu items:', allButtons.map(btn => btn.textContent?.trim())); // 尝试使用键盘快捷键删除 try { // 关闭菜单 document.body.click(); // 选中当前回合 turnElement.click(); // 等待一下然后按Delete键 setTimeout(() => { const deleteEvent = new KeyboardEvent('keydown', { key: 'Delete', code: 'Delete', keyCode: 46, which: 46, bubbles: true }); document.dispatchEvent(deleteEvent); setTimeout(() => resolve({ success: true }), 500); }, 200); } catch(e) { console.error('Keyboard shortcut failed:', e); // 检查是否为最后一个回合 const currentTurnCount = document.querySelectorAll(CHAT_TURN_SELECTOR).length; resolve({ success: false, isLastTurn: currentTurnCount === 1, reason: 'no_delete_button' }); } } }, 300); // 等待300ms让菜单内容完全加载 } }); // 开始观察 DOM 变化以检测菜单出现 menuObserver.observe(document.body, { childList: true, subtree: true }); // 点击选项按钮打开菜单 optionsButton.click(); // 设置超时处理,防止无限等待 setTimeout(() => { menuObserver.disconnect(); console.warn('Menu detection timeout'); resolve({ success: false, error: 'timeout' }); }, 3000); }); } /** * 为聊天回合添加"从此处删除"按钮 * @param {Element} chatTurn - 聊天回合元素 */ function addDeleteFromHereButton(chatTurn) { const optionsButton = chatTurn.querySelector(OPTIONS_BUTTON_SELECTOR); // 如果没有选项按钮或已经添加过删除按钮,则跳过 if (!optionsButton || chatTurn.querySelector('.delete-from-here-btn')) return; const btnContainer = optionsButton.parentElement; if (!btnContainer) return; // 创建删除按钮 const deleteBtn = document.createElement('button'); deleteBtn.className = 'delete-from-here-btn'; deleteBtn.textContent = '❌'; // 垃圾桶图标 deleteBtn.title = 'Delete From Here (Delete this and all subsequent turns)'; // 设置按钮样式(完全匹配其他按钮) deleteBtn.className = optionsButton.className.replace('mat-mdc-menu-trigger', 'delete-from-here-btn'); // 复制所有计算样式 const optionsStyle = getComputedStyle(optionsButton); Object.assign(deleteBtn.style, { background: 'none', border: 'none', cursor: 'pointer', opacity: '0.6', fontSize: '14px', transition: 'opacity 0.2s', marginRight: '4px', // 完全匹配选项按钮的样式 padding: optionsStyle.padding, height: optionsStyle.height, width: optionsStyle.width, minWidth: optionsStyle.minWidth, minHeight: optionsStyle.minHeight, display: optionsStyle.display, alignItems: optionsStyle.alignItems, justifyContent: optionsStyle.justifyContent, lineHeight: optionsStyle.lineHeight, boxSizing: optionsStyle.boxSizing, verticalAlign: optionsStyle.verticalAlign, flexShrink: optionsStyle.flexShrink, flexGrow: optionsStyle.flexGrow, alignSelf: optionsStyle.alignSelf }); // 添加鼠标悬停效果 deleteBtn.addEventListener('mouseenter', () => deleteBtn.style.opacity = '1'); deleteBtn.addEventListener('mouseleave', () => deleteBtn.style.opacity = '0.6'); // 添加点击事件处理器 deleteBtn.addEventListener('click', async (e) => { e.preventDefault(); e.stopPropagation(); // 确认删除操作 if (!confirm('确定要删除此回合及之后的所有对话吗?\nAre you sure you want to delete this and all subsequent turns?')) return; // 获取所有聊天回合 const allTurns = Array.from(document.querySelectorAll(CHAT_TURN_SELECTOR)); const currentIndex = allTurns.indexOf(chatTurn); if (currentIndex > -1) { // 计算需要删除的回合数量 const turnsToDeleteCount = allTurns.length - currentIndex; console.log(`Planning to delete ${turnsToDeleteCount} turns.`); // 从最后一个回合开始逐个删除 for (let i = 0; i < turnsToDeleteCount; i++) { // 重新获取当前的回合列表(因为删除操作会改变 DOM) const currentTurns = Array.from(document.querySelectorAll(CHAT_TURN_SELECTOR)); const turnToDelete = currentTurns[currentTurns.length - 1]; // 总是删除最后一个 if (turnToDelete) { // 高亮显示即将删除的回合 turnToDelete.style.backgroundColor = 'rgba(255, 100, 100, 0.2)'; // 使用直接删除方法 const directSuccess = await directDelete(turnToDelete); const result = directSuccess ? { success: true } : { success: false }; // 如果删除失败,检查原因并处理 if (!result.success) { console.log(`删除第 ${i + 1} 个回合失败,原因:`, result); // 检查是否是最后一个回合 const remainingTurns = Array.from(document.querySelectorAll(CHAT_TURN_SELECTOR)); if (remainingTurns.length === 1) { alert(`无法删除最后一个会话:这是 Google AI Studio 自身的限制。\n\n已成功删除 ${i} 个回合。`); turnToDelete.style.backgroundColor = ''; // 清除红色高亮 break; // 停止删除过程 } else if (result.reason === 'no_delete_button') { // UI变化导致找不到删除按钮 alert(`检测到 Google AI Studio 界面变化,无法找到删除按钮。\n请手动删除或等待脚本更新。\n\n已成功删除 ${i} 个回合。`); turnToDelete.style.backgroundColor = ''; // 清除红色高亮 break; } else { // 其他原因导致的失败 console.warn('删除操作失败,停止批量删除'); turnToDelete.style.backgroundColor = ''; // 清除红色高亮 break; } } else { console.log(`成功删除第 ${i + 1} 个回合`); // 快速等待DOM更新 await new Promise(resolve => setTimeout(resolve, 300)); } } else { console.warn("Could not find a turn to delete. Stopping."); break; } } console.log('Deletion process finished.'); } }); // 确保父容器有足够宽度横排显示按钮 Object.assign(btnContainer.style, { display: 'flex', flexDirection: 'row', alignItems: 'center', gap: '4px', minWidth: 'auto', width: 'auto', flexShrink: '0' }); // 将删除按钮插入到选项按钮之前 btnContainer.insertBefore(deleteBtn, optionsButton); } /** * 处理所有聊天回合,为每个回合添加删除按钮 */ function processAllTurns() { document.querySelectorAll(CHAT_TURN_SELECTOR).forEach(addDeleteFromHereButton); } // --- 增强的启动逻辑 --- let currentObserver = null; function initializeScript() { const chatSession = document.querySelector(CHAT_SESSION_SELECTOR); if (chatSession) { console.log('AI Studio Delete Script: Chat session found, initializing.'); // 停止之前的观察器 if (currentObserver) { currentObserver.disconnect(); } // 处理现有回合 processAllTurns(); // 创建新的观察器 currentObserver = new MutationObserver(() => { setTimeout(processAllTurns, 200); }); // 观察聊天会话变化 currentObserver.observe(chatSession, { childList: true, subtree: true }); console.log('AI Studio Delete Script: Observer started.'); return true; } return false; } // 初始化 if (!initializeScript()) { // 如果初始化失败,定期重试 const startupInterval = setInterval(() => { if (initializeScript()) { clearInterval(startupInterval); } }, 1000); } // 监听页面变化(新会话) const pageObserver = new MutationObserver(() => { // 检查是否是新会话或页面变化 if (document.querySelector(CHAT_SESSION_SELECTOR) && !document.querySelector('.delete-from-here-btn')) { setTimeout(initializeScript, 500); } }); pageObserver.observe(document.body, { childList: true, subtree: true }); })(); /** * 模块二:自动中文输出功能 */ (function() { 'use strict'; let processedTextareas = new WeakSet(); let isProcessing = false; let lastCheck = 0; function addText() { const now = Date.now(); if (isProcessing || now - lastCheck < 1000) return; lastCheck = now; isProcessing = true; try { let textarea = document.querySelector('textarea[aria-label="System instructions"]'); if (!textarea) { const button = document.querySelector('button[aria-label="System instructions"]'); if (button && !processedTextareas.has(button)) { button.click(); processedTextareas.add(button); setTimeout(() => { textarea = document.querySelector('textarea[aria-label="System instructions"]'); if (textarea && !textarea.value.includes('中文')) { processTextarea(textarea); } setTimeout(() => { const closeBtn = document.querySelector('button[aria-label="Close system instructions"]'); if (closeBtn) closeBtn.click(); }, 200); isProcessing = false; }, 100); return; } } else if (!textarea.value.includes('中文')) { processTextarea(textarea); } } catch (e) { console.error('[脚本错误]:', e); } isProcessing = false; } function processTextarea(textarea) { if (textarea && !processedTextareas.has(textarea) && !textarea.value.includes('中文')) { const text = '你始终用中文输出'; // 模拟真实用户输入 textarea.focus(); textarea.click(); // 使用 document.execCommand 或直接设置 if (document.execCommand) { textarea.value += (textarea.value ? '\n' : '') + text; document.execCommand('insertText', false, ''); } else { textarea.value += (textarea.value ? '\n' : '') + text; } // 触发多种事件 ['focus', 'input', 'change', 'blur', 'keyup'].forEach(eventType => { textarea.dispatchEvent(new Event(eventType, { bubbles: true, cancelable: true })); }); // 模拟键盘输入 textarea.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })); textarea.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', bubbles: true })); processedTextareas.add(textarea); console.log('[脚本] 已添加中文输出指令:', textarea.value); } } const observer = new MutationObserver(() => { if (!isProcessing) addText(); }); observer.observe(document.body, { childList: true, subtree: true }); addText(); setInterval(() => { if (!isProcessing) addText(); }, 3000); })(); /** * 模块三:HTML 预览功能 */ (function () { 'use strict'; // 定义一个全局唯一的策略对象 if (window.myHtmlPreviewPolicy === undefined) { window.myHtmlPreviewPolicy = null; if (window.trustedTypes && window.trustedTypes.createPolicy) { try { window.myHtmlPreviewPolicy = trustedTypes.createPolicy('html-preview-policy#userscript', { createHTML: input => input, }); } catch (e) { // 如果策略已存在 (例如脚本被注入两次),这会报错,但我们可以忽略 console.warn("Trusted Types 策略 'html-preview-policy#userscript' 可能已存在。"); } } } /** * 【关键修复】使用 document.createElement 安全地创建模态窗口 */ function createModal() { if (document.getElementById('html-preview-modal')) return; const modalContainer = document.createElement('div'); modalContainer.id = 'html-preview-modal'; const overlay = document.createElement('div'); overlay.className = 'modal-overlay'; overlay.style.cssText = `position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.6); z-index: 10000; display: none; align-items: center; justify-content: center;`; const content = document.createElement('div'); content.className = 'modal-content'; content.style.cssText = `background: white; border-radius: 8px; width: 90%; height: 90%; max-width: 1200px; display: flex; flex-direction: column; box-shadow: 0 5px 25px rgba(0, 0, 0, 0.3);`; const header = document.createElement('div'); header.className = 'modal-header'; header.style.cssText = `padding: 16px; border-bottom: 1px solid #e0e0e0; display: flex; justify-content: space-between; align-items: center; color: #333;`; const title = document.createElement('h3'); title.textContent = 'HTML 预览'; title.style.cssText = 'margin: 0; font-family: sans-serif;'; const closeBtn = document.createElement('button'); closeBtn.className = 'close-btn'; closeBtn.textContent = '×'; closeBtn.title = '关闭 (Esc)'; closeBtn.style.cssText = `background: none; border: none; font-size: 28px; cursor: pointer; color: #666; padding: 0 8px; line-height: 1;`; const iframe = document.createElement('iframe'); iframe.className = 'preview-frame'; iframe.style.cssText = 'width: 100%; flex-grow: 1; border: none;'; header.appendChild(title); header.appendChild(closeBtn); content.appendChild(header); content.appendChild(iframe); overlay.appendChild(content); modalContainer.appendChild(overlay); document.body.appendChild(modalContainer); function closeModal() { overlay.style.display = 'none'; iframe.srcdoc = ''; } closeBtn.addEventListener('click', closeModal); overlay.addEventListener('click', e => { if (e.target === overlay) closeModal(); }); document.addEventListener('keydown', e => { if (e.key === 'Escape' && overlay.style.display === 'flex') closeModal(); }); } function showPreview(htmlContent) { const modal = document.getElementById('html-preview-modal'); if (!modal) return; const overlay = modal.querySelector('.modal-overlay'); const iframe = modal.querySelector('.preview-frame'); const policy = window.myHtmlPreviewPolicy; if (policy) { iframe.srcdoc = policy.createHTML(htmlContent); } else { iframe.srcdoc = htmlContent; } overlay.style.display = 'flex'; } function isHtmlCodeBlock(codeBlock) { const titleElement = codeBlock.querySelector('mat-panel-title'); return titleElement && (titleElement.textContent.toLowerCase().includes('html') || titleElement.textContent.toLowerCase().includes('htm')); } function addPreviewButton(codeBlock) { if (!isHtmlCodeBlock(codeBlock) || codeBlock.querySelector('.html-preview-btn')) return; const actionsContainer = codeBlock.querySelector('.actions-container'); if (!actionsContainer) return; const previewBtn = document.createElement('button'); previewBtn.className = 'html-preview-btn'; previewBtn.title = '预览 HTML'; previewBtn.style.cssText = `background: none; border: none; cursor: pointer; padding: 8px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; color: var(--mat-icon-button-icon-color, currentColor); transition: background-color 0.2s;`; const iconSpan = document.createElement('span'); iconSpan.className = 'material-symbols-outlined notranslate'; iconSpan.style.fontSize = '20px'; iconSpan.textContent = 'visibility'; previewBtn.appendChild(iconSpan); previewBtn.addEventListener('mouseenter', () => { previewBtn.style.backgroundColor = 'rgba(0,0,0,0.08)'; }); previewBtn.addEventListener('mouseleave', () => { previewBtn.style.backgroundColor = 'transparent'; }); previewBtn.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); const codeElement = codeBlock.querySelector('code'); if (codeElement) showPreview(codeElement.textContent || ''); }); actionsContainer.appendChild(previewBtn); } function processCodeBlocks() { document.querySelectorAll('ms-code-block').forEach(addPreviewButton); } function initializeHtmlPreview() { console.log('AI Studio HTML 预览脚本: 初始化...'); createModal(); processCodeBlocks(); const observer = new MutationObserver(processCodeBlocks); observer.observe(document.body, { childList: true, subtree: true }); console.log('AI Studio HTML 预览脚本: 初始化成功。'); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializeHtmlPreview); } else { initializeHtmlPreview(); } })();