Codex次数统计

纯净的Codex使用次数统计工具 - 感谢zetaloop佬的思路

// ==UserScript==
// @name         Codex次数统计
// @namespace    https://github.com/your-username/codex-counter
// @version      1.0.1
// @description  纯净的Codex使用次数统计工具 - 感谢zetaloop佬的思路
// @author       schweigen
// @license      MIT
// @homepage     https://linux.do/t/topic/576007/44
// @match        *://chatgpt.com/codex*
// @grant        none
// @run-at       document-start
// ==/UserScript==

(function () {
    "use strict";

    function createCodexDisplay() {
        if (!document.body) {
            requestAnimationFrame(createCodexDisplay);
            return;
        }

        if (document.getElementById("codex-counter")) return;

        // 创建显示框
        const displayBox = document.createElement("div");
        displayBox.id = "codex-counter";
        displayBox.style.position = "fixed";
        displayBox.style.top = "20px";
        displayBox.style.right = "20px";
        displayBox.style.width = "200px";
        displayBox.style.padding = "12px";
        displayBox.style.backgroundColor = "rgba(0, 0, 0, 0.85)";
        displayBox.style.color = "#fff";
        displayBox.style.fontSize = "13px";
        displayBox.style.borderRadius = "8px";
        displayBox.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.3)";
        displayBox.style.zIndex = "10000";
        displayBox.style.fontFamily = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif";
        displayBox.style.display = "none"; // 初始隐藏,有数据时显示

        displayBox.innerHTML = `
        <div style="margin-bottom: 12px; font-weight: 600; color: #C26FFD;">
            📊 Codex 使用统计
        </div>
        <div style="margin-bottom: 8px;">
            使用情况: <span id="codex-usage">--/--</span>
        </div>
        <div style="margin-bottom: 10px;">
            <div style="width: 100%; height: 8px; background: #333; border-radius: 4px; overflow: hidden;">
                <div id="codex-progress-bar" style="height: 100%; width: 0%; background: linear-gradient(90deg, #C26FFD, #A855F7); border-radius: 4px; transition: width 0.3s ease;"></div>
            </div>
        </div>
        <div style="font-size: 12px; color: #ccc;">
            重置倒计时: <span id="codex-reset-timer">--:--</span>
        </div>`;

        document.body.appendChild(displayBox);
    }

    // 初始化显示
    createCodexDisplay();

    // 使用 MutationObserver 确保元素不被删除
    const observer = new MutationObserver(() => {
        if (!document.getElementById("codex-counter")) {
            createCodexDisplay();
        }
    });

    function startObserver() {
        if (!document.body) {
            requestAnimationFrame(startObserver);
            return;
        }
        observer.observe(document.body, { childList: true, subtree: true });
    }
    startObserver();

    // Codex数据存储
    let codexData = {
        limit: null,
        used: null,
        remaining: null,
        resetTime: null
    };

    // 更新 Codex 显示
    function updateCodexDisplay(limit, remaining, resetsAfter) {
        const displayBox = document.getElementById("codex-counter");
        const usageElement = document.getElementById("codex-usage");
        const progressBar = document.getElementById("codex-progress-bar");

        if (!displayBox || !usageElement || !progressBar) return;

        if (typeof limit !== "number" || typeof remaining !== "number") {
            displayBox.style.display = "none";
            return;
        }

        // 更新数据
        codexData.limit = limit;
        codexData.remaining = remaining;
        codexData.used = limit - remaining;
        codexData.resetTime = Date.now() + (resetsAfter * 1000);

        // 显示框
        displayBox.style.display = "block";

        // 更新使用情况
        usageElement.textContent = `${codexData.used}/${codexData.limit}`;

        // 更新进度条
        const percentage = Math.max(0, Math.min(100, (codexData.used / codexData.limit) * 100));
        progressBar.style.width = `${percentage}%`;

        // 根据使用率调整颜色
        if (percentage >= 90) {
            progressBar.style.background = "linear-gradient(90deg, #F44336, #d32f2f)";
        } else if (percentage >= 70) {
            progressBar.style.background = "linear-gradient(90deg, #FFC107, #ffa000)";
        } else {
            progressBar.style.background = "linear-gradient(90deg, #C26FFD, #A855F7)";
        }

        console.log(`[Codex统计] 已用: ${codexData.used}/${codexData.limit}, 剩余: ${remaining}`);
    }

    // 倒计时更新
    function updateCountdown() {
        const timerElement = document.getElementById("codex-reset-timer");
        if (!timerElement || !codexData.resetTime) return;

        const remainingMs = Math.max(0, codexData.resetTime - Date.now());
        const minutes = Math.floor(remainingMs / 60000);
        const seconds = Math.floor((remainingMs % 60000) / 1000);

        timerElement.textContent = `${minutes}:${seconds.toString().padStart(2, "0")}`;

        // 如果时间到了,清除数据等待下次更新
        if (remainingMs <= 0) {
            codexData.resetTime = null;
        }
    }

    // 启动倒计时
    setInterval(updateCountdown, 1000);

    // 拦截 fetch 请求,只关注 Codex 相关 API
    const originalFetch = window.fetch;
    window.fetch = async function (resource, options = {}) {
        const response = await originalFetch(resource, options);

        const requestUrl = typeof resource === "string" ? resource : resource?.url || "";
        const requestMethod = typeof resource === "object" && resource.method
            ? resource.method
            : options?.method || "GET";

        // 只处理 Codex 额度查询 API
        if (
            requestUrl.includes("/backend-api/wham/tasks/rate_limit") &&
            requestMethod.toUpperCase() === "GET" &&
            response.ok
        ) {
            let responseBodyText;
            try {
                responseBodyText = await response.text();
                const data = JSON.parse(responseBodyText);

                // 只在 codex 页面显示
                if (location.pathname.startsWith("/codex")) {
                    updateCodexDisplay(
                        data.limit,
                        data.remaining + 1,
                        data.resets_after
                    );
                }

                // 返回新的 Response 对象
                return new Response(responseBodyText, {
                    status: response.status,
                    statusText: response.statusText,
                    headers: response.headers,
                });
            } catch (error) {
                console.error("[Codex统计] 处理响应出错:", error);

                if (typeof responseBodyText === "string") {
                    return new Response(responseBodyText, {
                        status: response.status,
                        statusText: response.statusText,
                        headers: response.headers,
                    });
                }
                return response;
            }
        }

        return response;
    };

    // 页面切换时隐藏/显示
    let currentPath = location.pathname;
    setInterval(() => {
        if (location.pathname !== currentPath) {
            currentPath = location.pathname;
            const displayBox = document.getElementById("codex-counter");

            if (displayBox) {
                if (currentPath.startsWith("/codex") && codexData.limit !== null) {
                    displayBox.style.display = "block";
                } else {
                    displayBox.style.display = "none";
                }
            }
        }
    }, 1000);

})();