ChatGPT回复计时器

计时 ChatGPT 每次回复耗时(修复完整版 - 计时到回复完成)

当前为 2025-07-12 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         ChatGPT回复计时器
// @namespace    http://tampermonkey.net/
// @version      2.2
// @description  计时 ChatGPT 每次回复耗时(修复完整版 - 计时到回复完成)
// @author       schweigen
// @match        https://chatgpt.com/*
// @exclude      https://chatgpt.com/codex*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // 全局变量定义
    let startTime = null;
    let timerInterval = null;
    let timerDisplay = null;
    let timerContainer = null;
    let isGenerating = false;
    let lastRequestTime = 0;
    let confirmationTimer = null;

    // 防止重复初始化
    let isInitialized = false;

    // 用于清理的观察者列表
    let observers = [];
    let abortController = null;

    // 新增:流式响应监控
    let streamEndTimer = null;
    let lastStreamActivity = 0;
    let isStreamActive = false;

    // 节流函数
    function throttle(func, limit) {
        let inThrottle;
        return function() {
            const args = arguments;
            const context = this;
            if (!inThrottle) {
                func.apply(context, args);
                inThrottle = true;
                setTimeout(() => inThrottle = false, limit);
            }
        }
    }

    // 防抖函数
    function debounce(func, wait) {
        let timeout;
        return function executedFunction(...args) {
            const later = () => {
                clearTimeout(timeout);
                func(...args);
            };
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
        };
    }

    // 安全清理函数
    function cleanup() {
        if (timerInterval) {
            clearInterval(timerInterval);
            timerInterval = null;
        }

        if (confirmationTimer) {
            clearTimeout(confirmationTimer);
            confirmationTimer = null;
        }

        if (streamEndTimer) {
            clearTimeout(streamEndTimer);
            streamEndTimer = null;
        }

        observers.forEach(observer => {
            if (observer && observer.disconnect) {
                observer.disconnect();
            }
        });
        observers = [];

        if (abortController) {
            abortController.abort();
            abortController = null;
        }

        isGenerating = false;
        isStreamActive = false;
    }

    // 创建并添加计时器显示到页面
    function createTimerDisplay() {
        if (document.getElementById('chatgpt-mini-timer')) return;

        timerContainer = document.createElement('div');
        timerContainer.id = 'chatgpt-mini-timer';

        Object.assign(timerContainer.style, {
            position: 'fixed',
            right: '20px',
            top: '80%',
            transform: 'translateY(-50%)',
            zIndex: '10000',
            backgroundColor: 'rgba(0, 0, 0, 0.6)',
            color: 'white',
            padding: '8px 12px',
            borderRadius: '20px',
            fontFamily: 'monospace',
            fontSize: '16px',
            fontWeight: 'bold',
            boxShadow: '0 2px 5px rgba(0, 0, 0, 0.2)',
            transition: 'opacity 0.3s',
            opacity: '0.8',
            userSelect: 'none',
            cursor: 'pointer'
        });

        timerContainer.addEventListener('mouseenter', () => {
            timerContainer.style.opacity = '1';
        });

        timerContainer.addEventListener('mouseleave', () => {
            timerContainer.style.opacity = '0.8';
        });

        timerContainer.addEventListener('click', () => {
            if (isGenerating) {
                stopTimer();
            } else {
                startTimer(true);
            }
        });

        const statusText = document.createElement('div');
        statusText.textContent = '就绪';
        statusText.style.fontSize = '12px';
        statusText.style.marginBottom = '4px';
        statusText.style.textAlign = 'center';
        timerContainer.appendChild(statusText);

        timerDisplay = document.createElement('div');
        timerDisplay.textContent = '0.0s';
        timerDisplay.style.textAlign = 'center';
        timerContainer.appendChild(timerDisplay);

        document.body.appendChild(timerContainer);
        timerContainer.title = "点击可手动开始/停止计时\nAlt+S: 手动开始 | Alt+P: 手动停止";
    }

    // 开始计时
    function startTimer(immediate = false) {
        if (isGenerating) return;

        if (confirmationTimer) {
            clearTimeout(confirmationTimer);
            confirmationTimer = null;
        }

        if (!immediate) {
            confirmationTimer = setTimeout(() => {
                startTimer(true);
            }, 100);

            if (timerContainer) {
                timerContainer.firstChild.textContent = '准备计时...';
            }
            return;
        }

        isGenerating = true;
        isStreamActive = false;
        startTime = Date.now();
        lastStreamActivity = Date.now();

        if (timerDisplay) {
            timerDisplay.textContent = '0.0s';
        }
        if (timerContainer) {
            timerContainer.style.color = '#ffcc00';
            timerContainer.firstChild.textContent = '计时中...';
        }

        if (timerInterval) {
            clearInterval(timerInterval);
        }

        timerInterval = setInterval(() => {
            if (startTime && timerDisplay) {
                const elapsed = (Date.now() - startTime) / 1000;
                timerDisplay.textContent = `${elapsed.toFixed(1)}s`;
            }
        }, 100);

        console.log('[ChatGPT计时器] 开始计时');
    }

    // 停止计时
    function stopTimer() {
        if (!isGenerating) return;

        if (confirmationTimer) {
            clearTimeout(confirmationTimer);
            confirmationTimer = null;
        }

        if (streamEndTimer) {
            clearTimeout(streamEndTimer);
            streamEndTimer = null;
        }

        isGenerating = false;
        isStreamActive = false;

        if (timerInterval) {
            clearInterval(timerInterval);
            timerInterval = null;
        }

        if (startTime && timerDisplay && timerContainer) {
            const elapsed = (Date.now() - startTime) / 1000;
            timerDisplay.textContent = `${elapsed.toFixed(1)}s`;
            timerContainer.style.color = 'white';
            timerContainer.firstChild.textContent = '已完成';

            console.log('[ChatGPT计时器] 停止计时,总耗时:', elapsed.toFixed(1), '秒');

            setTimeout(() => {
                if (!isGenerating && timerContainer) {
                    timerContainer.firstChild.textContent = '就绪';
                }
            }, 5000);
        }
    }

    // 检查流是否真正结束的函数
    function checkStreamEnd() {
        if (!isGenerating || !isStreamActive) return;

        const now = Date.now();
        const timeSinceLastActivity = now - lastStreamActivity;

        // 如果超过2秒没有流活动,认为流已结束
        if (timeSinceLastActivity > 2000) {
            console.log('[ChatGPT计时器] 流活动停止超过2秒,检查是否真正完成');

            // 额外检查:看是否有"思考中"或加载状态
            const isThinking = document.querySelector('[data-message-author-role="assistant"]') &&
                             document.body.textContent.includes('Thinking');

            // 检查是否有停止生成按钮(表示还在生成中)
            const stopButton = document.querySelector('[data-testid="stop-button"]') ||
                             document.querySelector('button[aria-label*="stop"]') ||
                             document.querySelector('button[aria-label*="Stop"]');

            if (!isThinking && !stopButton) {
                console.log('[ChatGPT计时器] 确认回复完成,停止计时');
                stopTimer();
            } else {
                console.log('[ChatGPT计时器] 检测到仍在生成中,继续等待');
                // 重新设置检查
                streamEndTimer = setTimeout(checkStreamEnd, 1000);
            }
        } else {
            // 重新设置检查
            streamEndTimer = setTimeout(checkStreamEnd, 1000);
        }
    }

    // 修复版:使用Fetch API拦截器监控网络请求
    function setupNetworkMonitoring() {
        const originalFetch = window.fetch;

        window.fetch = async function(...args) {
            const [url, options] = args;

            const isChatGPTRequest = typeof url === 'string' && (
                url.includes('/api/conversation') ||
                url.includes('/backend-api/conversation') ||
                url.includes('/v1/chat/completions')
            );

            const isMessageSendRequest = isChatGPTRequest &&
                options &&
                options.method === 'POST' &&
                options.body;

            if (isMessageSendRequest && (Date.now() - lastRequestTime > 1000)) {
                console.log('[ChatGPT计时器] 检测到消息发送请求');
                lastRequestTime = Date.now();
                startTimer();

                try {
                    const response = await originalFetch.apply(this, args);
                    const originalResponse = response.clone();

                    if (response.ok) {
                        if (response.headers.get('content-type')?.includes('text/event-stream')) {
                            console.log('[ChatGPT计时器] 检测到流式响应');
                            isStreamActive = true;
                            lastStreamActivity = Date.now();

                            const reader = response.body.getReader();
                            const decoder = new TextDecoder();
                            let streamClosed = false;

                            // 开始检查流结束
                            if (streamEndTimer) {
                                clearTimeout(streamEndTimer);
                            }
                            streamEndTimer = setTimeout(checkStreamEnd, 2000);

                            const processStream = async () => {
                                try {
                                    while (!streamClosed) {
                                        const result = await Promise.race([
                                            reader.read(),
                                            new Promise((_, reject) =>
                                                setTimeout(() => reject(new Error('Read timeout')), 30000)
                                            )
                                        ]);

                                        if (result.done) {
                                            streamClosed = true;
                                            console.log('[ChatGPT计时器] 流数据读取完成');
                                            // 不要立即停止计时,等待检查机制确认
                                            break;
                                        } else {
                                            // 更新流活动时间
                                            lastStreamActivity = Date.now();

                                            // 解码数据以检查内容
                                            const chunk = decoder.decode(result.value, { stream: true });

                                            // 检查是否包含结束标志
                                            if (chunk.includes('[DONE]') || chunk.includes('data: [DONE]')) {
                                                console.log('[ChatGPT计时器] 检测到流结束标志');
                                                streamClosed = true;
                                                // 延迟一点再检查,确保UI更新完成
                                                setTimeout(() => {
                                                    if (streamEndTimer) {
                                                        clearTimeout(streamEndTimer);
                                                    }
                                                    streamEndTimer = setTimeout(checkStreamEnd, 500);
                                                }, 300);
                                                break;
                                            }
                                        }
                                    }
                                } catch (error) {
                                    streamClosed = true;
                                    console.log('[ChatGPT计时器] 流读取错误:', error.message);
                                    // 出错时也要检查是否真正完成
                                    setTimeout(() => {
                                        if (streamEndTimer) {
                                            clearTimeout(streamEndTimer);
                                        }
                                        streamEndTimer = setTimeout(checkStreamEnd, 1000);
                                    }, 500);
                                } finally {
                                    try {
                                        reader.releaseLock();
                                    } catch (e) {
                                        // 忽略释放锁的错误
                                    }
                                }
                            };

                            processStream();
                            return originalResponse;
                        } else {
                            console.log('[ChatGPT计时器] 检测到普通响应');
                            // 对于非流式响应,延迟停止以确保内容渲染完成
                            setTimeout(() => {
                                console.log('[ChatGPT计时器] 普通响应完成,停止计时');
                                stopTimer();
                            }, 1000);
                            return originalResponse;
                        }
                    } else {
                        console.log('[ChatGPT计时器] 请求失败,停止计时');
                        stopTimer();
                        return originalResponse;
                    }
                } catch (error) {
                    console.error('[ChatGPT计时器] 请求异常:', error);
                    stopTimer();
                    throw error;
                }
            }

            return originalFetch.apply(this, args);
        };
    }

    // 修复版:监控XMLHttpRequest请求
    function setupXHRMonitoring() {
        const originalOpen = XMLHttpRequest.prototype.open;
        const originalSend = XMLHttpRequest.prototype.send;

        XMLHttpRequest.prototype.open = function(method, url, ...args) {
            this._chatgptTimerUrl = url;
            this._chatgptTimerMethod = method;
            return originalOpen.apply(this, [method, url, ...args]);
        };

        XMLHttpRequest.prototype.send = function(body) {
            const url = this._chatgptTimerUrl;
            const method = this._chatgptTimerMethod;

            const isChatGPTRequest = typeof url === 'string' && (
                url.includes('/api/conversation') ||
                url.includes('/backend-api/conversation') ||
                url.includes('/v1/chat/completions')
            );

            const isMessageSendRequest = isChatGPTRequest &&
                method === 'POST' &&
                body;

            if (isMessageSendRequest && (Date.now() - lastRequestTime > 1000)) {
                console.log('[ChatGPT计时器] XHR: 检测到消息发送请求');
                lastRequestTime = Date.now();
                startTimer();

                const handleComplete = () => {
                    // XHR完成后也要延迟检查
                    setTimeout(() => {
                        console.log('[ChatGPT计时器] XHR: 请求完成,检查是否真正完成');
                        if (streamEndTimer) {
                            clearTimeout(streamEndTimer);
                        }
                        streamEndTimer = setTimeout(checkStreamEnd, 1000);
                    }, 500);
                };

                this.addEventListener('load', handleComplete, { once: true });
                this.addEventListener('error', () => stopTimer(), { once: true });
                this.addEventListener('abort', () => stopTimer(), { once: true });
            }

            return originalSend.apply(this, arguments);
        };
    }

    // 修复版:特殊监测"Thinking"文本
    function setupThinkingDetection() {
        const throttledCallback = throttle((mutations) => {
            if (isGenerating) return;

            const chatContainer = document.querySelector('main') ||
                                document.querySelector('[role="main"]') ||
                                document.body;

            const thinkingElements = chatContainer.querySelectorAll('*');
            for (let element of thinkingElements) {
                const text = element.textContent || '';
                if (text.includes('Thinking') ||
                    text.includes('thinking...') ||
                    text.includes('思考中') ||
                    text.includes('正在思考')) {
                    console.log('[ChatGPT计时器] 检测到"Thinking"文本,开始计时');
                    startTimer();
                    break;
                }
            }
        }, 500);

        const observer = new MutationObserver(throttledCallback);

        const targetNode = document.querySelector('main') || document.body;
        observer.observe(targetNode, {
            childList: true,
            subtree: true,
            characterData: true
        });

        observers.push(observer);
    }

    // 更精确的DOM完成检测
    function setupDOMCompletionDetection() {
        const throttledCallback = throttle((mutations) => {
            if (!isGenerating) return;

            // 检查停止按钮是否消失(更准确的完成指标)
            const stopButton = document.querySelector('[data-testid="stop-button"]') ||
                             document.querySelector('button[aria-label*="stop"]') ||
                             document.querySelector('button[aria-label*="Stop"]');

            // 检查是否还在思考
            const isThinking = document.body.textContent.includes('Thinking');

            // 检查操作按钮是否出现
            const actionButtons = document.querySelectorAll([
                '[data-testid="copy-turn-action-button"]',
                '[data-testid="good-response-turn-action-button"]',
                '[data-testid="bad-response-turn-action-button"]',
                '[data-testid="voice-play-turn-action-button"]'
            ].join(','));

            // 只有当没有停止按钮、没有思考状态、且有操作按钮时才认为完成
            if (!stopButton && !isThinking && actionButtons.length > 0) {
                // 额外延迟确保真正完成
                setTimeout(() => {
                    const stillGenerating = document.querySelector('[data-testid="stop-button"]') ||
                                          document.body.textContent.includes('Thinking');
                    if (!stillGenerating) {
                        console.log('[ChatGPT计时器] DOM检测确认回复完成,停止计时');
                        stopTimer();
                    }
                }, 1000);
            }
        }, 300);

        const observer = new MutationObserver(throttledCallback);
        observer.observe(document.body, {
            childList: true,
            subtree: true,
            attributes: true,
            attributeFilter: ['data-testid', 'aria-label']
        });

        observers.push(observer);
    }

    // 检测用户输入
    function setupUserInputDetection() {
        const handleFocusIn = (event) => {
            if (event.target.tagName === 'TEXTAREA' ||
                event.target.tagName === 'INPUT' ||
                event.target.getAttribute('role') === 'textbox' ||
                event.target.getAttribute('contenteditable') === 'true') {
                window._userIsTyping = true;
            }
        };

        const handleFocusOut = (event) => {
            if (event.target.tagName === 'TEXTAREA' ||
                event.target.tagName === 'INPUT' ||
                event.target.getAttribute('role') === 'textbox' ||
                event.target.getAttribute('contenteditable') === 'true') {
                window._userIsTyping = false;
            }
        };

        document.addEventListener('focusin', handleFocusIn);
        document.addEventListener('focusout', handleFocusOut);
    }

    // 添加键盘快捷键支持
    function setupKeyboardShortcuts() {
        document.addEventListener('keydown', (event) => {
            if (event.altKey) {
                if (event.key === 's' || event.key === 'S') {
                    startTimer(true);
                    event.preventDefault();
                } else if (event.key === 'p' || event.key === 'P') {
                    stopTimer();
                    event.preventDefault();
                }
            }
        });
    }

    // 初始化函数
    function initialize() {
        if (isInitialized) {
            console.log('[ChatGPT计时器] 已经初始化过了,跳过重复初始化');
            return;
        }

        try {
            createTimerDisplay();
            setupNetworkMonitoring();
            setupXHRMonitoring();
            setupThinkingDetection();
            setupDOMCompletionDetection();
            setupUserInputDetection();
            setupKeyboardShortcuts();

            isInitialized = true;
            console.log('[ChatGPT计时器] 初始化完成,等待使用...');
        } catch (error) {
            console.error('[ChatGPT计时器] 初始化失败:', error);
        }
    }

    // 页面卸载时清理
    window.addEventListener('beforeunload', cleanup);

    // 监听页面可见性变化
    document.addEventListener('visibilitychange', () => {
        if (document.hidden) {
            cleanup();
        }
    });

    // 页面加载完成后初始化
    if (document.readyState === 'complete') {
        setTimeout(initialize, 1000);
    } else {
        window.addEventListener('load', () => {
            setTimeout(initialize, 1000);
        });
    }

    // 备用初始化
    setTimeout(() => {
        if (!isInitialized) {
            initialize();
        }
    }, 3000);

})();