// ==UserScript==
// @name Folo自动阅读脚本
// @namespace http://tampermonkey.net/
// @version 1.5
// @description Folo网站的自动阅读功能,支持Microsoft Azure语音合成,Shadow DOM内容提取,智能页面切换检测,悬浮播放控制面板,安全配置管理,选择性朗读
// @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',
leftKey: 'KeyL',
rightKey: 'KeyH',
playPauseKey: 'Space',
rereadKey: 'KeyR',
autoStart: true,
readTitle: true,
readContent: true
};
// 全局变量
let config = {};
let contentQueue = [];
let currentIndex = 0;
let isPlaying = false;
let isPaused = false;
let settingsWindow = null;
let availableVoices = [];
let currentAudio = null;
let currentUrl = '';
let isNavigationTriggered = 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', startAutoReading);
} else {
startAutoReading();
}
}
// 加载配置
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 = `
`;
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 = `
`;
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 'quoted-text':
status = '引用';
break;
case 'article-sentence':
status = '句子';
break;
default:
status = '朗读中';
}
} else {
status = '朗读中';
}
} else if (isPaused) {
status = '已暂停';
} else if (contentQueue.length === 0) {
status = '无内容';
} else {
// 显示当前配置状态
if (config.readTitle && config.readContent) {
status = '标题+内容';
} else if (config.readTitle) {
status = '仅标题';
} else if (config.readContent) {
status = '仅内容';
} 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';
});
// 快捷键输入
const keyInputs = ['leftKeyInput', 'rightKeyInput', '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('#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('#leftKeyInput').value = config.leftKey;
panel.querySelector('#rightKeyInput').value = config.rightKey;
panel.querySelector('#playPauseKeyInput').value = config.playPauseKey;
panel.querySelector('#rereadKeyInput').value = config.rereadKey;
panel.querySelector('#readTitleCheckbox').checked = config.readTitle;
panel.querySelector('#readContentCheckbox').checked = config.readContent;
}
// 更新配置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 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 voiceSelect = panel.querySelector('#voiceSelect');
const rateSlider = panel.querySelector('#rateSlider');
const pitchSlider = panel.querySelector('#pitchSlider');
if (!voiceSelect.value) {
alert('请先选择一个语音');
return;
}
if (!config.region || !config.token) {
alert('请先配置Azure区域和密钥');
return;
}
const testText = '您好,这是语音测试。Hello, this is a voice test.';
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.region = panel.querySelector('#regionInput').value;
config.token = panel.querySelector('#tokenInput').value;
config.voice = panel.querySelector('#voiceSelect').value;
config.rate = panel.querySelector('#rateSlider').value;
config.pitch = panel.querySelector('#pitchSlider').value + 'Hz';
config.leftKey = panel.querySelector('#leftKeyInput').value;
config.rightKey = panel.querySelector('#rightKeyInput').value;
config.playPauseKey = panel.querySelector('#playPauseKeyInput').value;
config.rereadKey = panel.querySelector('#rereadKeyInput').value;
config.readTitle = panel.querySelector('#readTitleCheckbox').checked;
config.readContent = panel.querySelector('#readContentCheckbox').checked;
// 验证至少选择一种内容类型
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元素
const paragraphs = articleElement.querySelectorAll('p');
paragraphs.forEach((p, pIndex) => {
const text = p.textContent.trim();
if (text && text.length > 2) {
contentParts.push({
type: 'article-paragraph',
content: text,
element: p,
articleIndex: hostIndex,
paragraphIndex: pIndex
});
}
});
// 提取双引号包裹的文字内容
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 (paragraphs.length === 0 && quotedCount === 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} 提取完成: ${paragraphs.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) => {
const paragraphs = article.querySelectorAll('p');
paragraphs.forEach(p => {
const text = p.textContent.trim();
if (text && text.length > 2) {
contentParts.push({
type: 'article-paragraph',
content: text,
element: p,
articleIndex: index
});
}
});
// 提取双引号包裹的文字内容
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 === '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() {
// 监听URL变化(用于检测条目切换)
const observer = new MutationObserver((mutations) => {
const newUrl = window.location.href;
if (newUrl !== currentUrl) {
console.log('检测到URL变化:', currentUrl, '->', newUrl);
currentUrl = newUrl;
handleNavigationChange();
}
});
// 开始观察document的变化
observer.observe(document, {
childList: true,
subtree: true
});
// 也监听popstate事件(浏览器前进后退)
window.addEventListener('popstate', () => {
console.log('检测到popstate事件');
handleNavigationChange();
});
}
// 处理导航变化
function handleNavigationChange() {
console.log('处理导航变化...');
// 如果是用户触发的导航(点击←→键)
if (isNavigationTriggered) {
console.log('导航由用户触发,停止当前播放');
stopCurrentPlayback();
// 清除之前的重载计时器
if (reloadTimeoutId) {
clearTimeout(reloadTimeoutId);
}
// 延迟重新加载内容,等待新页面加载完成
reloadTimeoutId = setTimeout(() => {
console.log('新页面加载完成,重新提取内容');
reloadContent();
isNavigationTriggered = false;
}, 2000); // 等待2秒让新内容加载
} else {
// 非用户触发的导航,也停止播放并重新加载
console.log('检测到页面变化,重新加载内容');
stopCurrentPlayback();
if (reloadTimeoutId) {
clearTimeout(reloadTimeoutId);
}
reloadTimeoutId = setTimeout(() => {
reloadContent();
}, 1500);
}
}
// 停止当前播放
function stopCurrentPlayback() {
if (currentAudio) {
currentAudio.pause();
currentAudio = null;
}
isPlaying = false;
isPaused = false;
// 移除高亮
removeHighlight();
updateStatus('已停止 - 检测到页面切换');
console.log('已停止当前播放');
// 更新悬浮控制面板状态
updateFloatingControlStatus();
}
// 重新加载内容
function reloadContent() {
console.log('重新加载内容...');
contentQueue = extractContent();
currentIndex = 0;
if (contentQueue.length > 0) {
updateStatus('内容已重新加载');
updateProgress();
console.log('重新加载完成,共', contentQueue.length, '段内容');
// 如果之前在播放且有配置,自动开始新内容的播放
if (config.autoStart && config.region && config.token && isPlaying) {
setTimeout(() => {
playNext();
}, 500);
}
} else {
updateStatus('重新加载后未找到内容');
console.log('重新加载后未找到可阅读内容');
}
}
// 开始自动阅读
function startAutoReading() {
if (!config.autoStart) return;
setTimeout(() => {
contentQueue = extractContent();
currentIndex = 0;
if (contentQueue.length > 0) {
updateStatus('内容已加载,点击播放开始阅读');
updateProgress();
console.log('内容加载完成,共', contentQueue.length, '段内容');
// 如果有配置,则自动开始播放
if (config.region && config.token) {
playNext();
}
} else {
updateStatus('未找到可阅读内容');
console.log('页面内容提取失败,请检查页面结构');
}
}, 2000); // 等待页面完全加载
}
// 简化的调试内容提取功能
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} 段内容
`;
alert(summary);
}
// 播放下一段内容
function playNext() {
if (currentIndex >= contentQueue.length) {
updateStatus('阅读完成');
isPlaying = false;
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 continuePlayback(currentContent) {
isPlaying = true;
isPaused = false;
// 根据内容类型更新状态
let statusText = '正在阅读';
switch(currentContent.type) {
case 'title':
statusText = '正在阅读标题';
break;
case 'article-paragraph':
statusText = '正在阅读文章段落';
break;
case 'quoted-text':
statusText = '正在阅读引用文本';
break;
default:
statusText = '正在阅读';
}
updateStatus(statusText);
updateProgress();
updateCurrentContent(currentContent.content, currentContent.type);
// 更新悬浮控制面板状态
updateFloatingControlStatus();
// 高亮当前阅读的元素
highlightElement(currentContent.element);
// 使用Azure TTS合成语音
synthesizeText(currentContent.content, config.region, config.token, config.voice, config.rate, config.pitch)
.then(audioData => {
playAudioData(audioData)
.then(() => {
removeHighlight();
currentIndex++;
setTimeout(() => {
if (!isPaused) {
playNext();
}
}, 500); // 段落间短暂停顿
})
.catch(error => {
console.error('音频播放错误:', error);
updateStatus('播放错误');
isPlaying = false;
removeHighlight();
});
})
.catch(error => {
console.error('语音合成错误:', error);
updateStatus('合成失败');
isPlaying = false;
removeHighlight();
});
}
// 暂停/继续播放
function togglePlayPause() {
if (isPlaying && !isPaused) {
// 暂停当前播放
if (currentAudio) {
currentAudio.pause();
}
isPaused = true;
updateStatus('已暂停');
} else if (isPaused) {
// 继续播放
if (currentAudio) {
currentAudio.play();
}
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') {
console.log('检测到方向键:', e.code);
isNavigationTriggered = true;
// 不阻止默认行为,让页面正常切换
// 但标记这是用户触发的导航
return;
}
// 处理自定义快捷键
if (e.code === config.leftKey) {
e.preventDefault();
if (currentIndex > 0) {
currentIndex--;
rereadCurrent();
}
} else if (e.code === config.rightKey) {
e.preventDefault();
currentIndex++;
rereadCurrent();
} else if (e.code === config.playPauseKey) {
e.preventDefault();
togglePlayPause();
} else if (e.code === config.rereadKey) {
e.preventDefault();
rereadCurrent();
}
});
// 也监听keyup事件,确保导航标记被正确重置
document.addEventListener('keyup', (e) => {
if (e.code === 'ArrowLeft' || e.code === 'ArrowRight') {
// 给一点延迟,确保页面切换操作完成
setTimeout(() => {
console.log('方向键释放,准备处理页面切换');
}, 100);
}
});
}
// 更新状态
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语音合成函数
function synthesizeText(text, region, token, voice, rate = '1.0', pitch = '0Hz') {
return new Promise((resolve, reject) => {
const ssml = `
${escapeXml(text)}
`;
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) {
if (response.status === 200) {
resolve(response.response);
} else {
reject(new Error(`TTS请求失败: HTTP ${response.status} ${response.statusText}`));
}
},
onerror: function(error) {
reject(new Error('TTS请求网络错误: ' + error));
}
});
});
}
// 播放音频数据
function playAudioData(audioData) {
return new Promise((resolve, reject) => {
try {
const audioBlob = new Blob([audioData], { type: 'audio/mpeg' });
const audioUrl = URL.createObjectURL(audioBlob);
if (currentAudio) {
currentAudio.pause();
URL.revokeObjectURL(currentAudio.src);
}
currentAudio = new Audio(audioUrl);
currentAudio.onended = () => {
URL.revokeObjectURL(audioUrl);
currentAudio = null;
resolve();
};
currentAudio.onerror = (error) => {
URL.revokeObjectURL(audioUrl);
currentAudio = null;
reject(error);
};
currentAudio.play().catch(reject);
} catch (error) {
reject(error);
}
});
}
init();
})();