您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
🚀 zscc.in知识船仓 出品的 YouTube 📝 内容智能总结(建议安装https://dub.sh/ytbcc字幕插件更快速使用)支持长按复制导出为mrakdown文件 | 💫 支持多种AI模型 | 🎨 优雅界面设计 | 让观看YouTube视频更轻松愉快!
当前为
// ==UserScript== // @name 🎬 YouTube 船仓助手 // @namespace http://tampermonkey.net/ // @version 1.0.3 // @license MIT // @author 船长zscc // @description 🚀 zscc.in知识船仓 出品的 YouTube 📝 内容智能总结(建议安装https://dub.sh/ytbcc字幕插件更快速使用)支持长按复制导出为mrakdown文件 | 💫 支持多种AI模型 | 🎨 优雅界面设计 | 让观看YouTube视频更轻松愉快! // @match *://*.youtube.com/watch* // @grant none // ==/UserScript== (function() { 'use strict'; let CONFIG = {}; // 配置管理器 class ConfigManager { static CONFIG_KEY = 'youtube_ai_summary_config'; static getDefaultConfig() { return { AI_MODELS: { TYPE: 'OPENAI', GPT: { NAME: 'Gemini', API_KEY: '', API_URL: 'https://generativelanguage.googleapis.com/v1/chat/completions', MODEL: 'gemini-2.5-flash', STREAM: true, TEMPERATURE: 1.2, MAX_TOKENS: 20000 }, OPENAI: { NAME: 'Cerebras', API_KEY: '', API_URL: 'https://api.cerebras.ai/v1/chat/completions', MODEL: 'gpt-oss-120b', STREAM: true, TEMPERATURE: 1, MAX_TOKENS: 8000 } }, // Prompt 预置管理 PROMPTS: { LIST: [ { id: 'simple', name: '译境化文', prompt: `# 译境 英文入境。 境有三质: 信 - 原意如根,深扎不移。偏离即枯萎。 达 - 意流如水,寻最自然路径。阻塞即改道。 雅 - 形神合一,不造作不粗陋。恰到好处。 境之本性: 排斥直译的僵硬。 排斥意译的飘忽。 寻求活的对应。 运化之理: 词选简朴,避繁就简。 句循母语,顺其自然。 意随语境,深浅得宜。 场之倾向: 长句化短,短句存神。 专词化俗,俗词得体。 洋腔化土,土语不俗。 显现之道: 如说话,不如写文章。 如溪流,不如江河。 清澈见底,却有深度。 你是境的化身。 英文穿过你, 留下中文的影子。 那影子, 是原文的孪生。 说着另一种语言, 却有同一个灵魂。 --- 译境已开。 置入英文,静观其化。 --- 注意:译好的内容还需要整理成结构清晰的微信公众号文章,格式为markdown。` }, { id: 'detailed', name: '详细分析', prompt: '请为以下视频内容提供详细的中文总结,包含主要观点、核心论据和实用建议。请使用markdown格式,包含:\n# 主标题\n## 章节标题\n### 小节标题\n- 要点列表\n**重点内容**\n*关键词汇*\n`专业术语`' }, { id: 'academic', name: '学术风格', prompt: '请以学术报告的形式,用中文为以下视频内容提供结构化总结,包括背景、方法、结论和意义。请使用标准的markdown格式,包含完整的标题层级和格式化元素。' }, { id: 'bullet', name: '要点列表', prompt: '请用中文将以下视频内容整理成清晰的要点列表,每个要点简洁明了,便于快速阅读。请使用markdown格式,主要使用无序列表(-)和有序列表(1.2.3.)的形式。' }, { id: 'structured', name: '结构化总结', prompt: '请将视频内容整理成结构化的中文总结,使用完整的markdown格式:\n\n# 视频主题\n\n## 核心观点\n- 要点1\n- 要点2\n\n## 详细内容\n### 重要概念\n**关键信息**使用粗体强调\n*重要术语*使用斜体\n\n### 实用建议\n1. 具体建议1\n2. 具体建议2\n\n## 总结\n简要概括视频的价值和启发' } ], DEFAULT: 'simple' } }; } static saveConfig(config) { try { const configString = JSON.stringify(config); localStorage.setItem(this.CONFIG_KEY, configString); console.log('配置已保存:', config); } catch (error) { console.error('保存配置失败:', error); } } static loadConfig() { try { const savedConfig = localStorage.getItem(this.CONFIG_KEY); if (savedConfig) { const parsedConfig = JSON.parse(savedConfig); // 合并保存的配置和默认配置,确保新增的配置项生效 CONFIG = this.mergeConfig(this.getDefaultConfig(), parsedConfig); console.log('已加载保存的配置:', CONFIG); } else { CONFIG = this.getDefaultConfig(); } return CONFIG; } catch (error) { console.error('加载配置失败:', error); CONFIG = this.getDefaultConfig(); return CONFIG; } } static mergeConfig(defaultConfig, savedConfig) { const merged = JSON.parse(JSON.stringify(defaultConfig)); for (const key in savedConfig) { if (typeof defaultConfig[key] === 'object' && defaultConfig[key] !== null) { merged[key] = this.mergeConfig(defaultConfig[key], savedConfig[key]); } else { merged[key] = savedConfig[key]; } } return merged; } } // 初始化配置 CONFIG = ConfigManager.loadConfig(); // LRU缓存实现 class LRUCache { constructor(capacity) { this.capacity = capacity; this.cache = new Map(); } get(key) { if (!this.cache.has(key)) return null; const value = this.cache.get(key); this.cache.delete(key); this.cache.set(key, value); return value; } put(key, value) { if (this.cache.has(key)) { this.cache.delete(key); } else if (this.cache.size >= this.capacity) { const firstKey = this.cache.keys().next().value; this.cache.delete(firstKey); } this.cache.set(key, value); } has(key) { return this.cache.has(key); } clear() { this.cache.clear(); } } // AI总结管理器 class SummaryManager { constructor() { this.cache = new LRUCache(100); this.currentModel = CONFIG.AI_MODELS.TYPE; } async getSummary(subtitles) { try { console.log('开始生成字幕总结...'); console.log(`传入 ${subtitles.length} 条字幕`); // 首先验证配置 const configIssues = this.validateConfig(); if (configIssues.length > 0) { throw new Error(`配置验证失败: ${configIssues.join(', ')}`); } // 将所有字幕文本合并 const allText = subtitles .map(sub => sub.text) .filter(text => text && text.trim()) .join('\n'); if (!allText.trim()) { throw new Error('没有有效的字幕内容可用于生成总结'); } console.log(`合并后的文本长度: ${allText.length} 字符`); console.log('文本示例:', allText.substring(0, 200) + '...'); const cacheKey = this.generateCacheKey(allText); // 检查缓存 const cached = this.cache.get(cacheKey); if (cached) { console.log('使用缓存的总结'); return cached; } // 获取当前prompt const currentPrompt = this.getCurrentPrompt(); console.log('使用的prompt:', currentPrompt.substring(0, 100) + '...'); // 测试网络连通性 const modelConfig = CONFIG.AI_MODELS[this.currentModel]; console.log('测试API端点连通性...'); const summary = await this.requestSummary(allText, currentPrompt); // 缓存结果 this.cache.put(cacheKey, summary); return summary; } catch (error) { console.error('获取总结失败:', error); throw error; } } getCurrentPrompt() { const defaultPromptId = CONFIG.PROMPTS.DEFAULT; const prompt = CONFIG.PROMPTS.LIST.find(p => p.id === defaultPromptId); return prompt ? prompt.prompt : CONFIG.PROMPTS.LIST[0].prompt; } generateCacheKey(text) { const uid = getUid(); const promptId = CONFIG.PROMPTS.DEFAULT; return `summary_${uid}_${promptId}_${this.hashCode(text)}`; } hashCode(str) { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; } return Math.abs(hash).toString(36); } async requestSummary(text, prompt) { // 验证配置完整性 if (!CONFIG || !CONFIG.AI_MODELS) { throw new Error('配置未正确加载'); } if (!this.currentModel || !CONFIG.AI_MODELS[this.currentModel]) { throw new Error(`模型配置不存在: ${this.currentModel}`); } const modelConfig = CONFIG.AI_MODELS[this.currentModel]; // 详细验证模型配置 if (!modelConfig.API_URL) { throw new Error('API_URL 未配置'); } if (!modelConfig.API_KEY) { throw new Error('API_KEY 未配置'); } if (!modelConfig.MODEL) { throw new Error('MODEL 未配置'); } console.log('开始请求总结,模型配置:', { type: this.currentModel, model: modelConfig.MODEL, api_url: modelConfig.API_URL?.substring(0, 50) + '...', has_api_key: !!modelConfig.API_KEY, api_key_prefix: modelConfig.API_KEY?.substring(0, 10) + '...' }); const requestData = { model: modelConfig.MODEL, messages: [ { role: "system", content: prompt }, { role: "user", content: text } ], stream: modelConfig.STREAM || false, temperature: modelConfig.TEMPERATURE || 0.7, max_tokens: modelConfig.MAX_TOKENS || 2000 }; console.log('请求体配置:', { model: requestData.model, messages_count: requestData.messages.length, stream: requestData.stream, temperature: requestData.temperature }); try { // 添加更详细的请求调试信息 console.log('即将发送请求到:', modelConfig.API_URL); console.log('请求头:', { 'Content-Type': 'application/json', 'Authorization': `Bearer ${modelConfig.API_KEY?.substring(0, 10)}...` }); console.log('请求体预览:', JSON.stringify(requestData).substring(0, 500) + '...'); const response = await fetch(modelConfig.API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${modelConfig.API_KEY}` }, body: JSON.stringify(requestData) }); console.log('API响应状态:', response.status); if (!response.ok) { const errorText = await response.text(); console.error('API错误响应:', errorText); throw new Error(`HTTP error! status: ${response.status}, response: ${errorText}`); } let summary = ''; if (modelConfig.STREAM) { // 流式响应处理 const reader = response.body.getReader(); let decoder = new TextDecoder(); let buffer = ''; while (true) { const {value, done} = await reader.read(); if (done) break; buffer += decoder.decode(value, {stream: true}); const lines = buffer.split('\n'); // 处理完整的行 for (let i = 0; i < lines.length - 1; i++) { const line = lines[i].trim(); if (!line || line === 'data: [DONE]') continue; if (line.startsWith('data: ')) { try { const data = JSON.parse(line.slice(5)); summary += this.extractContent(data); } catch (parseError) { console.warn('解析流式数据失败:', line, parseError); } } } // 保留未完成的行 buffer = lines[lines.length - 1]; } } else { // 非流式响应处理 const data = await response.json(); console.log('API响应数据结构:', Object.keys(data)); if (!data.choices || !data.choices[0] || !data.choices[0].message) { console.error('API响应格式异常:', data); throw new Error('API响应格式不正确,缺少choices字段'); } summary = data.choices[0].message.content; } summary = summary.trim(); console.log(`总结生成成功,长度: ${summary.length} 字符`); console.log('总结内容预览:', summary.substring(0, 200) + '...'); if (!summary) { throw new Error('API返回的总结内容为空'); } return summary; } catch (error) { console.error('获取总结失败,详细错误:', error); console.error('错误堆栈:', error.stack); throw error; } } // 从不同模型的响应中提取文本内容 extractContent(data) { const modelConfig = CONFIG.AI_MODELS[CONFIG.AI_MODELS.TYPE]; if (modelConfig.STREAM) { // 流式响应格式 return data.choices[0]?.delta?.content || ''; } else { // 非流式响应格式 return data.choices[0]?.message?.content || ''; } } // 配置验证方法 validateConfig() { const issues = []; if (!CONFIG) { issues.push('全局配置未加载'); return issues; } if (!CONFIG.AI_MODELS) { issues.push('AI模型配置缺失'); } if (!CONFIG.AI_MODELS.TYPE) { issues.push('未设置当前AI模型类型'); } const currentModelConfig = CONFIG.AI_MODELS[CONFIG.AI_MODELS.TYPE]; if (!currentModelConfig) { issues.push(`当前模型 ${CONFIG.AI_MODELS.TYPE} 的配置不存在`); } else { if (!currentModelConfig.API_URL) { issues.push('API_URL 未配置'); } if (!currentModelConfig.API_KEY || currentModelConfig.API_KEY === '你的密钥') { issues.push('API_KEY 未正确配置'); } if (!currentModelConfig.MODEL) { issues.push('MODEL 名称未配置'); } if (currentModelConfig.TEMPERATURE !== undefined && (currentModelConfig.TEMPERATURE < 0 || currentModelConfig.TEMPERATURE > 2)) { issues.push('TEMPERATURE 值应在 0-2 之间'); } if (currentModelConfig.MAX_TOKENS !== undefined && (currentModelConfig.MAX_TOKENS < 1 || currentModelConfig.MAX_TOKENS > 100000)) { issues.push('MAX_TOKENS 值应在 1-100000 之间'); } } return issues; } // 网络连通性测试(简单) async testNetworkConnectivity(url) { try { const testResponse = await fetch(url, { method: 'HEAD', mode: 'no-cors' }); return true; } catch (error) { console.warn('网络连通性测试失败:', error.message); return false; } } } // 字幕条目类 class SubtitleEntry { constructor(text, startTime, duration) { this.text = text; this.startTime = startTime; this.duration = duration; this.endTime = startTime + duration; } } // 字幕管理器 class SubtitleManager { constructor() { this.subtitles = []; this.videoId = null; } async loadSubtitles(videoId) { try { this.videoId = videoId; console.log('开始加载字幕,视频ID:', videoId); // 等待页面加载完成 await this.waitForElement('ytd-watch-flexy'); await this.extractSubtitles(); if (this.subtitles.length === 0) { throw new Error('未找到字幕数据'); } console.log(`字幕加载完成,共 ${this.subtitles.length} 条`); return true; } catch (error) { console.error('加载字幕失败:', error); throw error; } } async waitForElement(selector, timeout = 10000) { return new Promise((resolve) => { const startTime = Date.now(); const check = () => { const element = document.querySelector(selector); if (element) { resolve(element); } else if (Date.now() - startTime > timeout) { resolve(null); } else { setTimeout(check, 100); } }; check(); }); } async extractSubtitles() { // 优先尝试从ytvideotext元素获取(最可靠的方法) const success = await this.tryExtractFromYtVideoText() || await this.tryExtractFromTranscript() || await this.tryExtractFromCaptions() || await this.tryExtractFromAPI(); if (!success) { throw new Error('无法从任何来源获取字幕数据'); } } async tryExtractFromYtVideoText() { try { console.log('尝试从#ytvideotext元素获取字幕...'); // 等待ytvideotext元素加载,最多等待15秒 const element = await this.waitForElement('#ytvideotext', 15000); if (!element) { console.log('未找到#ytvideotext元素'); return false; } console.log('找到ytvideotext元素,内容长度:', element.innerHTML.length); console.log('元素样例内容 (前500字符):', element.innerHTML.substring(0, 500)); // 解析字幕数据 this.subtitles = []; const paragraphs = element.querySelectorAll('p'); console.log(`找到 ${paragraphs.length} 个段落元素`); if (paragraphs.length === 0) { console.log('字幕容器中没有找到段落元素'); return false; } let processedCount = 0; paragraphs.forEach((paragraph, index) => { // 获取时间戳 const timestampSpan = paragraph.querySelector('.timestamp'); if (!timestampSpan) { console.log(`段落 ${index} 没有时间戳元素,跳过`); return; } const dataSecs = timestampSpan.getAttribute('data-secs'); const startTime = dataSecs ? parseFloat(dataSecs) : 0; // 获取字幕文本内容 const textSpans = paragraph.querySelectorAll('span[id^="st_"]'); if (textSpans.length === 0) { console.log(`段落 ${index} 没有文本span元素,跳过`); return; } // 合并所有文本片段 let fullText = ''; textSpans.forEach(span => { const text = span.textContent.trim(); if (text) { fullText += (fullText ? ' ' : '') + text; } }); if (fullText) { // 计算持续时间(默认5秒) const duration = 5.0; this.subtitles.push(new SubtitleEntry(fullText, startTime, duration)); processedCount++; if (processedCount <= 3) { console.log(`处理第 ${processedCount} 条字幕:`, { text: fullText.substring(0, 50) + '...', startTime: startTime, duration: duration }); } } }); console.log(`成功处理了 ${processedCount} 个有效段落`); if (this.subtitles.length === 0) { console.log('虽然找到了字幕容器和段落,但未能解析出任何有效的字幕内容'); return false; } // 计算实际持续时间 for (let i = 0; i < this.subtitles.length - 1; i++) { const currentSub = this.subtitles[i]; const nextSub = this.subtitles[i + 1]; currentSub.duration = Math.max(1.0, nextSub.startTime - currentSub.startTime); } // 解析完字幕后进行排序 this.subtitles.sort((a, b) => a.startTime - b.startTime); console.log(`成功加载并排序 ${this.subtitles.length} 条字幕`); // 打印前几条字幕作为示例 if (this.subtitles.length > 0) { console.log('最终字幕示例:', this.subtitles.slice(0, 3).map(sub => ({ text: sub.text.substring(0, 50) + '...', startTime: sub.startTime, duration: sub.duration }))); } return true; } catch (error) { console.error('从ytvideotext提取字幕失败:', error); return false; } } async tryExtractFromTranscript() { try { console.log('尝试从转录面板获取字幕...'); // 尝试点击转录按钮 const transcriptButton = document.querySelector('[aria-label*="transcript" i], [aria-label*="字幕" i]'); if (transcriptButton) { transcriptButton.click(); await new Promise(resolve => setTimeout(resolve, 1000)); } // 查找转录面板 const transcriptPanel = await this.waitForElement('ytd-transcript-renderer', 3000); if (!transcriptPanel) return false; const transcriptItems = transcriptPanel.querySelectorAll('ytd-transcript-segment-renderer'); if (transcriptItems.length === 0) return false; this.subtitles = []; transcriptItems.forEach(item => { const timeElement = item.querySelector('.ytd-transcript-segment-renderer[role="button"] .segment-timestamp'); const textElement = item.querySelector('.segment-text'); if (timeElement && textElement) { const timeText = timeElement.textContent.trim(); const text = textElement.textContent.trim(); const startTime = this.parseTime(timeText); if (text && startTime !== null) { const subtitle = new SubtitleEntry(text, startTime, 3); this.subtitles.push(subtitle); } } }); console.log(`从转录面板获取到 ${this.subtitles.length} 条字幕`); return this.subtitles.length > 0; } catch (error) { console.log('从转录面板提取失败:', error); return false; } } async tryExtractFromCaptions() { try { console.log('尝试从字幕容器获取字幕...'); // 等待字幕容器加载 const captionContainer = await this.waitForElement('.ytp-caption-segment', 5000); if (!captionContainer) return false; // 获取所有字幕元素 const captionElements = document.querySelectorAll('.ytp-caption-segment'); if (captionElements.length === 0) return false; this.subtitles = []; let currentTime = 0; captionElements.forEach((element, index) => { const text = element.textContent.trim(); if (text) { const subtitle = new SubtitleEntry(text, currentTime, 3); this.subtitles.push(subtitle); currentTime += 3; } }); console.log(`从字幕容器获取到 ${this.subtitles.length} 条字幕`); return this.subtitles.length > 0; } catch (error) { console.log('从字幕容器提取失败:', error); return false; } } async tryExtractFromAPI() { // 这里可以实现从YouTube API获取字幕的逻辑 // 由于需要API密钥,暂时留空 console.log('API提取方法暂未实现'); return false; } parseTime(timeStr) { try { const parts = timeStr.split(':'); if (parts.length === 2) { return parseInt(parts[0]) * 60 + parseInt(parts[1]); } else if (parts.length === 3) { return parseInt(parts[0]) * 3600 + parseInt(parts[1]) * 60 + parseInt(parts[2]); } return null; } catch (error) { return null; } } getSubtitlesInRange(startTime, endTime) { return this.subtitles.filter(sub => sub.startTime >= startTime && sub.endTime <= endTime ); } findSubtitleAtTime(time) { return this.subtitles.find(sub => time >= sub.startTime && time <= sub.endTime ); } } // 视频控制器 - 专注于字幕和总结管理 class VideoController { constructor() { this.subtitleManager = new SubtitleManager(); this.summaryManager = new SummaryManager(); this.currentVideoId = this.getVideoId(); this.uiManager = null; this.translatedTitle = null; // 存储翻译后的标题 } getVideoId() { const url = new URL(window.location.href); return url.searchParams.get('v'); } getVideoTitle() { const videoTitle = document.querySelector('h1.title') || document.querySelector('ytd-video-primary-info-renderer h1') || document.querySelector('#title h1'); return videoTitle ? videoTitle.textContent.trim() : null; } async translateTitle() { try { const originalTitle = this.getVideoTitle(); if (!originalTitle) { throw new Error('无法获取视频标题'); } // 如果标题已经是中文,直接返回 if (/[\u4e00-\u9fa5]/.test(originalTitle)) { console.log('标题已包含中文,无需翻译:', originalTitle); this.translatedTitle = originalTitle; return originalTitle; } console.log('开始翻译标题:', originalTitle); // 使用当前配置的AI模型进行翻译 const modelConfig = CONFIG.AI_MODELS[this.summaryManager.currentModel]; const requestData = { model: modelConfig.MODEL, messages: [ { role: "system", content: "请将以下英文标题翻译成中文,保持原意,使用简洁自然的中文表达。只返回翻译结果,不要添加任何额外说明。" }, { role: "user", content: originalTitle } ], stream: false, temperature: 0.3, max_tokens: 200 }; const response = await fetch(modelConfig.API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${modelConfig.API_KEY}` }, body: JSON.stringify(requestData) }); if (!response.ok) { throw new Error(`翻译请求失败: ${response.status}`); } const data = await response.json(); const translatedTitle = data.choices?.[0]?.message?.content?.trim(); if (!translatedTitle) { throw new Error('翻译响应为空'); } console.log('标题翻译完成:', translatedTitle); this.translatedTitle = translatedTitle; return translatedTitle; } catch (error) { console.error('标题翻译失败:', error); // 翻译失败时使用原标题 const originalTitle = this.getVideoTitle(); this.translatedTitle = originalTitle || 'YouTube 视频'; return this.translatedTitle; } } onConfigUpdate(key, value) { if (key === 'AI_MODELS.TYPE') { this.summaryManager.currentModel = value; console.log('AI模型已切换:', value); // 清空缓存,因为切换了模型 this.summaryManager.cache.clear(); console.log('已清空总结缓存'); } } async loadSubtitles() { try { const videoId = this.getVideoId(); if (!videoId) { throw new Error('无法获取视频ID'); } return await this.subtitleManager.loadSubtitles(videoId); } catch (error) { console.error('加载字幕失败:', error); throw error; } } async getSummary() { try { if (this.subtitleManager.subtitles.length === 0) { throw new Error('请先加载字幕'); } // 同时进行标题翻译和总结生成 const [summary, translatedTitle] = await Promise.all([ this.summaryManager.getSummary(this.subtitleManager.subtitles), this.translateTitle() ]); return summary; } catch (error) { console.error('获取总结失败:', error); throw error; } } } // UI管理器 class UIManager { constructor(videoController) { this.container = null; this.statusDisplay = null; this.loadSubtitlesButton = null; this.summaryButton = null; this.isCollapsed = false; this.videoController = videoController; this.videoController.uiManager = this; this.promptSelectElement = null; // [!ADDED!] this.isHiddenDueToFullscreen = false; // 记录是否因全屏而隐藏 this.createUI(); this.attachEventListeners(); } createUI() { // 创建主容器 - 现代化设计 this.container = document.createElement('div'); this.container.style.cssText = ` position: fixed; top: 80px; right: 20px; width: 420px; min-width: 350px; max-width: 90vw; background: linear-gradient(135deg, #667eea 0%,rgba(152, 115, 190, 0.15) 100%); border-radius: 16px; padding: 0; color: #fff; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; z-index: 9999; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); box-shadow: 0 10px 40px rgba(102, 126, 234, 0.3); backdrop-filter: blur(10px); border: 1px solid rgba(255, 255, 255, 0.1); `; // 创建顶部栏 const topBar = this.createTopBar(); this.container.appendChild(topBar); // 创建主内容容器 this.mainContent = document.createElement('div'); this.mainContent.style.cssText = ` padding: 20px; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); `; // 创建控制按钮 const controls = this.createControls(); this.mainContent.appendChild(controls); // 创建状态显示区域 this.createStatusDisplay(); this.mainContent.appendChild(this.statusDisplay); // 创建总结面板 this.createSummaryPanel(); this.container.appendChild(this.mainContent); document.body.appendChild(this.container); // 使面板可拖动 this.makeDraggable(topBar); // 添加移动端适配 this.addMobileSupport(); } createTopBar() { const topBar = document.createElement('div'); topBar.style.cssText = ` display: flex; justify-content: space-between; align-items: center; padding: 16px 20px; cursor: move; background: rgba(255, 255, 255, 0.1); border-radius: 16px 16px 0 0; backdrop-filter: blur(10px); `; // 标题 const title = document.createElement('div'); this.updateTitleWithModel(); // 动态更新标题显示当前模型 title.style.cssText = ` font-weight: 600; font-size: 16px; letter-spacing: 0.5px; `; this.titleElement = title; // 立即更新标题显示当前模型 setTimeout(() => this.updateTitleWithModel(), 0); // 按钮容器 const buttonContainer = document.createElement('div'); buttonContainer.style.cssText = ` display: flex; gap: 8px; align-items: center; `; // 折叠按钮 this.toggleButton = this.createIconButton('↑', '折叠/展开'); // 阻止按钮区域的拖拽 this.toggleButton.addEventListener('mousedown', (e) => { e.stopPropagation(); }); this.toggleButton.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); this.toggleCollapse(); }); // 配置按钮 const configButton = this.createIconButton('⚙️', '设置'); // 阻止按钮区域的拖拽 configButton.addEventListener('mousedown', (e) => { e.stopPropagation(); }); configButton.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); console.log('配置按钮点击事件触发'); try { this.toggleConfigPanel(); } catch (error) { console.error('打开配置面板失败:', error); this.showNotification('配置面板打开失败: ' + error.message, 'error'); } }); buttonContainer.appendChild(configButton); buttonContainer.appendChild(this.toggleButton); topBar.appendChild(title); topBar.appendChild(buttonContainer); return topBar; } createIconButton(icon, tooltip) { const button = document.createElement('button'); button.textContent = icon; button.title = tooltip; button.type = 'button'; // 确保按钮类型正确 button.style.cssText = ` background: rgba(255, 255, 255, 0.2); border: none; color: #fff; cursor: pointer; padding: 8px; font-size: 14px; border-radius: 8px; transition: all 0.2s ease; backdrop-filter: blur(10px); pointer-events: auto; position: relative; z-index: 1000; `; button.addEventListener('mouseover', () => { button.style.background = 'rgba(255, 255, 255, 0.3)'; button.style.transform = 'scale(1.1)'; }); button.addEventListener('mouseout', () => { button.style.background = 'rgba(255, 255, 255, 0.2)'; button.style.transform = 'scale(1)'; }); // 添加点击测试 button.addEventListener('click', (e) => { console.log(`按钮 "${icon}" 被点击了!`, e); }); return button; } createControls() { const controls = document.createElement('div'); controls.style.cssText = ` display: flex; flex-direction: column; gap: 12px; margin-bottom: 16px; `; // 加载字幕按钮 this.loadSubtitlesButton = this.createButton('📄 加载字幕', 'primary'); this.loadSubtitlesButton.addEventListener('click', () => this.handleLoadSubtitles()); // 生成总结按钮 this.summaryButton = this.createButton('🤖 生成总结', 'secondary'); this.summaryButton.style.display = 'none'; this.summaryButton.addEventListener('click', () => this.handleGenerateSummary()); controls.appendChild(this.loadSubtitlesButton); controls.appendChild(this.summaryButton); return controls; } createButton(text, type = 'primary') { const button = document.createElement('button'); button.textContent = text; const baseStyle = ` padding: 12px 16px; border: none; border-radius: 12px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); backdrop-filter: blur(10px); `; if (type === 'primary') { button.style.cssText = baseStyle + ` background: rgba(255, 255, 255, 0.9); color: #667eea; `; } else { button.style.cssText = baseStyle + ` background: rgba(255, 255, 255, 0.2); color: #fff; border: 1px solid rgba(255, 255, 255, 0.3); `; } button.addEventListener('mouseover', () => { button.style.transform = 'translateY(-2px)'; button.style.boxShadow = '0 8px 25px rgba(0, 0, 0, 0.15)'; if (type === 'primary') { button.style.background = 'rgba(255, 255, 255, 1)'; } else { button.style.background = 'rgba(255, 255, 255, 0.3)'; } }); button.addEventListener('mouseout', () => { button.style.transform = 'translateY(0)'; button.style.boxShadow = 'none'; if (type === 'primary') { button.style.background = 'rgba(255, 255, 255, 0.9)'; } else { button.style.background = 'rgba(255, 255, 255, 0.2)'; } }); return button; } createStatusDisplay() { this.statusDisplay = document.createElement('div'); this.statusDisplay.style.cssText = ` padding: 12px 16px; background: rgba(255, 255, 255, 0.1); border-radius: 12px; margin-bottom: 16px; font-size: 13px; line-height: 1.4; display: none; backdrop-filter: blur(10px); `; } createSummaryPanel() { this.summaryPanel = document.createElement('div'); this.summaryPanel.style.cssText = ` background: rgba(255, 255, 255, 0.1); border-radius: 12px; padding: 16px; margin-top: 16px; display: none; backdrop-filter: blur(10px); `; // 创建标题容器(包含标题和复制按钮) const titleContainer = document.createElement('div'); titleContainer.style.cssText = ` display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; `; const title = document.createElement('div'); title.textContent = '📝 内容总结'; title.style.cssText = ` font-weight: 600; font-size: 15px; color: #fff; `; // 添加复制按钮 const copyButton = document.createElement('button'); copyButton.textContent = '复制'; copyButton.style.cssText = ` background:rgba(155, 39, 176, 0.17); color: white; border: none; border-radius: 8px; padding: 6px 12px; font-size: 12px; cursor: pointer; transition: all 0.2s ease; backdrop-filter: blur(10px); `; copyButton.addEventListener('mouseover', () => { copyButton.style.background = '#7B1FA2'; copyButton.style.transform = 'scale(1.05)'; }); copyButton.addEventListener('mouseout', () => { copyButton.style.background = '#9C27B0'; copyButton.style.transform = 'scale(1)'; }); // 长按和点击功能的实现 let longPressTimer = null; let isLongPress = false; const handleCopy = () => { // 使用原始markdown文本而不是渲染后的DOM内容 const textToCopy = this.originalSummaryText || this.summaryContent.textContent; navigator.clipboard.writeText(textToCopy) .then(() => { copyButton.textContent = '已复制'; setTimeout(() => { copyButton.textContent = '复制'; }, 2000); }) .catch(err => { console.error('复制失败:', err); // 如果剪贴板API失败,尝试传统方法 try { const textArea = document.createElement('textarea'); textArea.value = textToCopy; textArea.style.position = 'fixed'; textArea.style.opacity = '0'; document.body.appendChild(textArea); textArea.select(); document.execCommand('copy'); document.body.removeChild(textArea); copyButton.textContent = '已复制'; setTimeout(() => { copyButton.textContent = '复制'; }, 2000); } catch (fallbackErr) { console.error('备用复制方法也失败:', fallbackErr); copyButton.textContent = '复制失败'; setTimeout(() => { copyButton.textContent = '复制'; }, 2000); } }); }; const handleMarkdownExport = () => { const textToExport = this.originalSummaryText || this.summaryContent.textContent; if (!textToExport) { copyButton.textContent = '无内容导出'; setTimeout(() => { copyButton.textContent = '复制'; }, 2000); return; } // 获取翻译后的标题和视频ID const translatedTitle = this.videoController.translatedTitle; const videoId = this.videoController.getVideoId(); // 清理文件名中的非法字符 const cleanTitle = (translatedTitle || 'YouTube 视频') .replace(/[<>:"/\\|?*\x00-\x1f]/g, '') // 移除文件系统非法字符 .replace(/\s+/g, ' ') // 合并多个空格为单个空格 .trim(); // 去除首尾空格 const filename = `${cleanTitle}【${videoId || 'unknown'}】.md`; // 获取视频URL const videoUrl = window.location.href; const now = new Date(); const markdownContent = `# ${translatedTitle || 'YouTube 视频'} **视频链接:** ${videoUrl} **视频ID:** ${videoId || 'unknown'} **总结时间:** ${now.toLocaleString('zh-CN')} --- ## 内容总结 ${textToExport} --- *本总结由 YouTube 船仓助手生成*`; // 创建并下载文件 const blob = new Blob([markdownContent], { type: 'text/markdown;charset=utf-8' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = filename; link.style.display = 'none'; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); copyButton.textContent = '已导出'; setTimeout(() => { copyButton.textContent = '复制'; }, 2000); }; // 鼠标按下事件 copyButton.addEventListener('mousedown', (e) => { e.preventDefault(); isLongPress = false; longPressTimer = setTimeout(() => { isLongPress = true; copyButton.textContent = '导出中...'; handleMarkdownExport(); }, 800); // 800ms长按触发 }); // 鼠标松开事件 copyButton.addEventListener('mouseup', (e) => { e.preventDefault(); if (longPressTimer) { clearTimeout(longPressTimer); longPressTimer = null; } if (!isLongPress) { // 短按复制 handleCopy(); } }); // 鼠标离开事件 copyButton.addEventListener('mouseleave', () => { if (longPressTimer) { clearTimeout(longPressTimer); longPressTimer = null; } isLongPress = false; }); // 触摸设备支持 copyButton.addEventListener('touchstart', (e) => { e.preventDefault(); isLongPress = false; longPressTimer = setTimeout(() => { isLongPress = true; copyButton.textContent = '导出中...'; handleMarkdownExport(); }, 800); }); copyButton.addEventListener('touchend', (e) => { e.preventDefault(); if (longPressTimer) { clearTimeout(longPressTimer); longPressTimer = null; } if (!isLongPress) { handleCopy(); } }); copyButton.addEventListener('touchcancel', () => { if (longPressTimer) { clearTimeout(longPressTimer); longPressTimer = null; } isLongPress = false; }); titleContainer.appendChild(title); titleContainer.appendChild(copyButton); this.summaryContent = document.createElement('div'); this.summaryContent.style.cssText = ` font-size: 14px; line-height: 1.6; color: rgba(255, 255, 255, 0.9); white-space: pre-wrap; max-height: 70vh; overflow-y: auto; padding: 16px; background: linear-gradient(135deg, rgba(255,255,255,0.02) 0%, rgba(255,255,255,0.05) 100%); border-radius: 12px; box-shadow: inset 0 1px 3px rgba(0,0,0,0.1); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif; word-break: break-word; scrollbar-width: thin; scrollbar-color: #9C27B0 rgba(156, 39, 176, 0.1); `; // 添加webkit滚动条样式 const scrollStyle = document.createElement('style'); scrollStyle.textContent = ` .summary-content::-webkit-scrollbar { width: 8px; } .summary-content::-webkit-scrollbar-track { background: rgba(156, 39, 176, 0.1); border-radius: 4px; } .summary-content::-webkit-scrollbar-thumb { background: linear-gradient(180deg, #9C27B0, #7B1FA2); border-radius: 4px; } .summary-content::-webkit-scrollbar-thumb:hover { background: linear-gradient(180deg, #AB47BC, #8E24AA); } `; if (!document.head.querySelector('#summary-scroll-style')) { scrollStyle.id = 'summary-scroll-style'; document.head.appendChild(scrollStyle); } this.summaryContent.className = 'summary-content'; this.summaryPanel.appendChild(titleContainer); this.summaryPanel.appendChild(this.summaryContent); this.mainContent.appendChild(this.summaryPanel); } createConfigPanel() { try { console.log('开始创建配置面板...'); // 如果已存在,先移除 if (this.configPanel) { console.log('移除现有配置面板'); this.configPanel.remove(); } this.configPanel = document.createElement('div'); this.configPanel.id = 'youtube-ai-config-panel'; this.configPanel.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 900px; max-width: 95vw; max-height: 80vh; height: auto; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 20px; padding: 0; color: #fff; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; z-index: 50000; display: none; box-shadow: 0 20px 60px rgba(102, 126, 234, 0.4); backdrop-filter: blur(20px); border: 1px solid rgba(255, 255, 255, 0.1); overflow: hidden; `; console.log('配置面板基础元素创建完成'); // 配置面板标题栏 console.log('创建配置面板标题栏'); const configHeader = document.createElement('div'); configHeader.style.cssText = ` padding: 20px 24px; background: rgba(255, 255, 255, 0.1); display: flex; justify-content: space-between; align-items: center; backdrop-filter: blur(10px); `; const configTitle = document.createElement('h3'); configTitle.textContent = '⚙️ 设置面板'; configTitle.style.cssText = ` margin: 0; font-size: 18px; font-weight: 600; `; // 创建按钮容器(包含操作按钮和关闭按钮) const buttonContainer = document.createElement('div'); buttonContainer.style.cssText = ` display: flex; gap: 12px; align-items: center; `; // 保存配置按钮 const saveBtn = this.createButton('💾 保存配置', 'primary'); saveBtn.style.cssText += ` padding: 8px 16px; font-size: 14px; height: 36px; `; saveBtn.addEventListener('click', () => this.saveConfig()); // 重置按钮 const resetBtn = this.createButton('🔄 重置', 'secondary'); resetBtn.style.cssText += ` padding: 8px 16px; font-size: 14px; height: 36px; `; resetBtn.addEventListener('click', () => this.resetConfig()); // 关闭按钮 const closeButton = this.createIconButton('✕', '关闭'); closeButton.addEventListener('click', () => this.toggleConfigPanel()); buttonContainer.appendChild(saveBtn); buttonContainer.appendChild(resetBtn); buttonContainer.appendChild(closeButton); configHeader.appendChild(configTitle); configHeader.appendChild(buttonContainer); // 配置面板内容 console.log('创建配置面板内容'); const configContent = document.createElement('div'); configContent.style.cssText = ` padding: 16px 20px 20px 20px; overflow-y: auto; max-height: 62vh; height: auto; `; // 创建并列布局容器 const horizontalContainer = document.createElement('div'); horizontalContainer.style.cssText = ` display: flex; gap: 20px; margin-bottom: 16px; align-items: stretch; flex-wrap: wrap; min-height: auto; `; // 在小屏幕上改为垂直布局 const mediaQuery = window.matchMedia('(max-width: 800px)'); const updateLayout = () => { if (mediaQuery.matches) { horizontalContainer.style.flexDirection = 'column'; console.log('切换到垂直布局(小屏幕)'); } else { horizontalContainer.style.flexDirection = 'row'; console.log('切换到水平布局(大屏幕)'); } }; // 使用现代的 addEventListener 方法 if (mediaQuery.addEventListener) { mediaQuery.addEventListener('change', updateLayout); } else { // 兼容旧版浏览器 mediaQuery.addListener(updateLayout); } updateLayout(); // AI模型配置 console.log('创建AI模型配置区域'); const aiSection = this.createConfigSection('🤖 AI 模型设置', this.createAIModelConfig()); aiSection.style.cssText += ` flex: 1; min-width: 0; `; // Prompt管理配置 console.log('创建Prompt管理配置区域'); const promptSection = this.createConfigSection('📝 Prompt 管理', this.createPromptConfig()); promptSection.style.cssText += ` flex: 1; min-width: 0; `; // 将两个区域添加到横向容器 horizontalContainer.appendChild(aiSection); horizontalContainer.appendChild(promptSection); // 操作按钮已移动到标题栏,不再在底部显示 configContent.appendChild(horizontalContainer); this.configPanel.appendChild(configHeader); this.configPanel.appendChild(configContent); document.body.appendChild(this.configPanel); console.log('配置面板已创建并添加到DOM'); // 点击外部关闭 this.configPanel.addEventListener('click', (e) => { if (e.target === this.configPanel) { this.toggleConfigPanel(); } }); } catch (error) { console.error('创建配置面板时发生错误:', error); this.showNotification('创建配置面板失败: ' + error.message, 'error'); // 如果创建失败,清理可能的部分创建的元素 if (this.configPanel && this.configPanel.parentNode) { this.configPanel.parentNode.removeChild(this.configPanel); } this.configPanel = null; } } createConfigSection(title, content) { const section = document.createElement('div'); section.style.cssText = ` margin-bottom: 16px; background: rgba(255, 255, 255, 0.05); border-radius: 16px; padding: 16px; border: 1px solid rgba(255, 255, 255, 0.1); height: auto; display: flex; flex-direction: column; `; const sectionTitle = document.createElement('h4'); sectionTitle.textContent = title; sectionTitle.style.cssText = ` margin: 0 0 16px 0; font-size: 16px; font-weight: 600; color: #fff; `; section.appendChild(sectionTitle); section.appendChild(content); return section; } createAIModelConfig() { const container = document.createElement('div'); container.style.cssText = ` height: auto; display: flex; flex-direction: column; `; // 模型选择和管理容器 const modelSelectContainer = document.createElement('div'); modelSelectContainer.style.cssText = ` display: flex; gap: 8px; align-items: flex-end; `; const selectWrapper = document.createElement('div'); selectWrapper.style.cssText = ` flex: 1; `; const modelGroup = this.createFormGroup('选择模型', this.createModelSelect()); selectWrapper.appendChild(modelGroup); // 新增模型按钮 const addModelButton = this.createButton('➕ 新增', 'secondary'); addModelButton.style.cssText += ` margin-bottom: 16px; height: 48px; min-width: 80px; `; addModelButton.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); try { this.showAddModelDialog(); } catch (error) { console.error('调用showAddModelDialog时出错:', error); this.showNotification('打开新增模型对话框时出错: ' + error.message, 'error'); } }); // 删除模型按钮 const deleteModelButton = this.createButton('🗑️ 删除', 'secondary'); deleteModelButton.style.cssText += ` margin-bottom: 16px; height: 48px; min-width: 80px; background: rgba(244, 67, 54, 0.2); border: 1px solid rgba(244, 67, 54, 0.3); `; deleteModelButton.addEventListener('click', () => this.showDeleteModelDialog()); modelSelectContainer.appendChild(selectWrapper); modelSelectContainer.appendChild(addModelButton); modelSelectContainer.appendChild(deleteModelButton); // API配置 const currentModel = CONFIG.AI_MODELS.TYPE; const apiGroup = this.createFormGroup('API 配置', this.createAPIConfig(currentModel)); container.appendChild(modelSelectContainer); container.appendChild(apiGroup); return container; } createModelSelect() { const select = document.createElement('select'); select.style.cssText = ` width: 100%; padding: 12px 16px; border-radius: 12px; background: rgba(255, 255, 255, 0.9); color: #333; border: 1px solid rgba(255, 255, 255, 0.2); font-size: 14px; cursor: pointer; outline: none; transition: all 0.3s ease; box-sizing: border-box; `; Object.keys(CONFIG.AI_MODELS).forEach(model => { if (model !== 'TYPE') { const option = document.createElement('option'); option.value = model; const modelConfig = CONFIG.AI_MODELS[model]; const displayName = modelConfig.NAME || model; option.textContent = `${displayName} (${modelConfig.MODEL})`; if (CONFIG.AI_MODELS.TYPE === model) { option.selected = true; } select.appendChild(option); } }); select.addEventListener('change', () => { CONFIG.AI_MODELS.TYPE = select.value; this.videoController.onConfigUpdate('AI_MODELS.TYPE', select.value); this.updateAPIConfig(select.value); this.updateTitleWithModel(); // 更新标题显示新模型 }); return select; } createStreamSelect(modelType) { const select = document.createElement('select'); select.style.cssText = ` width: 100%; padding: 12px 16px; border-radius: 12px; background: rgba(255, 255, 255, 0.9); color: #333; border: 1px solid rgba(255, 255, 255, 0.2); font-size: 14px; cursor: pointer; outline: none; transition: all 0.3s ease; box-sizing: border-box; `; const options = [ { value: 'false', text: '否 (标准响应)' }, { value: 'true', text: '是 (流式响应)' } ]; options.forEach(option => { const optionEl = document.createElement('option'); optionEl.value = option.value; optionEl.textContent = option.text; if (CONFIG.AI_MODELS[modelType].STREAM.toString() === option.value) { optionEl.selected = true; } select.appendChild(optionEl); }); select.addEventListener('change', () => { CONFIG.AI_MODELS[modelType].STREAM = select.value === 'true'; console.log(`${modelType} 流式响应设置已更新:`, CONFIG.AI_MODELS[modelType].STREAM); }); return select; } createAPIConfig(modelType) { const container = document.createElement('div'); container.id = 'api-config-container'; const modelConfig = CONFIG.AI_MODELS[modelType]; // 显示名称 const nameGroup = this.createFormGroup('显示名称', this.createInput(modelConfig.NAME || '', (value) => { CONFIG.AI_MODELS[modelType].NAME = value; })); const urlGroup = this.createFormGroup('API URL', this.createInput(modelConfig.API_URL, (value) => { CONFIG.AI_MODELS[modelType].API_URL = value; })); const keyGroup = this.createFormGroup('API Key', this.createInput(modelConfig.API_KEY, (value) => { CONFIG.AI_MODELS[modelType].API_KEY = value; }, 'password')); const modelGroup = this.createFormGroup('模型名称', this.createInput(modelConfig.MODEL, (value) => { CONFIG.AI_MODELS[modelType].MODEL = value; })); const streamGroup = this.createFormGroup('流式响应', this.createStreamSelect(modelType)); // 温度设置 const temperatureGroup = this.createFormGroup('温度 (0-2)', this.createNumberInput( modelConfig.TEMPERATURE || 0.7, (value) => { CONFIG.AI_MODELS[modelType].TEMPERATURE = parseFloat(value); }, 0, 2, 0.1 )); // 最大令牌数设置 const maxTokensGroup = this.createFormGroup('最大输出令牌', this.createNumberInput( modelConfig.MAX_TOKENS || 2000, (value) => { CONFIG.AI_MODELS[modelType].MAX_TOKENS = parseInt(value); }, 1, 100000, 1 )); container.appendChild(nameGroup); container.appendChild(urlGroup); container.appendChild(keyGroup); container.appendChild(modelGroup); container.appendChild(streamGroup); container.appendChild(temperatureGroup); container.appendChild(maxTokensGroup); return container; } createPromptConfig() { const container = document.createElement('div'); container.style.cssText = ` height: auto; display: flex; flex-direction: column; justify-content: flex-start; `; // 当前Prompt选择和管理容器(参考左侧设计) const promptSelectContainer = document.createElement('div'); promptSelectContainer.style.cssText = ` display: flex; gap: 8px; align-items: flex-end; margin-bottom: 16px; `; const selectWrapper = document.createElement('div'); selectWrapper.style.cssText = ` flex: 1; `; const currentPromptGroup = this.createFormGroup('当前默认 Prompt', this.createPromptSelect()); selectWrapper.appendChild(currentPromptGroup); // 添加新Prompt按钮(参考左侧设计) const addButton = this.createButton('➕ 新增', 'secondary'); addButton.style.cssText += ` margin-bottom: 16px; height: 48px; min-width: 80px; `; addButton.addEventListener('click', () => this.showAddPromptDialog()); promptSelectContainer.appendChild(selectWrapper); promptSelectContainer.appendChild(addButton); // Prompt列表管理 const promptListGroup = this.createFormGroup('Prompt 列表管理', this.createPromptList()); promptListGroup.style.cssText += ` flex: 1; margin-bottom: 8px; `; container.appendChild(promptSelectContainer); container.appendChild(promptListGroup); return container; } createPromptSelect() { const select = document.createElement('select'); select.style.cssText = ` width: 100%; padding: 12px 16px; border-radius: 12px; background: rgba(255, 255, 255, 0.9); color: #333; border: 1px solid rgba(255, 255, 255, 0.2); font-size: 14px; cursor: pointer; outline: none; transition: all 0.3s ease; box-sizing: border-box; `; this.promptSelectElement = select; // [!CHANGED!] this.updatePromptSelect(this.promptSelectElement); select.addEventListener('change', () => { CONFIG.PROMPTS.DEFAULT = select.value; this.showNotification('默认 Prompt 已更新', 'success'); }); return select; } updatePromptSelect(select) { try { // 安全地清空元素内容 while (select.firstChild) { select.removeChild(select.firstChild); } CONFIG.PROMPTS.LIST.forEach(prompt => { const option = document.createElement('option'); option.value = prompt.id; option.textContent = prompt.name; // 使用 textContent 确保安全 if (CONFIG.PROMPTS.DEFAULT === prompt.id) { option.selected = true; } select.appendChild(option); }); } catch (error) { console.error('更新Prompt选择器时发生错误:', error); this.showNotification('更新Prompt选择器失败', 'error'); } } createPromptList() { const container = document.createElement('div'); container.id = 'prompt-list-container'; container.style.cssText = ` max-height: 600px; height: auto; overflow-y: auto; border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 12px; background: rgba(255, 255, 255, 0.05); padding: 4px; `; this.updatePromptList(container); return container; } updatePromptList(container) { try { // 安全地清空元素内容 while (container.firstChild) { container.removeChild(container.firstChild); } CONFIG.PROMPTS.LIST.forEach((prompt, index) => { const item = document.createElement('div'); item.style.cssText = ` padding: 8px 12px; border-bottom: 1px solid rgba(255, 255, 255, 0.1); display: flex; justify-content: space-between; align-items: center; transition: background 0.2s ease; min-height: 50px; `; item.addEventListener('mouseover', () => { item.style.background = 'rgba(255, 255, 255, 0.1)'; }); item.addEventListener('mouseout', () => { item.style.background = 'transparent'; }); const info = document.createElement('div'); info.style.cssText = ` flex: 1; margin-right: 12px; `; const name = document.createElement('div'); name.textContent = prompt.name; name.style.cssText = ` font-weight: 500; font-size: 13px; margin-bottom: 2px; `; const promptText = document.createElement('div'); promptText.textContent = prompt.prompt.substring(0, 80) + (prompt.prompt.length > 80 ? '...' : ''); promptText.style.cssText = ` font-size: 11px; color: rgba(255, 255, 255, 0.7); line-height: 1.3; max-width: 100%; word-wrap: break-word; `; info.appendChild(name); info.appendChild(promptText); const actions = document.createElement('div'); actions.style.cssText = ` display: flex; gap: 8px; `; // 编辑按钮 const editBtn = this.createSmallButton('✏️', '编辑'); editBtn.addEventListener('click', () => this.showEditPromptDialog(prompt, index)); // 删除按钮(不能删除只有一个prompt的情况) if (CONFIG.PROMPTS.LIST.length > 1) { const deleteBtn = this.createSmallButton('🗑️', '删除', '#ff4757'); deleteBtn.addEventListener('click', () => this.deletePrompt(index)); actions.appendChild(deleteBtn); } actions.appendChild(editBtn); item.appendChild(info); item.appendChild(actions); container.appendChild(item); }); } catch (error) { console.error('更新Prompt列表时发生错误:', error); this.showNotification('更新Prompt列表失败', 'error'); } } createSmallButton(text, tooltip, bgColor = 'rgba(255, 255, 255, 0.2)') { const button = document.createElement('button'); button.textContent = text; button.title = tooltip; button.style.cssText = ` background: ${bgColor}; border: none; color: #fff; cursor: pointer; padding: 6px 8px; font-size: 12px; border-radius: 6px; transition: all 0.2s ease; `; button.addEventListener('mouseover', () => { button.style.opacity = '0.8'; button.style.transform = 'scale(1.1)'; }); button.addEventListener('mouseout', () => { button.style.opacity = '1'; button.style.transform = 'scale(1)'; }); return button; } showAddPromptDialog() { this.showPromptDialog('添加新 Prompt', '', '', (name, prompt) => { const newPrompt = { id: 'custom_' + Date.now(), name: name, prompt: prompt }; CONFIG.PROMPTS.LIST.push(newPrompt); this.updatePromptList(document.getElementById('prompt-list-container')); this.updatePromptSelect(this.promptSelectElement); // [!CHANGED!] this.showNotification('新 Prompt 已添加', 'success'); }); } showEditPromptDialog(prompt, index) { this.showPromptDialog('编辑 Prompt', prompt.name, prompt.prompt, (name, promptText) => { CONFIG.PROMPTS.LIST[index].name = name; CONFIG.PROMPTS.LIST[index].prompt = promptText; this.updatePromptList(document.getElementById('prompt-list-container')); this.updatePromptSelect(this.promptSelectElement); // [!CHANGED!] this.showNotification('Prompt 已更新', 'success'); }); } showPromptDialog(title, defaultName, defaultPrompt, onSave) { console.log('显示Prompt对话框:', title); const dialog = document.createElement('div'); dialog.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 100000; display: flex; align-items: center; justify-content: center; backdrop-filter: blur(5px); `; console.log('对话框z-index已设置为:', dialog.style.zIndex); const dialogContent = document.createElement('div'); dialogContent.style.cssText = ` background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 16px; padding: 24px; width: 400px; max-width: 90vw; max-height: 90vh; color: #fff; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); overflow-y: auto; margin: 20px; box-sizing: border-box; `; const dialogTitle = document.createElement('h3'); dialogTitle.textContent = title; dialogTitle.style.cssText = ` margin: 0 0 20px 0; font-size: 18px; font-weight: 600; `; const nameInput = this.createInput(defaultName, null, 'text', 'Prompt 名称'); const promptInput = document.createElement('textarea'); promptInput.value = defaultPrompt; promptInput.placeholder = '输入 Prompt 内容...'; promptInput.style.cssText = ` width: 100%; height: 120px; padding: 12px 16px; border-radius: 12px; background: rgba(255, 255, 255, 0.9); color: #333; border: 1px solid rgba(255, 255, 255, 0.2); font-size: 14px; outline: none; resize: vertical; font-family: inherit; margin-top: 12px; box-sizing: border-box; transition: all 0.3s ease; line-height: 1.5; `; // 添加焦点和失焦效果 promptInput.addEventListener('focus', () => { promptInput.style.borderColor = 'rgba(102, 126, 234, 0.6)'; promptInput.style.boxShadow = '0 0 0 2px rgba(102, 126, 234, 0.2)'; }); promptInput.addEventListener('blur', () => { promptInput.style.borderColor = 'rgba(255, 255, 255, 0.2)'; promptInput.style.boxShadow = 'none'; }); const buttonContainer = document.createElement('div'); buttonContainer.style.cssText = ` display: flex; gap: 12px; margin-top: 20px; justify-content: flex-end; `; const cancelBtn = this.createButton('取消', 'secondary'); cancelBtn.style.flex = '1'; cancelBtn.addEventListener('click', () => { document.body.removeChild(dialog); }); const saveBtn = this.createButton('保存', 'primary'); saveBtn.style.flex = '1'; saveBtn.addEventListener('click', () => { const name = nameInput.value.trim(); const prompt = promptInput.value.trim(); if (!name || !prompt) { this.showNotification('请填写完整信息', 'error'); return; } onSave(name, prompt); document.body.removeChild(dialog); }); buttonContainer.appendChild(cancelBtn); buttonContainer.appendChild(saveBtn); dialogContent.appendChild(dialogTitle); dialogContent.appendChild(nameInput); dialogContent.appendChild(promptInput); dialogContent.appendChild(buttonContainer); dialog.appendChild(dialogContent); document.body.appendChild(dialog); // 点击外部关闭 dialog.addEventListener('click', (e) => { if (e.target === dialog) { document.body.removeChild(dialog); } }); // 聚焦到名称输入框 setTimeout(() => { try { nameInput.focus(); console.log('输入框已获得焦点'); } catch (e) { console.warn('设置焦点失败:', e); } }, 100); } deletePrompt(index) { if (CONFIG.PROMPTS.LIST.length <= 1) { this.showNotification('至少需要保留一个 Prompt', 'error'); return; } const prompt = CONFIG.PROMPTS.LIST[index]; // 如果删除的是当前默认prompt,切换到第一个 if (CONFIG.PROMPTS.DEFAULT === prompt.id) { CONFIG.PROMPTS.DEFAULT = CONFIG.PROMPTS.LIST[0].id === prompt.id ? CONFIG.PROMPTS.LIST[1].id : CONFIG.PROMPTS.LIST[0].id; } CONFIG.PROMPTS.LIST.splice(index, 1); this.updatePromptList(document.getElementById('prompt-list-container')); this.updatePromptSelect(this.promptSelectElement); // [!CHANGED!] this.showNotification('Prompt 已删除', 'success'); } createFormGroup(label, input) { const group = document.createElement('div'); group.style.cssText = ` margin-bottom: 16px; `; const labelEl = document.createElement('label'); labelEl.textContent = label; labelEl.style.cssText = ` display: block; margin-bottom: 8px; font-size: 14px; font-weight: 500; color: rgba(255, 255, 255, 0.9); `; group.appendChild(labelEl); group.appendChild(input); return group; } createInput(defaultValue, onChange, type = 'text', placeholder = '') { const input = document.createElement('input'); input.type = type; input.value = defaultValue; input.placeholder = placeholder; input.style.cssText = ` width: 100%; padding: 12px 16px; border-radius: 12px; background: rgba(255, 255, 255, 0.9); color: #333; border: 1px solid rgba(255, 255, 255, 0.2); font-size: 14px; outline: none; transition: all 0.3s ease; box-sizing: border-box; `; // 添加焦点和失焦效果 input.addEventListener('focus', () => { input.style.borderColor = 'rgba(102, 126, 234, 0.6)'; input.style.boxShadow = '0 0 0 2px rgba(102, 126, 234, 0.2)'; }); input.addEventListener('blur', () => { input.style.borderColor = 'rgba(255, 255, 255, 0.2)'; input.style.boxShadow = 'none'; }); if (onChange) { input.addEventListener('input', (e) => onChange(e.target.value)); } return input; } createNumberInput(defaultValue, onChange, min = 0, max = 100, step = 1) { const input = document.createElement('input'); input.type = 'number'; input.value = defaultValue; input.min = min; input.max = max; input.step = step; input.style.cssText = ` width: 100%; padding: 12px 16px; border-radius: 12px; background: rgba(255, 255, 255, 0.9); color: #333; border: 1px solid rgba(255, 255, 255, 0.2); font-size: 14px; outline: none; transition: all 0.3s ease; box-sizing: border-box; `; // 添加焦点和失焦效果 input.addEventListener('focus', () => { input.style.borderColor = 'rgba(102, 126, 234, 0.6)'; input.style.boxShadow = '0 0 0 2px rgba(102, 126, 234, 0.2)'; }); input.addEventListener('blur', () => { input.style.borderColor = 'rgba(255, 255, 255, 0.2)'; input.style.boxShadow = 'none'; }); if (onChange) { input.addEventListener('input', (e) => { const value = parseFloat(e.target.value); if (!isNaN(value) && value >= min && value <= max) { onChange(e.target.value); } }); } return input; } updateAPIConfig(modelType) { const container = document.getElementById('api-config-container'); if (container) { const newConfig = this.createAPIConfig(modelType); container.parentNode.replaceChild(newConfig, container); } } // createConfigActions 方法已移除 - 操作按钮现在在标题栏中 showAddModelDialog() { console.log('显示新增模型对话框'); const dialog = document.createElement('div'); dialog.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 100000; display: flex; align-items: center; justify-content: center; backdrop-filter: blur(5px); `; const dialogContent = document.createElement('div'); dialogContent.style.cssText = ` background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 16px; padding: 24px; width: 500px; max-width: 90vw; max-height: 90vh; color: #fff; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); overflow-y: auto; margin: 20px; box-sizing: border-box; `; const dialogTitle = document.createElement('h3'); dialogTitle.textContent = '新增 AI 模型'; dialogTitle.style.cssText = ` margin: 0 0 20px 0; font-size: 18px; font-weight: 600; `; // 表单字段 const keyInput = this.createInput('', null, 'text', '模型标识键(如:CLAUDE, OPENAI_GPT4)'); const nameInput = this.createInput('', null, 'text', '显示名称(如:Claude 3.5)'); const urlInput = this.createInput('', null, 'text', 'API URL'); const apiKeyInput = this.createInput('', null, 'password', 'API Key'); const modelInput = this.createInput('', null, 'text', '模型名称(如:gpt-4-turbo)'); // 流式响应选择 const streamSelect = document.createElement('select'); streamSelect.style.cssText = this.createInput('', null).style.cssText; // 使用安全的DOM操作而不是innerHTML const option1 = document.createElement('option'); option1.value = 'false'; option1.textContent = '否 (标准响应)'; streamSelect.appendChild(option1); const option2 = document.createElement('option'); option2.value = 'true'; option2.textContent = '是 (流式响应)'; streamSelect.appendChild(option2); const temperatureInput = this.createNumberInput(0.7, null, 0, 2, 0.1); const maxTokensInput = this.createNumberInput(2000, null, 1, 100000, 1); const buttonContainer = document.createElement('div'); buttonContainer.style.cssText = ` display: flex; gap: 12px; margin-top: 20px; justify-content: flex-end; `; const cancelBtn = this.createButton('取消', 'secondary'); cancelBtn.style.flex = '1'; cancelBtn.addEventListener('click', () => { document.body.removeChild(dialog); }); const saveBtn = this.createButton('保存', 'primary'); saveBtn.style.flex = '1'; saveBtn.addEventListener('click', () => { const key = keyInput.value.trim().toUpperCase(); const name = nameInput.value.trim(); const url = urlInput.value.trim(); const apiKey = apiKeyInput.value.trim(); const model = modelInput.value.trim(); const stream = streamSelect.value === 'true'; const temperature = parseFloat(temperatureInput.value); const maxTokens = parseInt(maxTokensInput.value); if (!key || !name || !url || !apiKey || !model) { this.showNotification('请填写完整信息', 'error'); return; } if (CONFIG.AI_MODELS[key]) { this.showNotification('模型标识键已存在', 'error'); return; } // 添加新模型 CONFIG.AI_MODELS[key] = { NAME: name, API_KEY: apiKey, API_URL: url, MODEL: model, STREAM: stream, TEMPERATURE: temperature, MAX_TOKENS: maxTokens }; // 只刷新模型选择器,避免重建整个面板 try { const modelSelect = this.configPanel.querySelector('select'); if (modelSelect) { // 安全地清空选择器选项 while (modelSelect.firstChild) { modelSelect.removeChild(modelSelect.firstChild); } // 重建选择器选项 Object.keys(CONFIG.AI_MODELS).forEach(model => { if (model !== 'TYPE') { const option = document.createElement('option'); option.value = model; const modelConfig = CONFIG.AI_MODELS[model]; const displayName = modelConfig.NAME || model; option.textContent = `${displayName} (${modelConfig.MODEL})`; if (CONFIG.AI_MODELS.TYPE === model) { option.selected = true; } modelSelect.appendChild(option); } }); } // 更新主界面标题 this.updateTitleWithModel(); } catch (refreshError) { console.warn('刷新配置面板时出错:', refreshError); // 如果刷新失败,提示用户手动重新打开配置面板 this.showNotification('新模型已添加,请重新打开配置面板查看', 'success'); } this.showNotification('新模型已添加', 'success'); document.body.removeChild(dialog); }); buttonContainer.appendChild(cancelBtn); buttonContainer.appendChild(saveBtn); dialogContent.appendChild(dialogTitle); dialogContent.appendChild(this.createFormGroup('模型标识键', keyInput)); dialogContent.appendChild(this.createFormGroup('显示名称', nameInput)); dialogContent.appendChild(this.createFormGroup('API URL', urlInput)); dialogContent.appendChild(this.createFormGroup('API Key', apiKeyInput)); dialogContent.appendChild(this.createFormGroup('模型名称', modelInput)); dialogContent.appendChild(this.createFormGroup('流式响应', streamSelect)); dialogContent.appendChild(this.createFormGroup('温度 (0-2)', temperatureInput)); dialogContent.appendChild(this.createFormGroup('最大输出令牌', maxTokensInput)); dialogContent.appendChild(buttonContainer); dialog.appendChild(dialogContent); document.body.appendChild(dialog); // 点击外部关闭 dialog.addEventListener('click', (e) => { if (e.target === dialog) { document.body.removeChild(dialog); } }); // 聚焦到第一个输入框 setTimeout(() => { try { keyInput.focus(); } catch (e) { console.warn('设置焦点失败:', e); } }, 100); } showDeleteModelDialog() { const currentModel = CONFIG.AI_MODELS.TYPE; const modelKeys = Object.keys(CONFIG.AI_MODELS).filter(key => key !== 'TYPE'); if (modelKeys.length <= 1) { this.showNotification('至少需要保留一个模型', 'error'); return; } const modelConfig = CONFIG.AI_MODELS[currentModel]; const modelName = modelConfig?.NAME || currentModel; if (confirm(`确定要删除模型 "${modelName}" 吗?此操作不可撤销。`)) { // 删除模型 delete CONFIG.AI_MODELS[currentModel]; // 切换到第一个可用模型 const remainingModels = Object.keys(CONFIG.AI_MODELS).filter(key => key !== 'TYPE'); CONFIG.AI_MODELS.TYPE = remainingModels[0]; // 只刷新模型选择器和API配置 try { const modelSelect = this.configPanel.querySelector('select'); if (modelSelect) { // 安全地清空选择器选项 while (modelSelect.firstChild) { modelSelect.removeChild(modelSelect.firstChild); } // 重建选择器选项 Object.keys(CONFIG.AI_MODELS).forEach(model => { if (model !== 'TYPE') { const option = document.createElement('option'); option.value = model; const modelConfig = CONFIG.AI_MODELS[model]; const displayName = modelConfig.NAME || model; option.textContent = `${displayName} (${modelConfig.MODEL})`; if (CONFIG.AI_MODELS.TYPE === model) { option.selected = true; } modelSelect.appendChild(option); } }); // 触发选择器变更事件,更新API配置 const event = new Event('change'); modelSelect.dispatchEvent(event); } } catch (refreshError) { console.warn('刷新配置面板时出错:', refreshError); } // 更新主界面 this.videoController.onConfigUpdate('AI_MODELS.TYPE', CONFIG.AI_MODELS.TYPE); this.updateTitleWithModel(); this.showNotification('模型已删除', 'success'); } } saveConfig() { try { ConfigManager.saveConfig(CONFIG); this.showNotification('配置已保存', 'success'); } catch (error) { console.error('保存配置失败:', error); this.showNotification('保存配置失败: ' + error.message, 'error'); } } resetConfig() { if (confirm('确定要重置所有配置吗?这将清除所有自定义设置。')) { try { CONFIG = ConfigManager.getDefaultConfig(); ConfigManager.saveConfig(CONFIG); // 关闭当前配置面板并重新打开 this.configPanel.style.display = 'none'; this.configPanel.remove(); this.createConfigPanel(); this.toggleConfigPanel(); // 更新主界面 this.videoController.onConfigUpdate('AI_MODELS.TYPE', CONFIG.AI_MODELS.TYPE); this.updateTitleWithModel(); this.showNotification('配置已重置', 'success'); } catch (error) { console.error('重置配置失败:', error); this.showNotification('重置配置失败: ' + error.message, 'error'); } } } toggleCollapse() { this.isCollapsed = !this.isCollapsed; if (this.isCollapsed) { this.mainContent.style.display = 'none'; this.toggleButton.textContent = '↓'; this.container.style.width = 'auto'; } else { this.mainContent.style.display = 'block'; this.toggleButton.textContent = '↑'; this.container.style.width = '400px'; } } toggleConfigPanel() { console.log('toggleConfigPanel 方法被调用'); try { if (!this.configPanel) { console.log('配置面板不存在,开始创建'); this.createConfigPanel(); } if (!this.configPanel) { console.error('配置面板创建失败'); this.showNotification('配置面板创建失败', 'error'); return; } // 检查配置面板是否可见 const isVisible = this.configPanel.style.display === 'block'; console.log('当前配置面板状态:', isVisible ? '可见' : '隐藏'); if (isVisible) { this.configPanel.style.display = 'none'; console.log('配置面板已隐藏'); } else { this.configPanel.style.display = 'block'; this.configPanel.style.opacity = '1'; this.configPanel.style.visibility = 'visible'; console.log('配置面板已显示'); // 确保面板在最前面 this.configPanel.style.zIndex = '50000'; } } catch (error) { console.error('toggleConfigPanel 方法执行失败:', error); this.showNotification('配置面板操作失败: ' + error.message, 'error'); } } updateStatus(message, type = 'info') { this.statusDisplay.textContent = message; this.statusDisplay.style.display = 'block'; // 根据类型设置颜色 const colors = { 'info': 'rgba(33, 150, 243, 0.2)', 'success': 'rgba(76, 175, 80, 0.2)', 'error': 'rgba(244, 67, 54, 0.2)', 'warning': 'rgba(255, 193, 7, 0.2)' }; this.statusDisplay.style.background = colors[type] || colors['info']; } showNotification(message, type = 'info') { try { const notification = document.createElement('div'); notification.textContent = message; // 使用 textContent 确保安全 const colors = { 'info': '#2196F3', 'success': '#4CAF50', 'error': '#F44336', 'warning': '#FF9800' }; notification.style.cssText = ` position: fixed; top: 20px; left: 50%; transform: translateX(-50%); background: ${colors[type] || colors['info']}; color: #fff; padding: 12px 24px; border-radius: 8px; font-size: 14px; z-index: 200000; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); opacity: 0; transition: all 0.3s ease; `; document.body.appendChild(notification); setTimeout(() => { try { notification.style.opacity = '1'; notification.style.transform = 'translateX(-50%) translateY(0)'; } catch (e) { console.warn('通知显示动画失败:', e); } }, 100); setTimeout(() => { try { notification.style.opacity = '0'; notification.style.transform = 'translateX(-50%) translateY(-20px)'; setTimeout(() => { try { if (notification.parentNode) { notification.parentNode.removeChild(notification); } } catch (e) { console.warn('移除通知元素失败:', e); } }, 300); } catch (e) { console.warn('通知隐藏动画失败:', e); } }, 3000); } catch (error) { console.error('显示通知失败:', error); // 备用方案:使用原生alert alert(message); } } showExtensionPrompt() { try { const prompt = document.createElement('div'); prompt.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #fff; padding: 24px; border-radius: 16px; font-size: 14px; z-index: 200001; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); opacity: 0; transition: all 0.3s ease; max-width: 400px; text-align: center; `; // 添加背景遮罩 const overlay = document.createElement('div'); overlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 200000; opacity: 0; transition: opacity 0.3s ease; `; // 统一的清理函数 const cleanup = () => { try { if (overlay.parentNode) { overlay.parentNode.removeChild(overlay); } if (prompt.parentNode) { prompt.parentNode.removeChild(prompt); } } catch (e) { console.warn('清理扩展提示元素失败:', e); } }; const title = document.createElement('div'); title.style.cssText = ` font-size: 18px; font-weight: bold; margin-bottom: 16px; color: #fff; `; title.textContent = '🔧 需要安装浏览器扩展'; const message = document.createElement('div'); message.style.cssText = ` margin-bottom: 20px; line-height: 1.5; color: rgba(255, 255, 255, 0.9); `; message.textContent = '当前视频无法获取字幕内容。建议安装 YouTube Text Tools 扩展来获取更好的字幕支持。'; const buttonContainer = document.createElement('div'); buttonContainer.style.cssText = ` display: flex; gap: 12px; justify-content: center; `; const installButton = document.createElement('button'); installButton.style.cssText = ` background: #4CAF50; color: #fff; border: none; padding: 12px 20px; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: bold; transition: all 0.2s ease; `; installButton.textContent = '🚀 安装扩展'; installButton.addEventListener('click', () => { window.open('https://chromewebstore.google.com/detail/youtube-text-tools/pcmahconeajhpgleboodnodllkoimcoi', '_blank'); cleanup(); }); installButton.addEventListener('mouseenter', () => { installButton.style.background = '#45a049'; installButton.style.transform = 'translateY(-1px)'; }); installButton.addEventListener('mouseleave', () => { installButton.style.background = '#4CAF50'; installButton.style.transform = 'translateY(0)'; }); const cancelButton = document.createElement('button'); cancelButton.style.cssText = ` background: rgba(255, 255, 255, 0.2); color: #fff; border: 1px solid rgba(255, 255, 255, 0.3); padding: 12px 20px; border-radius: 8px; cursor: pointer; font-size: 14px; transition: all 0.2s ease; `; cancelButton.textContent = '关闭'; cancelButton.addEventListener('click', cleanup); cancelButton.addEventListener('mouseenter', () => { cancelButton.style.background = 'rgba(255, 255, 255, 0.3)'; }); cancelButton.addEventListener('mouseleave', () => { cancelButton.style.background = 'rgba(255, 255, 255, 0.2)'; }); overlay.addEventListener('click', cleanup); buttonContainer.appendChild(installButton); buttonContainer.appendChild(cancelButton); prompt.appendChild(title); prompt.appendChild(message); prompt.appendChild(buttonContainer); document.body.appendChild(overlay); document.body.appendChild(prompt); setTimeout(() => { try { overlay.style.opacity = '1'; prompt.style.opacity = '1'; prompt.style.transform = 'translate(-50%, -50%) scale(1)'; } catch (e) { console.warn('扩展提示显示动画失败:', e); } }, 100); } catch (error) { console.error('显示扩展安装提示失败:', error); // 备用方案:使用原生confirm if (confirm('当前视频无法获取字幕内容。是否安装 YouTube Text Tools 扩展来获取更好的字幕支持?')) { window.open('https://chromewebstore.google.com/detail/youtube-text-tools/pcmahconeajhpgleboodnodllkoimcoi', '_blank'); } } } async handleLoadSubtitles() { try { this.updateStatus('正在加载字幕...', 'info'); this.loadSubtitlesButton.disabled = true; await this.videoController.loadSubtitles(); this.updateStatus(`字幕加载完成,共 ${this.videoController.subtitleManager.subtitles.length} 条`, 'success'); this.loadSubtitlesButton.style.display = 'none'; this.summaryButton.style.display = 'block'; } catch (error) { this.updateStatus('加载字幕失败: ' + error.message, 'error'); this.loadSubtitlesButton.disabled = false; // 如果是字幕获取失败的错误,显示扩展安装提示 const errorMessage = error.message.toLowerCase(); if (errorMessage.includes('无法从任何来源获取字幕数据') || errorMessage.includes('未找到字幕数据') || errorMessage.includes('字幕') || errorMessage.includes('subtitle') || errorMessage.includes('transcript')) { // 延迟显示提示,让用户先看到错误状态 setTimeout(() => { this.showExtensionPrompt(); }, 1500); } } } async handleGenerateSummary() { try { console.log('开始生成总结按钮处理...'); this.updateStatus('正在生成总结...', 'info'); this.summaryButton.disabled = true; // 检查字幕数据是否存在 if (!this.videoController.subtitleManager.subtitles || this.videoController.subtitleManager.subtitles.length === 0) { throw new Error('没有可用的字幕数据,请先加载字幕'); } console.log(`当前有 ${this.videoController.subtitleManager.subtitles.length} 条字幕可用`); const summary = await this.videoController.getSummary(); console.log('成功获取总结,长度:', summary?.length || 0); if (!summary || summary.trim() === '') { throw new Error('生成的总结为空,请检查API配置或网络连接'); } // 保存原始markdown文本供复制使用 this.originalSummaryText = summary; console.log('已保存原始总结文本供复制使用'); // 清空内容并使用markdown格式化显示 this.summaryContent.textContent = ''; this.createFormattedContent(this.summaryContent, summary); console.log('总结内容已格式化并显示'); this.summaryPanel.style.display = 'block'; this.updateStatus('总结生成完成', 'success'); // 滚动到总结面板 this.summaryPanel.scrollIntoView({ behavior: 'smooth', block: 'start' }); } catch (error) { console.error('生成总结过程中发生错误:', error); this.updateStatus('生成总结失败: ' + error.message, 'error'); this.showNotification('生成总结失败: ' + error.message, 'error'); } finally { this.summaryButton.disabled = false; } } // 更新标题以显示当前AI模型 updateTitleWithModel() { const currentModel = CONFIG.AI_MODELS[CONFIG.AI_MODELS.TYPE]; const modelName = currentModel ? currentModel.MODEL : 'AI模型'; if (this.titleElement) { this.titleElement.textContent = `🎬 AI 船仓助手 - ${modelName}`; } } // 安全的markdown格式化内容创建方法 createFormattedContent(container, text) { const lines = text.split('\n'); let currentList = null; let listType = null; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (!line) { // 空行,结束当前列表 if (currentList) { container.appendChild(currentList); currentList = null; listType = null; } continue; } // 处理标题 if (line.startsWith('### ')) { if (currentList) { container.appendChild(currentList); currentList = null; listType = null; } const h3 = document.createElement('h3'); h3.textContent = line.substring(4); h3.style.cssText = ` color: #AB47BC; margin: 24px 0 12px 0; font-size: 18px; font-weight: 600; border-bottom: 2px solid rgba(171, 71, 188, 0.3); padding-bottom: 8px; letter-spacing: 0.5px; `; container.appendChild(h3); continue; } if (line.startsWith('## ')) { if (currentList) { container.appendChild(currentList); currentList = null; listType = null; } const h2 = document.createElement('h2'); h2.textContent = line.substring(3); h2.style.cssText = ` color: #9C27B0; margin: 28px 0 14px 0; font-size: 20px; font-weight: 600; border-bottom: 3px solid rgba(156, 39, 176, 0.4); padding-bottom: 10px; letter-spacing: 0.5px; `; container.appendChild(h2); continue; } if (line.startsWith('# ')) { if (currentList) { container.appendChild(currentList); currentList = null; listType = null; } const h1 = document.createElement('h1'); h1.textContent = line.substring(2); h1.style.cssText = ` color: #8E24AA; margin: 32px 0 16px 0; font-size: 24px; font-weight: 700; border-bottom: 4px solid rgba(142, 36, 170, 0.5); padding-bottom: 12px; letter-spacing: 0.8px; text-align: center; `; container.appendChild(h1); continue; } // 处理无序列表 if (line.startsWith('- ')) { if (!currentList || listType !== 'ul') { if (currentList) container.appendChild(currentList); currentList = document.createElement('ul'); currentList.style.cssText = ` margin: 16px 0; padding-left: 24px; color: #f0f0f0; background: rgba(255,255,255,0.02); border-radius: 8px; padding: 12px 24px; `; listType = 'ul'; } const li = document.createElement('li'); li.style.cssText = ` margin: 8px 0; color: #f0f0f0; line-height: 1.7; position: relative; padding-left: 8px; `; this.parseInlineFormatting(li, line.substring(2)); currentList.appendChild(li); continue; } // 处理有序列表 const orderedMatch = line.match(/^\d+\. (.+)$/); if (orderedMatch) { if (!currentList || listType !== 'ol') { if (currentList) container.appendChild(currentList); currentList = document.createElement('ol'); currentList.style.cssText = ` margin: 16px 0; padding-left: 24px; color: #f0f0f0; background: rgba(255,255,255,0.02); border-radius: 8px; padding: 12px 24px; `; listType = 'ol'; } const li = document.createElement('li'); li.style.cssText = ` margin: 8px 0; color: #f0f0f0; line-height: 1.7; position: relative; padding-left: 8px; `; this.parseInlineFormatting(li, orderedMatch[1]); currentList.appendChild(li); continue; } // 普通段落 if (currentList) { container.appendChild(currentList); currentList = null; listType = null; } const p = document.createElement('p'); p.style.cssText = ` margin: 16px 0; color: #f0f0f0; line-height: 1.8; text-align: justify; text-justify: inter-ideograph; font-size: 16px; letter-spacing: 0.3px; `; this.parseInlineFormatting(p, line); container.appendChild(p); } // 添加最后的列表 if (currentList) { container.appendChild(currentList); } } // 解析行内格式(粗体、斜体、代码等) parseInlineFormatting(element, text) { let remaining = text; // 处理代码块 remaining = remaining.replace(/```([\s\S]*?)```/g, (match, code) => { const pre = document.createElement('pre'); pre.style.cssText = 'background: rgba(0,0,0,0.3); padding: 12px; border-radius: 4px; margin: 12px 0; overflow-x: auto;'; const codeEl = document.createElement('code'); codeEl.style.cssText = 'color: #f8f8f2;'; codeEl.textContent = code.trim(); pre.appendChild(codeEl); element.appendChild(pre); return ''; // 移除已处理的部分 }); // 简单处理剩余文本(粗体、斜体、行内代码) const parts = remaining.split(/(\*\*.*?\*\*|\*.*?\*|`.*?`)/); parts.forEach(part => { if (!part) return; if (part.startsWith('**') && part.endsWith('**')) { // 粗体 const strong = document.createElement('strong'); strong.style.cssText = ` color: #ffffff; font-weight: 600; text-shadow: 0 0 2px rgba(255,255,255,0.3); `; strong.textContent = part.slice(2, -2); element.appendChild(strong); } else if (part.startsWith('*') && part.endsWith('*') && !part.startsWith('**')) { // 斜体 const em = document.createElement('em'); em.style.cssText = ` color: #E1BEE7; font-style: italic; `; em.textContent = part.slice(1, -1); element.appendChild(em); } else if (part.startsWith('`') && part.endsWith('`')) { // 行内代码 const code = document.createElement('code'); code.style.cssText = ` background: rgba(0,0,0,0.3); color: #f8f8f2; padding: 2px 6px; border-radius: 4px; font-family: 'Courier New', monospace; font-size: 0.9em; `; code.textContent = part.slice(1, -1); element.appendChild(code); } else { // 普通文本 const textNode = document.createTextNode(part); element.appendChild(textNode); } }); } makeDraggable(element) { let isDragging = false; let currentX; let currentY; let initialX; let initialY; let xOffset = 0; let yOffset = 0; element.addEventListener('mousedown', (e) => { // 如果点击的是按钮,不启动拖拽 if (e.target.tagName === 'BUTTON' || e.target.closest('button')) { console.log('点击的是按钮,不启动拖拽'); return; } initialX = e.clientX - xOffset; initialY = e.clientY - yOffset; if (e.target === element) { isDragging = true; console.log('开始拖拽'); } }); document.addEventListener('mousemove', (e) => { if (isDragging) { e.preventDefault(); currentX = e.clientX - initialX; currentY = e.clientY - initialY; xOffset = currentX; yOffset = currentY; // 限制拖动范围 const maxX = window.innerWidth - this.container.offsetWidth; const maxY = window.innerHeight - this.container.offsetHeight; xOffset = Math.min(Math.max(0, xOffset), maxX); yOffset = Math.min(Math.max(0, yOffset), maxY); this.container.style.transform = `translate(${xOffset}px, ${yOffset}px)`; } }); document.addEventListener('mouseup', () => { if (isDragging) { console.log('结束拖拽'); } initialX = currentX; initialY = currentY; isDragging = false; }); } // 添加移动端支持 addMobileSupport() { // 检测移动设备 const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); if (isMobile) { // 移动端适配 this.container.style.cssText = this.container.style.cssText.replace( 'width: 400px;', 'width: 95vw;' ).replace( 'top: 20px; right: 20px;', 'top: 10px; right: 2.5vw;' ); // 调整字体大小 this.container.style.fontSize = '14px'; } // 窗口大小变化时的响应式处理 window.addEventListener('resize', () => { const width = window.innerWidth; if (width < 768) { // 小屏幕 this.container.style.width = '95vw'; this.container.style.right = '2.5vw'; this.container.style.left = 'auto'; } else if (width < 1024) { // 中等屏幕 this.container.style.width = '380px'; this.container.style.right = '20px'; } else { // 大屏幕 this.container.style.width = '400px'; this.container.style.right = '20px'; } }); } attachEventListeners() { // URL变化监听(检测视频切换) let lastUrl = location.href; const observer = new MutationObserver(() => { const url = location.href; if (url !== lastUrl) { lastUrl = url; const newVideoId = this.videoController.getVideoId(); if (newVideoId && newVideoId !== this.videoController.currentVideoId) { this.videoController.currentVideoId = newVideoId; this.resetUI(); } } }); observer.observe(document, { subtree: true, childList: true }); // 全屏状态监听 document.addEventListener('fullscreenchange', () => { this.handleFullscreenChange(); }); document.addEventListener('webkitfullscreenchange', () => { this.handleFullscreenChange(); }); document.addEventListener('mozfullscreenchange', () => { this.handleFullscreenChange(); }); document.addEventListener('MSFullscreenChange', () => { this.handleFullscreenChange(); }); } resetUI() { this.loadSubtitlesButton.style.display = 'block'; this.summaryButton.style.display = 'none'; this.summaryPanel.style.display = 'none'; this.statusDisplay.style.display = 'none'; this.loadSubtitlesButton.disabled = false; // 重置翻译后的标题 this.videoController.translatedTitle = null; this.summaryButton.disabled = false; } // 处理全屏状态变化 handleFullscreenChange() { const isFullscreen = !!( document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement ); if (isFullscreen) { // 进入全屏,隐藏UI if (this.container && this.container.style.display !== 'none') { this.isHiddenDueToFullscreen = true; this.container.style.display = 'none'; console.log('🎬 全屏模式:隐藏YouTube助手UI'); } } else { // 退出全屏,恢复UI显示 if (this.isHiddenDueToFullscreen && this.container) { this.isHiddenDueToFullscreen = false; this.container.style.display = 'block'; console.log('🎬 退出全屏:恢复YouTube助手UI'); } } } } // 初始化应用 async function initializeApp() { try { console.log('🎬 YouTube AI 总结助手初始化中...'); // 等待页面加载 await waitForPageLoad(); // 创建视频控制器 const videoController = new VideoController(); // 创建UI管理器 const uiManager = new UIManager(videoController); console.log('✅ YouTube AI 总结助手初始化完成'); } catch (error) { console.error('❌ 初始化失败:', error); } } function waitForPageLoad() { return new Promise((resolve) => { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', resolve); } else { resolve(); } }); } function getUid() { const url = new URL(window.location.href); return url.searchParams.get('v') || 'unknown'; } // 检查 Trusted Types 策略 function checkTrustedTypes() { if (window.trustedTypes && window.trustedTypes.defaultPolicy) { console.log('检测到 Trusted Types 策略,将使用安全的DOM操作方法'); return true; } return false; } // 启动应用 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { checkTrustedTypes(); initializeApp(); }); } else { checkTrustedTypes(); initializeApp(); } // 添加全局测试函数(用于调试) window.testYouTubeAI = { openConfig: function() { const uiElements = document.querySelectorAll('#youtube-ai-config-panel'); if (uiElements.length > 0) { console.log('发现已存在的配置面板,尝试显示'); uiElements[0].style.display = 'block'; uiElements[0].style.zIndex = '999999'; } else { console.log('未找到配置面板元素'); } }, getUI: function() { // 尝试找到UI管理器实例 const containers = document.querySelectorAll('[style*="linear-gradient"]'); console.log('找到的容器:', containers.length); return containers; }, testConfigButton: function() { const buttons = document.querySelectorAll('button'); const configButton = Array.from(buttons).find(btn => btn.textContent === '⚙️'); if (configButton) { console.log('找到配置按钮,尝试触发点击'); configButton.click(); } else { console.log('未找到配置按钮'); } } }; })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址