一个自用可以网页查看额度重置信息和执行重置的脚本,分享大家用
nobiyou
2026-07-02 16:59
1
原由其实很简单,自己的team账号要过二验,没有办法只能用令牌登录,但令牌登录无法查看额度重置信息,也不能重置,登录codex是很方便的,但是我无法登录。就写了这个脚本,网页登录,通过脚本来查看额度信息和执行重置额度。
这是昨天的额度重置信息:

这是今早执行额度重置后的信息:

一个查看和执行额度重置的脚本,方便自己用的,分享出来,有需要自行修改使用。
脚本代码:
// ==UserScript==
// @name ChatGPT Rate Limit Reset Credits
// @namespace https://chatgpt.com/
// @version 3.0
// @description 查询可用重置额度并在有 reset credit 时直接执行重置
// @author nobiyou
// @match https://chatgpt.com/*
// @run-at document-idle
// @grant none
// ==/UserScript==
(function () {
"use strict";
const SESSION_URL = "/api/auth/session";
const WHAM_USAGE_URL = "/backend-api/wham/usage";
const WHAM_CREDITS_URL = "/backend-api/wham/rate-limit-reset-credits";
const WHAM_CONSUME_URL = "/backend-api/wham/rate-limit-reset-credits/consume";
let panel = null;
let contentDiv = null;
let launcherBtn = null;
let countdownTimer = null;
let accessToken = "";
let chatgptAccountId = "";
let isFedrampCompliantWorkspace = false;
let isCollapsed = true;
let isConsumingReset = false;
let latestDashboard = null;
let actionNotice = null;
function normalizeNumber(value, fallback = 0) {
if (typeof value === "number" && Number.isFinite(value)) return value;
if (typeof value === "string" && value.trim() !== "") {
const parsed = Number(value);
if (Number.isFinite(parsed)) return parsed;
}
return fallback;
}
function parseApiDate(value) {
if (value === null || value === undefined || value === "") return null;
const date = typeof value === "string"
? new Date(value)
: new Date(value > 1e12 ? value : value * 1000);
if (Number.isNaN(date.getTime())) return null;
return date;
}
function getTimezoneLabel(date = new Date()) {
const offset = -date.getTimezoneOffset();
const sign = offset >= 0 ? "+" : "-";
const abs = Math.abs(offset);
const hours = String(Math.floor(abs / 60)).padStart(2, "0");
const minutes = String(abs % 60).padStart(2, "0");
return `UTC${sign}${hours}:${minutes}`;
}
function getDateDisplayData(value) {
const date = parseApiDate(value);
if (!date) {
return { dateText: "未知", timeText: "--:--:--" };
}
const dateText = date.toLocaleDateString("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit"
});
const timeText = date.toLocaleTimeString("zh-CN", {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false
});
return { dateText, timeText };
}
function getRemainingTimeData(expiresAt) {
const expireDate = parseApiDate(expiresAt);
if (!expireDate) {
return {
label: "剩余",
dayText: "--天",
clockText: "--时 --分 --秒",
labelColor: "#64748b",
textColor: "#475569"
};
}
const diff = expireDate.getTime() - Date.now();
if (diff <= 0) {
return {
label: "剩余",
dayText: "0天",
clockText: "00时 00分 00秒",
labelColor: "#b91c1c",
textColor: "#dc2626"
};
}
const totalSeconds = Math.floor(diff / 1000);
const days = Math.floor(totalSeconds / 86400);
const hours = Math.floor((totalSeconds % 86400) / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
return {
label: "剩余",
dayText: `${days}天`,
clockText: `${String(hours).padStart(2, "0")}时 ${String(minutes).padStart(2, "0")}分 ${String(seconds).padStart(2, "0")}秒`,
labelColor: "#166534",
textColor: "#15803d"
};
}
function getRemainingTime(expiresAt) {
const info = getRemainingTimeData(expiresAt);
const safeExpiresAt = expiresAt ? String(expiresAt).replace(/'/g, "'") : "";
return `
<div class="rl-remaining-box" data-expires-at='${safeExpiresAt}'>
<div class="rl-remaining-label" style="font-size:11px;color:${info.labelColor};letter-spacing:0.04em">${info.label}</div>
<div class="rl-remaining-days" style="margin-top:6px;font-size:18px;font-weight:700;color:${info.textColor};line-height:1.15">${info.dayText}</div>
<div class="rl-remaining-clock" style="margin-top:4px;font-size:13px;font-weight:600;color:${info.textColor};line-height:1.25;white-space:nowrap">${info.clockText}</div>
</div>`;
}
function updateCountdowns() {
if (!contentDiv) return;
contentDiv.querySelectorAll(".rl-remaining-box").forEach((box) => {
const expiresAt = box.getAttribute("data-expires-at");
const info = getRemainingTimeData(expiresAt);
const label = box.querySelector(".rl-remaining-label");
const days = box.querySelector(".rl-remaining-days");
const clock = box.querySelector(".rl-remaining-clock");
if (label) {
label.textContent = info.label;
label.style.color = info.labelColor;
}
if (days) {
days.textContent = info.dayText;
days.style.color = info.textColor;
}
if (clock) {
clock.textContent = info.clockText;
clock.style.color = info.textColor;
}
});
}
function startCountdownTimer() {
if (countdownTimer) clearInterval(countdownTimer);
countdownTimer = window.setInterval(updateCountdowns, 1000);
updateCountdowns();
}
function stopCountdownTimer() {
if (countdownTimer) {
clearInterval(countdownTimer);
countdownTimer = null;
}
}
function localizeCreditTitle(title) {
if (!title) return "可用重置额度";
let text = String(title).trim();
const replacements = [
[/Full reset/gi, "完整重置"],
[/Weekly/gi, "每周"],
[/Daily/gi, "每日"],
[/Monthly/gi, "每月"],
[/Credits?/gi, "额度"],
[/Bonus/gi, "奖励"],
[/Reset/gi, "重置"],
[/Hours?/gi, "小时"],
[/\bhr\b/gi, "小时"]
];
replacements.forEach(([pattern, value]) => {
text = text.replace(pattern, value);
});
return text
.replace(/\(([^)]+)\)/g, "($1)")
.replace(/\s*\+\s*/g, " + ")
.replace(/\s{2,}/g, " ")
.replace(/(\d+)\s*小时/gi, "$1小时")
.trim();
}
function decodeJwtPayload(token) {
try {
const parts = String(token || "").split(".");
if (parts.length < 2) return null;
const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
const padded = base64 + "=".repeat((4 - (base64.length % 4 || 4)) % 4);
const json = decodeURIComponent(
atob(padded)
.split("")
.map((ch) => `%${ch.charCodeAt(0).toString(16).padStart(2, "0")}`)
.join("")
);
return JSON.parse(json);
} catch (error) {
return null;
}
}
function deriveAccountIdFromToken(token) {
const payload = decodeJwtPayload(token);
if (!payload || typeof payload !== "object") return "";
const auth = payload["https://api.openai.com/auth"];
if (auth && typeof auth === "object") {
return String(auth.chatgpt_account_id || auth.chatgptAccountId || "").trim();
}
return "";
}
async function getSession() {
const res = await fetch(SESSION_URL, {
credentials: "include",
cache: "no-store"
});
if (!res.ok) {
throw new Error(`读取会话失败: HTTP ${res.status}`);
}
const data = await res.json();
accessToken = String(data.accessToken || data.access_token || "").trim();
chatgptAccountId = String(data?.account?.id || "").trim() || deriveAccountIdFromToken(accessToken);
isFedrampCompliantWorkspace = Boolean(data?.account?.isFedrampCompliantWorkspace);
return {
accessToken,
chatgptAccountId,
isFedrampCompliantWorkspace
};
}
async function getAccessToken() {
try {
const session = await getSession();
return session.accessToken;
} catch (error) {
accessToken = "";
chatgptAccountId = "";
isFedrampCompliantWorkspace = false;
return "";
}
}
function buildWhamHeaders({ json = false } = {}) {
if (!accessToken) {
throw new Error("当前页面会话中未找到 access token");
}
const headers = {
accept: "application/json",
authorization: `Bearer ${accessToken}`,
"openai-beta": "codex-1",
"oai-language": "zh-CN",
originator: "Codex Desktop"
};
if (chatgptAccountId) {
headers["chatgpt-account-id"] = chatgptAccountId;
}
if (json) {
headers["content-type"] = "application/json";
}
if (isFedrampCompliantWorkspace) {
headers["x-openai-fedramp"] = "true";
}
return headers;
}
async function parseResponseBody(response) {
const text = await response.text();
if (!text) return null;
try {
return JSON.parse(text);
} catch (error) {
return text;
}
}
function summarizeErrorBody(body) {
if (!body) return "";
if (typeof body === "string") {
return body.slice(0, 240);
}
const parts = [];
if (body.code) parts.push(String(body.code));
if (body.error) parts.push(typeof body.error === "string" ? body.error : JSON.stringify(body.error));
if (body.message) parts.push(String(body.message));
if (body.detail) parts.push(String(body.detail));
if (!parts.length) {
try {
return JSON.stringify(body).slice(0, 240);
} catch (error) {
return "";
}
}
return parts.join(" | ").slice(0, 240);
}
async function callBackendApi(path, options = {}, allowRetry = true) {
if (!accessToken) {
await getSession();
}
const init = {
method: options.method || "GET",
credentials: "include",
cache: "no-store",
headers: {
...buildWhamHeaders({ json: options.json === true }),
...(options.headers || {})
}
};
if (options.body !== undefined) {
init.body = options.json === true ? JSON.stringify(options.body) : options.body;
}
const response = await fetch(path, init);
if (response.status === 401 && allowRetry) {
await getSession();
return callBackendApi(path, options, false);
}
const payload = await parseResponseBody(response);
if (!response.ok) {
const error = new Error(summarizeErrorBody(payload) || `HTTP ${response.status}`);
error.status = response.status;
error.payload = payload;
throw error;
}
return payload;
}
function firstNonEmptyArray(...lists) {
for (const list of lists) {
if (Array.isArray(list) && list.length > 0) return list;
}
return [];
}
function normalizeCreditItem(raw) {
if (!raw || typeof raw !== "object") return null;
const expiresAt = String(raw.expires_at || raw.expiresAt || "").trim();
const grantedAt = String(raw.granted_at || raw.grantedAt || "").trim();
const title = String(raw.title || raw.name || raw.reset_type || raw.resetType || "").trim();
const status = String(raw.status || "").trim();
const redeemedAt = String(raw.redeemed_at || raw.redeemedAt || "").trim();
if (!expiresAt && !grantedAt && !title && !status && !redeemedAt) return null;
return {
title,
status,
granted_at: grantedAt,
expires_at: expiresAt,
redeemed_at: redeemedAt
};
}
function parseCreditsPayload(payload) {
if (!payload) return [];
const items = Array.isArray(payload)
? payload
: firstNonEmptyArray(
payload.credits,
payload.rate_limit_reset_credits,
payload.rateLimitResetCredits,
payload.items,
payload.data
);
return items.map(normalizeCreditItem).filter(Boolean);
}
function buildDashboard(usagePayload, creditsPayload, rateLimitsError) {
const usageCredits = usagePayload?.rate_limit_reset_credits || usagePayload?.rateLimitResetCredits || null;
const credits = parseCreditsPayload(creditsPayload);
const availableResetCount = normalizeNumber(
usageCredits?.available_count ?? usageCredits?.availableCount,
credits.length
);
const availableCreditCount = credits.length || availableResetCount;
return {
usagePayload,
credits,
availableResetCount,
availableCreditCount,
rateLimitsError
};
}
function updateLauncher(value) {
if (!launcherBtn) return;
const badge = launcherBtn.querySelector("#rl-launcher-badge");
if (!badge) return;
badge.textContent = String(value);
}
function renderActionNotice() {
if (!actionNotice) return "";
const palette = {
success: {
background: "#ecfdf5",
border: "#86efac",
color: "#166534"
},
warning: {
background: "#fffbeb",
border: "#fcd34d",
color: "#92400e"
},
error: {
background: "#fef2f2",
border: "#fca5a5",
color: "#b91c1c"
},
info: {
background: "#eff6ff",
border: "#93c5fd",
color: "#1d4ed8"
}
};
const tone = palette[actionNotice.type] || palette.info;
return `
<div style="margin-bottom:10px;padding:10px 12px;border:1px solid ${tone.border};background:${tone.background};color:${tone.color};border-radius:12px;font-size:12px;line-height:1.5;font-weight:600">
${actionNotice.message}
</div>`;
}
function setActionNotice(type, message) {
actionNotice = { type, message };
}
function clearActionNotice() {
actionNotice = null;
}
function setCollapsed(nextCollapsed) {
isCollapsed = nextCollapsed;
if (panel) {
if (isCollapsed) {
panel.style.opacity = "0";
panel.style.transform = "translateY(-6px) scale(0.985)";
panel.style.pointerEvents = "none";
setTimeout(() => {
if (isCollapsed && panel) panel.style.display = "none";
}, 160);
} else {
panel.style.display = "block";
panel.style.pointerEvents = "auto";
requestAnimationFrame(() => {
panel.style.opacity = "1";
panel.style.transform = "translateY(0) scale(1)";
});
updateCountdowns();
}
}
if (launcherBtn) launcherBtn.style.display = isCollapsed ? "flex" : "none";
}
function showError(message) {
updateLauncher("!");
if (!contentDiv) return;
contentDiv.innerHTML = `
<div style="padding:18px;border:1px solid #fecaca;background:#fef2f2;border-radius:14px;color:#b91c1c;text-align:center;font-weight:600;line-height:1.6">
${message}
</div>`;
}
function extractErrorMessage(error) {
if (!error) return "请求失败";
const parts = [];
if (error.status) parts.push(`HTTP ${error.status}`);
if (error.message) parts.push(String(error.message));
return parts.join(" - ") || "请求失败";
}
async function fetchRateLimit() {
clearActionNotice();
try {
if (!accessToken) {
await getSession();
}
const [usageResult, creditsResult] = await Promise.allSettled([
callBackendApi(WHAM_USAGE_URL),
callBackendApi(WHAM_CREDITS_URL)
]);
if (usageResult.status !== "fulfilled") {
throw usageResult.reason;
}
const rateLimitsError = creditsResult.status === "rejected"
? {
status: creditsResult.reason?.status || "请求失败",
message: extractErrorMessage(creditsResult.reason)
}
: null;
latestDashboard = buildDashboard(
usageResult.value,
creditsResult.status === "fulfilled" ? creditsResult.value : null,
rateLimitsError
);
updateLauncher(latestDashboard.availableResetCount);
showResult(latestDashboard);
} catch (error) {
latestDashboard = null;
showError(`请求失败,请确保已登录并具备 Codex 重置额度访问权限。<br><span style="font-size:12px;font-weight:500">${extractErrorMessage(error)}</span>`);
}
}
function makeRedeemRequestId() {
if (window.crypto?.randomUUID) {
return window.crypto.randomUUID();
}
const bytes = new Uint8Array(16);
window.crypto.getRandomValues(bytes);
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
}
async function consumeRateLimitReset() {
const availableResetCount = normalizeNumber(latestDashboard?.availableResetCount, 0);
if (isConsumingReset || availableResetCount <= 0) return;
const confirmed = window.confirm(`这会消耗 1 个 reset credit,当前剩余 ${availableResetCount} 个。是否继续?`);
if (!confirmed) return;
isConsumingReset = true;
setActionNotice("info", "正在提交重置请求...");
if (latestDashboard) {
showResult(latestDashboard);
}
try {
const result = await callBackendApi(WHAM_CONSUME_URL, {
method: "POST",
json: true,
body: {
redeem_request_id: makeRedeemRequestId()
}
});
const code = String(result?.code || "").trim();
if (code === "noCredit") {
setActionNotice("warning", "当前没有可用 reset credit。");
} else if (code === "nothingToReset") {
setActionNotice("warning", "当前窗口没有需要立即恢复的额度。");
} else if (code === "alreadyRedeemed") {
setActionNotice("info", "本次重置请求已被上游判定为重复提交。");
} else {
const windowsReset = normalizeNumber(result?.windows_reset ?? result?.windowsReset, 0);
setActionNotice("success", windowsReset > 0
? `重置成功,已恢复 ${windowsReset} 个额度窗口。`
: "重置请求已提交成功。");
}
await fetchRateLimit();
if (actionNotice && latestDashboard) {
showResult(latestDashboard);
}
} catch (error) {
setActionNotice("error", `重置失败:${extractErrorMessage(error)}`);
if (latestDashboard) {
showResult(latestDashboard);
} else {
showError(`重置失败:${extractErrorMessage(error)}`);
}
} finally {
isConsumingReset = false;
if (latestDashboard) {
showResult(latestDashboard);
}
}
}
function showResult(dashboard) {
if (!contentDiv) return;
const availableResetCount = normalizeNumber(dashboard?.availableResetCount, 0);
const availableCreditCount = normalizeNumber(dashboard?.availableCreditCount, 0);
const creditCount = Array.isArray(dashboard?.credits) ? dashboard.credits.length : 0;
const rateLimitsError = dashboard?.rateLimitsError || null;
const canConsume = availableResetCount > 0 && !isConsumingReset;
const consumeText = availableResetCount > 0 ? (isConsumingReset ? "重置中" : "执行重置") : "暂无重置";
const resetSourceNote = rateLimitsError
? `单独重置数接口暂不可用(${rateLimitsError.status || "未知错误"}),当前回退为 usage 可用数`
: "可用重置数来自当前账户 usage 接口";
let html = `
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px;margin-bottom:9px">
<div>
<h2 style="margin:0;color:#0f172a;font-size:17px;letter-spacing:0.01em">额度重置信息</h2>
<div style="margin-top:3px;color:#64748b;font-size:11px">本地时区:${getTimezoneLabel()}</div>
</div>
<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap;justify-content:flex-end">
<button id="collapseBtn" style="padding:6px 10px;background:#ffffff;color:#475569;border:1px solid #cbd5e1;border-radius:10px;cursor:pointer;font-size:12px;font-weight:600">收起</button>
<button id="consumeBtn" data-disabled="${canConsume ? "false" : "true"}" style="padding:6px 11px;background:linear-gradient(135deg,#16a34a,#15803d);color:white;border:none;border-radius:10px;cursor:${canConsume ? "pointer" : "not-allowed"};font-size:12px;font-weight:700;box-shadow:0 8px 18px rgba(21,128,61,0.16);opacity:${canConsume ? "1" : "0.55"};transition:opacity .18s ease">${consumeText}</button>
<button id="refreshBtn" style="padding:6px 11px;background:linear-gradient(135deg,#2563eb,#1d4ed8);color:white;border:none;border-radius:10px;cursor:pointer;font-size:12px;font-weight:700;box-shadow:0 8px 18px rgba(37,99,235,0.16);transition:opacity .18s ease">刷新</button>
</div>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;background:linear-gradient(135deg,#f8fbff,#f0fdf4);padding:10px 12px;border-radius:13px;margin-bottom:10px;font-size:14px;border:1px solid #dbeafe;box-shadow:inset 0 1px 0 rgba(255,255,255,0.85)">
<div>
<div style="color:#64748b;font-size:11px;letter-spacing:0.04em">当前可用</div>
<div style="margin-top:2px;color:#0f172a;font-weight:700">重置额度</div>
<div style="margin-top:4px;color:#64748b;font-size:11px;line-height:1.35">可用 credit ${availableCreditCount} · 记录 ${creditCount}</div>
<div style="margin-top:4px;color:${rateLimitsError ? "#b45309" : "#64748b"};font-size:10.5px;line-height:1.4">${resetSourceNote}</div>
</div>
<div style="text-align:right">
<div style="font-size:25px;font-weight:800;color:#15803d;letter-spacing:-0.02em">${availableResetCount}</div>
<div style="margin-top:2px;color:#64748b;font-size:11px">可用重置数</div>
</div>
</div>
${renderActionNotice()}`;
if (Array.isArray(dashboard?.credits) && dashboard.credits.length > 0) {
html += `<div style="font-weight:600;margin:6px 0 5px;color:#5b7bb2;font-size:11px;letter-spacing:0.06em">额度列表</div>`;
dashboard.credits.forEach((credit, index) => {
const grantedDisplay = getDateDisplayData(credit.granted_at);
const expiresDisplay = getDateDisplayData(credit.expires_at);
html += `
<div style="border:1px solid #dbeafe;background:linear-gradient(180deg,#ffffff,#f8fbff);padding:10px;border-radius:15px;margin-bottom:9px;box-shadow:0 10px 22px rgba(15,23,42,0.045)">
<div style="display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:7px">
<div style="font-weight:700;color:#0f172a;font-size:13px;letter-spacing:0.01em">#${index + 1} ${localizeCreditTitle(credit.title)}</div>
<span style="color:#15803d;font-weight:700;background:#dcfce7;padding:3px 8px;border-radius:999px;font-size:10px;letter-spacing:0.04em">${credit.status || "可用"}</span>
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(142px,1fr));gap:7px;align-items:stretch">
<div style="min-height:84px;padding:8px 10px;border:1px solid #e2e8f0;border-radius:11px;background:#f8fafc;display:flex;flex-direction:column;justify-content:flex-start">
<div style="font-size:11px;color:#64748b;letter-spacing:0.04em">获得时间</div>
<div style="margin-top:8px;color:#0f172a;font-size:13px;font-weight:700;line-height:1.15;white-space:nowrap">${grantedDisplay.dateText}</div>
<div style="margin-top:6px;color:#334155;font-size:12px;font-weight:600;line-height:1.15;white-space:nowrap">${grantedDisplay.timeText}</div>
</div>
<div style="min-height:84px;padding:8px 10px;border:1px solid #e2e8f0;border-radius:11px;background:#f8fafc;display:flex;flex-direction:column;justify-content:flex-start">
<div style="font-size:11px;color:#64748b;letter-spacing:0.04em">过期时间</div>
<div style="margin-top:8px;color:#0f172a;font-size:13px;font-weight:700;line-height:1.15;white-space:nowrap">${expiresDisplay.dateText}</div>
<div style="margin-top:6px;color:#334155;font-size:12px;font-weight:600;line-height:1.15;white-space:nowrap">${expiresDisplay.timeText}</div>
</div>
<div style="min-height:84px;padding:8px 10px;border-radius:11px;background:linear-gradient(180deg,#f9fffb,#eefcf4);border:1px solid #c7f3d7;box-shadow:inset 0 1px 0 rgba(255,255,255,0.9);display:flex;flex-direction:column;justify-content:flex-start">
${getRemainingTime(credit.expires_at)}
</div>
</div>
</div>`;
});
} else {
html += `<p style="text-align:center;color:#64748b;padding:26px 16px;border:1px dashed #cbd5e1;border-radius:12px;background:#f8fafc">暂无可用额度</p>`;
}
contentDiv.innerHTML = html;
contentDiv.querySelector("#refreshBtn")?.addEventListener("click", fetchRateLimit);
contentDiv.querySelector("#collapseBtn")?.addEventListener("click", () => setCollapsed(true));
contentDiv.querySelector("#consumeBtn")?.addEventListener("click", consumeRateLimitReset);
startCountdownTimer();
}
function createLauncher() {
launcherBtn = document.createElement("button");
launcherBtn.type = "button";
launcherBtn.title = "查看额度详情";
launcherBtn.style.cssText = `
position:fixed; top:70px; right:20px; width:52px; height:52px; display:flex;
align-items:center; justify-content:center; border:none; border-radius:16px;
background:linear-gradient(135deg,#2563eb,#1d4ed8); box-shadow:0 16px 30px rgba(37,99,235,0.28);
z-index:100000; cursor:pointer; padding:0; box-sizing:border-box; transition:transform .18s ease, box-shadow .18s ease;
`;
launcherBtn.innerHTML = `
<div style="position:relative;width:20px;height:20px;border:2px solid rgba(255,255,255,0.96);border-radius:999px;box-sizing:border-box">
<span style="position:absolute;left:8px;top:3px;width:2px;height:6px;background:#ffffff;border-radius:999px"></span>
<span style="position:absolute;left:8px;top:8px;width:6px;height:2px;background:#ffffff;border-radius:999px"></span>
</div>
<span id="rl-launcher-badge" style="position:absolute;top:-5px;right:-5px;min-width:24px;height:24px;padding:0 6px;border-radius:999px;background:#dcfce7;color:#166534;font-size:11px;font-weight:800;display:flex;align-items:center;justify-content:center;border:2px solid #ffffff;box-sizing:border-box;line-height:1">0</span>
`;
launcherBtn.addEventListener("mouseenter", () => {
launcherBtn.style.transform = "translateY(-1px) scale(1.03)";
launcherBtn.style.boxShadow = "0 18px 34px rgba(37,99,235,0.34)";
});
launcherBtn.addEventListener("mouseleave", () => {
launcherBtn.style.transform = "translateY(0) scale(1)";
launcherBtn.style.boxShadow = "0 16px 30px rgba(37,99,235,0.28)";
});
launcherBtn.addEventListener("click", () => setCollapsed(false));
document.body.appendChild(launcherBtn);
}
function createPanel() {
createLauncher();
panel = document.createElement("div");
panel.style.cssText = `
position:fixed; top:70px; right:20px; width:min(500px, calc(100vw - 20px));
background:linear-gradient(180deg,rgba(255,255,255,0.98),rgba(248,250,252,0.98));
border:1px solid #bfdbfe; border-radius:18px; box-shadow:0 20px 54px rgba(15,23,42,0.16);
backdrop-filter:blur(10px); z-index:99999; padding:14px; box-sizing:border-box;
font-family:"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif; font-size:14px;
color:#1f2937; max-height:85vh; overflow:auto; display:none; opacity:0;
transform:translateY(-6px) scale(0.985); pointer-events:none;
transition:opacity .16s ease, transform .16s ease;
`;
panel.innerHTML = `
<div id="rl-content">
<div style="padding:16px;border:1px solid #dbeafe;background:#f8fbff;border-radius:14px;color:#475569;text-align:center;font-weight:600">
正在加载额度信息...
</div>
</div>`;
document.body.appendChild(panel);
contentDiv = panel.querySelector("#rl-content");
setCollapsed(true);
setTimeout(() => getAccessToken().then(() => fetchRateLimit()), 800);
}
document.addEventListener("pointerdown", (event) => {
if (isCollapsed || !panel || !launcherBtn) return;
const target = event.target;
if (panel.contains(target) || launcherBtn.contains(target)) return;
setCollapsed(true);
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape" && !isCollapsed) setCollapsed(true);
});
window.addEventListener("beforeunload", stopCountdownTimer);
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", createPanel);
} else {
createPanel();
}
})();
最新回复 (3)
-
Shively 07-02 17:021楼好东西,用上了很方便,谢谢佬分享
-
sanhua1 07-03 11:242楼感谢佬友 对不用app的太方便了
-
nobiyou 楼主 07-03 14:243楼主要是二验,没办法登录,没有办法重置额度,用codex登录重置额度也麻烦,在网页就能操作,还是很方便的。
* 帖子来源Linux.do
附近帖子
- ↑自建sub2api感觉找到vps答案了~ vmissJP.TKY.TRI.Basic-1c1g100Mbps400GB
- ↑土区订阅gpt plus失败
- ↑原来PayPal绑银行账户还能充钱验证
- ↑L站搜索不能用了
- ↑【CHY公益站】上架longcat2.0
- 📍 一个自用可以网页查看额度重置信息和执行重置的脚本,分享大家用
- ↓chatgpt2api生图求解
- ↓【富可敌国】【程序员隐身 AI 面试工具】GhostInterview 反馈赠送 pro 名额还剩不少
- ↓求可以开发票的有codex套餐的中转站
- ↓暑假放得早好无聊,想做点小东西 求点需求/idea,给泥免费打工...
- ↓【V-API】公益站正式上线,GLM/Deepseek....太热情了6核6G小鸡压力大-【15:00在送第三波X100个】
飞读
nobiyou
|
主题数 1 |
帖子数 1 |
注册排名 3 |