// ==UserScript==
// @name Folo自动阅读脚本
// @namespace http://tampermonkey.net/
// @version 1.8.2
// @description Folo网站的自动阅读功能,支持Microsoft Azure语音合成,Shadow DOM内容提取,智能页面切换检测,悬浮播放控制面板,安全配置管理,选择性朗读,双TTS引擎支持(Azure+系统),播放模式(正常/循环),播放控制快捷键,用户导航时智能重新朗读
// @author 您的名字
// @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 = `
1.0
0Hz
🎵 循环模式会重复播放当前文章
`;
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();
togglePlayPause();
});
// 设置按钮
control.querySelector('#floatingSettingsBtn').addEventListener('click', (e) => {
e.stopPropagation();
showSettingsPanel();
});
// 双击悬浮面板重新加载内容
control.addEventListener('dblclick', (e) => {
e.stopPropagation();
reloadContent();
});
}
// 更新悬浮控制面板状态
function updateFloatingControlStatus() {
if (!floatingControl) return;
const playBtn = floatingControl.querySelector('#floatingPlayBtn');
const statusText = floatingControl.querySelector('#floatingStatus');
const progressText = floatingControl.querySelector('#floatingProgress');
// 更新播放按钮
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';
}
}
// 绑定面板事件
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() {
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();
})();