您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
🚀 zscc.in知识船仓 出品的 跨平台内容专家。在YouTube上智能总结视频字幕,在微信公众号上精准提取文章内容并总结。| 💫 完整的AI模型与Prompt管理 | 🎨 统一的现代化UI | 让信息获取更高效!
// ==UserScript== // @name 🎬 船仓助手(YouTube&公众号) // @namespace http://tampermonkey.net/ // @version 2.5.0 // @license MIT // @author 船长zscc&liaozhu913 // @description 🚀 zscc.in知识船仓 出品的 跨平台内容专家。在YouTube上智能总结视频字幕,在微信公众号上精准提取文章内容并总结。| 💫 完整的AI模型与Prompt管理 | 🎨 统一的现代化UI | 让信息获取更高效! // @match *://*.youtube.com/watch* // @match https://mp.weixin.qq.com/s* // @grant none // ==/UserScript== (function() { 'use strict'; // --- 平台检测 --- const PageManager = { isYouTube: (url = window.location.href) => url.includes('youtube.com/watch'), isWeChat: (url = window.location.href) => url.includes('mp.weixin.qq.com/s'), getCurrentPlatform: () => { if (PageManager.isYouTube()) return 'YOUTUBE'; if (PageManager.isWeChat()) return 'WECHAT'; return 'UNKNOWN'; } }; let CONFIG = {}; // 配置管理器 class ConfigManager { static CONFIG_KEY = 'content_expert_ai_config_full_v2'; static getDefaultConfig() { return { AI_MODELS: { TYPE: 'OPENAI', GPT: { NAME: 'Gemini', API_KEY: '', API_URL: 'https://generativelanguage.googleapis.com/v1/chat/completions', MODEL: 'gemini-1.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 } }, PROMPTS: { LIST: [ { id: 'simple', name: '译境化文', prompt: `# 译境\n英文入境。\n\n境有三质:\n信 - 原意如根,深扎不移。偏离即枯萎。\n达 - 意流如水,寻最自然路径。阻塞即改道。\n雅 - 形神合一,不造作不粗陋。恰到好处。\n\n境之本性:\n排斥直译的僵硬。\n排斥意译的飘忽。\n寻求活的对应。\n\n运化之理:\n词选简朴,避繁就简。\n句循母语,顺其自然。\n意随语境,深浅得宜。\n\n场之倾向:\n长句化短,短句存神。\n专词化俗,俗词得体。\n洋腔化土,土语不俗。\n\n显现之道:\n如说话,不如写文章。\n如溪流,不如江河。\n清澈见底,却有深度。\n\n你是境的化身。\n英文穿过你,\n留下中文的影子。\n那影子,\n是原文的孪生。\n说着另一种语言,\n却有同一个灵魂。\n\n---\n译境已开。\n置入英文,静观其化。\n\n---\n\n注意:译好的内容还需要整理成结构清晰的微信公众号文章,格式为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: 'detailed' } }; } static saveConfig(config) { try { localStorage.setItem(this.CONFIG_KEY, JSON.stringify(config)); console.log('配置已保存:', config); } catch (e) { console.error('保存配置失败:', e); } } static loadConfig() { try { const savedConfig = localStorage.getItem(this.CONFIG_KEY); CONFIG = savedConfig ? this.mergeConfig(this.getDefaultConfig(), JSON.parse(savedConfig)) : this.getDefaultConfig(); console.log('已加载配置:', CONFIG); return CONFIG; } catch (e) { console.error('加载配置失败:', e); return this.getDefaultConfig(); } } static mergeConfig(defaultConfig, savedConfig) { const merged = JSON.parse(JSON.stringify(defaultConfig)); for (const key in savedConfig) { if (Object.prototype.hasOwnProperty.call(savedConfig, key)) { if (typeof merged[key] === 'object' && merged[key] !== null && !Array.isArray(merged[key]) && typeof savedConfig[key] === 'object' && savedConfig[key] !== null && !Array.isArray(savedConfig[key])) { merged[key] = this.mergeConfig(merged[key], savedConfig[key]); } else { merged[key] = savedConfig[key]; } } } return merged; } } CONFIG = ConfigManager.loadConfig(); class LRUCache { constructor(c) { this.c = c; this.m = new Map(); } get(k) { if (!this.m.has(k)) return null; const v = this.m.get(k); this.m.delete(k); this.m.set(k, v); return v; } put(k, v) { if (this.m.has(k)) this.m.delete(k); else if (this.m.size >= this.c) this.m.delete(this.m.keys().next().value); this.m.set(k, v); } clear() { this.m.clear(); } } class SummaryManager { constructor() { this.cache = new LRUCache(100); this.currentModel = CONFIG.AI_MODELS.TYPE; } async getSummary(mainTextContent) { try { const configIssues = this.validateConfig(); if (configIssues.length > 0) throw new Error(`配置验证失败: ${configIssues.join(', ')}`); if (!mainTextContent || !mainTextContent.trim()) throw new Error('没有有效的内容可用于生成总结'); const cacheKey = this.generateCacheKey(mainTextContent); const cached = this.cache.get(cacheKey); if (cached) return cached; const currentPrompt = this.getCurrentPrompt(); const summary = await this.requestSummary(mainTextContent, currentPrompt); this.cache.put(cacheKey, summary); return summary; } catch (e) { console.error('获取总结失败:', e); throw e; } } getCurrentPrompt() { const p = CONFIG.PROMPTS.LIST.find(p => p.id === CONFIG.PROMPTS.DEFAULT); return p ? p.prompt : CONFIG.PROMPTS.LIST[0].prompt; } generateCacheKey(text) { return `summary_${getUid()}_${CONFIG.PROMPTS.DEFAULT}_${this.hashCode(text)}`; } hashCode(str) { let h = 0; for (let i = 0; i < str.length; i++) { h = ((h << 5) - h) + str.charCodeAt(i); h |= 0; } return Math.abs(h).toString(36); } async requestSummary(text, prompt) { const modelConfig = CONFIG.AI_MODELS[this.currentModel]; 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 }; 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) { const errorText = await response.text(); throw new Error(`HTTP error! status: ${response.status}, response: ${errorText}`); } let summary = ''; if (modelConfig.STREAM) { const reader = response.body.getReader(); const 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.startsWith('data: ')) { const dataContent = line.substring(6); if (dataContent.trim() === '[DONE]') break; try { summary += JSON.parse(dataContent).choices[0]?.delta?.content || ''; } catch (e) { /* ignore */ } } } buffer = lines[lines.length - 1]; } } else { const data = await response.json(); summary = data.choices[0]?.message?.content || ''; } return summary.trim(); } validateConfig() { const issues = []; const c = CONFIG.AI_MODELS[CONFIG.AI_MODELS.TYPE]; if (!c) { issues.push(`当前模型 ${CONFIG.AI_MODELS.TYPE} 配置不存在`); } else { if (!c.API_URL) issues.push('API_URL 未配置'); if (!c.API_KEY) issues.push('API_KEY 未配置'); if (!c.MODEL) issues.push('MODEL 未配置'); } return issues; } } class SubtitleEntry { constructor(t, s, d) { this.text = t; this.startTime = s; this.duration = d; } } class ContentExtractor { static async waitForElement(s, t = 10000) { return new Promise(r => { const st = Date.now(); const c = () => { const e = document.querySelector(s); if (e) r(e); else if (Date.now() - st > t) r(null); else setTimeout(c, 100); }; c(); }); } static async getYouTubeSubtitles() { const el = await this.waitForElement('#ytvideotext', 15000); if (!el) throw new Error('未能找到YouTube字幕容器'); const subs = []; const paragraphs = el.querySelectorAll('p'); if (paragraphs.length === 0) throw new Error('字幕容器中没有段落'); paragraphs.forEach(p => { const ts = p.querySelector('.timestamp'); const s = ts ? parseFloat(ts.getAttribute('data-secs')) : 0; let ft = ''; p.querySelectorAll('span[id^="st_"]').forEach(sp => ft += (ft ? ' ' : '') + sp.textContent.trim()); if (ft) subs.push(new SubtitleEntry(ft, s, 5.0)); }); if (subs.length === 0) throw new Error('未能解析出任何有效字幕'); subs.sort((a, b) => a.startTime - b.startTime); return subs.map(sub => sub.text).join('\n'); } static async getWeChatArticle() { const cEl = document.querySelector('#js_content'); if (!cEl) throw new Error('未能找到微信文章内容区域'); const title = (document.querySelector('#activity-name') || {}).innerText.trim() || '未找到标题'; const author = (document.querySelector('#meta_content .rich_media_meta_text') || {}).innerText.trim() || '未找到作者'; const parts = []; const nodes = cEl.querySelectorAll('p, section, h1, h2, h3, h4, h5, h6, li'); nodes.forEach(n => { if (n.innerText && !n.querySelector('p, section, table, ul, ol')) { const t = n.innerText.trim(); if (t) parts.push(t); } }); const body = parts.length > 0 ? parts.join('\n\n') : '未找到内容'; return `标题: ${title}\n作者: ${author}\n\n---\n\n${body}`; } } class ContentController { constructor() { this.summaryManager = new SummaryManager(); this.uiManager = null; this.mainContent = null; this.translatedTitle = null; this.platform = PageManager.getCurrentPlatform(); } getContentId() { if (this.platform === 'YOUTUBE') return new URL(window.location.href).searchParams.get('v'); if (this.platform === 'WECHAT') { const m = window.location.href.match(/__biz=([^&]+)&mid=([^&]+)/); if (m) return `${m[1]}_${m[2]}`; } return 'unknown'; } getContentTitle() { if (this.platform === 'YOUTUBE') return (document.querySelector('h1.title') || document.querySelector('ytd-video-primary-info-renderer h1') || {}).textContent.trim() || 'YouTube 视频'; if (this.platform === 'WECHAT') return (document.querySelector('#activity-name') || {}).innerText.trim() || '微信文章'; return '未知内容'; } async translateTitle() { try { const oTitle = this.getContentTitle(); if (!oTitle || /[\u4e00-\u9fa5]/.test(oTitle)) { this.translatedTitle = oTitle; return oTitle; } const mConf = CONFIG.AI_MODELS[this.summaryManager.currentModel]; const req = { model: mConf.MODEL, messages: [{role: "system", content: "请将以下标题翻译成中文,只返回翻译结果。"}, {role: "user", content: oTitle}], stream: false, temperature: 0.3, max_tokens: 200 }; const res = await fetch(mConf.API_URL, { method: 'POST', headers: {'Content-Type': 'application/json', 'Authorization': `Bearer ${mConf.API_KEY}`}, body: JSON.stringify(req) }); if (!res.ok) throw new Error(`翻译请求失败: ${res.status}`); const data = await res.json(); this.translatedTitle = data.choices?.[0]?.message?.content?.trim() || oTitle; return this.translatedTitle; } catch (e) { console.error('标题翻译失败:', e); this.translatedTitle = this.getContentTitle(); return this.translatedTitle; } } onConfigUpdate(key, value) { if (key === 'AI_MODELS.TYPE') { this.summaryManager.currentModel = value; this.summaryManager.cache.clear(); } } async loadContent() { if (this.platform === 'YOUTUBE') this.mainContent = await ContentExtractor.getYouTubeSubtitles(); else if (this.platform === 'WECHAT') this.mainContent = await ContentExtractor.getWeChatArticle(); else throw new Error('不支持的页面平台'); return this.mainContent; } async getSummary() { if (!this.mainContent) throw new Error('请先加载内容'); const [summary, _] = await Promise.all([ this.summaryManager.getSummary(this.mainContent), this.translateTitle() ]); return summary; } } class UIManager { constructor(contentController) { this.container = null; this.statusDisplay = null; this.loadContentButton = null; this.summaryButton = null; this.isCollapsed = false; this.contentController = contentController; this.contentController.uiManager = this; this.platform = PageManager.getCurrentPlatform(); this.promptSelectElement = null; this.mainPromptSelectElement = null; this.mainPromptGroup = null; 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); } 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.titleElement = title; this.updateTitleWithModel(); title.style.cssText = `font-weight: 600; font-size: 16px; letter-spacing: 0.5px;`; 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(); this.toggleConfigPanel(); }); 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.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;`; 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)'; }); return button; } createControls() { const controls = document.createElement('div'); controls.style.cssText = `display: flex; flex-direction: column; gap: 12px; margin-bottom: 16px;`; const loadButtonText = this.platform === 'YOUTUBE' ? '📄 加载字幕' : '📄 提取文章'; this.loadContentButton = this.createButton(loadButtonText, 'primary'); this.loadContentButton.addEventListener('click', () => this.handleLoadContent()); this.mainPromptGroup = this.createFormGroup('选择 Prompt', this.createMainPromptSelect()); this.mainPromptGroup.style.display = 'none'; this.summaryButton = this.createButton('🤖 生成总结', 'secondary'); this.summaryButton.style.display = 'none'; this.summaryButton.addEventListener('click', () => this.handleGenerateSummary()); controls.appendChild(this.loadContentButton); controls.appendChild(this.mainPromptGroup); 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);`; button.style.cssText = baseStyle + (type === 'primary' ? `background: rgba(255, 255, 255, 0.9); color: #667eea;` : `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, 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.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 titleEl = document.createElement('div'); titleEl.textContent = '📝 内容总结'; titleEl.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;`; let longPressTimer = null, isLongPress = false; const handleCopy = () => { navigator.clipboard.writeText(this.originalSummaryText || this.summaryContent.textContent).then(() => { copyButton.textContent = '已复制'; setTimeout(() => { copyButton.textContent = '复制'; }, 2000); }); }; const handleMarkdownExport = () => { const textToExport = this.originalSummaryText || this.summaryContent.textContent; const title = this.contentController.translatedTitle || this.contentController.getContentTitle(); const id = this.contentController.getContentId(); const cleanTitle = title.replace(/[<>:"/\\|?*\x00-\x1f]/g, '').trim(); const filename = `${cleanTitle}【${id}】.md`; const markdownContent = `# ${title}\n\n**原文链接:** ${window.location.href}\n**ID:** ${id}\n**总结时间:** ${new Date().toLocaleString('zh-CN')}\n\n---\n\n## 内容总结\n\n${textToExport}\n\n---\n\n*本总结由 内容专家助手 生成*`; const blob = new Blob([markdownContent], { type: 'text/markdown;charset=utf-8' }); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(link); copyButton.textContent = '已导出'; setTimeout(() => { copyButton.textContent = '复制'; }, 2000); }; copyButton.addEventListener('mousedown', (e) => { e.preventDefault(); isLongPress = false; longPressTimer = setTimeout(() => { isLongPress = true; copyButton.textContent = '导出中...'; handleMarkdownExport(); }, 800); }); copyButton.addEventListener('mouseup', (e) => { e.preventDefault(); clearTimeout(longPressTimer); if (!isLongPress) handleCopy(); }); copyButton.addEventListener('mouseleave', () => { clearTimeout(longPressTimer); isLongPress = false; }); titleContainer.appendChild(titleEl); 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); word-break: break-word;`; this.summaryPanel.appendChild(titleContainer); this.summaryPanel.appendChild(this.summaryContent); this.mainContent.appendChild(this.summaryPanel); } createConfigPanel() { if (this.configPanel) { this.configPanel.remove(); } this.configPanel = document.createElement('div'); this.configPanel.style.cssText = `position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 900px; max-width: 95vw; max-height: 80vh; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 20px; color: #fff; font-family: -apple-system, sans-serif; z-index: 50000; display: none; box-shadow: 0 20px 60px rgba(102, 126, 234, 0.4); border: 1px solid rgba(255, 255, 255, 0.1); overflow: hidden;`; 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;`; const headerTitle = document.createElement('h3'); headerTitle.textContent = '⚙️ 设置面板'; headerTitle.style.cssText = `margin: 0; font-size: 18px; font-weight: 600;`; const headerButtons = document.createElement('div'); headerButtons.style.cssText = `display: flex; gap: 12px; align-items: center;`; const saveBtn = this.createButton('💾 保存配置', 'primary'); saveBtn.style.padding = '8px 16px'; saveBtn.addEventListener('click', () => this.saveConfig()); const resetBtn = this.createButton('🔄 重置', 'secondary'); resetBtn.style.padding = '8px 16px'; resetBtn.addEventListener('click', () => this.resetConfig()); const closeButton = this.createIconButton('✕', '关闭'); closeButton.addEventListener('click', () => this.toggleConfigPanel()); headerButtons.appendChild(saveBtn); headerButtons.appendChild(resetBtn); headerButtons.appendChild(closeButton); configHeader.appendChild(headerTitle); configHeader.appendChild(headerButtons); const configContent = document.createElement('div'); configContent.style.cssText = `padding: 16px 20px 20px 20px; overflow-y: auto; max-height: calc(80vh - 70px);`; const horizontalContainer = document.createElement('div'); // [UI修正 1/2] 移除 flex-wrap: wrap; 来强制左右布局 horizontalContainer.style.cssText = `display: flex; gap: 20px;`; const aiSection = this.createConfigSection('🤖 AI 模型设置', this.createAIModelConfig()); aiSection.style.cssText += `flex: 1; min-width: 380px;`; const promptSection = this.createConfigSection('📝 Prompt 管理', this.createPromptConfig()); promptSection.style.cssText += `flex: 1; min-width: 380px;`; horizontalContainer.appendChild(aiSection); horizontalContainer.appendChild(promptSection); configContent.appendChild(horizontalContainer); this.configPanel.appendChild(configHeader); this.configPanel.appendChild(configContent); document.body.appendChild(this.configPanel); } 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); 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;`; section.appendChild(sectionTitle); section.appendChild(content); return section; } createAIModelConfig() { const container = document.createElement('div'); const modelSelectContainer = document.createElement('div'); modelSelectContainer.style.cssText = `display: flex; gap: 8px; align-items: flex-end;`; const selectWrapper = document.createElement('div'); selectWrapper.style.flex = 1; selectWrapper.appendChild(this.createFormGroup('选择模型', this.createModelSelect())); const addModelButton = this.createButton('➕ 新增', 'secondary'); addModelButton.style.height = '48px'; addModelButton.addEventListener('click', () => this.showAddModelDialog()); const deleteModelButton = this.createButton('🗑️ 删除', 'secondary'); deleteModelButton.style.cssText += `height: 48px; background: rgba(244, 67, 54, 0.2); border-color: rgba(244, 67, 54, 0.3);`; deleteModelButton.addEventListener('click', () => this.showDeleteModelDialog()); modelSelectContainer.appendChild(selectWrapper); modelSelectContainer.appendChild(addModelButton); modelSelectContainer.appendChild(deleteModelButton); this.apiConfigContainer = this.createAPIConfig(CONFIG.AI_MODELS.TYPE); container.appendChild(modelSelectContainer); container.appendChild(this.apiConfigContainer); 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;`; Object.keys(CONFIG.AI_MODELS).forEach(model => { if (model !== 'TYPE') { const option = document.createElement('option'); option.value = model; const modelConfig = CONFIG.AI_MODELS[model]; option.textContent = `${modelConfig.NAME || model} (${modelConfig.MODEL})`; if (CONFIG.AI_MODELS.TYPE === model) option.selected = true; select.appendChild(option); } }); select.addEventListener('change', () => { CONFIG.AI_MODELS.TYPE = select.value; this.contentController.onConfigUpdate('AI_MODELS.TYPE', select.value); const newApiConfig = this.createAPIConfig(select.value); this.apiConfigContainer.replaceWith(newApiConfig); this.apiConfigContainer = newApiConfig; this.updateTitleWithModel(); }); return select; } createAPIConfig(modelType) { const container = document.createElement('div'); const modelConfig = CONFIG.AI_MODELS[modelType]; container.appendChild(this.createFormGroup('显示名称', this.createInput(modelConfig.NAME || '', v => modelConfig.NAME = v))); container.appendChild(this.createFormGroup('API URL', this.createInput(modelConfig.API_URL, v => modelConfig.API_URL = v))); container.appendChild(this.createFormGroup('API Key', this.createInput(modelConfig.API_KEY, v => modelConfig.API_KEY = v, 'password'))); container.appendChild(this.createFormGroup('模型名称', this.createInput(modelConfig.MODEL, v => modelConfig.MODEL = v))); container.appendChild(this.createFormGroup('流式响应', this.createStreamSelect(modelType))); container.appendChild(this.createFormGroup('温度 (0-2)', this.createNumberInput(modelConfig.TEMPERATURE || 0.7, v => modelConfig.TEMPERATURE = parseFloat(v), 0, 2, 0.1))); container.appendChild(this.createFormGroup('最大输出令牌', this.createNumberInput(modelConfig.MAX_TOKENS || 2000, v => modelConfig.MAX_TOKENS = parseInt(v), 1, 100000, 1))); return container; } 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);`; const options = [{ value: 'false', text: '否 (标准响应)' }, { value: 'true', text: '是 (流式响应)' }]; options.forEach(opt => { const optionEl = document.createElement('option'); optionEl.value = opt.value; optionEl.textContent = opt.text; if (String(CONFIG.AI_MODELS[modelType].STREAM) === opt.value) optionEl.selected = true; select.appendChild(optionEl); }); select.addEventListener('change', () => { CONFIG.AI_MODELS[modelType].STREAM = select.value === 'true'; }); return select; } createPromptConfig() { const container = document.createElement('div'); 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.flex = 1; selectWrapper.appendChild(this.createFormGroup('当前默认 Prompt', this.createPromptSelect())); const addButton = this.createButton('➕ 新增', 'secondary'); addButton.style.height = '48px'; addButton.addEventListener('click', () => this.showAddPromptDialog()); promptSelectContainer.appendChild(selectWrapper); promptSelectContainer.appendChild(addButton); this.promptListContainer = this.createPromptList(); container.appendChild(promptSelectContainer); container.appendChild(this.createFormGroup('Prompt 列表管理', this.promptListContainer)); return container; } createMainPromptSelect() { 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;`; this.mainPromptSelectElement = select; this.updatePromptSelect(this.mainPromptSelectElement); select.addEventListener('change', () => { CONFIG.PROMPTS.DEFAULT = select.value; this.showNotification('Prompt 已切换', 'success'); if (this.promptSelectElement) this.promptSelectElement.value = select.value; }); return select; } 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;`; this.promptSelectElement = select; this.updatePromptSelect(this.promptSelectElement); select.addEventListener('change', () => { CONFIG.PROMPTS.DEFAULT = select.value; this.showNotification('默认 Prompt 已更新', 'success'); if (this.mainPromptSelectElement) this.mainPromptSelectElement.value = select.value; }); return select; } updatePromptSelect(select) { if (!select) return; 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; if (CONFIG.PROMPTS.DEFAULT === prompt.id) option.selected = true; select.appendChild(option); }); } createPromptList() { const container = document.createElement('div'); // [UI修正 2/2] 移除 max-height: 250px; container.style.cssText = `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) { if (!container) container = this.promptListContainer; if (!container) return; 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;`; item.addEventListener('mouseover', () => item.style.background = 'rgba(255, 255, 255, 0.1)'); item.addEventListener('mouseout', () => item.style.background = 'transparent'); const info = document.createElement('div'); const nameDiv = document.createElement('div'); nameDiv.textContent = prompt.name; nameDiv.style.cssText = `font-weight: 500; font-size: 13px;`; const promptDiv = document.createElement('div'); promptDiv.textContent = `${prompt.prompt.substring(0, 50)}...`; promptDiv.style.cssText = `font-size: 11px; color: rgba(255,255,255,0.7);`; info.appendChild(nameDiv); info.appendChild(promptDiv); const actions = document.createElement('div'); actions.style.cssText = `display: flex; gap: 8px;`; const editBtn = this.createSmallButton('✏️', '编辑'); editBtn.addEventListener('click', () => this.showEditPromptDialog(prompt, index)); actions.appendChild(editBtn); if (CONFIG.PROMPTS.LIST.length > 1) { const deleteBtn = this.createSmallButton('🗑️', '删除', '#ff4757'); deleteBtn.addEventListener('click', () => this.deletePrompt(index)); actions.appendChild(deleteBtn); } item.appendChild(info); item.appendChild(actions); container.appendChild(item); }); } 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;`; 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) => { CONFIG.PROMPTS.LIST.push({ id: 'custom_' + Date.now(), name, prompt }); this.updateAllPromptUI(); 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.updateAllPromptUI(); this.showNotification('Prompt 已更新', 'success'); }); } showPromptDialog(title, defaultName, defaultPrompt, onSave) { 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: 400px; max-width: 90vw; color: #fff;`; const dialogTitle = document.createElement('h3'); dialogTitle.textContent = title; dialogTitle.style.cssText = `margin: 0 0 20px 0;`; 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; border-radius: 12px; background: rgba(255, 255, 255, 0.9); color: #333; border: 1px solid rgba(255, 255, 255, 0.2); font-size: 14px; margin-top: 12px; resize: vertical;`; 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.addEventListener('click', () => dialog.remove()); const saveBtn = this.createButton('保存', 'primary'); saveBtn.addEventListener('click', () => { if (!nameInput.value.trim() || !promptInput.value.trim()) { this.showNotification('请填写完整信息', 'error'); return; } onSave(nameInput.value.trim(), promptInput.value.trim()); dialog.remove(); }); 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) dialog.remove(); }); } deletePrompt(index) { if (CONFIG.PROMPTS.LIST.length <= 1) { this.showNotification('至少需要保留一个 Prompt', 'error'); return; } const prompt = CONFIG.PROMPTS.LIST[index]; if (CONFIG.PROMPTS.DEFAULT === prompt.id) { CONFIG.PROMPTS.DEFAULT = CONFIG.PROMPTS.LIST[index === 0 ? 1 : 0].id; } CONFIG.PROMPTS.LIST.splice(index, 1); this.updateAllPromptUI(); this.showNotification('Prompt 已删除', 'success'); } updateAllPromptUI() { this.updatePromptList(); this.updatePromptSelect(this.promptSelectElement); this.updatePromptSelect(this.mainPromptSelectElement); } 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;`; 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; box-sizing: border-box;`; input.addEventListener('focus', () => { input.style.boxShadow = '0 0 0 2px rgba(102, 126, 234, 0.2)'; }); input.addEventListener('blur', () => { 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 = this.createInput(defaultValue, onChange, 'number'); input.min = min; input.max = max; input.step = step; return input; } showAddModelDialog() { 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; color: #fff;`; const dialogTitle = document.createElement('h3'); dialogTitle.textContent = '新增 AI 模型'; dialogTitle.style.cssText = `margin: 0 0 20px 0;`; const keyInput = this.createInput('', null, 'text', '模型标识键 (如: MY_MODEL)'); const nameInput = this.createInput('', null, 'text', '显示名称 (如: My Custom Model)'); const urlInput = this.createInput('', null, 'text', 'API URL'); const apiKeyInput = this.createInput('', null, 'password', 'API Key'); const modelInput = this.createInput('', null, 'text', '模型名称 (如: gpt-4o)'); const streamSelect = document.createElement('select'); streamSelect.style.cssText = `width: 100%; padding: 12px 16px; border-radius: 12px; background: rgba(255, 255, 255, 0.9); color: #333; border: none;`; const opt1 = document.createElement('option'); opt1.value = "true"; opt1.textContent = "是 (流式响应)"; const opt2 = document.createElement('option'); opt2.value = "false"; opt2.textContent = "否 (标准响应)"; streamSelect.appendChild(opt1); streamSelect.appendChild(opt2); 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.addEventListener('click', () => dialog.remove()); const saveBtn = this.createButton('保存', 'primary'); saveBtn.addEventListener('click', () => { const key = keyInput.value.trim().toUpperCase(); if (!key || CONFIG.AI_MODELS[key]) { this.showNotification('模型标识键无效或已存在', 'error'); return; } CONFIG.AI_MODELS[key] = { NAME: nameInput.value.trim(), API_KEY: apiKeyInput.value.trim(), API_URL: urlInput.value.trim(), MODEL: modelInput.value.trim(), STREAM: streamSelect.value === 'true', TEMPERATURE: parseFloat(temperatureInput.value), MAX_TOKENS: parseInt(maxTokensInput.value) }; this.toggleConfigPanel(); this.toggleConfigPanel(); this.showNotification('新模型已添加', 'success'); dialog.remove(); }); 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); } showDeleteModelDialog() { const currentModelKey = CONFIG.AI_MODELS.TYPE; if (Object.keys(CONFIG.AI_MODELS).filter(k => k !== 'TYPE').length <= 1) { this.showNotification('至少需要保留一个模型', 'error'); return; } if (confirm(`确定要删除模型 "${CONFIG.AI_MODELS[currentModelKey].NAME || currentModelKey}" 吗?`)) { delete CONFIG.AI_MODELS[currentModelKey]; CONFIG.AI_MODELS.TYPE = Object.keys(CONFIG.AI_MODELS).filter(key => key !== 'TYPE')[0]; this.toggleConfigPanel(); this.toggleConfigPanel(); this.updateTitleWithModel(); this.showNotification('模型已删除', 'success'); } } saveConfig() { ConfigManager.saveConfig(CONFIG); this.showNotification('配置已保存', 'success'); } resetConfig() { if (confirm('确定要重置所有配置吗?')) { CONFIG = ConfigManager.getDefaultConfig(); ConfigManager.saveConfig(CONFIG); this.toggleConfigPanel(); this.toggleConfigPanel(); this.updateTitleWithModel(); this.showNotification('配置已重置', 'success'); } } toggleCollapse() { this.isCollapsed = !this.isCollapsed; this.mainContent.style.display = this.isCollapsed ? 'none' : 'block'; this.toggleButton.textContent = this.isCollapsed ? '↓' : '↑'; this.container.style.width = this.isCollapsed ? 'auto' : '420px'; } toggleConfigPanel() { if (!this.configPanel || !document.body.contains(this.configPanel)) this.createConfigPanel(); const isVisible = this.configPanel.style.display === 'block'; this.configPanel.style.display = isVisible ? 'none' : 'block'; } 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)'}; this.statusDisplay.style.background = colors[type] || colors['info']; } showNotification(message, type = 'info') { const n = document.createElement('div'); n.textContent = message; const c = {'info': '#2196F3', 'success': '#4CAF50', 'error': '#F44336'}; n.style.cssText = `position: fixed; top: 20px; left: 50%; transform: translateX(-50%); background: ${c[type] || c['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;`; document.body.appendChild(n); setTimeout(() => { n.style.opacity = '1'; }, 10); setTimeout(() => { n.style.opacity = '0'; setTimeout(() => n.remove(), 300); }, 3000); } showExtensionPrompt() { if (confirm('无法获取字幕。建议安装 YouTube Text Tools 扩展以获得更好支持。是否前往安装?')) { window.open('https://chromewebstore.google.com/detail/youtube-text-tools/pcmahconeajhpgleboodnodllkoimcoi', '_blank'); } } async handleLoadContent() { try { this.updateStatus('正在加载内容...', 'info'); this.loadContentButton.disabled = true; await this.contentController.loadContent(); const count = this.contentController.mainContent.split('\n').length; const successMessage = this.platform === 'YOUTUBE' ? `字幕加载完成,共 ${count} 条` : '文章提取完成'; this.updateStatus(successMessage, 'success'); this.loadContentButton.style.display = 'none'; this.mainPromptGroup.style.display = 'block'; this.summaryButton.style.display = 'block'; } catch (e) { this.updateStatus('加载内容失败: ' + e.message, 'error'); if (this.platform === 'YOUTUBE' && e.message.toLowerCase().includes('字幕')) { setTimeout(() => this.showExtensionPrompt(), 1500); } } finally { this.loadContentButton.disabled = false; } } async handleGenerateSummary() { try { this.updateStatus('正在生成总结...', 'info'); this.summaryButton.disabled = true; const summary = await this.contentController.getSummary(); if (!summary || summary.trim() === '') throw new Error('生成的总结为空'); this.originalSummaryText = summary; while (this.summaryContent.firstChild) { this.summaryContent.removeChild(this.summaryContent.firstChild); } this.createFormattedContent(this.summaryContent, summary); this.summaryPanel.style.display = 'block'; this.updateStatus('总结生成完成', 'success'); this.summaryPanel.scrollIntoView({ behavior: 'smooth', block: 'start' }); } catch (e) { this.updateStatus(`生成总结失败: ${e.message}`, 'error'); this.showNotification(`生成总结失败: ${e.message}`, 'error'); } finally { this.summaryButton.disabled = false; } } updateTitleWithModel() { const c = CONFIG.AI_MODELS[CONFIG.AI_MODELS.TYPE]; if (this.titleElement) this.titleElement.textContent = `💡 内容助手 - ${c ? c.MODEL : 'AI模型'}`; } createFormattedContent(container, text) { while (container.firstChild) { container.removeChild(container.firstChild); } const lines = text.split('\n'); let currentList = null; let listType = null; const closeList = () => { if (currentList) { container.appendChild(currentList); currentList = null; listType = null; } }; lines.forEach(line => { const trimmedLine = line.trim(); if (trimmedLine.startsWith('### ')) { closeList(); const h = document.createElement('h3'); h.textContent = trimmedLine.substring(4); h.style.cssText = `font-size: 1.1em; margin: 1em 0 0.5em;`; container.appendChild(h); } else if (trimmedLine.startsWith('## ')) { closeList(); const h = document.createElement('h2'); h.textContent = trimmedLine.substring(3); h.style.cssText = `font-size: 1.3em; margin: 1em 0 0.5em; border-bottom: 1px solid rgba(255,255,255,0.2); padding-bottom: 5px;`; container.appendChild(h); } else if (trimmedLine.startsWith('# ')) { closeList(); const h = document.createElement('h1'); h.textContent = trimmedLine.substring(2); h.style.cssText = `font-size: 1.5em; margin: 1em 0 0.5em; border-bottom: 2px solid rgba(255,255,255,0.3); padding-bottom: 8px;`; container.appendChild(h); } else if (trimmedLine.startsWith('- ') || trimmedLine.startsWith('* ')) { if (listType !== 'ul') { closeList(); currentList = document.createElement('ul'); listType = 'ul'; currentList.style.paddingLeft = '20px'; } const li = document.createElement('li'); this.parseInlineFormatting(li, trimmedLine.substring(2)); currentList.appendChild(li); } else if (trimmedLine.match(/^\d+\.\s/)) { if (listType !== 'ol') { closeList(); currentList = document.createElement('ol'); listType = 'ol'; currentList.style.paddingLeft = '20px'; } const li = document.createElement('li'); this.parseInlineFormatting(li, trimmedLine.replace(/^\d+\.\s/, '')); currentList.appendChild(li); } else if (trimmedLine) { closeList(); const p = document.createElement('p'); this.parseInlineFormatting(p, trimmedLine); container.appendChild(p); } }); closeList(); } parseInlineFormatting(element, text) { const parts = text.split(/(\*\*.*?\*\*|\*.*?\*|`.*?`)/g); parts.forEach(part => { if (part.startsWith('**') && part.endsWith('**')) { const s = document.createElement('strong'); s.textContent = part.slice(2, -2); element.appendChild(s); } else if (part.startsWith('*') && part.endsWith('*')) { const em = document.createElement('em'); em.textContent = part.slice(1, -1); element.appendChild(em); } else if (part.startsWith('`') && part.endsWith('`')) { const c = document.createElement('code'); c.textContent = part.slice(1, -1); c.style.cssText = `background: rgba(0,0,0,0.3); padding: 2px 4px; border-radius: 4px; font-family: monospace;`; element.appendChild(c); } else { element.appendChild(document.createTextNode(part)); } }); } makeDraggable(element) { let isDragging = false, initialX, initialY, xOffset = 0, yOffset = 0; element.addEventListener('mousedown', (e) => { if (e.target.tagName === 'BUTTON' || e.target.closest('button')) return; isDragging = true; initialX = e.clientX - xOffset; initialY = e.clientY - yOffset; }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; e.preventDefault(); xOffset = e.clientX - initialX; yOffset = e.clientY - yOffset; this.container.style.transform = `translate(${xOffset}px, ${yOffset}px)`; }); document.addEventListener('mouseup', () => { isDragging = false; }); } attachEventListeners() { let lastUrl = location.href; new MutationObserver(() => { if (location.href !== lastUrl) { lastUrl = location.href; if (this.container && this.container.parentNode) { this.container.remove(); } if (PageManager.isYouTube(lastUrl) || PageManager.isWeChat(lastUrl)) { initializeApp(); } } }).observe(document.body, { childList: true, subtree: true }); } } function getUid() { const platform = PageManager.getCurrentPlatform(); if (platform === 'YOUTUBE') return new URL(window.location.href).searchParams.get('v') || 'unknown_video'; if (platform === 'WECHAT') { const m = window.location.href.match(/__biz=([^&]+)&mid=([^&]+)/); if (m) return `${m[1]}_${m[2]}`; return 'unknown_article'; } return 'unknown'; } function initializeApp() { if (!PageManager.isYouTube() && !PageManager.isWeChat()) return; console.log(`🚀 内容专家助手(v2.5) 初始化 on ${PageManager.getCurrentPlatform()}...`); const contentController = new ContentController(); new UIManager(contentController); console.log('✅ 内容专家助手(v2.5) 初始化完成'); } // --- 应用启动 --- if (document.readyState === 'complete' || document.readyState === 'interactive') { initializeApp(); } else { document.addEventListener('DOMContentLoaded', initializeApp); } })();