// ==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请求。");
// 注意:出于安全原因,不建议在前端脚本中硬编码或保存云服务密钥。
// 以下仅为逻辑演示,实际生产需配合后端。
}
};
})();