ChatGPT回复计时器

计时 ChatGPT 每次回复耗时

// ==UserScript==
// @name         ChatGPT回复计时器
// @namespace    http://tampermonkey.net/
// @version      2.0
// @description  计时 ChatGPT 每次回复耗时
// @author       schweigen
// @match        https://chatgpt.com/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // 全局变量定义
    let startTime = null;      // 记录开始时间的时间戳
    let timerInterval = null;  // 计时器的间隔ID
    let timerDisplay = null;   // 显示时间的DOM元素
    let timerContainer = null; // 计时器容器DOM元素
    let isGenerating = false;  // 标记是否正在生成回答
    let lastRequestTime = 0;   // 上次请求的时间戳(用于防止重复计时)

    // 创建并添加计时器显示到页面
    function createTimerDisplay() {
        if (document.getElementById('chatgpt-mini-timer')) return; // 如果计时器已存在则不重复创建

        // 创建主容器
        timerContainer = document.createElement('div');
        timerContainer.id = 'chatgpt-mini-timer';
        timerContainer.style.position = 'fixed';
        timerContainer.style.right = '20px'; // 定位在右侧
        timerContainer.style.top = '80%';    // 靠下位置
        timerContainer.style.transform = 'translateY(-50%)';
        timerContainer.style.zIndex = '10000';
        timerContainer.style.backgroundColor = 'rgba(0, 0, 0, 0.6)';
        timerContainer.style.color = 'white';
        timerContainer.style.padding = '8px 12px';
        timerContainer.style.borderRadius = '20px';
        timerContainer.style.fontFamily = 'monospace';
        timerContainer.style.fontSize = '16px';
        timerContainer.style.fontWeight = 'bold';
        timerContainer.style.boxShadow = '0 2px 5px rgba(0, 0, 0, 0.2)';
        timerContainer.style.transition = 'opacity 0.3s';
        timerContainer.style.opacity = '0.8';
        timerContainer.style.userSelect = 'none';
        timerContainer.style.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();
            }
        });

        // 创建状态文本元素
        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() {
        if (isGenerating) return; // 如果已经在计时中则不重复启动

        isGenerating = true;
        startTime = Date.now(); // 记录开始时间
        timerDisplay.textContent = '0.0s';
        timerContainer.style.color = '#ffcc00'; // 计时中显示黄色
        timerContainer.firstChild.textContent = '计时中...'; // 更新状态文本

        // 设置定时器每100毫秒更新一次显示时间
        timerInterval = setInterval(() => {
            const elapsed = (Date.now() - startTime) / 1000;
            timerDisplay.textContent = `${elapsed.toFixed(1)}s`;
        }, 100);

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

    // 停止计时
    function stopTimer() {
        if (!isGenerating) return; // 如果没有在计时则不执行

        isGenerating = false;

        // 清除计时器
        if (timerInterval) {
            clearInterval(timerInterval);
            timerInterval = null;
        }

        // 计算并显示最终时间
        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), '秒');

        // 5秒后重置状态文本
        setTimeout(() => {
            if (!isGenerating) {
                timerContainer.firstChild.textContent = '就绪';
            }
        }, 5000);
    }

    // 使用Fetch API拦截器监控网络请求
    function setupNetworkMonitoring() {
        // 保存原始的fetch函数
        const originalFetch = window.fetch;

        // 重写fetch函数以监控请求
        window.fetch = async function(...args) {
            const [url, options] = args;

            // 检查是否是ChatGPT的API请求
            const isChatGPTRequest = typeof url === 'string' && (
                url.includes('/api/conversation') ||
                url.includes('/backend-api/conversation') ||
                url.includes('/v1/chat/completions')
            );

            // 检查是否是发送消息的POST请求
            const isMessageSendRequest = isChatGPTRequest &&
                options &&
                options.method === 'POST' &&
                options.body;

            // 如果是发送消息请求,且距离上次请求已经过了至少2秒(防止重复触发)
            if (isMessageSendRequest && (Date.now() - lastRequestTime > 2000)) {
                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计时器] 检测到流式响应');

                            // 创建一个Reader来读取流
                            const reader = response.body.getReader();
                            let streamClosed = false;

                            // 读取流直到结束
                            const processStream = async () => {
                                try {
                                    while (!streamClosed) {
                                        const { done } = await reader.read();
                                        if (done) {
                                            streamClosed = true;
                                            console.log('[ChatGPT计时器] 流结束,停止计时');
                                            stopTimer();
                                            break;
                                        }
                                    }
                                } catch (error) {
                                    console.log('[ChatGPT计时器] 流读取错误,停止计时');
                                    stopTimer();
                                }
                            };

                            // 启动流处理
                            processStream();

                            // 为了不影响原始响应,我们返回克隆的响应
                            return originalResponse;
                        } else {
                            // 普通响应,响应完成时停止计时
                            console.log('[ChatGPT计时器] 检测到普通响应');
                            response.json().then(() => {
                                console.log('[ChatGPT计时器] 响应完成,停止计时');
                                stopTimer();
                            }).catch(() => {
                                // 如果无法解析为JSON,也停止计时
                                console.log('[ChatGPT计时器] 响应解析错误,停止计时');
                                stopTimer();
                            });

                            return originalResponse;
                        }
                    } else {
                        // 请求失败,停止计时
                        console.log('[ChatGPT计时器] 请求失败,停止计时');
                        stopTimer();
                        return originalResponse;
                    }
                } catch (error) {
                    // 捕获请求错误,停止计时
                    console.error('[ChatGPT计时器] 请求异常:', error);
                    stopTimer();
                    throw error; // 重新抛出异常
                }
            }

            // 对于其他请求,直接使用原始fetch
            return originalFetch.apply(this, args);
        };
    }

    // 监控XMLHttpRequest请求(作为Fetch API的备份)
    function setupXHRMonitoring() {
        const originalOpen = XMLHttpRequest.prototype.open;
        const originalSend = XMLHttpRequest.prototype.send;

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

        // 重写send方法
        XMLHttpRequest.prototype.send = function(body) {
            const url = this._chatgptTimerUrl;
            const method = this._chatgptTimerMethod;

            // 检查是否是ChatGPT的API请求
            const isChatGPTRequest = typeof url === 'string' && (
                url.includes('/api/conversation') ||
                url.includes('/backend-api/conversation') ||
                url.includes('/v1/chat/completions')
            );

            // 检查是否是发送消息的POST请求
            const isMessageSendRequest = isChatGPTRequest &&
                method === 'POST' &&
                body;

            // 如果是发送消息请求,且距离上次请求已经过了至少2秒(防止重复触发)
            if (isMessageSendRequest && (Date.now() - lastRequestTime > 2000)) {
                console.log('[ChatGPT计时器] XHR: 检测到消息发送请求');
                lastRequestTime = Date.now();

                // 开始计时
                startTimer();

                // 添加加载完成事件监听器
                this.addEventListener('load', () => {
                    console.log('[ChatGPT计时器] XHR: 请求完成,停止计时');
                    stopTimer();
                });

                // 添加错误事件监听器
                this.addEventListener('error', () => {
                    console.log('[ChatGPT计时器] XHR: 请求错误,停止计时');
                    stopTimer();
                });

                // 添加终止事件监听器
                this.addEventListener('abort', () => {
                    console.log('[ChatGPT计时器] XHR: 请求中止,停止计时');
                    stopTimer();
                });
            }

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

    // 特殊监测"Thinking"文本
    function setupThinkingDetection() {
        // 创建一个MutationObserver来监视DOM变化
        const observer = new MutationObserver((mutations) => {
            // 如果已经在计时中,不需要重复检测
            if (isGenerating) return;

            // 检查是否有"Thinking"相关文本
            const thinkingElements = document.evaluate(
                "//*[contains(text(), 'Thinking') or contains(text(), 'thinking...') or contains(text(), '思考中') or contains(text(), '正在思考')]",
                document,
                null,
                XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
                null
            );

            // 如果找到包含"Thinking"的元素
            if (thinkingElements.snapshotLength > 0) {
                console.log('[ChatGPT计时器] 检测到"Thinking"文本,开始计时');
                startTimer();
            }
        });

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

    // 监测DOM变化以检测回复完成
    function setupDOMCompletionDetection() {
        const observer = new MutationObserver((mutations) => {
            // 如果没有在计时,则不需要检查
            if (!isGenerating) return;

            // 检查是否出现了回复完成后的操作按钮
            const copyButton = document.querySelector('[data-testid="copy-turn-action-button"]');
            const goodResponseButton = document.querySelector('[data-testid="good-response-turn-action-button"]');
            const badResponseButton = document.querySelector('[data-testid="bad-response-turn-action-button"]');
            const voicePlayButton = document.querySelector('[data-testid="voice-play-turn-action-button"]');

            // 如果检测到这些按钮之一,说明回复已经完成
            if (copyButton || goodResponseButton || badResponseButton || voicePlayButton) {
                console.log('[ChatGPT计时器] 检测到回复完成的DOM元素,停止计时');
                stopTimer();
            }
        });

        // 监测整个文档的变化
        observer.observe(document.body, {
            childList: true,
            subtree: true,
            attributes: true,
            attributeFilter: ['data-testid']
        });
    }

    // 添加键盘快捷键支持
    function setupKeyboardShortcuts() {
        document.addEventListener('keydown', (event) => {
            // Alt+S 开始计时,Alt+P 停止计时
            if (event.altKey) {
                if (event.key === 's' || event.key === 'S') {
                    startTimer();
                    event.preventDefault(); // 阻止默认行为
                } else if (event.key === 'p' || event.key === 'P') {
                    stopTimer();
                    event.preventDefault(); // 阻止默认行为
                }
            }
        });
    }

    // 初始化函数
    function initialize() {
        createTimerDisplay();          // 创建计时器显示
        setupNetworkMonitoring();      // 设置网络请求监控(使用Fetch API)
        setupXHRMonitoring();          // 设置XHR请求监控(备用方案)
        setupThinkingDetection();      // 设置"Thinking"文本检测
        setupDOMCompletionDetection(); // 设置DOM完成检测
        setupKeyboardShortcuts();      // 设置键盘快捷键

        console.log('[ChatGPT计时器] 初始化完成,等待使用...');
    }

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

    // 备用初始化方法,以防主方法失败
    setTimeout(() => {
        if (!document.getElementById('chatgpt-mini-timer')) {
            initialize();
        }
    }, 3000);
})();