您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
在 zetaloop 佬的思路基础上,简单包了个 UI(双击徽章切换至仅小圆点模式),详见 https://linux.do/t/topic/576007/44
// ==UserScript== // @name Codex次数统计 // @namespace https://github.com/your-username/codex-counter // @version 1.2.0 // @description 在 zetaloop 佬的思路基础上,简单包了个 UI(双击徽章切换至仅小圆点模式),详见 https://linux.do/t/topic/576007/44 // @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"; // ====== state ====== const LS_KEY_COLLAPSED = "codex_counter_collapsed"; let codexData = { limit: null, used: null, remaining: null, resetTime: null, // epoch ms }; let showResetOnHover = false; // ====== utils ====== const getCollapsed = () => { try { return localStorage.getItem(LS_KEY_COLLAPSED) === "1"; } catch { return false; } }; const setCollapsed = (v) => { try { localStorage.setItem(LS_KEY_COLLAPSED, v ? "1" : "0"); } catch {} }; const clamp = (n, a, b) => Math.max(a, Math.min(b, n)); const fmtMMSS = (ms) => { if (ms == null || ms <= 0) return "--:--"; const m = Math.floor(ms / 60000); const s = Math.floor((ms % 60000) / 1000); return `${m}:${String(s).padStart(2, "0")}`; }; // ====== UI ====== function createCodexDisplay() { if (!document.body) { requestAnimationFrame(createCodexDisplay); return; } if (document.getElementById("codex-counter")) return; const box = document.createElement("div"); box.id = "codex-counter"; // 超小徽章:一行、极少留白 Object.assign(box.style, { position: "fixed", top: "8px", right: "8px", padding: "2px 6px", background: "rgba(0,0,0,0.6)", color: "#fff", fontSize: "10px", lineHeight: "1", borderRadius: "9999px", zIndex: "2147483647", fontFamily: "-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Arial,sans-serif", display: "none", // 初始隐藏 userSelect: "none", WebkitFontSmoothing: "antialiased", backdropFilter: "blur(2px)", cursor: "default", }); const row = document.createElement("div"); Object.assign(row.style, { display: "inline-flex", alignItems: "center", gap: "4px", pointerEvents: "auto", }); const dot = document.createElement("span"); dot.id = "codex-dot"; Object.assign(dot.style, { width: "6px", height: "6px", borderRadius: "50%", display: "inline-block", background: "#6ee7b7", // 初始绿 flex: "0 0 auto", }); const usage = document.createElement("span"); usage.id = "codex-usage"; usage.textContent = "--/--"; usage.style.fontWeight = "600"; row.appendChild(dot); row.appendChild(usage); box.appendChild(row); // 双击切换“仅小圆点”超微型模式 box.addEventListener("dblclick", () => { const collapsed = !getCollapsed(); setCollapsed(collapsed); applyCollapsedState(box, collapsed); }); // 悬停时切换显示倒计时 box.addEventListener("mouseenter", () => { showResetOnHover = true; renderUsageText(); }); box.addEventListener("mouseleave", () => { showResetOnHover = false; renderUsageText(); }); document.body.appendChild(box); applyCollapsedState(box, getCollapsed()); } function applyCollapsedState(box, collapsed) { const usage = box.querySelector("#codex-usage"); const dot = box.querySelector("#codex-dot"); if (!usage || !dot) return; if (collapsed) { usage.style.display = "none"; box.style.padding = "0"; box.style.width = "10px"; box.style.height = "10px"; box.style.top = "10px"; box.style.right = "10px"; Object.assign(dot.style, { width: "10px", height: "10px" }); } else { usage.style.display = ""; box.style.padding = "2px 6px"; box.style.width = "auto"; box.style.height = "auto"; Object.assign(dot.style, { width: "6px", height: "6px" }); } } // 初始化显示 createCodexDisplay(); // 防止被删 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(); // ====== render / title ====== function renderUsageText() { const usageEl = document.getElementById("codex-usage"); if (!usageEl) return; if (typeof codexData.limit !== "number") { usageEl.textContent = "--/--"; return; } if (showResetOnHover) { const remainMs = Math.max(0, (codexData.resetTime || 0) - Date.now()); usageEl.textContent = `重置 ${fmtMMSS(remainMs)}`; } else { usageEl.textContent = `${codexData.used}/${codexData.limit}`; } } function updateBadge(limit, remaining, resetsAfterSeconds) { const box = document.getElementById("codex-counter"); const dot = document.getElementById("codex-dot"); if (!box || !dot) return; if (typeof limit !== "number" || typeof remaining !== "number") { box.style.display = "none"; return; } // 数据 codexData.limit = limit; codexData.remaining = remaining; codexData.used = clamp(limit - remaining, 0, limit); codexData.resetTime = Date.now() + (Number(resetsAfterSeconds) || 0) * 1000; // 显示 if (location.pathname.startsWith("/codex")) { box.style.display = "inline-block"; } else { box.style.display = "none"; } // 使用文本 renderUsageText(); // 状态点颜色:<70% 绿,70~90% 橙,>=90% 红 const pct = (codexData.used / (codexData.limit || 1)) * 100; if (pct >= 90) { dot.style.background = "#fca5a5"; // 红 } else if (pct >= 70) { dot.style.background = "#fde68a"; // 橙 } else { dot.style.background = "#6ee7b7"; // 绿 } // 原生 tooltip(悬停即见详细信息) updateTooltipTitle(); } function updateTooltipTitle() { const box = document.getElementById("codex-counter"); const usageEl = document.getElementById("codex-usage"); const dot = document.getElementById("codex-dot"); if (!box) return; const targets = [box, usageEl, dot].filter(Boolean); if (codexData.limit == null) { targets.forEach((el) => el.removeAttribute("title")); return; } const remainMs = Math.max(0, (codexData.resetTime || 0) - Date.now()); const lines = [ "Codex 使用统计", `使用:${codexData.used}/${codexData.limit}`, `剩余:${codexData.remaining}`, `重置倒计时:${fmtMMSS(remainMs)}` ]; const tooltip = lines.join("\n"); targets.forEach((el) => { el.title = tooltip; }); } // 每秒刷新 tooltip 的倒计时,同时在悬停时刷新显示 setInterval(() => { updateTooltipTitle(); if (showResetOnHover) { renderUsageText(); } }, 1000); // ====== fetch 拦截 ====== const originalFetch = window.fetch; window.fetch = async function (resource, options = {}) { const res = await originalFetch(resource, options); try { const url = typeof resource === "string" ? resource : resource?.url || ""; const method = (typeof resource === "object" && resource?.method) ? resource.method : (options?.method || "GET"); if ( url.includes("/backend-api/wham/tasks/rate_limit") && String(method).toUpperCase() === "GET" && res.ok ) { // 读取文本并克隆响应 const text = await res.text(); // 尝试解析 try { const data = JSON.parse(text); if (location.pathname.startsWith("/codex")) { updateBadge( data.limit, (data.remaining ?? 0) + 1, // 与原脚本一致 data.resets_after ); } } catch (e) { // 忽略解析错误,原样返回 } return new Response(text, { status: res.status, statusText: res.statusText, headers: res.headers, }); } } catch (e) { // 静默失败,继续返回原响应 } return res; }; // ====== 路由切换时隐显 ====== let currentPath = location.pathname; setInterval(() => { if (location.pathname !== currentPath) { currentPath = location.pathname; const box = document.getElementById("codex-counter"); if (box) { if (currentPath.startsWith("/codex") && codexData.limit !== null) { box.style.display = "inline-block"; } else { box.style.display = "none"; } } } }, 1000); })();