// ==UserScript==
// @name 🎬 YouTube 船仓助手
// @namespace http://tampermonkey.net/
// @version 1.0.1
// @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.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.updatePromptSelect(select);
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.configPanel.querySelector('select'));
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.configPanel.querySelector('select'));
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.configPanel.querySelector('select'));
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 });
}
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;
}
}
// 初始化应用
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('未找到配置按钮');
}
}
};
})();