ChatGPT 回答完成提示音(WebAudio 方式)

使用 Web Audio API 播放提示音,不依赖外链,避免无声,稳定可靠;每次回答后提醒一次。

// ==UserScript==
// @name         ChatGPT 回答完成提示音(WebAudio 方式)
// @namespace    https://github.com/xiaozhang
// @version      1.6
// @description  使用 Web Audio API 播放提示音,不依赖外链,避免无声,稳定可靠;每次回答后提醒一次。
// @author       小张
// @match        https://chatgpt.com/*
// @grant        none
// @run-at       document-end
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    let lastContentLength = 0;
    let isAnswering = false;
    let hasAlerted = false;
    let lastAnswerId = null;
    let soundUnlocked = false;
    let audioCtx = null;
    let stableCounter = 0;  // 新增:稳定不变次数

    const STABLE_THRESHOLD = 10;  // 100ms × 10 = 1秒未变化才触发提醒

    // 解锁音频(需要用户首次点击)
    window.addEventListener('click', function unlockAudio() {
        if (!soundUnlocked) {
            audioCtx = new (window.AudioContext || window.webkitAudioContext)();
            playBeep(); // 解锁一次
            soundUnlocked = true;
            console.log('🔓 音频权限已解锁');
        }
        window.removeEventListener('click', unlockAudio);
    });

    // 用 Web Audio 播放 Beep
    function playBeep() {
        if (!audioCtx) return;
        const oscillator = audioCtx.createOscillator();
        const gainNode = audioCtx.createGain();

        oscillator.type = 'sine';
        oscillator.frequency.setValueAtTime(1000, audioCtx.currentTime);
        gainNode.gain.setValueAtTime(0.2, audioCtx.currentTime);

        oscillator.connect(gainNode);
        gainNode.connect(audioCtx.destination);

        oscillator.start();
        oscillator.stop(audioCtx.currentTime + 0.2);
    }

    setInterval(() => {
        const chatContainer = document.querySelector('main');
        if (!chatContainer) return;

        const chatContents = chatContainer.querySelectorAll('[data-message-author-role="assistant"]');
        if (chatContents.length === 0) return;

        const lastContent = chatContents[chatContents.length - 1];
        const currentText = lastContent.innerText.trim();
        const currentLength = currentText.length;

        const currentId = lastContent.getAttribute('data-message-id') || lastContent.innerHTML.slice(0, 50);

        // 如果是新回答,重置所有状态
        if (currentId !== lastAnswerId) {
            lastAnswerId = currentId;
            lastContentLength = 0;
            isAnswering = false;
            hasAlerted = false;
            stableCounter = 0;
        }

        // 回答还在增长中
        if (currentLength > lastContentLength + 5) {
            isAnswering = true;
            hasAlerted = false;
            lastContentLength = currentLength;
            stableCounter = 0;
        }
        // 回答稳定不变
        else if (isAnswering && !hasAlerted && currentLength === lastContentLength) {
            stableCounter++;
            if (stableCounter >= STABLE_THRESHOLD) {
                playBeep();
                hasAlerted = true;
                isAnswering = false;
            }
        }

    }, 100);
})();