// ==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);
})();