2025年10月22日星期三

Folo任务脚本

1.购买服务器阿里云:服务器购买地址https://t.aliyun.com/U/Bg6shY若失效,可用地址

1.购买服务器

阿里云:

服务器购买地址

https://t.aliyun.com/U/Bg6shY

若失效,可用地址

https://www.aliyun.com/daily-act/ecs/activity_selection?source=5176.29345612&userCode=49hts92d

腾讯云:

https://curl.qcloud.com/wJpWmSfU

若失效,可用地址

https://cloud.tencent.com/act/cps/redirect?redirect=2446&cps_key=ad201ee2ef3b771157f72ee5464b1fea&from=console

华为云

https://activity.huaweicloud.com/cps.html?fromacct=64b5cf7cc11b4840bb4ed2ea0b2f4468&utm_source=V1g3MDY4NTY=&utm_medium=cps&utm_campaign=201905

2.部署教程

2024年最新青龙面板跑脚本教程(一)持续更新中

3.代码如下

(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',        autoStarttrue,        readTitletrue,        readContenttrue,        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,                timestampDate.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 = `                <div style="                    background: white;                    padding: 30px;                    border-radius: 12px;                    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);                    max-width: 500px;                    width: 90%;                ">                    <h3 style="margin: 0 0 20px 0; color: #333; text-align: center;">                        🎵 Folo自动阅读配置                    </h3>                    <p style="margin: 0 0 20px 0; color: #666; line-height: 1.5;">                        需要配置Azure语音服务才能使用朗读功能:                    </p>
                    <div style="margin-bottom: 15px;">                        <label style="display: block; margin-bottom: 5px; font-weight: bold; color: #333;">                            Azure区域 (如: eastasia, westus2):                        </label>                        <input type="text" id="promptRegion" placeholder="例如: eastasia" style="                            width: 100%;                            padding: 10px;                            border: 1px solid #ddd;                            border-radius: 6px;                            box-sizing: border-box;                            font-size: 14px;                        ">                    </div>
                    <div style="margin-bottom: 20px;">                        <label style="display: block; margin-bottom: 5px; font-weight: bold; color: #333;">                            Azure语音服务密钥:                        </label>                        <input type="password" id="promptToken" placeholder="输入您的Azure语音服务密钥" style="                            width: 100%;                            padding: 10px;                            border: 1px solid #ddd;                            border-radius: 6px;                            box-sizing: border-box;                            font-size: 14px;                        ">                        <small style="color: #999; font-size: 12px;">                            配置将安全保存在本地,用于语音合成服务                        </small>                    </div>
                    <div style="text-align: center;">                        <button id="promptConfirm" style="                            background: #007acc;                            color: white;                            border: none;                            padding: 12px 24px;                            border-radius: 6px;                            cursor: pointer;                            font-size: 14px;                            margin-right: 10px;                        ">确认配置</button>                        <button id="promptCancel" style="                            background: #6c757d;                            color: white;                            border: none;                            padding: 12px 24px;                            border-radius: 6px;                            cursor: pointer;                            font-size: 14px;                        ">取消</button>                    </div>
                    <div style="margin-top: 15px; text-align: center;">                        <small style="color: #999; font-size: 11px;">                            💡 提示: 也可以通过右下角悬浮面板的设置按钮进行配置                        </small>                    </div>                </div>            `;
            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 = `            <div class="folo-reader-header">                <div class="folo-reader-title">Folo自动阅读设置</div>                <div class="folo-reader-controls">                    <button class="folo-reader-btn" id="minimizeBtn" title="最小化">−</button>                    <button class="folo-reader-btn" id="closeBtn" title="关闭">×</button>                </div>            </div>            <div class="folo-reader-content" id="panelContent">                <div class="setting-group">                    <label class="setting-label">TTS引擎:</label>                    <select class="setting-select" id="ttsEngineSelect">                        <option value="azure">Azure TTS</option>                        <option value="system">系统TTS</option>                    </select>                </div>
                <div class="setting-group" id="azureSettings">                    <label class="setting-label">Azure语音区域:</label>                    <input type="text" class="setting-input" id="regionInput" placeholder="例如: eastasia">                </div>
                <div class="setting-group" id="azureTokenGroup">                    <label class="setting-label">Azure语音Token:</label>                    <input type="password" class="setting-input" id="tokenInput" placeholder="输入您的Azure语音服务Token">                </div>
                <div class="setting-group" id="azureTestGroup">                    <button class="setting-button" id="testConnectionBtn">测试连接</button>                    <span id="connectionStatus"></span>                </div>
                <div class="setting-group" id="voiceGroup">                    <label class="setting-label">语音选择:</label>                    <select class="setting-select" id="voiceSelect">                        <option value="">正在加载语音列表...</option>                    </select>                    <select class="setting-select" id="systemVoiceSelect" style="display: none;">                        <option value="">正在加载系统语音...</option>                    </select>                </div>
                <div class="setting-group" id="voiceTestGroup">                    <button class="setting-button" id="testVoiceBtn">测试发音</button>                </div>
                <div class="setting-group">                    <label class="setting-label">语速 (0.5-2.0):</label>                    <input type="range" min="0.5" max="2.0" step="0.1" id="rateSlider" class="setting-input">                    <span id="rateValue">1.0</span>                </div>
                <div class="setting-group">                    <label class="setting-label">音调 (-200Hz到+200Hz):</label>                    <input type="range" min="-200" max="200" step="10" id="pitchSlider" class="setting-input">                    <span id="pitchValue">0Hz</span>                </div>
                <div class="setting-group">
                <div class="setting-group">                    <label class="setting-label">快捷键设置:</label>                    <div class="key-binding">                        <span class="key-label">播放/暂停:</span>                        <input type="text" class="key-input" id="playPauseKeyInput" readonly>                    </div>                    <div class="key-binding">                        <span class="key-label">重新朗读:</span>                        <input type="text" class="key-input" id="rereadKeyInput" readonly>                    </div>                </div>
                <div class="setting-group">                    <label class="setting-label">朗读内容选择:</label>                    <div style="display: flex; gap: 15px; align-items: center;">                        <label style="display: flex; align-items: center; gap: 5px; font-weight: normal;">                            <input type="checkbox" id="readTitleCheckbox" style="margin: 0;">                            <span>朗读标题</span>                        </label>                        <label style="display: flex; align-items: center; gap: 5px; font-weight: normal;">                            <input type="checkbox" id="readContentCheckbox" style="margin: 0;">                            <span>朗读内容</span>                        </label>                    </div>                    <small style="color: #666; font-size: 11px; margin-top: 5px; display: block;">                        💡 可以根据频道特点选择只读标题或只读内容,或者两者都读                    </small>                </div>
                <div class="setting-group">                    <label class="setting-label">播放模式:</label>                    <select class="setting-select" id="playModeSelect">                        <option value="normal">正常模式 - 播放完成后停止</option>                        <option value="loop">循环模式 - 自动重复当前条目</option>                    </select>                    <small style="color: #666; font-size: 11px; margin-top: 5px; display: block;">                        🎵 循环模式会重复播放当前文章                    </small>                </div>
                <div class="setting-group">                    <button class="setting-button" id="saveBtn">保存设置</button>                    <button class="setting-button secondary" id="resetBtn">重置默认</button>                </div>
                <div class="setting-group">                    <button class="setting-button secondary" id="refreshContentBtn">重新提取内容</button>                    <button class="setting-button secondary" id="debugContentBtn">调试内容提取</button>                </div>
                <div class="progress-info" id="progressInfo">                    <div>状态: <span id="readerStatus">未开始</span></div>                    <div>进度: <span id="readerProgress">0/0</span></div>                    <div>当前内容: <span id="currentContent">无</span></div>                </div>            </div>        `;
        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 = `            <button class="folo-control-btn play-pause" id="floatingPlayBtn" title="播放/暂停">                ▶            </button>            <div class="folo-control-status" id="floatingStatus">                准备就绪            </div>            <div class="folo-progress-indicator" id="floatingProgress">                0/0            </div>            <button class="folo-control-btn" id="floatingSettingsBtn" title="打开设置">                ⚙            </button>        `;
        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.regiontokenLength: 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 = '<span class="status-indicator status-error"></span>请填写完整的区域和Token';            return;        }
        statusSpan.innerHTML = '<span class="status-indicator status-warning"></span>测试中...';
        // 测试语音合成        const testText = '连接测试成功';        synthesizeText(testText, testRegion, testToken, config.voice)            .then(audioData => {                statusSpan.innerHTML = '<span class="status-indicator status-success"></span>连接成功';                // 播放测试音频                playAudioData(audioData);                // 加载语音列表                loadAvailableVoices(testRegion, testToken);            })            .catch(error => {                console.error('连接测试失败:', error);                statusSpan.innerHTML = '<span class="status-indicator status-error"></span>连接失败: ' + 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 = '<option value="">请选择系统语音</option>';
            // 筛选中文语音或默认语音            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, { oncetrue });            }        } else {            systemVoiceSelect.innerHTML = '<option value="">浏览器不支持系统TTS</option>';        }    }
    // 加载可用语音    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 = '<option value="">正在加载语音列表...</option>';
        GM_xmlhttpRequest({            method'GET',            urlgetVoicesEndpoint(targetRegion),            headers: {                'Ocp-Apim-Subscription-Key': targetToken            },            onloadfunction(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 = '<option value="">加载语音失败</option>';                }            },            onerrorfunction(error) {                console.error('获取语音列表失败:', error);                voiceSelect.innerHTML = '<option value="">加载语音失败</option>';            }        });    }
    // 测试发音    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.readTitlereadContent: 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(0100);            }            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, {             childListtrue            subtreetrue,            attributestrue,            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(0100));            });        });
        // 检查文章元素        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(03).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(050) + '...',            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(050) + '...' : 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'&amp;')                  .replace(/</g'&lt;')                  .replace(/>/g'&gt;')                  .replace(/"/g'&quot;')                  .replace(/'/g'&apos;');    }
    // 使元素可拖拽    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 = `<speak version="1.0" xmlns="http://www.w3.org/2025/10/synthesis" xml:lang="zh-CN">                <voice name="${voice}">                    <prosody rate="${rate}" pitch="${pitch}">                        ${escapeXml(text)}                    </prosody>                </voice>            </speak>`;
            console.log('生成的SSML:', ssml.substring(0200) + '...');            console.log('TTS端点:'getTTSEndpoint(region));
            GM_xmlhttpRequest({                method'POST',                urlgetTTSEndpoint(region),                headers: {                    'Ocp-Apim-Subscription-Key': token,                    'Content-Type''application/ssml+xml',                    'X-Microsoft-OutputFormat''audio-16khz-128kbitrate-mono-mp3'                },                data: ssml,                responseType'arraybuffer',                onloadfunction(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));                    }                },                onerrorfunction(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();})();
解析

该脚本为网站 Folo (https://app.folo.is) 开发,用于实现自动语音阅读文章的增强体验。

功能模块
作用说明
 自动朗读功能
自动提取页面中的标题和正文内容,并使用语音合成(TTS)朗读。
 双TTS引擎支持
支持 Microsoft Azure TTS(云端高质量发音)与浏览器系统自带 TTS(离线备用)。
 智能页面切换检测
可检测用户浏览下一页或切换文章,自动重新提取内容并继续朗读。
 播放模式
支持「正常播放」与「循环播放」两种模式。
 播放控制与快捷键
可通过悬浮控制面板或键盘快捷键播放 / 暂停 / 重新朗读。
 设置面板
提供完整可视化配置界面(TTS 区域、Token、语音选择、语速、音调、快捷键等)。
 Shadow DOM 内容提取
能正确识别 Folo 内部的 Shadow DOM 结构并抓取其中的文章段落。
 安全配置管理
支持 GM 存储 + Cookie 双存储,确保 Azure Token 长期可用。
 悬浮播放面板
页面右下角显示播放状态、进度、快捷控制按钮。
 自动高亮当前段落
正在朗读的内容会在页面中自动高亮。

主要方法

方法名
功能说明
init()
启动脚本,加载配置、样式、UI、事件监听,并在页面加载后启动自动朗读流程。
loadConfig() / saveConfig()
从 GM 存储或 Cookie 中加载 / 保存用户配置(包括 Azure Token、语音设置等)。
createSettingsPanel()
创建完整的设置界面,供用户修改 TTS 参数和控制朗读行为。
createFloatingControl()
创建右下角悬浮播放控制条,可播放、暂停、进入设置等。
extractContent()
自动提取 Folo 页面中的标题、段落、引用、句子,支持 Shadow DOM 与普通 DOM。
synthesizeVoice()
根据配置选择使用 Azure TTS 或 系统 TTS 生成音频。
synthesizeText()
向 Azure Speech API 发送 SSML 请求,返回 MP3 音频二进制流。
synthesizeSystemTTS()
调用浏览器内置语音引擎(speechSynthesis)朗读文本。
playAudioData()
播放 Azure TTS 返回的音频数据,自动检测播放完成事件。
togglePlayPause()
切换播放与暂停状态。
rereadCurrent()
重新朗读当前段落。
setupNavigationListener()
监听 URL 和 DOM 变化,检测页面切换时重新加载内容。
checkAndPromptConfig() / showConfigPrompt()
检查是否配置了 Azure 语音服务,如未配置则弹窗提示用户输入。
updateFloatingControlStatus()
更新悬浮面板上的播放状态、进度与朗读模式显示。
highlightElement() / removeHighlight()
高亮正在朗读的 DOM 元素。
bindKeyEvents()
监听快捷键(默认空格播放暂停、R重新朗读、方向键检测导航)。
handlePlaybackEnd()
控制播放结束后的行为(停止或循环播放)。

执行逻辑

页面加载  init()    加载配置(Azure区域Token语音语速音调等)    创建设置面板 + 悬浮面板    提取文章标题和内容(支持 Shadow DOM)    检查配置(Azure 凭证有效性)    调用 synthesizeVoice()  Azure 或 系统 TTS    playAudioData() 播放语音    播放完成  下一段内容    若全部播放完毕  根据播放模式执行(停止 / 循环)

用户交互界面说明

1.  设置面板(完整控制)

包括:

  • 语音引擎(Azure / 系统)

  • Azure 地区与 Token

  • 语音选择、语速、音调

  • 播放快捷键绑定

  • 是否朗读标题/内容

  • 播放模式选择(正常/循环)

  • 内容调试与刷新功能

2. 🎵 悬浮面板(快捷控制)

位于页面右下角,包含:

  • ▶ / ⏸ 播放暂停按钮

  • 状态显示(标题/段落/引用/暂停)

  • 当前进度(如 3/12

  • ⚙ 设置按钮

支持拖拽、双击刷新内容。

技术亮点

技术点
说明
Shadow DOM 解析
使用 shadowRoot.querySelector() 深度提取封装内容。
Azure TTS API 调用
使用 GM_xmlhttpRequest 直接向微软语音服务发起 HTTPS 请求。
跨域安全管理
利用油猴授权的跨域 GM 接口实现安全请求。
语音合成标记语言 (SSML)
通过 <prosody rate pitch> 控制发音语速与音高。
播放状态同步
悬浮面板与设置面板共享播放状态,实时更新。
自动错误恢复
网络错误、导航切换、播放失败时能智能恢复或重启朗读。



注意

本文部分变量已做脱敏处理,仅用于测试和学习研究,禁止用于商业用途,不能保证其合法性,准确性,完整性和有效性,请根据情况自行判断。技术层面需要提供帮助,可以通过打赏的方式进行探讨。


历史脚本txt文件获取>>
服务器搭建,人工服务咨询>>

没有评论:

发表评论

Github+Cloudflare免费部署网站教程

点击上方蓝字关注我们Cloudflare对我们来说,是一个可以免费白嫖很多东西的超级网站,像CDN、网站托管等 点击上方 蓝字 关注我们 Cloudflare对我们来说,是一个可以免费白嫖很多东西的超级网站,像CDN、网站托管等等,今天我们来介绍一下如何把网站部署到 Clou...