// ==UserScript==
// @name ChatGPT Auto Read Aloud
// @namespace http://tampermonkey.net/
// @version 1.9
// @description GPT回复完成后自动点击朗读按钮 - 支持设置持久化存储
// @author schweigen
// @license MIT
// @match https://chatgpt.com/*
// @grant GM_registerMenuCommand
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
// 默认配置
const DEFAULT_CONFIG = {
enabled: true,
delay: 500,
maxWaitTime: 30000,
debug: true
};
// 从LocalStorage加载配置
function loadConfig() {
try {
const savedConfig = localStorage.getItem('chatgpt_auto_read_config');
if (savedConfig) {
const parsed = JSON.parse(savedConfig);
return { ...DEFAULT_CONFIG, ...parsed };
}
} catch (e) {
console.warn('[ChatGPT Auto Read] 加载配置失败:', e);
}
return { ...DEFAULT_CONFIG };
}
// 保存配置到LocalStorage
function saveConfig() {
try {
const configToSave = {
enabled: CONFIG.enabled,
delay: CONFIG.delay,
maxWaitTime: CONFIG.maxWaitTime,
debug: CONFIG.debug
};
localStorage.setItem('chatgpt_auto_read_config', JSON.stringify(configToSave));
debugLog('配置已保存到LocalStorage');
} catch (e) {
console.error('[ChatGPT Auto Read] 保存配置失败:', e);
}
}
// 初始化配置
const CONFIG = loadConfig();
let lastProcessedMessageId = null;
let observer = null;
let checkTimer = null;
let isDragging = false;
let dragStartX, dragStartY;
let ballStartX, ballStartY;
let hasMoved = false;
let isExpanded = false;
function debugLog(message) {
if (CONFIG.debug) {
console.log('[ChatGPT Auto Read]', new Date().toLocaleTimeString(), message);
}
}
// 保存和读取位置(使用LocalStorage)
function savePosition(x, y) {
try {
localStorage.setItem('chatgpt_auto_read_position', JSON.stringify({x, y}));
debugLog(`位置已保存: ${x}, ${y}`);
} catch (e) {
console.error('[ChatGPT Auto Read] 保存位置失败:', e);
}
}
function loadPosition() {
try {
const saved = localStorage.getItem('chatgpt_auto_read_position');
return saved ? JSON.parse(saved) : null;
} catch (e) {
console.warn('[ChatGPT Auto Read] 加载位置失败:', e);
return null;
}
}
// 重置位置
function resetPosition() {
const ball = document.getElementById('autoReadBall');
if (ball) {
ball.style.left = '30px';
ball.style.top = '50%';
ball.style.transform = 'translateY(-50%)';
ball.style.right = 'auto';
ball.style.bottom = 'auto';
savePosition(30, window.innerHeight / 2);
showNotification('🏠 位置已重置');
}
}
// 切换启用状态的函数
function toggleEnabled() {
CONFIG.enabled = !CONFIG.enabled;
saveConfig(); // 保存配置
const toggle = document.getElementById('autoReadToggle');
const status = document.getElementById('autoReadStatus');
const ball = document.getElementById('autoReadBall');
if (toggle) {
toggle.checked = CONFIG.enabled;
}
if (status) {
status.textContent = CONFIG.enabled ? '启用' : '禁用';
status.style.color = CONFIG.enabled ? '#10a37f' : '#dc2626';
}
if (ball) {
ball.style.background = CONFIG.enabled ? 'rgba(16, 163, 127, 0.8)' : 'rgba(128, 128, 128, 0.8)';
ball.style.borderColor = CONFIG.enabled ? 'rgba(16, 163, 127, 1)' : 'rgba(128, 128, 128, 1)';
}
showNotification(CONFIG.enabled ? '✅ 已启用自动朗读' : '❌ 已禁用自动朗读');
debugLog(`自动朗读已${CONFIG.enabled ? '启用' : '禁用'}`);
}
// 更新延迟时间的函数
function updateDelay(newDelay) {
CONFIG.delay = parseInt(newDelay);
saveConfig(); // 保存配置
const delayValue = document.getElementById('delayValue');
if (delayValue) {
delayValue.textContent = CONFIG.delay;
}
debugLog(`延迟时间设置为: ${CONFIG.delay}ms`);
}
// 注册油猴菜单
GM_registerMenuCommand('重置圆球位置', resetPosition);
GM_registerMenuCommand('切换启用状态', toggleEnabled);
// 创建拖拽圆球控制器
function createFloatingBall() {
// 获取保存的位置
const savedPos = loadPosition();
const defaultLeft = '30px';
const defaultTop = '50%';
// 主圆球 - 根据启用状态设置颜色
const ball = document.createElement('div');
ball.id = 'autoReadBall';
ball.style.cssText = `
position: fixed;
left: ${savedPos ? savedPos.x + 'px' : defaultLeft};
top: ${savedPos ? savedPos.y + 'px' : defaultTop};
${!savedPos ? 'transform: translateY(-50%);' : ''}
width: 28px;
height: 28px;
background: ${CONFIG.enabled ? 'rgba(16, 163, 127, 0.8)' : 'rgba(128, 128, 128, 0.8)'};
border: 1.5px solid ${CONFIG.enabled ? 'rgba(16, 163, 127, 1)' : 'rgba(128, 128, 128, 1)'};
border-radius: 50%;
color: white;
font-size: 15px;
font-weight: bold;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif;
display: flex;
align-items: center;
justify-content: center;
cursor: grab;
z-index: 10000;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
transition: all 0.2s ease;
user-select: none;
backdrop-filter: blur(5px);
line-height: 1;
`;
ball.textContent = '读';
// 设置面板
const panel = document.createElement('div');
panel.id = 'autoReadPanel';
panel.style.cssText = `
position: fixed;
left: 70px;
top: 50%;
transform: translateY(-50%);
width: 240px;
background: rgba(45, 45, 45, 0.95);
color: white;
padding: 15px;
border-radius: 12px;
border: 1px solid rgba(85, 85, 85, 0.8);
z-index: 9999;
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 12px;
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
backdrop-filter: blur(10px);
transform: translateY(-50%) scale(0) translateX(-20px);
opacity: 0;
transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
transform-origin: left center;
pointer-events: none;
`;
panel.innerHTML = `
<div style="margin-bottom: 12px; font-weight: bold; color: #10a37f; border-bottom: 1px solid rgba(85,85,85,0.5); padding-bottom: 6px;">
🔊 自动朗读设置
</div>
<label style="display: flex; align-items: center; cursor: pointer; margin-bottom: 12px; padding: 6px; border-radius: 6px; transition: background 0.2s;" onmouseover="this.style.background='rgba(255,255,255,0.1)'" onmouseout="this.style.background='transparent'">
<input type="checkbox" id="autoReadToggle" ${CONFIG.enabled ? 'checked' : ''}
style="margin-right: 8px; transform: scale(1.1);">
<span style="font-size: 13px;">启用自动朗读</span>
</label>
<div style="margin-bottom: 12px; padding: 8px; background: rgba(255,255,255,0.05); border-radius: 6px;">
<label style="display: block; margin-bottom: 6px; font-size: 11px; color: #a0a0a0;">
延迟时间: <span id="delayValue" style="color: #10a37f; font-weight: bold;">${CONFIG.delay}</span>ms
</label>
<input type="range" id="delaySlider" min="200" max="3000" step="100" value="${CONFIG.delay}"
style="width: 100%; height: 4px; background: #555; border-radius: 2px; outline: none; -webkit-appearance: none;">
<div style="display: flex; justify-content: space-between; font-size: 9px; color: #888; margin-top: 2px;">
<span>200ms</span>
<span>3000ms</span>
</div>
</div>
<button id="testReadButton" style="width: 100%; padding: 8px; background: linear-gradient(135deg, #10a37f, #0d8764); color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 12px; margin-bottom: 8px; transition: all 0.2s; box-shadow: 0 2px 4px rgba(0,0,0,0.2);" onmouseover="this.style.transform='translateY(-1px)'; this.style.boxShadow='0 4px 8px rgba(0,0,0,0.3)'" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='0 2px 4px rgba(0,0,0,0.2)'">
🎯 测试朗读
</button>
<button id="resetPosButton" style="width: 100%; padding: 6px; background: rgba(128, 128, 128, 0.3); color: white; border: 1px solid rgba(128, 128, 128, 0.5); border-radius: 6px; cursor: pointer; font-size: 11px; margin-bottom: 12px; transition: all 0.2s;" onmouseover="this.style.background='rgba(128, 128, 128, 0.5)'" onmouseout="this.style.background='rgba(128, 128, 128, 0.3)'">
🏠 重置位置
</button>
<button id="clearConfigButton" style="width: 100%; padding: 6px; background: rgba(220, 38, 38, 0.3); color: white; border: 1px solid rgba(220, 38, 38, 0.5); border-radius: 6px; cursor: pointer; font-size: 11px; margin-bottom: 12px; transition: all 0.2s;" onmouseover="this.style.background='rgba(220, 38, 38, 0.5)'" onmouseout="this.style.background='rgba(220, 38, 38, 0.3)'">
🗑️ 清除所有设置
</button>
<div style="font-size: 10px; color: #888; border-top: 1px solid rgba(85,85,85,0.3); padding-top: 8px; line-height: 1.4;">
<div style="margin-bottom: 4px;">
状态: <span id="autoReadStatus" style="color: ${CONFIG.enabled ? '#10a37f' : '#dc2626'}">${CONFIG.enabled ? '启用' : '禁用'}</span>
</div>
<div style="margin-bottom: 4px;">
存储: <span style="color: #10a37f">LocalStorage</span>
</div>
<div style="margin-bottom: 4px;">
最后处理: <span id="lastProcessed" style="color: #a0a0a0;">无</span>
</div>
<div style="font-size: 9px; color: #666;">
💡 点击圆球展开/收起面板
</div>
</div>
`;
document.body.appendChild(ball);
document.body.appendChild(panel);
// 添加CSS样式
const style = document.createElement('style');
style.textContent = `
#autoReadBall:hover {
background: ${CONFIG.enabled ? 'rgba(16, 163, 127, 1)' : 'rgba(128, 128, 128, 1)'} !important;
transform: scale(1.15) ${!savedPos ? 'translateY(-50%)' : ''};
box-shadow: 0 3px 12px ${CONFIG.enabled ? 'rgba(16, 163, 127, 0.4)' : 'rgba(128, 128, 128, 0.4)'};
}
#autoReadBall.dragging {
cursor: grabbing !important;
transform: scale(0.9);
box-shadow: 0 4px 15px rgba(0,0,0,0.5);
z-index: 10001;
}
#delaySlider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 16px;
height: 16px;
background: #10a37f;
border-radius: 50%;
cursor: pointer;
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
#delaySlider::-moz-range-thumb {
width: 16px;
height: 16px;
background: #10a37f;
border-radius: 50%;
cursor: pointer;
border: none;
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
`;
document.head.appendChild(style);
// 绑定事件
setupBallEvents(ball, panel);
setupPanelEvents();
}
// 设置圆球交互事件
function setupBallEvents(ball, panel) {
let mouseDownTime = 0;
let dragTimer = null;
// 鼠标按下事件
ball.addEventListener('mousedown', function(e) {
if (e.button !== 0) return; // 只响应左键
e.preventDefault();
e.stopPropagation();
mouseDownTime = Date.now();
isDragging = false;
hasMoved = false;
// 记录开始位置
dragStartX = e.clientX;
dragStartY = e.clientY;
const rect = ball.getBoundingClientRect();
ballStartX = rect.left;
ballStartY = rect.top;
// 延迟150ms才开始拖拽模式,避免误触
dragTimer = setTimeout(() => {
if (!hasMoved) {
isDragging = true;
ball.classList.add('dragging');
// 添加全局事件监听
document.addEventListener('mousemove', handleMouseMove, true);
document.addEventListener('mouseup', handleMouseUp, true);
// 禁用页面选择
document.body.style.userSelect = 'none';
debugLog('开始拖拽模式');
}
}, 150);
});
// 鼠标移动处理
function handleMouseMove(e) {
if (!isDragging) {
// 还没进入拖拽模式,检查移动距离
const deltaX = e.clientX - dragStartX;
const deltaY = e.clientY - dragStartY;
// 提高移动阈值到8px,减少误触
if (Math.abs(deltaX) > 8 || Math.abs(deltaY) > 8) {
hasMoved = true;
debugLog('检测到移动,标记为已移动');
}
return;
}
e.preventDefault();
e.stopPropagation();
const deltaX = e.clientX - dragStartX;
const deltaY = e.clientY - dragStartY;
// 计算新位置
let newX = ballStartX + deltaX;
let newY = ballStartY + deltaY;
// 边界限制
const maxX = window.innerWidth - ball.offsetWidth;
const maxY = window.innerHeight - ball.offsetHeight;
newX = Math.max(0, Math.min(newX, maxX));
newY = Math.max(0, Math.min(newY, maxY));
// 应用位置
ball.style.left = newX + 'px';
ball.style.top = newY + 'px';
ball.style.right = 'auto';
ball.style.bottom = 'auto';
ball.style.transform = 'none';
}
// 鼠标释放处理
function handleMouseUp(e) {
const mouseUpTime = Date.now();
const clickDuration = mouseUpTime - mouseDownTime;
// 清除拖拽定时器
if (dragTimer) {
clearTimeout(dragTimer);
dragTimer = null;
}
if (isDragging) {
e.preventDefault();
e.stopPropagation();
isDragging = false;
ball.classList.remove('dragging');
// 移除全局事件监听
document.removeEventListener('mousemove', handleMouseMove, true);
document.removeEventListener('mouseup', handleMouseUp, true);
// 恢复页面选择
document.body.style.userSelect = '';
// 保存位置
const rect = ball.getBoundingClientRect();
savePosition(rect.left, rect.top);
debugLog('拖拽结束,位置已保存');
} else {
// 没有进入拖拽模式,处理点击事件
if (!hasMoved && clickDuration < 500) {
debugLog('单击 - 切换面板');
togglePanel();
}
}
}
// 显示/隐藏面板
function togglePanel() {
isExpanded = !isExpanded;
if (isExpanded) {
showPanel();
} else {
hidePanel();
}
}
function showPanel() {
panel.style.pointerEvents = 'auto';
panel.style.opacity = '1';
// 根据球的位置调整面板位置
const ballRect = ball.getBoundingClientRect();
const panelWidth = 240;
const panelHeight = 320;
let panelLeft, panelTop;
// 水平位置判断
if (ballRect.left + ballRect.width + panelWidth + 20 <= window.innerWidth) {
// 球的右侧有足够空间
panelLeft = ballRect.right + 10;
panel.style.transformOrigin = 'left center';
panel.style.transform = 'translateY(-50%) scale(1) translateX(0)';
} else {
// 球的左侧显示
panelLeft = ballRect.left - panelWidth - 10;
panel.style.transformOrigin = 'right center';
panel.style.transform = 'translateY(-50%) scale(1) translateX(0)';
}
// 垂直位置调整
panelTop = ballRect.top + ballRect.height / 2;
// 确保面板不超出屏幕
if (panelTop + panelHeight / 2 > window.innerHeight) {
panelTop = window.innerHeight - panelHeight / 2 - 10;
}
if (panelTop - panelHeight / 2 < 0) {
panelTop = panelHeight / 2 + 10;
}
panel.style.left = Math.max(10, panelLeft) + 'px';
panel.style.top = panelTop + 'px';
panel.style.right = 'auto';
}
function hidePanel() {
panel.style.pointerEvents = 'none';
panel.style.opacity = '0';
panel.style.transform = 'translateY(-50%) scale(0) translateX(-20px)';
isExpanded = false;
}
// 全局鼠标释放事件(备用)
document.addEventListener('mouseup', handleMouseUp);
}
// 设置面板事件
function setupPanelEvents() {
const toggle = document.getElementById('autoReadToggle');
const testButton = document.getElementById('testReadButton');
const resetPosButton = document.getElementById('resetPosButton');
const clearConfigButton = document.getElementById('clearConfigButton');
const delaySlider = document.getElementById('delaySlider');
// 启用/禁用切换
toggle.addEventListener('change', function() {
CONFIG.enabled = this.checked;
saveConfig(); // 保存配置
const status = document.getElementById('autoReadStatus');
status.textContent = CONFIG.enabled ? '启用' : '禁用';
status.style.color = CONFIG.enabled ? '#10a37f' : '#dc2626';
const ball = document.getElementById('autoReadBall');
ball.style.background = CONFIG.enabled ? 'rgba(16, 163, 127, 0.8)' : 'rgba(128, 128, 128, 0.8)';
ball.style.borderColor = CONFIG.enabled ? 'rgba(16, 163, 127, 1)' : 'rgba(128, 128, 128, 1)';
// 更新CSS样式
const hoverStyle = document.querySelector('style').textContent;
const newHoverStyle = hoverStyle.replace(
/background: rgba\([\d, ]+\) !important;/,
`background: ${CONFIG.enabled ? 'rgba(16, 163, 127, 1)' : 'rgba(128, 128, 128, 1)'} !important;`
).replace(
/box-shadow: 0 3px 12px rgba\([\d, ]+\);/,
`box-shadow: 0 3px 12px ${CONFIG.enabled ? 'rgba(16, 163, 127, 0.4)' : 'rgba(128, 128, 128, 0.4)'};`
);
document.querySelector('style').textContent = newHoverStyle;
debugLog(`自动朗读已${CONFIG.enabled ? '启用' : '禁用'}`);
showNotification(CONFIG.enabled ? '✅ 已启用自动朗读' : '❌ 已禁用自动朗读');
});
// 延迟时间滑块
delaySlider.addEventListener('input', function() {
updateDelay(this.value);
});
// 测试朗读按钮
testButton.addEventListener('click', function() {
debugLog('手动测试朗读');
const lastMessage = findLatestAssistantMessage();
if (lastMessage) {
clickReadAloudButton(lastMessage, true);
} else {
showNotification('❌ 未找到assistant消息');
}
});
// 重置位置按钮
resetPosButton.addEventListener('click', function() {
resetPosition();
});
// 清除所有设置按钮
clearConfigButton.addEventListener('click', function() {
if (confirm('确定要清除所有设置吗?这将重置为默认配置。')) {
// 清除LocalStorage
localStorage.removeItem('chatgpt_auto_read_config');
localStorage.removeItem('chatgpt_auto_read_position');
showNotification('🗑️ 所有设置已清除,请刷新页面');
debugLog('所有设置已清除');
// 延迟刷新页面
setTimeout(() => {
location.reload();
}, 1500);
}
});
// 点击面板外部收起
document.addEventListener('click', function(e) {
const panel = document.getElementById('autoReadPanel');
const ball = document.getElementById('autoReadBall');
if (!panel.contains(e.target) && !ball.contains(e.target) && !isDragging && isExpanded) {
panel.style.pointerEvents = 'none';
panel.style.opacity = '0';
panel.style.transform = 'translateY(-50%) scale(0) translateX(-20px)';
isExpanded = false;
}
});
}
// 查找最新的assistant消息
function findLatestAssistantMessage() {
const assistantMessages = document.querySelectorAll('[data-message-author-role="assistant"]');
if (assistantMessages.length > 0) {
const latestMessage = assistantMessages[assistantMessages.length - 1];
debugLog(`找到 ${assistantMessages.length} 个assistant消息,最新ID: ${latestMessage.getAttribute('data-message-id')}`);
return latestMessage;
}
debugLog('未找到assistant消息');
return null;
}
// 简化的消息完成判断
function isMessageComplete(messageElement) {
const hasContent = messageElement.querySelector('.markdown, .prose, p, div[data-start]');
if (!hasContent) {
debugLog('消息无内容');
return false;
}
const loadingSelectors = [
'.animate-pulse',
'.loading',
'.spinner',
'[class*="animate-spin"]',
'[class*="animate-bounce"]',
'.cursor-blink'
];
for (let selector of loadingSelectors) {
if (messageElement.querySelector(selector)) {
debugLog(`发现加载指示器: ${selector}`);
return false;
}
}
const generatingIndicators = [
'[data-testid*="stop"]',
'button[aria-label*="Stop"]',
'.text-generating',
'[aria-label*="generating"]'
];
for (let selector of generatingIndicators) {
if (document.querySelector(selector)) {
debugLog(`发现全局生成指示器: ${selector}`);
return false;
}
}
const inputBox = document.querySelector('textarea[placeholder*="Message"], textarea[data-testid*="prompt"]');
if (inputBox && inputBox.disabled) {
debugLog('输入框被禁用,可能正在生成');
return false;
}
const sendButton = document.querySelector('[data-testid="send-button"], button[aria-label*="Send"]');
if (sendButton && sendButton.disabled) {
debugLog('发送按钮被禁用,可能正在生成');
return false;
}
debugLog('消息判断为已完成');
return true;
}
// 查找并点击朗读按钮
function clickReadAloudButton(messageElement, isManualTest = false) {
if (!CONFIG.enabled && !isManualTest) {
debugLog('自动朗读已禁用,跳过');
return false;
}
const articleElement = messageElement.closest('article');
if (!articleElement) {
debugLog('未找到article容器');
showNotification('❌ 未找到消息容器');
return false;
}
const buttonContainer = articleElement.querySelector('.group\\/turn-messages, [class*="turn-action"], .flex.min-h-\\[46px\\]');
if (buttonContainer) {
buttonContainer.style.pointerEvents = 'auto';
buttonContainer.style.opacity = '1';
buttonContainer.style.maskPosition = '0% 0%';
buttonContainer.style.maskImage = 'none';
buttonContainer.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
}
setTimeout(() => {
let readButton = null;
readButton = articleElement.querySelector('[data-testid="voice-play-turn-action-button"]');
if (!readButton) {
readButton = articleElement.querySelector('[aria-label="Read aloud"]');
}
if (!readButton) {
const buttons = articleElement.querySelectorAll('button');
for (let button of buttons) {
const svg = button.querySelector('svg path[d*="11 4.9099"], svg path[d*="10.1621 4.54132"]');
if (svg) {
readButton = button;
break;
}
}
}
if (!readButton) {
const actionButtons = articleElement.querySelectorAll('button[aria-label]');
if (actionButtons.length >= 4) {
const fourthButton = actionButtons[3];
if (fourthButton.querySelector('svg path[fill-rule="evenodd"]')) {
readButton = fourthButton;
}
}
}
if (!readButton) {
debugLog('未找到朗读按钮');
showNotification('❌ 未找到朗读按钮');
return false;
}
debugLog(`找到朗读按钮: ${readButton.getAttribute('aria-label') || '未知'}`);
try {
readButton.click();
debugLog('已点击朗读按钮');
const lastProcessedSpan = document.getElementById('lastProcessed');
if (lastProcessedSpan) {
lastProcessedSpan.textContent = new Date().toLocaleTimeString();
}
// 圆球闪烁效果
const ball = document.getElementById('autoReadBall');
const originalBg = ball.style.background;
ball.style.background = CONFIG.enabled ? 'rgba(16, 163, 127, 1)' : 'rgba(128, 128, 128, 1)';
ball.style.boxShadow = `0 0 15px ${CONFIG.enabled ? 'rgba(16, 163, 127, 0.8)' : 'rgba(128, 128, 128, 0.8)'}`;
setTimeout(() => {
ball.style.background = originalBg;
ball.style.boxShadow = '0 2px 8px rgba(0,0,0,0.3)';
}, 500);
showNotification(isManualTest ? '🔊 手动测试朗读' : '🔊 自动朗读已启动');
return true;
} catch (e) {
debugLog('点击失败: ' + e.message);
showNotification('❌ 点击朗读按钮失败');
return false;
}
}, 200);
}
// 显示通知
function showNotification(message) {
const notification = document.createElement('div');
notification.style.cssText = `
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
background: ${message.includes('❌') ? '#dc2626' : message.includes('⚠️') ? '#d97706' : '#10a37f'};
color: white;
padding: 8px 16px;
border-radius: 18px;
z-index: 10001;
font-size: 12px;
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
opacity: 0;
transform: translateX(-50%) translateY(-20px);
transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
backdrop-filter: blur(10px);
max-width: 250px;
text-align: center;
`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.style.opacity = '1';
notification.style.transform = 'translateX(-50%) translateY(0)';
}, 100);
setTimeout(() => {
notification.style.opacity = '0';
notification.style.transform = 'translateX(-50%) translateY(-20px)';
setTimeout(() => {
if (document.body.contains(notification)) {
document.body.removeChild(notification);
}
}, 300);
}, 2500);
}
// 处理新消息
function handleNewMessage() {
const latestMessage = findLatestAssistantMessage();
if (!latestMessage) {
return;
}
const messageId = latestMessage.getAttribute('data-message-id');
if (!messageId || messageId === lastProcessedMessageId) {
return;
}
debugLog(`检测到新的assistant消息: ${messageId}`);
if (checkTimer) {
clearTimeout(checkTimer);
}
let checkCount = 0;
const maxChecks = CONFIG.maxWaitTime / 500;
const checkComplete = () => {
checkCount++;
if (checkCount > maxChecks) {
debugLog('超时,强制执行朗读');
lastProcessedMessageId = messageId;
clickReadAloudButton(latestMessage);
return;
}
if (isMessageComplete(latestMessage)) {
debugLog(`消息生成完成(检查了${checkCount}次),准备自动朗读`);
lastProcessedMessageId = messageId;
setTimeout(() => {
clickReadAloudButton(latestMessage);
}, CONFIG.delay);
} else {
debugLog(`消息仍在生成中(第${checkCount}次检查),继续等待...`);
checkTimer = setTimeout(checkComplete, 500);
}
};
checkComplete();
}
// 初始化DOM监听
function initObserver() {
if (observer) {
observer.disconnect();
}
observer = new MutationObserver((mutations) => {
let shouldCheck = false;
mutations.forEach((mutation) => {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
for (let node of mutation.addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.querySelector && node.querySelector('[data-message-author-role="assistant"]')) {
shouldCheck = true;
break;
}
}
}
}
if (mutation.type === 'attributes' &&
mutation.target.closest &&
mutation.target.closest('[data-message-author-role="assistant"]')) {
shouldCheck = true;
}
});
if (shouldCheck) {
setTimeout(handleNewMessage, 300);
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['data-is-last-node', 'data-message-id', 'data-state', 'class']
});
debugLog('DOM监听器已启动');
}
// 初始化脚本
function init() {
debugLog('ChatGPT自动朗读脚本已启动 v1.9 - 作者: schweigen');
debugLog(`当前配置: 启用=${CONFIG.enabled}, 延迟=${CONFIG.delay}ms`);
const checkInterface = () => {
if (document.querySelector('article, [data-testid*="conversation"]')) {
debugLog('ChatGPT界面已加载');
createFloatingBall();
initObserver();
setTimeout(handleNewMessage, 2000);
} else {
debugLog('等待ChatGPT界面加载...');
setTimeout(checkInterface, 1000);
}
};
checkInterface();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();