// ==UserScript== // @name Folo自动阅读脚本 // @namespace http://tampermonkey.net/ // @version 1.8.5 // @description Folo网站的自动阅读功能,支持Microsoft Azure语音合成,Shadow DOM内容提取,智能页面切换检测,悬浮播放控制面板,安全配置管理,选择性朗读,双TTS引擎支持(Azure+系统),播放模式(正常/循环),播放控制快捷键,用户导航时智能重新朗读 // @author Stream-L // @match https://app.folo.is/timeline/view-0/* // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_xmlhttpRequest // @grant unsafeWindow // ==/UserScript== (function() { 'use strict'; // 默认配置 const DEFAULT_CONFIG = { region: '', token: '', voice: 'zh-CN-XiaoxiaoNeural', rate: '1.0', pitch: '0Hz', ttsEngine: 'azure', // 'azure' 或 'system' systemVoice: '', // 系统语音名称 playPauseKey: 'Space', rereadKey: 'KeyR', autoStart: true, readTitle: true, readContent: true, playMode: 'normal' // 'normal': 正常结束, 'loop': 循环播放 }; // 全局变量 let config = {}; let contentQueue = []; let currentIndex = 0; let isPlaying = false; let isPaused = false; let settingsWindow = null; let availableVoices = []; let currentAudio = null; let currentUrl = ''; let userNavigatedWhilePlaying = false; // 用户在朗读时导航的标记 let justFinishedReading = false; // 刚完成朗读的标记 let reloadTimeoutId = null; let floatingControl = null; // CSS样式 const styles = ` .folo-reader-panel { position: fixed; top: 20px; right: 20px; width: 350px; background: white; border: 1px solid #ccc; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 10000; font-family: Arial, sans-serif; font-size: 14px; } .folo-reader-header { background: #007acc; color: white; padding: 10px 15px; border-radius: 8px 8px 0 0; display: flex; justify-content: space-between; align-items: center; cursor: move; } .folo-reader-title { font-weight: bold; font-size: 16px; } .folo-reader-controls { display: flex; gap: 5px; } .folo-reader-btn { background: rgba(255,255,255,0.2); border: none; color: white; width: 24px; height: 24px; border-radius: 4px; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 12px; } .folo-reader-btn:hover { background: rgba(255,255,255,0.3); } .folo-reader-content { padding: 15px; max-height: 400px; overflow-y: auto; } .folo-reader-content.minimized { display: none; } .setting-group { margin-bottom: 15px; } .setting-label { display: block; margin-bottom: 5px; font-weight: bold; color: #333; } .setting-input { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box; } .setting-select { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box; } .setting-button { background: #007acc; color: white; border: none; padding: 8px 15px; border-radius: 4px; cursor: pointer; margin-right: 5px; margin-bottom: 5px; } .setting-button:hover { background: #005999; } .setting-button.secondary { background: #6c757d; } .setting-button.secondary:hover { background: #545b62; } .status-indicator { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 5px; } .status-success { background: #28a745; } .status-error { background: #dc3545; } .status-warning { background: #ffc107; } .progress-info { margin-top: 10px; padding: 10px; background: #f8f9fa; border-radius: 4px; font-size: 12px; } .key-binding { display: flex; align-items: center; margin-bottom: 8px; } .key-label { width: 80px; font-size: 12px; } .key-input { flex: 1; padding: 4px 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 12px; } /* 悬浮播放控制面板样式 */ .folo-floating-control { position: fixed; bottom: 30px; right: 30px; background: linear-gradient(135deg, #007acc, #0056b3); border-radius: 50px; padding: 12px 20px; box-shadow: 0 6px 20px rgba(0,122,204,0.3); z-index: 9999; font-family: Arial, sans-serif; display: flex; align-items: center; gap: 12px; cursor: move; transition: all 0.3s ease; backdrop-filter: blur(10px); border: 1px solid rgba(255,255,255,0.2); } .folo-floating-control:hover { box-shadow: 0 8px 25px rgba(0,122,204,0.4); transform: translateY(-2px); } .folo-control-btn { background: rgba(255,255,255,0.15); border: none; color: white; width: 36px; height: 36px; border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; transition: all 0.2s ease; backdrop-filter: blur(5px); } .folo-control-btn:hover { background: rgba(255,255,255,0.25); transform: scale(1.1); } .folo-control-btn:active { transform: scale(0.95); } .folo-control-btn.play-pause { width: 42px; height: 42px; font-size: 16px; background: rgba(255,255,255,0.2); } .folo-control-btn.play-pause:hover { background: rgba(255,255,255,0.3); } .folo-control-status { color: white; font-size: 12px; font-weight: 500; max-width: 150px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; text-shadow: 0 1px 2px rgba(0,0,0,0.3); } .folo-progress-indicator { color: rgba(255,255,255,0.8); font-size: 10px; background: rgba(255,255,255,0.1); padding: 2px 6px; border-radius: 10px; backdrop-filter: blur(5px); } `; // 初始化 function init() { console.log('Folo自动阅读脚本已启动'); // 记录初始URL currentUrl = window.location.href; // 加载配置 loadConfig(); // 添加样式 addStyles(); // 创建设置面板 createSettingsPanel(); // 创建悬浮控制面板 createFloatingControl(); // 绑定键盘事件 bindKeyEvents(); // 添加导航监听 setupNavigationListener(); // 注册菜单命令 GM_registerMenuCommand('打开设置面板', showSettingsPanel); // 等待页面加载完成后开始自动阅读 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { // DOMContentLoaded后再等待一点时间确保动态内容加载 setTimeout(startAutoReading, 1000); }); } else { // 页面已经加载完成,稍等一下再开始 setTimeout(startAutoReading, 1000); } } // 加载配置 function loadConfig() { config = Object.assign({}, DEFAULT_CONFIG); for (let key in DEFAULT_CONFIG) { const savedValue = GM_getValue(key); if (savedValue !== undefined) { config[key] = savedValue; } } // 如果GM存储中没有region和token,尝试从Cookie加载 if ((!config.region || !config.token)) { loadFromCookie(); } console.log('配置加载完成:', { hasRegion: !!config.region, hasToken: !!config.token, region: config.region }); } // 保存配置 function saveConfig() { for (let key in config) { GM_setValue(key, config[key]); } // 同时保存到cookie作为备份 saveToCookie(); } // 保存到Cookie function saveToCookie() { if (config.region && config.token) { const configData = { region: config.region, token: config.token, timestamp: Date.now() }; // 设置cookie过期时间为30天 const expires = new Date(); expires.setTime(expires.getTime() + (30 * 24 * 60 * 60 * 1000)); const cookieValue = encodeURIComponent(JSON.stringify(configData)); const cookieString = `folo_reader_config=${cookieValue}; expires=${expires.toUTCString()}; path=/`; document.cookie = cookieString; console.log('配置已保存到Cookie:', { region: configData.region, tokenLength: configData.token.length, expires: expires.toUTCString() }); } else { console.warn('配置不完整,跳过Cookie保存:', { hasRegion: !!config.region, hasToken: !!config.token }); } } // 从Cookie加载配置 function loadFromCookie() { const cookies = document.cookie.split(';'); for (let cookie of cookies) { const [name, value] = cookie.trim().split('='); if (name === 'folo_reader_config') { try { const configData = JSON.parse(decodeURIComponent(value)); if (configData.region && configData.token) { // 如果GM存储中没有配置,则使用Cookie中的配置 if (!config.region || !config.token) { config.region = configData.region; config.token = configData.token; // 保存到GM存储 GM_setValue('region', config.region); GM_setValue('token', config.token); console.log('从Cookie加载配置成功并同步到GM存储'); return true; } } } catch (error) { console.error('解析Cookie配置失败:', error); } break; } } return false; } // 检查并提示输入必要配置 function checkAndPromptConfig() { return new Promise((resolve, reject) => { // 如果已有配置,直接返回 if (config.region && config.token) { resolve(true); return; } // 尝试从Cookie加载 if (loadFromCookie() && config.region && config.token) { // 同步到设置面板UI updateConfigUI(); resolve(true); return; } // 显示配置提示弹窗 showConfigPrompt() .then((success) => { if (success) { console.log('配置弹窗完成,已保存配置'); resolve(true); } else { reject(new Error('用户取消配置')); } }) .catch(reject); }); } // 显示配置提示弹窗 function showConfigPrompt() { return new Promise((resolve) => { // 创建提示弹窗 const promptDialog = document.createElement('div'); promptDialog.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 99999; display: flex; align-items: center; justify-content: center; font-family: Arial, sans-serif; `; promptDialog.innerHTML = `

🎵 Folo自动阅读配置

需要配置Azure语音服务才能使用朗读功能:

配置将安全保存在本地,用于语音合成服务
💡 提示: 也可以通过右下角悬浮面板的设置按钮进行配置
`; document.body.appendChild(promptDialog); // 绑定事件 const regionInput = promptDialog.querySelector('#promptRegion'); const tokenInput = promptDialog.querySelector('#promptToken'); const confirmBtn = promptDialog.querySelector('#promptConfirm'); const cancelBtn = promptDialog.querySelector('#promptCancel'); // 如果有部分配置,预填充 if (settingsWindow) { const panel = settingsWindow; const currentRegion = panel.querySelector('#regionInput').value; const currentToken = panel.querySelector('#tokenInput').value; regionInput.value = currentRegion || config.region || ''; tokenInput.value = currentToken || config.token || ''; } else { regionInput.value = config.region || ''; tokenInput.value = config.token || ''; } confirmBtn.addEventListener('click', () => { const region = regionInput.value.trim(); const token = tokenInput.value.trim(); if (!region || !token) { alert('请填写完整的区域和密钥信息'); return; } config.region = region; config.token = token; // 保存配置到本地存储和Cookie saveConfig(); // 同步到设置面板UI updateConfigUI(); console.log('配置已保存并同步到设置面板'); document.body.removeChild(promptDialog); resolve(true); }); cancelBtn.addEventListener('click', () => { document.body.removeChild(promptDialog); resolve(false); }); // ESC键取消 const handleEsc = (e) => { if (e.key === 'Escape') { document.body.removeChild(promptDialog); document.removeEventListener('keydown', handleEsc); resolve(false); } }; document.addEventListener('keydown', handleEsc); // 聚焦到第一个输入框 setTimeout(() => regionInput.focus(), 100); }); } // 添加样式 function addStyles() { const styleSheet = document.createElement('style'); styleSheet.textContent = styles; document.head.appendChild(styleSheet); } // 创建设置面板 function createSettingsPanel() { const panel = document.createElement('div'); panel.className = 'folo-reader-panel'; panel.innerHTML = `
Folo自动阅读设置
1.0
0Hz
播放/暂停:
重新朗读:
💡 可以根据频道特点选择只读标题或只读内容,或者两者都读
🎵 循环模式会重复播放当前文章
状态: 未开始
进度: 0/0
当前内容:
`; document.body.appendChild(panel); settingsWindow = panel; // 绑定面板事件 bindPanelEvents(); // 加载当前配置到界面 loadConfigToUI(); // 默认隐藏面板 panel.style.display = 'none'; } // 创建悬浮控制面板 function createFloatingControl() { const control = document.createElement('div'); control.className = 'folo-floating-control'; control.innerHTML = `
准备就绪
0/0
`; document.body.appendChild(control); floatingControl = control; // 绑定悬浮控制面板事件 bindFloatingControlEvents(); // 使悬浮面板可拖拽 makeElementDraggable(control, control); // 初始化状态 updateFloatingControlStatus(); } // 绑定悬浮控制面板事件 function bindFloatingControlEvents() { const control = floatingControl; // 播放/暂停按钮 control.querySelector('#floatingPlayBtn').addEventListener('click', (e) => { e.stopPropagation(); e.preventDefault(); console.log('悬浮播放按钮被点击,当前状态:', { isPlaying, isPaused }); try { togglePlayPause(); } catch (error) { console.error('悬浮播放按钮处理出错:', error); updateStatus('播放控制出错: ' + error.message); } }); // 设置按钮 control.querySelector('#floatingSettingsBtn').addEventListener('click', (e) => { e.stopPropagation(); showSettingsPanel(); }); // 双击悬浮面板重新加载内容 control.addEventListener('dblclick', (e) => { e.stopPropagation(); reloadContent(); }); } // 更新悬浮控制面板状态 function updateFloatingControlStatus() { if (!floatingControl) { console.log('悬浮控制面板不存在,跳过状态更新'); return; } try { const playBtn = floatingControl.querySelector('#floatingPlayBtn'); const statusText = floatingControl.querySelector('#floatingStatus'); const progressText = floatingControl.querySelector('#floatingProgress'); if (!playBtn || !statusText || !progressText) { console.error('悬浮控制面板元素不完整'); return; } console.log('更新悬浮控制面板状态:', { isPlaying, isPaused, currentIndex, queueLength: contentQueue.length }); // 更新播放按钮 if (isPlaying && !isPaused) { playBtn.textContent = '⏸'; playBtn.title = '暂停'; } else { playBtn.textContent = '▶'; playBtn.title = '播放'; } // 更新状态文本 let status = '准备就绪'; if (isPlaying && !isPaused) { const currentContent = contentQueue[currentIndex]; if (currentContent) { switch(currentContent.type) { case 'title': status = '标题'; break; case 'article-paragraph': status = '段落'; break; case 'article-heading': status = '文章标题'; break; case 'quoted-text': status = '引用'; break; case 'article-sentence': status = '句子'; break; default: status = '朗读中'; } } else { status = '朗读中'; } } else if (isPaused) { status = '已暂停'; } else if (contentQueue.length === 0) { status = '无内容'; } else { // 显示当前配置状态 const modeText = { 'normal': '普通', 'loop': '循环', 'sequential': '顺序' }[config.playMode] || '普通'; if (config.readTitle && config.readContent) { status = `标题+内容·${modeText}`; } else if (config.readTitle) { status = `仅标题·${modeText}`; } else if (config.readContent) { status = `仅内容·${modeText}`; } else { status = '未配置'; } } statusText.textContent = status; // 更新进度 if (contentQueue.length > 0) { progressText.textContent = `${currentIndex + 1}/${contentQueue.length}`; } else { progressText.textContent = '0/0'; } } catch (error) { console.error('更新悬浮控制面板状态时出错:', error); } } // 绑定面板事件 function bindPanelEvents() { const panel = settingsWindow; // 最小化按钮 panel.querySelector('#minimizeBtn').addEventListener('click', () => { const content = panel.querySelector('#panelContent'); content.classList.toggle('minimized'); const btn = panel.querySelector('#minimizeBtn'); btn.textContent = content.classList.contains('minimized') ? '+' : '−'; }); // 关闭按钮 panel.querySelector('#closeBtn').addEventListener('click', hideSettingsPanel); // 测试连接按钮 panel.querySelector('#testConnectionBtn').addEventListener('click', testConnection); // 测试发音按钮 panel.querySelector('#testVoiceBtn').addEventListener('click', testVoice); // 保存设置按钮 panel.querySelector('#saveBtn').addEventListener('click', saveSettings); // 重置默认按钮 panel.querySelector('#resetBtn').addEventListener('click', resetToDefault); // 重新提取内容按钮 panel.querySelector('#refreshContentBtn').addEventListener('click', () => { contentQueue = extractContent(); currentIndex = 0; updateProgress(); updateStatus(`重新提取完成,共${contentQueue.length}段内容`); }); // 调试内容提取按钮 panel.querySelector('#debugContentBtn').addEventListener('click', debugContentExtraction); // 语速滑块 const rateSlider = panel.querySelector('#rateSlider'); const rateValue = panel.querySelector('#rateValue'); rateSlider.addEventListener('input', (e) => { rateValue.textContent = e.target.value; }); // 音调滑块 const pitchSlider = panel.querySelector('#pitchSlider'); const pitchValue = panel.querySelector('#pitchValue'); pitchSlider.addEventListener('input', (e) => { pitchValue.textContent = e.target.value + 'Hz'; }); // TTS引擎切换 const ttsEngineSelect = panel.querySelector('#ttsEngineSelect'); ttsEngineSelect.addEventListener('change', (e) => { const isAzure = e.target.value === 'azure'; // 显示/隐藏相关设置组 panel.querySelector('#azureSettings').style.display = isAzure ? 'block' : 'none'; panel.querySelector('#azureTokenGroup').style.display = isAzure ? 'block' : 'none'; panel.querySelector('#azureTestGroup').style.display = isAzure ? 'block' : 'none'; // 切换语音选择器 panel.querySelector('#voiceSelect').style.display = isAzure ? 'block' : 'none'; panel.querySelector('#systemVoiceSelect').style.display = isAzure ? 'none' : 'block'; // 如果切换到系统TTS,加载系统语音 if (!isAzure) { loadSystemVoices(); } }); // 保留的快捷键输入(播放暂停和重新阅读) const keyInputs = ['playPauseKeyInput', 'rereadKeyInput']; keyInputs.forEach(inputId => { const input = panel.querySelector('#' + inputId); input.addEventListener('keydown', (e) => { e.preventDefault(); input.value = e.code; }); }); // 拖拽功能 makeElementDraggable(panel, panel.querySelector('.folo-reader-header')); // 朗读选项变化时的提示 const readTitleCheckbox = panel.querySelector('#readTitleCheckbox'); const readContentCheckbox = panel.querySelector('#readContentCheckbox'); readTitleCheckbox.addEventListener('change', () => { if (!readTitleCheckbox.checked && !readContentCheckbox.checked) { alert('至少需要选择一种内容类型进行朗读!'); readTitleCheckbox.checked = true; } }); readContentCheckbox.addEventListener('change', () => { if (!readTitleCheckbox.checked && !readContentCheckbox.checked) { alert('至少需要选择一种内容类型进行朗读!'); readContentCheckbox.checked = true; } }); } // 加载配置到UI function loadConfigToUI() { const panel = settingsWindow; if (!panel) return; panel.querySelector('#ttsEngineSelect').value = config.ttsEngine || 'azure'; panel.querySelector('#regionInput').value = config.region; panel.querySelector('#tokenInput').value = config.token; panel.querySelector('#rateSlider').value = config.rate; panel.querySelector('#rateValue').textContent = config.rate; panel.querySelector('#pitchSlider').value = config.pitch.replace('Hz', ''); panel.querySelector('#pitchValue').textContent = config.pitch; panel.querySelector('#playPauseKeyInput').value = config.playPauseKey; panel.querySelector('#rereadKeyInput').value = config.rereadKey; panel.querySelector('#readTitleCheckbox').checked = config.readTitle; panel.querySelector('#readContentCheckbox').checked = config.readContent; panel.querySelector('#playModeSelect').value = config.playMode; // 触发TTS引擎切换事件以显示/隐藏相关设置 const ttsEngineSelect = panel.querySelector('#ttsEngineSelect'); ttsEngineSelect.dispatchEvent(new Event('change')); // 如果是系统TTS,设置系统语音选择 if (config.ttsEngine === 'system' && config.systemVoice) { setTimeout(() => { const systemVoiceSelect = panel.querySelector('#systemVoiceSelect'); if (systemVoiceSelect) { systemVoiceSelect.value = config.systemVoice; } }, 100); } } // 更新配置UI(用于同步弹窗输入的配置) function updateConfigUI() { if (!settingsWindow) return; const panel = settingsWindow; panel.querySelector('#regionInput').value = config.region; panel.querySelector('#tokenInput').value = config.token; console.log('设置面板UI已更新:', { region: config.region, tokenLength: config.token.length }); } // 显示设置面板 function showSettingsPanel() { if (settingsWindow) { settingsWindow.style.display = 'block'; loadAvailableVoices(); } } // 隐藏设置面板 function hideSettingsPanel() { if (settingsWindow) { settingsWindow.style.display = 'none'; } } // 获取Azure TTS端点 function getTTSEndpoint(region) { return `https://${region}.tts.speech.microsoft.com/cognitiveservices/v1`; } // 获取语音列表端点 function getVoicesEndpoint(region) { return `https://${region}.tts.speech.microsoft.com/cognitiveservices/voices/list`; } // 测试连接 function testConnection() { const panel = settingsWindow; const regionInput = panel.querySelector('#regionInput'); const tokenInput = panel.querySelector('#tokenInput'); const statusSpan = panel.querySelector('#connectionStatus'); const testRegion = regionInput.value.trim() || config.region; const testToken = tokenInput.value.trim() || config.token; if (!testRegion || !testToken) { statusSpan.innerHTML = '请填写完整的区域和Token'; return; } statusSpan.innerHTML = '测试中...'; // 测试语音合成 const testText = '连接测试成功'; synthesizeText(testText, testRegion, testToken, config.voice) .then(audioData => { statusSpan.innerHTML = '连接成功'; // 播放测试音频 playAudioData(audioData); // 加载语音列表 loadAvailableVoices(testRegion, testToken); }) .catch(error => { console.error('连接测试失败:', error); statusSpan.innerHTML = '连接失败: ' + error.message; }); } // 加载系统语音 function loadSystemVoices() { const panel = settingsWindow; if (!panel) return; const systemVoiceSelect = panel.querySelector('#systemVoiceSelect'); if (!systemVoiceSelect) return; if ('speechSynthesis' in window) { const voices = speechSynthesis.getVoices(); systemVoiceSelect.innerHTML = ''; // 筛选中文语音或默认语音 const chineseVoices = voices.filter(voice => voice.lang.includes('zh') || voice.lang.includes('cn') || voice.name.includes('Chinese') ); const voicesToShow = chineseVoices.length > 0 ? chineseVoices : voices; voicesToShow.forEach(voice => { const option = document.createElement('option'); option.value = voice.name; option.textContent = `${voice.name} (${voice.lang})`; systemVoiceSelect.appendChild(option); }); // 如果没有找到语音,等待语音加载完成 if (voices.length === 0) { speechSynthesis.addEventListener('voiceschanged', loadSystemVoices, { once: true }); } } else { systemVoiceSelect.innerHTML = ''; } } // 加载可用语音 function loadAvailableVoices(region = null, token = null) { const panel = settingsWindow; const voiceSelect = panel.querySelector('#voiceSelect'); const targetRegion = region || config.region; const targetToken = token || config.token; if (!targetRegion || !targetToken) { console.error('缺少区域或Token信息'); return; } voiceSelect.innerHTML = ''; GM_xmlhttpRequest({ method: 'GET', url: getVoicesEndpoint(targetRegion), headers: { 'Ocp-Apim-Subscription-Key': targetToken }, onload: function(response) { try { if (response.status === 200) { const voices = JSON.parse(response.responseText); availableVoices = voices.filter(voice => voice.Locale.startsWith('zh-CN')); voiceSelect.innerHTML = ''; availableVoices.forEach(voice => { const option = document.createElement('option'); option.value = voice.ShortName; option.textContent = `${voice.LocalName} (${voice.Gender})`; if (voice.ShortName === config.voice) { option.selected = true; } voiceSelect.appendChild(option); }); console.log('成功加载语音列表:', availableVoices.length, '个中文语音'); } else { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } } catch (error) { console.error('解析语音列表失败:', error); voiceSelect.innerHTML = ''; } }, onerror: function(error) { console.error('获取语音列表失败:', error); voiceSelect.innerHTML = ''; } }); } // 测试发音 function testVoice() { const panel = settingsWindow; const ttsEngineSelect = panel.querySelector('#ttsEngineSelect'); const voiceSelect = panel.querySelector('#voiceSelect'); const systemVoiceSelect = panel.querySelector('#systemVoiceSelect'); const rateSlider = panel.querySelector('#rateSlider'); const pitchSlider = panel.querySelector('#pitchSlider'); const testText = '您好,这是语音测试。Hello, this is a voice test.'; if (ttsEngineSelect.value === 'system') { // 系统TTS测试 if (!systemVoiceSelect.value) { alert('请先选择一个系统语音'); return; } // 临时设置配置进行测试 const originalConfig = { ...config }; config.ttsEngine = 'system'; config.systemVoice = systemVoiceSelect.value; config.rate = rateSlider.value; synthesizeSystemTTS(testText) .then(() => { console.log('系统TTS测试完成'); }) .catch(error => { console.error('测试发音失败:', error); alert('测试发音失败: ' + error.message); }) .finally(() => { // 恢复原配置 Object.assign(config, originalConfig); }); } else { // Azure TTS测试 if (!voiceSelect.value) { alert('请先选择一个语音'); return; } if (!config.region || !config.token) { alert('请先配置Azure区域和密钥'); return; } synthesizeText(testText, config.region, config.token, voiceSelect.value, rateSlider.value, pitchSlider.value + 'Hz') .then(audioData => { playAudioData(audioData); }) .catch(error => { console.error('测试发音失败:', error); alert('测试发音失败: ' + error.message); }); } } // 保存设置 function saveSettings() { const panel = settingsWindow; config.ttsEngine = panel.querySelector('#ttsEngineSelect').value; config.region = panel.querySelector('#regionInput').value; config.token = panel.querySelector('#tokenInput').value; config.voice = panel.querySelector('#voiceSelect').value; config.systemVoice = panel.querySelector('#systemVoiceSelect').value; config.rate = panel.querySelector('#rateSlider').value; config.pitch = panel.querySelector('#pitchSlider').value + 'Hz'; config.playPauseKey = panel.querySelector('#playPauseKeyInput').value; config.rereadKey = panel.querySelector('#rereadKeyInput').value; config.readTitle = panel.querySelector('#readTitleCheckbox').checked; config.readContent = panel.querySelector('#readContentCheckbox').checked; config.playMode = panel.querySelector('#playModeSelect').value; // 验证至少选择一种内容类型 if (!config.readTitle && !config.readContent) { alert('请至少选择朗读标题或朗读内容中的一项!'); // 恢复默认设置 panel.querySelector('#readTitleCheckbox').checked = true; panel.querySelector('#readContentCheckbox').checked = true; config.readTitle = true; config.readContent = true; } saveConfig(); console.log('设置已保存到GM存储和Cookie:', { region: config.region, tokenLength: config.token.length, readTitle: config.readTitle, readContent: config.readContent }); alert('设置已保存'); } // 重置默认设置 function resetToDefault() { if (confirm('确定要重置为默认设置吗?')) { config = Object.assign({}, DEFAULT_CONFIG); saveConfig(); loadConfigToUI(); alert('已重置为默认设置'); } } // 新的内容提取逻辑(支持Shadow DOM) function extractContent() { const contentParts = []; // 1. 提取标题内容(根据配置决定是否包含) if (config.readTitle) { const titleElements = document.querySelectorAll('#follow-app-grid-container > div > div.\\@container.relative.flex.size-full.flex-col.overflow-hidden.print\\:size-auto.print\\:overflow-visible > div > div > div > div > article > div.group.relative.block.min-w-0.rounded-lg > div > a > div > div'); titleElements.forEach((titleContainer, index) => { const titleParagraphs = titleContainer.querySelectorAll('p'); titleParagraphs.forEach(p => { const text = p.textContent.trim(); if (text && text.length > 2) { contentParts.push({ type: 'title', content: text, element: p, index: index }); } }); }); } // 2. 提取文章内容(根据配置决定是否包含) if (config.readContent) { const shadowHostSelector = '#follow-app-grid-container > div > div.\\@container.relative.flex.size-full.flex-col.overflow-hidden.print\\:size-auto.print\\:overflow-visible > div > div > div > div > article > div:nth-child(2) > div.mx-auto.mb-32.mt-8.max-w-full.cursor-auto.text-\\[0\\.94rem\\] > div'; const shadowHosts = document.querySelectorAll(shadowHostSelector); console.log(`找到 ${shadowHosts.length} 个可能包含Shadow DOM的元素`); shadowHosts.forEach((shadowHost, hostIndex) => { // 检查是否有shadowRoot if (shadowHost.shadowRoot) { console.log(`Shadow DOM主机 ${hostIndex} 有shadowRoot`); // 在shadowRoot中查找 #follow-entry-render const articleElement = shadowHost.shadowRoot.querySelector('#follow-entry-render'); if (articleElement) { console.log(`在Shadow DOM中找到 #follow-entry-render`); // 按原文顺序提取所有相关元素(p, h1-h6) const allElements = articleElement.querySelectorAll('p, h1, h2, h3, h4, h5, h6'); allElements.forEach((element, elementIndex) => { const text = element.textContent.trim(); if (text && text.length > 2) { let elementType, additionalInfo = {}; if (element.tagName.toLowerCase() === 'p') { elementType = 'article-paragraph'; additionalInfo.paragraphIndex = elementIndex; } else if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(element.tagName.toLowerCase())) { elementType = 'article-heading'; additionalInfo.headingLevel = element.tagName.toLowerCase(); additionalInfo.headingIndex = elementIndex; } contentParts.push({ type: elementType, content: text, element: element, articleIndex: hostIndex, elementIndex: elementIndex, ...additionalInfo }); } }); // 提取双引号包裹的文字内容 const textContent = articleElement.textContent || articleElement.innerText || ''; const quotedTextRegex = /"([^"]+)"/g; let match; let quotedCount = 0; while ((match = quotedTextRegex.exec(textContent)) !== null) { const quotedText = match[1].trim(); if (quotedText && quotedText.length > 2) { contentParts.push({ type: 'quoted-text', content: quotedText, element: articleElement, articleIndex: hostIndex, quotedIndex: quotedCount }); quotedCount++; } } // 如果没有找到p和标题元素,按句子分割 if (allElements.length === 0) { const fullText = articleElement.textContent || ''; if (fullText.trim()) { // 按句子分割(中英文句号、问号、感叹号) const sentences = fullText.split(/[。!?.!?]+/).filter(s => s.trim().length > 5); sentences.forEach((sentence, sentIndex) => { const text = sentence.trim(); if (text) { contentParts.push({ type: 'article-sentence', content: text, element: articleElement, articleIndex: hostIndex, sentenceIndex: sentIndex }); } }); } } console.log(`Shadow DOM文章 ${hostIndex} 提取完成: ${allElements.length}个元素, ${quotedCount}个引号文本`); } else { console.log(`Shadow DOM主机 ${hostIndex} 中未找到 #follow-entry-render`); } } else { console.log(`元素 ${hostIndex} 没有shadowRoot属性`); } }); // 3. 备用方法:直接查找普通DOM中的 #follow-entry-render(如果存在) const directArticleElements = document.querySelectorAll('#follow-entry-render'); if (directArticleElements.length > 0) { console.log(`备用方法:在普通DOM中找到 ${directArticleElements.length} 个 #follow-entry-render 元素`); directArticleElements.forEach((article, index) => { // 按原文顺序提取所有相关元素(p, h1-h6) const allElements = article.querySelectorAll('p, h1, h2, h3, h4, h5, h6'); allElements.forEach((element, elementIndex) => { const text = element.textContent.trim(); if (text && text.length > 2) { let elementType, additionalInfo = {}; if (element.tagName.toLowerCase() === 'p') { elementType = 'article-paragraph'; } else if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(element.tagName.toLowerCase())) { elementType = 'article-heading'; additionalInfo.headingLevel = element.tagName.toLowerCase(); } contentParts.push({ type: elementType, content: text, element: element, articleIndex: index, elementIndex: elementIndex, ...additionalInfo }); } }); // 提取双引号包裹的文字内容 const textContent = article.textContent || article.innerText || ''; const quotedTextRegex = /"([^"]+)"/g; let match; while ((match = quotedTextRegex.exec(textContent)) !== null) { const quotedText = match[1].trim(); if (quotedText && quotedText.length > 2) { contentParts.push({ type: 'quoted-text', content: quotedText, element: article, articleIndex: index }); } } }); } } // 去重处理 - 移除重复的内容 const uniqueContent = []; const seenTexts = new Set(); contentParts.forEach(item => { const normalizedText = item.content.replace(/\s+/g, ' ').trim(); if (!seenTexts.has(normalizedText) && normalizedText.length > 2) { seenTexts.add(normalizedText); uniqueContent.push(item); } }); console.log('提取到的内容数量:', uniqueContent.length); console.log('标题数量:', uniqueContent.filter(item => item.type === 'title').length); console.log('文章段落数量:', uniqueContent.filter(item => item.type === 'article-paragraph').length); console.log('文章标题数量:', uniqueContent.filter(item => item.type === 'article-heading').length); console.log('引号文本数量:', uniqueContent.filter(item => item.type === 'quoted-text').length); console.log('句子数量:', uniqueContent.filter(item => item.type === 'article-sentence').length); console.log('配置状态:', { readTitle: config.readTitle, readContent: config.readContent }); return uniqueContent; } // 设置导航监听器 function setupNavigationListener() { let lastContentHash = ''; // 计算内容哈希值 function getContentHash() { const contentContainer = document.querySelector('#follow-app-grid-container'); if (contentContainer) { return contentContainer.innerHTML.length + contentContainer.textContent.slice(0, 100); } return window.location.href; } // 初始化内容哈希 lastContentHash = getContentHash(); // 监听URL变化和内容变化 const observer = new MutationObserver((mutations) => { const newUrl = window.location.href; const newContentHash = getContentHash(); // URL变化 if (newUrl !== currentUrl) { console.log('检测到URL变化:', currentUrl, '->', newUrl); currentUrl = newUrl; lastContentHash = newContentHash; handleNavigationChange(); return; } // 内容显著变化(可能是SPA路由) if (newContentHash !== lastContentHash) { console.log('检测到内容变化,可能是页面切换'); lastContentHash = newContentHash; // 延迟一点时间确保是真正的页面切换而不是小的DOM更新 clearTimeout(this.contentChangeTimeout); this.contentChangeTimeout = setTimeout(() => { handleNavigationChange(); }, 500); } }); // 开始观察document的变化 observer.observe(document, { childList: true, subtree: true, attributes: true, attributeFilter: ['class', 'style'] }); // 也监听popstate事件(浏览器前进后退) window.addEventListener('popstate', () => { console.log('检测到popstate事件'); handleNavigationChange(); }); // 监听hashchange事件 window.addEventListener('hashchange', () => { console.log('检测到hashchange事件'); handleNavigationChange(); }); } // 处理导航变化 function handleNavigationChange() { console.log('检测到页面变化,重新加载内容'); const shouldRestart = userNavigatedWhilePlaying || justFinishedReading; // 保存重启标记 stopCurrentPlayback(); // 清除之前的重载计时器 if (reloadTimeoutId) { clearTimeout(reloadTimeoutId); } // 延迟重新加载内容,等待新页面加载完成 reloadTimeoutId = setTimeout(() => { console.log('新页面加载完成,重新提取内容'); reloadContent(); // 如果用户在朗读时导航或刚完成朗读,自动重新开始朗读 if (shouldRestart) { const reason = userNavigatedWhilePlaying ? '朗读时导航' : '完成朗读后切换'; console.log(`${reason},准备重新开始朗读`); updateStatus('页面切换完成,正在重新开始朗读...'); userNavigatedWhilePlaying = false; // 重置标记 justFinishedReading = false; // 重置标记 // 稍微延迟一下确保内容完全加载 setTimeout(() => { checkAndPromptConfig() .then(() => { contentQueue = extractContent(); currentIndex = 0; if (contentQueue.length > 0) { updateStatus('重新开始朗读'); playNext(); } else { updateStatus('新页面无内容可播放'); } }) .catch((error) => { console.error('重新开始朗读失败:', error); updateStatus('重新开始朗读失败: ' + error.message); }); }, 500); } }, 2000); // 等待2秒让新内容加载 } // 停止当前播放 function stopCurrentPlayback() { console.log('停止当前播放, TTS引擎:', config.ttsEngine); if (currentAudio) { if (config.ttsEngine === 'system') { // 系统TTS完全停止 if (speechSynthesis.speaking || speechSynthesis.paused) { speechSynthesis.cancel(); console.log('系统TTS已取消'); } } else { // Azure TTS停止 try { currentAudio.pause(); currentAudio.currentTime = 0; // 重置播放位置 console.log('Azure TTS已停止'); } catch (error) { console.log('Azure TTS停止时出错:', error); } } currentAudio = null; } isPlaying = false; isPaused = false; // 移除高亮 removeHighlight(); console.log('播放状态已重置'); updateStatus('已停止 - 检测到页面切换'); console.log('已停止当前播放'); // 更新悬浮控制面板状态 updateFloatingControlStatus(); } // 重新加载内容 function reloadContent() { console.log('重新加载内容...'); contentQueue = extractContent(); currentIndex = 0; if (contentQueue.length > 0) { updateStatus('内容已重新加载'); updateProgress(); console.log('重新加载完成,共', contentQueue.length, '段内容'); // 如果之前在播放且有配置,检查配置后自动开始新内容的播放 // 但如果是用户导航或刚完成朗读触发的,则不在这里自动播放(会在handleNavigationChange中处理) if (config.autoStart && isPlaying && !userNavigatedWhilePlaying && !justFinishedReading) { const playModeText = { 'normal': '普通模式', 'loop': '循环模式', 'sequential': '顺序模式' }[config.playMode] || '普通模式'; updateStatus(`${playModeText} - 加载新内容`); setTimeout(() => { checkAndPromptConfig() .then(() => { console.log('重新加载后自动播放 (', config.playMode, '模式)'); playNext(); }) .catch((error) => { console.log('重新加载后配置检查失败:', error.message); updateStatus('需要配置后点击播放'); isPlaying = false; updateFloatingControlStatus(); }); }, 500); } } else { updateStatus('重新加载后未找到内容'); console.log('重新加载后未找到可阅读内容'); } } // 开始自动阅读 function startAutoReading() { if (!config.autoStart) { console.log('自动开始已关闭,等待手动启动'); return; } console.log('开始自动阅读流程...'); // 使用智能等待,检测页面内容是否已加载 function waitForContent(attempts = 0) { const maxAttempts = 10; // 最多尝试10次 const interval = 1000; // 每次间隔1秒 console.log(`尝试提取内容 (第${attempts + 1}次)`); contentQueue = extractContent(); currentIndex = 0; if (contentQueue.length > 0) { updateStatus('内容已加载,检查配置...'); updateProgress(); console.log('内容加载完成,共', contentQueue.length, '段内容'); // 检查配置,如果有效则自动开始播放 checkAndPromptConfig() .then(() => { console.log('配置检查通过,开始自动播放'); updateStatus('配置检查通过,开始播放'); // 延迟一点时间确保状态更新完成 setTimeout(() => { playNext(); }, 500); }) .catch((error) => { console.log('配置检查失败或用户取消:', error.message); updateStatus('点击播放按钮开始阅读'); isPlaying = false; updateFloatingControlStatus(); }); } else if (attempts < maxAttempts) { console.log(`第${attempts + 1}次未找到内容,${interval/1000}秒后重试...`); updateStatus(`等待内容加载... (${attempts + 1}/${maxAttempts})`); setTimeout(() => waitForContent(attempts + 1), interval); } else { updateStatus('未找到可阅读内容'); console.log('达到最大尝试次数,页面内容提取失败'); } } // 开始等待内容 setTimeout(() => waitForContent(), 2000); // 首次延迟2秒开始 } // 简化的调试内容提取功能 function debugContentExtraction() { console.clear(); console.log('=== 内容提取调试开始 ==='); // 检查标题元素 const titleElements = document.querySelectorAll('#follow-app-grid-container > div > div.\\@container.relative.flex.size-full.flex-col.overflow-hidden.print\\:size-auto.print\\:overflow-visible > div > div > div > div > article > div.group.relative.block.min-w-0.rounded-lg > div > a > div > div'); console.log('找到标题容器数量:', titleElements.length); titleElements.forEach((titleContainer, index) => { const titleParagraphs = titleContainer.querySelectorAll('p'); console.log(`标题容器 ${index} 包含 ${titleParagraphs.length} 个p元素`); titleParagraphs.forEach((p, pIndex) => { console.log(` 标题 ${index}-${pIndex}:`, p.textContent.substring(0, 100)); }); }); // 检查文章元素 const articleElements = document.querySelectorAll('#follow-entry-render'); console.log('找到文章元素数量:', articleElements.length); articleElements.forEach((article, index) => { console.log(`文章 ${index}:`, article); const paragraphs = article.querySelectorAll('p'); console.log(` 包含 ${paragraphs.length} 个p元素`); // 检查双引号文本 const textContent = article.textContent || ''; const quotedMatches = textContent.match(/"[^"]+"/g); console.log(` 找到 ${quotedMatches ? quotedMatches.length : 0} 个双引号文本`); if (quotedMatches) { quotedMatches.slice(0, 3).forEach((match, i) => { console.log(` 引号文本 ${i}:`, match); }); } }); // 测试当前提取方法 const extractedContent = extractContent(); console.log('当前提取方法结果:', extractedContent); console.log('=== 内容提取调试结束 ==='); // 显示结果给用户 const summary = ` 调试完成!检查浏览器控制台查看详细信息。 - 找到 ${titleElements.length} 个标题容器 - 找到 ${articleElements.length} 个文章元素 - 总共提取 ${extractedContent.length} 段内容 💡 如果遇到播放错误,请: 1. 打开浏览器开发者工具 (F12) 2. 查看控制台 (Console) 标签页 3. 查找红色的错误信息 4. 检查网络 (Network) 标签页是否有失败的请求 常见问题: - 403错误:检查Azure密钥是否正确 - 网络错误:检查网络连接 - 音频播放错误:可能是浏览器策略问题,尝试手动点击播放 `; alert(summary); } // 播放下一段内容 function playNext() { if (currentIndex >= contentQueue.length) { // 播放完成,根据播放模式决定下一步操作 handlePlaybackEnd(); return; } const currentContent = contentQueue[currentIndex]; if (!currentContent || !currentContent.content) { currentIndex++; playNext(); return; } // 检查配置 checkAndPromptConfig() .then(() => { continuePlayback(currentContent); }) .catch((error) => { console.error('配置检查失败:', error); updateStatus('需要配置Azure语音服务'); isPlaying = false; updateFloatingControlStatus(); }); } // 处理播放结束 function handlePlaybackEnd() { console.log('播放结束,当前播放模式:', config.playMode); switch (config.playMode) { case 'loop': // 循环模式 - 重新播放当前条目 updateStatus('循环播放 - 重新开始'); console.log('循环模式:重新开始播放'); setTimeout(() => { currentIndex = 0; // 重置到开头 playNext(); }, 1000); // 稍微停顿1秒 break; case 'normal': default: // 正常模式 - 播放完成后停止 updateStatus('阅读完成'); console.log('正常模式:播放完成,停止播放'); isPlaying = false; isPaused = false; justFinishedReading = true; // 设置刚完成朗读标记 updateFloatingControlStatus(); break; } } function continuePlayback(currentContent) { isPlaying = true; isPaused = false; // 根据内容类型更新状态 let statusText = '正在阅读'; switch(currentContent.type) { case 'title': statusText = '正在阅读标题'; break; case 'article-paragraph': statusText = '正在阅读文章段落'; break; case 'article-heading': statusText = `正在阅读文章${currentContent.headingLevel}标题`; break; case 'quoted-text': statusText = '正在阅读引用文本'; break; default: statusText = '正在阅读'; } updateStatus(statusText); updateProgress(); updateCurrentContent(currentContent.content, currentContent.type); // 更新悬浮控制面板状态 updateFloatingControlStatus(); // 高亮当前阅读的元素 highlightElement(currentContent.element); // 使用Azure TTS合成语音 console.log('开始语音合成:', { text: currentContent.content.substring(0, 50) + '...', region: config.region, voice: config.voice, rate: config.rate, pitch: config.pitch }); synthesizeVoice(currentContent.content) .then(audioData => { console.log('语音合成成功,开始播放音频'); if (config.ttsEngine === 'system') { // 系统TTS直接播放,不需要playAudioData console.log('音频播放完成'); removeHighlight(); currentIndex++; setTimeout(() => { if (!isPaused) { playNext(); } }, 500); // 段落间短暂停顿 } else { // Azure TTS需要播放音频数据 playAudioData(audioData) .then(() => { console.log('音频播放完成'); removeHighlight(); currentIndex++; setTimeout(() => { if (!isPaused) { playNext(); } }, 500); // 段落间短暂停顿 }) .catch(error => { console.error('音频播放错误:', error); console.error('音频播放错误详细信息:', { error: error, message: error.message, stack: error.stack }); updateStatus('播放错误: ' + error.message); isPlaying = false; removeHighlight(); updateFloatingControlStatus(); }); } }) .catch(error => { console.error('语音合成错误:', error); // 构建更清楚的错误信息 let errorMsg = ''; let originalError = ''; if (error.message) { originalError = error.message; errorMsg = error.message; // 特殊错误的友好提示(但保留原始信息) if (errorMsg.includes('Session rule count exceeded')) { errorMsg = `Azure TTS错误: ${originalError} (可能是会话限制而非配额问题)`; } else if (errorMsg.includes('Unauthorized')) { errorMsg = `认证失败: ${originalError}`; } else if (errorMsg.includes('network')) { errorMsg = `网络错误: ${originalError}`; } } else if (typeof error === 'string') { errorMsg = error; } else if (error.status) { errorMsg = `HTTP ${error.status}: ${error.statusText || '请求失败'}`; } else { errorMsg = '未知错误'; } console.error('语音合成错误详细信息:', { error: error, message: errorMsg, stack: error.stack, config: { region: config.region, hasToken: !!config.token, voice: config.voice } }); updateStatus('合成失败: ' + errorMsg); isPlaying = false; removeHighlight(); updateFloatingControlStatus(); }); } // 暂停/继续播放 function togglePlayPause() { console.log('执行togglePlayPause,当前状态:', { isPlaying, isPaused }); if (isPlaying && !isPaused) { // 暂停当前播放 if (currentAudio) { if (config.ttsEngine === 'system') { // 系统TTS暂停 if (speechSynthesis.speaking) { speechSynthesis.pause(); console.log('系统TTS已暂停'); } } else { // Azure TTS暂停 currentAudio.pause(); console.log('Azure TTS已暂停'); } } isPaused = true; updateStatus('已暂停'); } else if (isPaused) { // 继续播放 if (currentAudio) { if (config.ttsEngine === 'system') { // 系统TTS恢复 if (speechSynthesis.paused) { speechSynthesis.resume(); console.log('系统TTS已恢复'); } else { // 如果暂停状态异常,重新开始当前内容 console.log('系统TTS状态异常,重新播放当前内容'); if (currentIndex < contentQueue.length) { const currentContent = contentQueue[currentIndex]; synthesizeText(currentContent.content); } } } else { // Azure TTS恢复 const playResult = currentAudio.play(); if (playResult instanceof Promise) { playResult.catch(error => { console.log('Azure TTS恢复失败,重新播放:', error); // 如果恢复失败,重新播放当前内容 if (currentIndex < contentQueue.length) { const currentContent = contentQueue[currentIndex]; synthesizeText(currentContent.content); } }); } console.log('Azure TTS已恢复'); } } isPaused = false; updateStatus('正在阅读'); } else { // 重新开始 - 需要检查配置 checkAndPromptConfig() .then(() => { contentQueue = extractContent(); currentIndex = 0; if (contentQueue.length > 0) { playNext(); } else { updateStatus('无内容可播放'); } }) .catch((error) => { console.error('配置检查失败:', error); updateStatus('需要配置Azure语音服务'); // 自动打开设置面板 showSettingsPanel(); }); } // 更新悬浮控制面板状态 updateFloatingControlStatus(); } // 重新朗读当前段落 function rereadCurrent() { if (currentIndex > 0) { currentIndex--; } if (currentAudio) { currentAudio.pause(); currentAudio = null; } playNext(); } // 绑定键盘事件(只保留播放暂停和重新阅读功能,监听导航键以便重新朗读) function bindKeyEvents() { document.addEventListener('keydown', (e) => { // 防止在输入框中触发 if (e.target.tagName.toLowerCase() === 'input' || e.target.tagName.toLowerCase() === 'textarea') { return; } // 检测←→方向键(如果正在朗读,标记需要重新开始) if (e.code === 'ArrowLeft' || e.code === 'ArrowRight') { if (isPlaying && !isPaused) { console.log('检测到用户导航操作,正在朗读中,标记需要重新开始'); updateStatus('检测到导航操作,将在新页面加载后重新朗读'); // 设置标记,表示用户手动导航且需要重新开始朗读 userNavigatedWhilePlaying = true; stopCurrentPlayback(); } // 不阻止默认行为,让页面正常切换 return; } // 只处理播放暂停和重新阅读快捷键 if (e.code === config.playPauseKey) { e.preventDefault(); togglePlayPause(); } else if (e.code === config.rereadKey) { e.preventDefault(); rereadCurrent(); } }); } // 更新状态 function updateStatus(status) { console.log('状态:', status); if (settingsWindow) { const statusElement = settingsWindow.querySelector('#readerStatus'); if (statusElement) { statusElement.textContent = status; } } // 更新悬浮控制面板 updateFloatingControlStatus(); } // 更新进度 function updateProgress() { if (settingsWindow) { const progressElement = settingsWindow.querySelector('#readerProgress'); if (progressElement) { progressElement.textContent = `${currentIndex + 1}/${contentQueue.length}`; } } // 更新悬浮控制面板 updateFloatingControlStatus(); } // 更新当前内容显示 function updateCurrentContent(content, type = '') { if (settingsWindow) { const contentElement = settingsWindow.querySelector('#currentContent'); if (contentElement) { const typePrefix = type ? `[${type}] ` : ''; const displayContent = content.length > 50 ? content.substring(0, 50) + '...' : content; contentElement.textContent = typePrefix + displayContent; } } } // 高亮当前元素 function highlightElement(element) { if (element) { element.style.backgroundColor = '#ffeb3b'; element.style.outline = '2px solid #ff9800'; element.scrollIntoView({ behavior: 'smooth', block: 'center' }); } } // 移除高亮 function removeHighlight() { const highlighted = document.querySelectorAll('[style*="background-color: rgb(255, 235, 59)"]'); highlighted.forEach(el => { el.style.backgroundColor = ''; el.style.outline = ''; }); } // XML转义 function escapeXml(text) { return text.replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } // 使元素可拖拽 function makeElementDraggable(element, handle) { let isDragging = false; let currentX; let currentY; let initialX; let initialY; let xOffset = 0; let yOffset = 0; handle.addEventListener('mousedown', dragStart); document.addEventListener('mousemove', drag); document.addEventListener('mouseup', dragEnd); function dragStart(e) { initialX = e.clientX - xOffset; initialY = e.clientY - yOffset; if (e.target === handle) { isDragging = true; } } function drag(e) { if (isDragging) { e.preventDefault(); currentX = e.clientX - initialX; currentY = e.clientY - initialY; xOffset = currentX; yOffset = currentY; element.style.transform = `translate3d(${currentX}px, ${currentY}px, 0)`; } } function dragEnd() { initialX = currentX; initialY = currentY; isDragging = false; } } // 统一的语音合成函数(根据配置选择Azure TTS或系统TTS) function synthesizeVoice(text) { if (config.ttsEngine === 'system') { return synthesizeSystemTTS(text); } else { return synthesizeText(text, config.region, config.token, config.voice, config.rate, config.pitch); } } // 系统TTS合成函数 function synthesizeSystemTTS(text) { return new Promise((resolve, reject) => { if (!('speechSynthesis' in window)) { reject(new Error('浏览器不支持系统TTS')); return; } // 确保之前的播放已经停止 if (speechSynthesis.speaking || speechSynthesis.paused) { speechSynthesis.cancel(); console.log('取消之前的系统TTS播放'); } const utterance = new SpeechSynthesisUtterance(text); // 设置语音 if (config.systemVoice) { const voices = speechSynthesis.getVoices(); const selectedVoice = voices.find(voice => voice.name === config.systemVoice); if (selectedVoice) { utterance.voice = selectedVoice; } } // 设置语速和音调 utterance.rate = parseFloat(config.rate) || 1.0; utterance.pitch = 1.0; // 系统TTS的音调调整有限 let isResolved = false; // 防止重复resolve utterance.onstart = () => { console.log('系统TTS开始播放'); currentAudio = { pause: () => { if (speechSynthesis.speaking && !speechSynthesis.paused) { speechSynthesis.pause(); console.log('系统TTS暂停成功'); } }, resume: () => { if (speechSynthesis.paused) { speechSynthesis.resume(); console.log('系统TTS恢复成功'); } }, stop: () => { speechSynthesis.cancel(); console.log('系统TTS停止成功'); } }; }; utterance.onend = () => { console.log('系统TTS播放完成'); currentAudio = null; if (!isResolved) { isResolved = true; resolve(); } }; utterance.onerror = (error) => { console.error('系统TTS错误:', error); currentAudio = null; if (!isResolved) { isResolved = true; reject(new Error('系统TTS播放失败: ' + error.error)); } }; // 添加超时机制,防止卡死 const timeout = setTimeout(() => { if (!isResolved) { console.log('系统TTS播放超时,强制结束'); speechSynthesis.cancel(); currentAudio = null; isResolved = true; reject(new Error('系统TTS播放超时')); } }, 30000); // 30秒超时 utterance.onend = () => { console.log('系统TTS播放完成'); clearTimeout(timeout); currentAudio = null; if (!isResolved) { isResolved = true; resolve(); } }; try { speechSynthesis.speak(utterance); console.log('系统TTS已开始speak调用'); } catch (error) { clearTimeout(timeout); console.error('系统TTS speak调用失败:', error); if (!isResolved) { isResolved = true; reject(error); } } }); } // Azure TTS语音合成函数 function synthesizeText(text, region, token, voice, rate = '1.0', pitch = '0Hz') { return new Promise((resolve, reject) => { console.log('synthesizeText调用参数:', { textLength: text.length, region: region, hasToken: !!token, tokenLength: token ? token.length : 0, voice: voice, rate: rate, pitch: pitch }); if (!region || !token) { const error = `TTS配置不完整: region=${region}, hasToken=${!!token}`; console.error(error); reject(new Error(error)); return; } const ssml = ` ${escapeXml(text)} `; console.log('生成的SSML:', ssml.substring(0, 200) + '...'); console.log('TTS端点:', getTTSEndpoint(region)); GM_xmlhttpRequest({ method: 'POST', url: getTTSEndpoint(region), headers: { 'Ocp-Apim-Subscription-Key': token, 'Content-Type': 'application/ssml+xml', 'X-Microsoft-OutputFormat': 'audio-16khz-128kbitrate-mono-mp3' }, data: ssml, responseType: 'arraybuffer', onload: function(response) { console.log('TTS响应:', { status: response.status, statusText: response.statusText, responseSize: response.response ? response.response.byteLength : 0 }); if (response.status === 200) { console.log('TTS合成成功,音频数据大小:', response.response.byteLength); resolve(response.response); } else { const error = `TTS请求失败: HTTP ${response.status} ${response.statusText}`; console.error(error); console.error('TTS错误响应头:', response.responseHeaders); reject(new Error(error)); } }, onerror: function(error) { let errorMsg = 'TTS请求网络错误'; let errorDetails = ''; if (error && error.error) { errorDetails = error.error; } else if (error && error.message) { errorDetails = error.message; } else if (error && typeof error === 'string') { errorDetails = error; } else if (error) { try { errorDetails = JSON.stringify(error); } catch (e) { errorDetails = String(error); } } if (errorDetails) { errorMsg += ': ' + errorDetails; } console.error(errorMsg); console.error('网络错误详细信息:', error); // 输出可选中的错误信息到控制台 console.log('=== 可复制的错误信息 ==='); console.log(errorMsg); console.log('======================'); reject(new Error(errorMsg)); } }); }); } // 播放音频数据 function playAudioData(audioData) { return new Promise((resolve, reject) => { try { console.log('开始播放音频,数据大小:', audioData.byteLength, '字节'); const audioBlob = new Blob([audioData], { type: 'audio/mpeg' }); const audioUrl = URL.createObjectURL(audioBlob); console.log('音频Blob创建成功,URL:', audioUrl); if (currentAudio) { currentAudio.pause(); URL.revokeObjectURL(currentAudio.src); } currentAudio = new Audio(audioUrl); currentAudio.onended = () => { console.log('音频播放结束'); URL.revokeObjectURL(audioUrl); currentAudio = null; resolve(); }; currentAudio.onerror = (error) => { console.error('音频播放出错:', error); console.error('音频错误详细信息:', { error: error, audioUrl: audioUrl, readyState: currentAudio?.readyState, networkState: currentAudio?.networkState }); URL.revokeObjectURL(audioUrl); currentAudio = null; reject(new Error('音频播放失败: ' + (error.message || 'Unknown error'))); }; currentAudio.oncanplaythrough = () => { console.log('音频可以开始播放'); }; currentAudio.onloadstart = () => { console.log('开始加载音频'); }; currentAudio.onloadeddata = () => { console.log('音频数据加载完成'); }; console.log('开始播放音频...'); currentAudio.play().catch(playError => { console.error('Audio.play()失败:', playError); URL.revokeObjectURL(audioUrl); currentAudio = null; reject(new Error('音频播放启动失败: ' + playError.message)); }); } catch (error) { console.error('播放音频时发生异常:', error); reject(new Error('播放音频异常: ' + error.message)); } }); } init(); })();