对话完成提示器(AI Moinitor)

监测AI聊天生成是否完成并通知提醒(带音效选择)

// ==UserScript==
// @name         对话完成提示器(AI Moinitor)
// @namespace    http://tampermonkey.net/
// @version      3.2
// @description  监测AI聊天生成是否完成并通知提醒(带音效选择)
// @description:zh-CN 适配已做好,音效未做
// @author       who
// @license GPL-3.0-or-later
// @match https://yuanbao.tencent.com/*
// @match https://chatgpt.com/*
// @match https://chat.deepseek.com/*
// @match https://yiyan.baidu.com/*
// @match https://www.tongyi.com/*
// @grant        GM_addStyle
// @grant        GM_notification
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    // 全局配置变量
    let CONFIG = {};
    let state = {};


    // 域名到完整配置的映射表(包含所有参数)
    const DOMAIN_CONFIG_MAP = {
        // 元宝(腾讯)
        'yuanbao.tencent.com': {
            // 元素选择器
            // 选择器可以是类名、ID或其他CSS选择器
            // 用 # . 来区分ID和类名, className里有多个值就不加.
            TARGET_CLASS: "agent-chat__toolbar__copy__icon",
            SEND_BUTTON_SELECTOR: "#yuanbao-send-btn",
            INPUT_SELECTOR: ".style__text-area__edit___d1yNy",
        },
        // ChatGPT(OpenAI)
        'chatgpt.com': {
            TARGET_CLASS: "text-center",
            SEND_BUTTON_SELECTOR: "#composer-submit-button",
            INPUT_SELECTOR: "ProseMirror",
        },
        // DeepSeek
        'chat.deepseek.com': {
            TARGET_CLASS: "._965abe9",
            SEND_BUTTON_SELECTOR: "bcc55ca1",
            INPUT_SELECTOR: "_77cefa5",
        },
        // doubao  暂不支持
        'www.doubao.com': {
            TARGET_CLASS: "suggest-message-LJeEWd",
            SEND_BUTTON_SELECTOR: ".semi-button-content",
            INPUT_SELECTOR: ".editor-wrapper-AdiwSu",
        },
        // 百度一言
        'yiyan.baidu.com': {
            TARGET_CLASS: ".copy__OMlDWQ7D",
            SEND_BUTTON_SELECTOR: "#sendBtn",
            INPUT_SELECTOR: "yc-editor",
        },
        // 通义千问
        'www.tongyi.com': {
            TARGET_CLASS: "btn--YtZqkWMA",
            SEND_BUTTON_SELECTOR: ".operateBtn--qMhYIdIu",
            INPUT_SELECTOR: ".chatTextarea--RVTXJYOh",
        },
    };

    // 根据域名获取配置
    function getSiteConfig() {
        CONFIG = {
            // 时间设置
            MONITOR_TIMEOUT: 600000, // 10分钟
            ALERT_TIMEOUT: 3000,     // 3秒

            // 音效设置
            DEFAULT_SOUND: "chime",
            DEFAULT_DURATION: 2
        }

        // 状态变量
        state = {
            currentDomain: window.location.hostname,
            toolbarDetected: false,
            sendActionTriggered: false,
            toolbarObserver: null,
            monitoringTimeout: null,
            initialToolbarCount: 0,
            alertContainer: null,
            statusIndicator: null,
            soundSelector: null,
            selectedSound: GM_getValue('selectedSound', CONFIG.DEFAULT_SOUND),
            soundDuration: GM_getValue('soundDuration', CONFIG.DEFAULT_DURATION),
            customSoundUrl: GM_getValue('customSoundUrl', '')
        };

        // 匹配域名和配置
        console.log("当前域名:", state.currentDomain);
        CONFIG = {
            ...CONFIG,
            ...DOMAIN_CONFIG_MAP[state.currentDomain]
        };
    }



    // 添加核心样式 - 确保最高优先级
    GM_addStyle(`
        /* 状态指示器 - 强制显示 */
        #tm-status-indicator {
            position: fixed !important;
            z-index: 2147483647 !important; /* 最高优先级 */
            background: white !important;
            color: black !important;
            border-radius: 8px !important;
            padding: 12px 16px !important;
            font-size: 14px !important;
            box-shadow: 0 4px 12px rgba(0,0,0,0.15) !important;
            border: 1px solid rgba(255,255,255,0.1) !important;
            min-width: 150px !important;
            cursor: move !important;
            user-select: none !important;
            display: block !important;
            font-family: Arial, sans-serif !important;
        }

        /* 状态点 */
        #ai-status-dot {
            width: 15px !important;
            height: 15px !important;
            border-radius: 50% !important;
            margin-right: 8px !important;
        }

        /* 状态文本 */
        #ai-status-text {
            display: inline-block !important;
            vertical-align: middle !important;
        }

        /* 设置按钮 */
        #ai-settings-btn {
            background: none !important;
            border: none !important;
            cursor: pointer !important;
        }

        /* 设置面板 */
        #ai-settings-panel {
            margin-top: 8px !important;
            display: none !important;
        }

        #ai-settings-panel.show {
            display: block !important;
        }

        /* 弹窗 - 强制显示 */
        .tm-toolbar-alert {
            position: fixed !important;
            top: 20px !important;
            right: 20px !important;
            width: 320px !important;
            background: linear-gradient(135deg, #1a2a6c, #2c3e50) !important;
            color: white !important;
            border-radius: 12px !important;
            box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3) !important;
            padding: 20px !important;
            z-index: 2147483646 !important; /* 仅次于状态指示器 */
            font-family: 'Segoe UI', Tahoma, sans-serif !important;
            animation: slideIn 0.6s ease-out forwards !important;
            border: 1px solid rgba(255, 255, 255, 0.1) !important;
            display: block !important;
        }

        @keyframes slideIn {
            0% { transform: translateX(120%); opacity: 0; }
            100% { transform: translateX(0); opacity: 1; }
        }

        .tm-toolbar-header {
            display: flex !important;
            align-items: center !important;
            margin-bottom: 15px !important;
            border-bottom: 1px solid rgba(255, 255, 255, 0.15) !important;
            padding-bottom: 12px !important;
        }

        .tm-toolbar-icon {
            width: 40px !important;
            height: 40px !important;
            background: rgba(255, 255, 255, 0.15) !important;
            border-radius: 50% !important;
            margin-right: 15px !important;
            display: flex !important;
            align-items: center !important;
            justify-content: center !important;
            font-size: 22px !important;
        }

        .tm-toolbar-title {
            font-size: 18px !important;
            font-weight: 600 !important;
        }

        .tm-toolbar-desc {
            font-size: 14px !important;
            line-height: 1.5 !important;
            margin-bottom: 18px !important;
            color: rgba(255, 255, 255, 0.85) !important;
        }

        .tm-toolbar-buttons {
            display: flex !important;
            justify-content: space-between !important;
            gap: 10px !important;
        }

        .tm-toolbar-btn {
            flex: 1 !important;
            padding: 8px 12px !important;
            background: rgba(255, 255, 255, 0.1) !important;
            border: none !important;
            color: white !important;
            border-radius: 8px !important;
            cursor: pointer !important;
            font-weight: 500 !important;
            font-size: 14px !important;
            transition: all 0.25s ease !important;
        }

        .tm-toolbar-btn:hover {
            background: rgba(255, 255, 255, 0.2) !important;
            transform: translateY(-2px) !important;
        }

        .tm-toolbar-btn.main {
            background: #3498db !important;
            font-weight: 600 !important;
        }

        .tm-toolbar-btn.main:hover {
            background: #2980b9 !important;
        }
    `);

    // 初始化函数
    function init() {
        console.log("当前域名:", window.location.hostname);
        // 允许浏览器通知
        if (Notification.permission !== "granted") {
            Notification.requestPermission();
        }

        // 根据不同AI配置选择器
        getSiteConfig();

        // 确保在DOM准备好后执行
        if (document.readyState === 'loading') {
            console.log("当前域名:", window.location.hostname);
            document.addEventListener('DOMContentLoaded', createStatusIndicator);
        } else {
            console.log("当前域名:", window.location.hostname);
            setTimeout(createStatusIndicator, 100);
        }
    }

    // 创建状态指示器 - 核心组件
    function createStatusIndicator() {
        // 如果已存在,先移除
        const existingIndicator = document.getElementById('tm-status-indicator');
        if (existingIndicator) existingIndicator.remove();

        // 创建容器
        state.statusIndicator = document.createElement('div');
        state.statusIndicator.id = 'tm-status-indicator';

        // 加载保存的位置
        const savedPosition = GM_getValue('indicatorPosition', {
            x: window.innerWidth - 280,
            y: window.innerHeight - 100
        });
        state.statusIndicator.style.left = `${savedPosition.x}px`;
        state.statusIndicator.style.top = `${savedPosition.y}px`;

        // 添加新面板结构
        state.statusIndicator.innerHTML = `
            <div style="display:flex;align-items:center;justify-content:space-between;">
                <div style="display:flex;align-items:center;">
                    <div id="ai-status-dot" style="width:15px;height:15px;border-radius:50%;margin-right:8px;"></div>
                    <span id="ai-status-text">等待初始化</span>
                </div>
                <button id="ai-settings-btn" style="background:none;border:none;cursor:pointer;">
                    <svg xmlns="http://www.w3.org/2000/svg" style="width:18px;height:18px;fill:#555;" viewBox="0 0 24 24">
                        <path d="M12 15.5A3.5 3.5 0 1 0 12 8.5a3.5 3.5 0 0 0 0 7Zm7.43-2.06 1.97 1.54c.2.16.25.45.11.67l-1.87 3.24c-.14.23-.43.31-.66.21l-2.32-.93a7.05 7.05 0 0 1-1.52.88l-.35 2.48a.5.5 0 0 1-.5.43h-3.74a.5.5 0 0 1-.5-.43l-.35-2.48a7.05 7.05 0 0 1-1.52-.88l-2.32.93a.5.5 0 0 1-.66-.21l-1.87-3.24a.5.5 0 0 1 .11-.67l1.97-1.54a6.97 6.97 0 0 1 0-1.76l-1.97-1.54a.5.5 0 0 1-.11-.67l1.87-3.24c.14-.23.43-.31.66-.21l2.32.93c.47-.36.98-.66 1.52-.88l.35-2.48a.5.5 0 0 1 .5-.43h3.74a.5.5 0 0 1 .5.43l.35 2.48c.54.22 1.05.52 1.52.88l2.32-.93c.23-.1.52-.02.66.21l1.87 3.24c.14.22.09.51-.11.67l-1.97 1.54c.07.29.11.59.11.88s-.04.59-.11.88Z"/>
                    </svg>
                </button>
            </div>
            <div id="ai-settings-panel" style="margin-top:8px;">
                <label><input type="radio" name="soundType" value="off">关</label><br>
                <label><input type="radio" name="soundType" value="bell">铃声</label><br>
                <label><input type="radio" name="soundType" value="dingdong">叮咚</label><br>
                <label>播放时长: <input type="number" id="ai-sound-duration" min="0" max="5" step="1" style="width:40px;"> 秒</label><br>
                <button id="ai-save-settings">保存</button>
            </div>
        `;

        // 添加到文档
        document.body.appendChild(state.statusIndicator);

        // 更新初始状态
        updateStatus('idle', '已初始化');

        // 设置初始音效选择
        setupSoundSelector();

        // 添加事件监听器
        document.getElementById('ai-settings-btn').addEventListener('click', toggleSettingsPanel);
        document.getElementById('ai-save-settings').addEventListener('click', saveSoundSettings);

        // 添加拖动功能
        addDragFunctionality();

        // 初始化其他组件
        setupEventListeners();
    }

    // 添加拖动功能
    function addDragFunctionality() {
        let isDragging = false;
        let dragOffsetX = 0;
        let dragOffsetY = 0;

        state.statusIndicator.addEventListener('mousedown', function(e) {
            // 防止在设置按钮上拖动
            if (e.target.closest('#ai-settings-btn') || e.target.closest('#ai-settings-panel')) {
                return;
            }

            isDragging = true;
            dragOffsetX = e.clientX - state.statusIndicator.getBoundingClientRect().left;
            dragOffsetY = e.clientY - state.statusIndicator.getBoundingClientRect().top;
            document.addEventListener('mousemove', dragIndicator);
            document.addEventListener('mouseup', stopDragging);
        });

        function dragIndicator(e) {
            if (!isDragging) return;

            const x = e.clientX - dragOffsetX;
            const y = e.clientY - dragOffsetY;

            // 限制在窗口范围内
            const maxX = window.innerWidth - state.statusIndicator.offsetWidth;
            const maxY = window.innerHeight - state.statusIndicator.offsetHeight;

            state.statusIndicator.style.left = `${Math.max(0, Math.min(x, maxX))}px`;
            state.statusIndicator.style.top = `${Math.max(0, Math.min(y, maxY))}px`;
        }

        function stopDragging() {
            isDragging = false;

            // 保存位置
            const rect = state.statusIndicator.getBoundingClientRect();
            GM_setValue('indicatorPosition', {
                x: rect.left + window.scrollX,
                y: rect.top + window.scrollY
            });

            document.removeEventListener('mousemove', dragIndicator);
            document.removeEventListener('mouseup', stopDragging);
        }
    }

    // 切换设置面板显示
    function toggleSettingsPanel(e) {
        e.stopPropagation();
        const settingsPanel = document.getElementById('ai-settings-panel');
        settingsPanel.classList.toggle('show');
    }

    // 设置初始音效选择
    function setupSoundSelector() {
        const settingsPanel = document.getElementById('ai-settings-panel');

        // 设置选中的音效类型
        const soundTypeMap = {
            chime: 'bell',
            ding: 'dingdong',
            off: 'off'
        };

        const reverseMap = {
            bell: 'chime',
            dingdong: 'ding',
            off: 'off'
        };

        const currentSelection = reverseMap[state.selectedSound] || state.selectedSound;
        const radioBtn = settingsPanel.querySelector(`input[value="${currentSelection}"]`);
        if (radioBtn) radioBtn.checked = true;

        // 设置音效时长
        const durationInput = document.getElementById('ai-sound-duration');
        durationInput.value = state.soundDuration;
    }

    // 保存音效设置
    function saveSoundSettings() {
        const soundType = document.querySelector('input[name="soundType"]:checked')?.value || 'chime';
        const durationInput = document.getElementById('ai-sound-duration');
        const duration = parseFloat(durationInput.value) || state.soundDuration;

        // 映射到原音效类型
        const soundMap = {
            bell: 'chime',
            dingdong: 'ding',
            off: 'off'
        };

        state.selectedSound = soundMap[soundType] || soundType;
        state.soundDuration = duration;

        GM_setValue('selectedSound', state.selectedSound);
        GM_setValue('soundDuration', state.soundDuration);

        document.getElementById('ai-settings-panel').classList.remove('show');
    }

    // 更新状态显示
    function updateStatus(stateType, message) {
        const statusDot = document.getElementById('ai-status-dot');
        const statusText = document.getElementById('ai-status-text');

        if (!statusDot || !statusText) {
            // 如果元素不存在,重新创建状态指示器
            createStatusIndicator();
            return;
        }

        switch(stateType) {
            case 'idle':
                statusDot.style.background =  '#2ecc71';
                break;
            case 'waiting':
                statusDot.style.background = '#f1c40f';
                break;
            case 'active':
                statusDot.style.background = '#e74c3c';
                break;
        }

        statusText.textContent = message;
        console.log(`[AI Monitor] ${message}`);
    }

    // 设置事件监听器
    function setupEventListeners() {
        console.log("CONFIG:", CONFIG);
        console.log("发送键选择器",CONFIG.SEND_BUTTON_SELECTOR);
        console.log("输入框选择器:", CONFIG.INPUT_SELECTOR);
        console.log("当前目标个数:", getToolbarCount());
        // 查找发送按钮
        findAndMonitorElement(CONFIG.SEND_BUTTON_SELECTOR, (button) => {
            button.addEventListener('click', handleSendAction);
            console.log("发送键等待发送", button);
            updateStatus('idle', '等待发送');
        });

        // 查找输入框
        findAndMonitorElement(CONFIG.INPUT_SELECTOR, (input) => {
            input.addEventListener('keydown', handleKeyDown);
            console.log("输入框等待发送", input);
            updateStatus('idle', '等待发送');
        });
    }

    // 通用元素查找和监控
    function findAndMonitorElement(selector, callback) {
        // 处理各种选择器类型
        const normalizedSelector = normalizeSelector(selector);

        // 如果已经有匹配元素立即执行回调
        const existingElement = document.querySelector(normalizedSelector);
        if (existingElement) {
            callback(existingElement);
            return;
        }

        // 监听DOM变化以捕获动态加载的元素
        const observer = new MutationObserver(() => {
            const element = document.querySelector(normalizedSelector);
            if (element) {
                observer.disconnect(); // 找到后停止观察
                callback(element);
            }
        });

        // 开始观察整个文档
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    }

    // 标准化选择器处理
    function normalizeSelector(selector) {
        // 如果选择器以#开头,直接使用(ID选择器)
        if (selector.startsWith('#')) {
            return selector;
        }

        // 如果选择器以.开头,直接使用(类选择器)
        if (selector.startsWith('.')) {
            return selector;
        }

        // 如果包含空格说明是复合选择器,直接使用
        if (selector.includes(' ')) {
            return selector;
        }

        // 尝试用类选择器查找
        return `.${selector}`;
    }

    // 处理键盘事件
    function handleKeyDown(event) {
        if ((event.key === 'Enter' || event.keyCode === 13) && !event.shiftKey) {
            handleSendAction();
        }
    }

    // 处理发送动作
    function handleSendAction() {
        // 关闭现有弹窗
        closeAlert();

        // 重置状态
        resetMonitoringState();

        // 开始新监测
        startToolbarMonitoring();
    }

    // 启动工具栏监控
    function startToolbarMonitoring() {
        // 清除现有监控
        if (state.toolbarObserver) state.toolbarObserver.disconnect();
        if (state.monitoringTimeout) clearTimeout(state.monitoringTimeout);

        // 记录初始状态
        state.toolbarDetected = false;
        state.sendActionTriggered = true;
        state.initialToolbarCount = getToolbarCount();

        updateStatus('waiting', '生成中...');

        // 初始检查
        if (checkForNewToolbar()) return;

        // 设置DOM监控
        state.toolbarObserver = new MutationObserver(() => {
            if (checkForNewToolbar()) {
                state.toolbarObserver.disconnect();
            }
        });

        state.toolbarObserver.observe(document.body, {
            childList: true,
            subtree: true
        });

        // 设置超时
        state.monitoringTimeout = setTimeout(() => {
            if (state.toolbarObserver) state.toolbarObserver.disconnect();
            updateStatus('idle', '监控超时,准备下一次发送');
        }, CONFIG.MONITOR_TIMEOUT);
    }

    // 检查新工具栏
    function checkForNewToolbar() {
        const currentCount = getToolbarCount();
        if (currentCount > state.initialToolbarCount && !state.toolbarDetected) {
            state.toolbarDetected = true;
            showToolbarAlert();
            state.initialToolbarCount = currentCount;
            return true;
        }
        return false;
    }

    // 获取工具栏数量
    function getToolbarCount() {
        try{
            return document.querySelectorAll(`.${CONFIG.TARGET_CLASS}`).length
        }catch(e) {
            console.warn("获取工具栏数量失败:", e);
            try{
                return document.querySelectorAll(CONFIG.TARGET_CLASS).length;
            }catch(e2) {
                console.error("再次尝试获取工具栏数量失败:", e2);
                console.error("生成结束标志选择有误,尝试使用类名获取工具栏数量");
            }
        }
    }

    // 显示工具栏通知
    function showToolbarAlert() {
        // 关闭现有弹窗
        closeAlert();

        // 创建新弹窗
        state.alertContainer = document.createElement('div');
        state.alertContainer.className = 'tm-toolbar-alert';
        state.alertContainer.innerHTML = `
            <div class="tm-toolbar-desc">
                内容已生成完毕
            </div>
            <div class="tm-toolbar-buttons">
                <button class="tm-toolbar-btn" id="tm-close-alert">关闭</button>
            </div>
        `;

        // 浏览器通知
        if (Notification.permission === "granted") {
            new Notification("内容已生成完毕", { body: "请到浏览器查看" });
        }

        // Tampermonkey 系统通知
        GM_notification({
            title: "内容生成完毕",
            timeout: 5000,
            onclick: () => {
                window.focus();
            }
        });

        document.body.appendChild(state.alertContainer);

        // 添加事件
        document.getElementById('tm-close-alert').addEventListener('click', closeAlert);

        // 设置自动关闭
        setTimeout(closeAlert, CONFIG.ALERT_TIMEOUT);

        // 更新状态
        updateStatus('idle', '等待发送');
        playSound();
    }

    // 播放音效
    function playSound() {
        if (state.selectedSound === 'off') return;

        let soundUrl;

        switch(state.selectedSound) {
            case 'chime':
                soundUrl = 'https://assets.mixkit.co/sfx/preview/mixkit-melodic-bonus-collect-1938.mp3';
                break;
            case 'beep':
                soundUrl = 'https://assets.mixkit.co/sfx/preview/mixkit-retro-game-notification-212.mp3';
                break;
            case 'ding':
                soundUrl = 'https://assets.mixkit.co/sfx/preview/mixkit-correct-answer-tone-2870.mp3';
                break;
            case 'bell':
                soundUrl = 'https://assets.mixkit.co/sfx/preview/mixkit-winning-chimes-2015.mp3';
                break;
            case 'custom':
                soundUrl = state.customSoundUrl;
                break;
            default:
                soundUrl = 'https://assets.mixkit.co/sfx/preview/mixkit-melodic-bonus-collect-1938.mp3';
        }

        if (!soundUrl) return;

        try {
            const audio = new Audio(soundUrl);
            audio.volume = 0.5;
            audio.play();

            // 设置停止时间
            setTimeout(() => {
                audio.pause();
                audio.currentTime = 0;
            }, state.soundDuration * 1000);
        } catch (e) {
            console.error('音效播放失败:', e);
        }
    }

    // 关闭弹窗
    function closeAlert() {
        if (state.alertContainer && state.alertContainer.parentNode) {
            state.alertContainer.parentNode.removeChild(state.alertContainer);
            state.alertContainer = null;
        }
    }



    // 重置监控状态
    function resetMonitoringState() {
        if (state.toolbarObserver) {
            state.toolbarObserver.disconnect();
            state.toolbarObserver = null;
        }

        if (state.monitoringTimeout) {
            clearTimeout(state.monitoringTimeout);
            state.monitoringTimeout = null;
        }

        state.toolbarDetected = false;
        state.sendActionTriggered = false;
        updateStatus('idle', '准备下一次发送');
    }

    // 启动脚本
    init();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址