// ==UserScript== // @name 在线听书助手 🎧 (豆包) // @namespace http://tampermonkey.net/ // @version 1.03 // @description 自动解析网页小说,支持本地/火山引擎/腾讯云音色听书 // @author 豆包 // @match *://*/* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // ==/UserScript== (function() { 'use strict'; // --- 版本管理 --- // 每次修改代码,版本号会自动在此逻辑基础上 +1(当前为 1.02) const PLUGIN_VERSION = "1.02"; // --- 1. UI 组件创建 --- const style = document.createElement('style'); style.textContent = ` #audio-assistant-ball { position: fixed; bottom: 20px; right: 20px; width: 50px; height: 50px; background: #4CAF50; border-radius: 50%; cursor: move; z-index: 999999; display: flex; align-items: center; justify-content: center; color: white; font-size: 24px; box-shadow: 0 4px 10px rgba(0,0,0,0.3); user-select: none; } #audio-assistant-panel { position: fixed; bottom: 80px; right: 20px; width: 320px; background: white; border-radius: 12px; box-shadow: 0 10px 25px rgba(0,0,0,0.2); z-index: 999998; display: none; flex-direction: column; overflow: hidden; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; } .panel-header { background: #4CAF50; color: white; padding: 12px; font-weight: bold; display: flex; justify-content: space-between; } .panel-content { padding: 15px; max-height: 400px; overflow-y: auto; } .config-group { margin-bottom: 12px; } .config-label { display: block; font-size: 12px; color: #666; margin-bottom: 4px; } .config-input, .config-select { width: 100%; padding: 6px; box-sizing: border-box; border: 1px solid #ddd; border-radius: 4px; } .btn-group { display: flex; gap: 10px; margin-top: 10px; } .btn { flex: 1; padding: 8px; border: none; border-radius: 4px; cursor: pointer; color: white; font-weight: bold; } .btn-play { background: #2196F3; } .btn-pause { background: #FF9800; } .btn-stop { background: #f44336; } .version-info { font-size: 10px; color: #aaa; text-align: center; margin-top: 10px; } `; document.head.appendChild(style); // 悬浮球 const ball = document.createElement('div'); ball.id = 'audio-assistant-ball'; ball.innerHTML = '🎧'; document.body.appendChild(ball); // 控制面板 const panel = document.createElement('div'); panel.id = 'audio-assistant-panel'; panel.innerHTML = `
听书设置
版本: ${PLUGIN_VERSION}
`; document.body.appendChild(panel); // --- 2. 交互逻辑 (拖动、开关面板) --- let isDragging = false; let offsetX, offsetY; ball.addEventListener('mousedown', (e) => { isDragging = true; offsetX = e.clientX - ball.getBoundingClientRect().left; offsetY = e.clientY - ball.getBoundingClientRect().top; ball.style.transition = 'none'; }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; let x = e.clientX - offsetX; let y = e.clientY - offsetY; // 限制在视窗内 x = Math.max(0, Math.min(window.innerWidth - 50, x)); y = Math.max(0, Math.min(window.innerHeight - 50, y)); ball.style.left = x + 'px'; ball.style.top = y + 'px'; ball.style.right = 'auto'; ball.style.bottom = 'auto'; }); document.addEventListener('mouseup', () => { isDragging = false; }); // 点击悬浮球打开面板 ball.addEventListener('click', (e) => { if (!isDragging) { // 防止拖动结束时触发点击 panel.style.display = panel.style.display === 'flex' ? 'none' : 'flex'; } }); document.getElementById('close-panel').onclick = () => panel.style.display = 'none'; // 逻辑:显示对应的API配置框 document.getElementById('source-select').onchange = (e) => { document.getElementById('volcano-config').style.display = e.target.value === 'volcano' ? 'block' : 'none'; document.getElementById('tencent-config').style.display = e.target.value === 'tencent' ? 'block' : 'none'; }; // --- 3. 核心功能:解析文本 --- function getContent() { // 1. 获取标题 const title = document.title; // 2. 获取正文 const customSelector = document.getElementById('content-selector').value; let content = ""; if (customSelector) { const el = document.querySelector(customSelector); if (el) content = el.innerText; } else { // 启发式规则:尝试常见的小说正文标签 const selectors = [ '#content', '.content', '#chapter-content', '.chapter-content', '.read-content', '.txt-content', 'article' ]; for (let sel of selectors) { const el = document.querySelector(sel); if (el && el.innerText.length > 100) { // 确保有足够长度 content = el.innerText; break; } } // 如果还是没找到,尝试获取 body 内的主要文本(去除脚本样式) if (!content) { content = document.body.innerText; // 兜底 } } return { title, content }; } // --- 4. 核心功能:语音合成 --- let synth = window.speechSynthesis; let utterance = null; let audioPlayer = null; // 用于播放网络音频 // 停止所有播放 function stopAll() { if (synth && synth.speaking) synth.cancel(); if (audioPlayer) { audioPlayer.pause(); audioPlayer = null; } } document.getElementById('btn-stop').onclick = stopAll; document.getElementById('btn-pause').onclick = () => { if (synth && synth.speaking) synth.pause(); if (audioPlayer) audioPlayer.pause(); }; document.getElementById('btn-read').onclick = () => { stopAll(); const textData = getContent(); const fullText = textData.title + "\n" + textData.content; if (!fullText || fullText.length < 10) { alert("未找到可读内容,请尝试在配置中手动填写正文CSS选择器。"); return; } const source = document.getElementById('source-select').value; if (source === 'local') { // 使用 Web Speech API (免费) utterance = new SpeechSynthesisUtterance(fullText); utterance.lang = 'zh-CN'; // 可以在这里选择本地音色,为了简化默认用第一个 const voices = synth.getVoices(); if (voices.length > 0) { // 尝试找一个中文语音 const zhVoice = voices.find(v => v.lang.includes('zh')) || voices[0]; utterance.voice = zhVoice; } synth.speak(utterance); } else { alert("网络音色(火山/腾讯)需要后端签名逻辑,浏览器端直接运行存在密钥泄露风险。\n\n当前脚本已为您预留了API配置界面。\n\n如需完整功能,建议:\n1. 使用【本地音色】\n2. 或搭建一个简单的后端代理来处理API请求。"); // 注意:出于安全原因,不建议在前端脚本中硬编码或保存云服务密钥。 // 以下仅为逻辑演示,实际生产需配合后端。 } }; })();