
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Codex 周额度</title>
<style>
:root {
color-scheme: light dark;
font-family: "Microsoft YaHei", sans-serif;
--surface: rgba(248, 249, 251, 0.96);
--surface-strong: rgba(255, 255, 255, 0.78);
--surface-soft: rgba(224, 227, 232, 0.72);
--text: #272a2f;
--muted: #777c84;
--line: rgba(28, 33, 40, 0.11);
--accent: #58a4b1;
--accent-hover: #4b929e;
--accent-soft: rgba(88, 164, 177, 0.15);
--success: #3c9568;
--success-soft: rgba(60, 149, 104, 0.14);
--warning: #d69235;
--warning-soft: rgba(214, 146, 53, 0.15);
--danger: #d75258;
--danger-soft: rgba(215, 82, 88, 0.12);
--button-hover: rgba(32, 37, 44, 0.07);
--button-active: rgba(32, 37, 44, 0.13);
--shadow: 0 10px 30px rgba(20, 28, 38, 0.13);
}
@media (prefers-color-scheme: dark) {
:root {
--surface: rgba(32, 36, 41, 0.96);
--surface-strong: rgba(48, 52, 58, 0.82);
--surface-soft: rgba(59, 63, 70, 0.78);
--text: #f1f2f4;
--muted: #adb1b8;
--line: rgba(255, 255, 255, 0.1);
--accent: #68b5c1;
--accent-hover: #77c2ce;
--accent-soft: rgba(104, 181, 193, 0.16);
--success: #6fc697;
--success-soft: rgba(111, 198, 151, 0.14);
--warning-soft: rgba(214, 146, 53, 0.16);
--danger-soft: rgba(215, 82, 88, 0.15);
--button-hover: rgba(255, 255, 255, 0.07);
--button-active: rgba(255, 255, 255, 0.13);
--shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
}
}
* {
box-sizing: border-box;
}
html,
body {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
background: transparent;
}
body,
button,
input {
font-family: "Microsoft YaHei", sans-serif;
}
button {
color: inherit;
}
[hidden] {
display: none !important;
}
.widget {
width: 100%;
height: 100%;
min-width: 250px;
min-height: 120px;
display: flex;
flex-direction: column;
overflow: hidden;
border-radius: 14px;
background: var(--surface);
color: var(--text);
box-shadow: var(--shadow);
backdrop-filter: blur(18px);
}
.header {
min-height: 44px;
flex: 0 0 auto;
padding: 8px 9px 5px 13px;
display: flex;
align-items: center;
gap: 8px;
}
.heading {
min-width: 0;
flex: 1;
}
.title-row {
min-width: 0;
display: flex;
align-items: center;
gap: 6px;
}
.title {
flex: 0 1 auto;
overflow: hidden;
font-size: 15px;
font-weight: 700;
line-height: 20px;
text-overflow: ellipsis;
white-space: nowrap;
}
.badge {
max-width: 104px;
min-width: 0;
flex: 0 1 auto;
padding: 2px 6px;
overflow: hidden;
border-radius: 5px;
font-size: 10px;
font-weight: 700;
line-height: 15px;
text-overflow: ellipsis;
white-space: nowrap;
}
.plan-badge {
background: var(--accent-soft);
color: var(--accent);
}
.status-badge.normal {
background: var(--success-soft);
color: var(--success);
}
.status-badge.warning {
background: var(--warning-soft);
color: var(--warning);
}
.status-badge.danger {
background: var(--danger-soft);
color: var(--danger);
}
.updated {
min-height: 14px;
margin-top: 1px;
overflow: hidden;
color: var(--muted);
font-size: 10px;
line-height: 14px;
text-overflow: ellipsis;
white-space: nowrap;
}
.actions {
flex: 0 0 auto;
display: flex;
align-items: center;
gap: 1px;
}
.icon-button {
width: 29px;
height: 29px;
padding: 6px;
display: inline-flex;
align-items: center;
justify-content: center;
border: 0;
border-radius: 8px;
background: transparent;
cursor: pointer;
transition: background-color 120ms ease, transform 80ms ease, opacity 120ms ease;
}
.icon-button:hover:not(:disabled) {
background: var(--button-hover);
}
.icon-button:active:not(:disabled) {
background: var(--button-active);
transform: scale(0.94);
}
.icon-button:focus-visible,
.text-button:focus-visible,
.switch input:focus-visible + .switch-track,
summary:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.icon-button:disabled,
.text-button:disabled {
cursor: default;
opacity: 0.42;
}
.icon-button svg {
width: 17px;
height: 17px;
fill: none;
stroke: currentColor;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 1.8;
}
.icon-button.spinning svg {
animation: spin 850ms linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.content {
min-height: 0;
flex: 1;
padding: 2px 13px 10px;
overflow-x: hidden;
overflow-y: auto;
scrollbar-width: thin;
}
.quota {
height: 100%;
min-height: 86px;
display: grid;
grid-template-columns: minmax(104px, auto) minmax(0, 1fr);
grid-template-rows: auto auto;
column-gap: 14px;
row-gap: 7px;
align-content: center;
}
.remaining {
grid-row: 1 / 3;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
white-space: nowrap;
}
.remaining-value {
display: flex;
align-items: baseline;
gap: 3px;
}
.remaining-number {
font-size: clamp(31px, 8vw, 39px);
font-weight: 760;
letter-spacing: -1.3px;
line-height: 1;
}
.remaining-unit {
color: var(--muted);
font-size: 12px;
}
.used {
margin-top: 4px;
color: var(--muted);
font-size: 10px;
line-height: 14px;
}
.quota-main {
min-width: 0;
align-self: end;
}
.quota-meta {
min-width: 0;
margin-bottom: 6px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
font-size: 11px;
line-height: 15px;
}
.quota-label {
flex: 0 0 auto;
font-weight: 700;
}
.reset-countdown {
min-width: 0;
overflow: hidden;
color: var(--muted);
font-weight: 600;
text-align: right;
text-overflow: ellipsis;
white-space: nowrap;
}
.progress {
height: 9px;
overflow: hidden;
border-radius: 999px;
background: var(--surface-soft);
}
.progress-fill {
width: 0;
height: 100%;
border-radius: inherit;
background: var(--accent);
transition: width 260ms ease, background-color 160ms ease;
}
.progress-fill.warning {
background: var(--warning);
}
.progress-fill.danger {
background: var(--danger);
}
.detail-row {
min-width: 0;
align-self: start;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
font-size: 10px;
line-height: 19px;
}
.absolute-reset {
min-width: 0;
flex: 1;
overflow: hidden;
color: var(--muted);
text-overflow: ellipsis;
white-space: nowrap;
}
.reset-credit {
min-width: 0;
max-width: 150px;
flex: 0 1 auto;
padding: 1px 7px;
overflow: hidden;
border: 1px solid rgba(88, 164, 177, 0.24);
border-radius: 999px;
background: var(--accent-soft);
color: var(--accent);
font-weight: 700;
text-overflow: ellipsis;
white-space: nowrap;
}
.empty {
min-height: 82px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
color: var(--muted);
text-align: center;
}
.empty-text {
max-width: 340px;
overflow: hidden;
font-size: 11px;
line-height: 16px;
text-overflow: ellipsis;
white-space: nowrap;
}
.button-row {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 6px;
}
.text-button {
min-height: 29px;
padding: 4px 10px;
border: 1px solid var(--line);
border-radius: 8px;
background: transparent;
cursor: pointer;
transition: background-color 120ms ease, border-color 120ms ease, transform 80ms ease;
}
.text-button:hover:not(:disabled) {
border-color: rgba(88, 164, 177, 0.46);
background: var(--accent-soft);
}
.text-button:active:not(:disabled) {
transform: scale(0.97);
}
.text-button.primary {
border-color: transparent;
background: var(--accent);
color: #fff;
font-weight: 600;
}
.text-button.primary:hover:not(:disabled) {
border-color: transparent;
background: var(--accent-hover);
}
.settings {
display: grid;
gap: 10px;
padding: 2px 0 1px;
}
.setting-row {
min-width: 0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.setting-info {
min-width: 0;
flex: 1;
}
.setting-name {
font-size: 12px;
font-weight: 600;
line-height: 17px;
}
.setting-value {
margin-top: 1px;
overflow: hidden;
color: var(--muted);
font-size: 10px;
line-height: 15px;
text-overflow: ellipsis;
white-space: nowrap;
}
.setting-actions {
flex: 0 0 auto;
display: flex;
gap: 5px;
}
.setting-actions .text-button {
min-height: 27px;
padding: 3px 8px;
font-size: 11px;
}
.switch {
position: relative;
width: 36px;
height: 21px;
flex: 0 0 auto;
cursor: pointer;
}
.switch input {
position: absolute;
width: 1px;
height: 1px;
opacity: 0;
}
.switch-track {
position: absolute;
inset: 0;
border-radius: 999px;
background: var(--surface-soft);
transition: background-color 150ms ease;
}
.switch-track::after {
position: absolute;
top: 3px;
left: 3px;
width: 15px;
height: 15px;
border-radius: 50%;
background: var(--muted);
content: "";
transition: background-color 150ms ease, transform 150ms ease;
}
.switch input:checked + .switch-track {
background: var(--accent-soft);
}
.switch input:checked + .switch-track::after {
background: var(--accent);
transform: translateX(15px);
}
.privacy-note {
padding-top: 7px;
border-top: 1px solid var(--line);
color: var(--muted);
font-size: 9px;
line-height: 14px;
}
.error {
margin-top: 7px;
padding: 6px 7px;
border-radius: 8px;
background: var(--danger-soft);
color: var(--danger);
font-size: 10px;
line-height: 15px;
}
.error-main {
display: flex;
align-items: center;
gap: 7px;
}
.error-message {
min-width: 0;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.error .text-button {
min-height: 24px;
padding: 2px 7px;
border-color: rgba(215, 82, 88, 0.3);
color: inherit;
font-size: 10px;
}
.error-details {
margin-top: 4px;
border-top: 1px solid rgba(215, 82, 88, 0.2);
padding-top: 4px;
}
.error-details summary {
width: max-content;
max-width: 100%;
border-radius: 4px;
cursor: pointer;
user-select: none;
}
.error-details pre {
max-height: 90px;
margin: 4px 0 0;
padding: 6px;
overflow: auto;
border-radius: 6px;
background: rgba(0, 0, 0, 0.06);
color: inherit;
font: 10px/1.45 Consolas, monospace;
white-space: pre-wrap;
word-break: break-word;
}
@media (max-width: 350px) {
.status-badge {
display: none;
}
.quota {
grid-template-columns: 94px minmax(0, 1fr);
column-gap: 10px;
}
.remaining-number {
font-size: 31px;
}
.reset-credit {
max-width: 110px;
}
}
@media (max-width: 285px) {
.plan-badge {
display: none;
}
.content {
padding-right: 10px;
padding-left: 10px;
}
.quota {
grid-template-columns: 78px minmax(0, 1fr);
column-gap: 7px;
}
.remaining-number {
font-size: 27px;
}
.remaining-unit,
.absolute-reset {
display: none;
}
}
@media (max-height: 135px) {
.header {
min-height: 39px;
padding-top: 5px;
padding-bottom: 3px;
}
.updated {
display: none;
}
.content {
padding-bottom: 7px;
}
.quota {
min-height: 74px;
row-gap: 5px;
}
}
</style>
</head>
<body>
<section class="widget" aria-label="Codex 周额度">
<header class="header">
<div class="heading">
<div class="title-row">
<div class="title">Codex 周额度</div>
<div id="planBadge" class="badge plan-badge" hidden></div>
<div id="statusBadge" class="badge status-badge normal" hidden></div>
</div>
<div id="updatedText" class="updated">正在检测 Codex 登录信息</div>
</div>
<div class="actions">
<button id="refreshButton" class="icon-button" type="button" title="刷新额度" aria-label="刷新额度" disabled>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M20 6v5h-5"></path>
<path d="M4 18v-5h5"></path>
<path d="M18.1 9a7 7 0 0 0-11.6-2.6L4 9"></path>
<path d="M5.9 15a7 7 0 0 0 11.6 2.6L20 15"></path>
</svg>
</button>
<button id="settingsButton" class="icon-button" type="button" title="设置" aria-label="设置" aria-expanded="false">
<svg viewBox="0 0 24 24" aria-hidden="true">
<circle cx="12" cy="12" r="3"></circle>
<path d="M19.4 15a1.7 1.7 0 0 0 .34 1.88l.06.06-2.83 2.83-.06-.06A1.7 1.7 0 0 0 15 19.4a1.7 1.7 0 0 0-1 .6 1.7 1.7 0 0 0-.4 1.1V21h-4v-.09A1.7 1.7 0 0 0 8.6 19.4a1.7 1.7 0 0 0-1.88.34l-.06.06-2.83-2.83.06-.06A1.7 1.7 0 0 0 4.6 15a1.7 1.7 0 0 0-.6-1 1.7 1.7 0 0 0-1.1-.4H3v-4h.09A1.7 1.7 0 0 0 4.6 8.6a1.7 1.7 0 0 0-.34-1.88l-.06-.06 2.83-2.83.06.06A1.7 1.7 0 0 0 9 4.6a1.7 1.7 0 0 0 1-.6 1.7 1.7 0 0 0 .4-1.1V3h4v.09A1.7 1.7 0 0 0 15.4 4.6a1.7 1.7 0 0 0 1.88-.34l.06-.06 2.83 2.83-.06.06A1.7 1.7 0 0 0 19.4 9a1.7 1.7 0 0 0 .6 1 1.7 1.7 0 0 0 1.1.4h.09v4h-.09A1.7 1.7 0 0 0 19.4 15z"></path>
</svg>
</button>
</div>
</header>
<main class="content">
<div id="quotaView" class="quota" hidden>
<div class="remaining">
<div class="remaining-value">
<span id="remainingNumber" class="remaining-number">—</span>
<span class="remaining-unit">% 剩余</span>
</div>
<div id="usedText" class="used">已用 —%</div>
</div>
<div class="quota-main">
<div class="quota-meta">
<span class="quota-label">周额度</span>
<span id="resetCountdown" class="reset-countdown">—</span>
</div>
<div id="progressElement" class="progress" role="progressbar" aria-label="周额度剩余比例" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">
<div id="progressFill" class="progress-fill"></div>
</div>
</div>
<div class="detail-row">
<span id="absoluteReset" class="absolute-reset">重置时间未知</span>
<span id="resetCredit" class="reset-credit" hidden></span>
</div>
</div>
<div id="emptyView" class="empty" hidden>
<div id="emptyText" class="empty-text">未找到 Codex 登录文件。</div>
<div class="button-row">
<button id="detectButton" class="text-button primary" type="button">重新检测</button>
<button id="selectAuthButton" class="text-button" type="button">选择 auth.json</button>
</div>
</div>
<div id="settingsView" class="settings" hidden>
<div class="setting-row">
<div class="setting-info">
<div class="setting-name">Codex 登录文件</div>
<div id="authPathText" class="setting-value">正在检测</div>
</div>
<div class="setting-actions">
<button id="detectSettingsButton" class="text-button" type="button">自动检测</button>
<button id="changeAuthButton" class="text-button" type="button">选择</button>
</div>
</div>
<div class="setting-row">
<div class="setting-info">
<div class="setting-name">自动刷新</div>
<div class="setting-value">每 5 分钟读取一次</div>
</div>
<label class="switch" title="自动刷新">
<input id="autoRefreshInput" type="checkbox" checked>
<span class="switch-track"></span>
</label>
</div>
<div class="privacy-note">
默认读取用户目录下的 .codex\auth.json。令牌仅用于额度请求,不会保存到组件状态;重置次数仅展示,不会自动消耗。
</div>
</div>
<div id="errorBox" class="error" role="status" hidden>
<div class="error-main">
<span id="errorMessage" class="error-message"></span>
<button id="retryButton" class="text-button" type="button">重试</button>
</div>
<details id="errorDetails" class="error-details">
<summary>查看详情</summary>
<pre id="errorDetailText"></pre>
</details>
</div>
</main>
</section>
<script>
const host = window.widgetHost;
const planBadge = document.getElementById("planBadge");
const statusBadge = document.getElementById("statusBadge");
const updatedText = document.getElementById("updatedText");
const refreshButton = document.getElementById("refreshButton");
const settingsButton = document.getElementById("settingsButton");
const quotaView = document.getElementById("quotaView");
const emptyView = document.getElementById("emptyView");
const emptyText = document.getElementById("emptyText");
const settingsView = document.getElementById("settingsView");
const remainingNumber = document.getElementById("remainingNumber");
const usedText = document.getElementById("usedText");
const progressElement = document.getElementById("progressElement");
const progressFill = document.getElementById("progressFill");
const resetCountdown = document.getElementById("resetCountdown");
const absoluteReset = document.getElementById("absoluteReset");
const resetCredit = document.getElementById("resetCredit");
const detectButton = document.getElementById("detectButton");
const selectAuthButton = document.getElementById("selectAuthButton");
const authPathText = document.getElementById("authPathText");
const detectSettingsButton = document.getElementById("detectSettingsButton");
const changeAuthButton = document.getElementById("changeAuthButton");
const autoRefreshInput = document.getElementById("autoRefreshInput");
const errorBox = document.getElementById("errorBox");
const errorMessage = document.getElementById("errorMessage");
const errorDetails = document.getElementById("errorDetails");
const errorDetailText = document.getElementById("errorDetailText");
const retryButton = document.getElementById("retryButton");
const STATE_AUTH_PATH = "codexWeekly.authPath";
const STATE_AUTO_REFRESH = "codexWeekly.autoRefresh";
const STATE_SNAPSHOT = "codexWeekly.snapshot";
const REFRESH_INTERVAL_MS = 5 * 60 * 1000;
const USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
let authPath = "";
let defaultAuthPath = "";
let codexDirectory = "";
let autoRefresh = true;
let snapshot = null;
let settingsOpen = false;
let busy = false;
let autoRefreshTimer = null;
let clockTimer = null;
class ComponentError extends Error {
constructor(message, detail = "") {
super(message);
this.name = "ComponentError";
this.detail = detail;
}
}
function errorText(error) {
return String(error?.message || error);
}
function showError(error, message) {
const basic = errorText(error);
const detail = String(error?.detail || basic);
errorMessage.textContent = message || basic || "操作失败";
errorDetailText.textContent = detail;
errorBox.title = basic;
errorDetails.open = false;
errorBox.hidden = false;
}
function clearError() {
errorMessage.textContent = "";
errorDetailText.textContent = "";
errorBox.title = "";
errorDetails.open = false;
errorBox.hidden = true;
}
function joinWindowsPath(...parts) {
const cleaned = parts
.filter(part => typeof part === "string" && part.length > 0)
.map((part, index) => index === 0
? part.replace(/[\\/]+$/g, "")
: part.replace(/^[\\/]+|[\\/]+$/g, ""))
.filter(Boolean);
return cleaned.join("\\");
}
function fileNameFromPath(path) {
const parts = String(path || "").split(/[\\/]/);
return parts[parts.length - 1] || "";
}
function clampPercent(value) {
const number = Number(value);
return Number.isFinite(number) ? Math.min(100, Math.max(0, number)) : null;
}
function epochToMilliseconds(value) {
const number = Number(value);
if (!Number.isFinite(number) || number <= 0) {
return null;
}
return number < 100000000000 ? number * 1000 : number;
}
function firstDefined(object, keys) {
if (!object || typeof object !== "object") {
return undefined;
}
for (const key of keys) {
if (object[key] !== undefined && object[key] !== null) {
return object[key];
}
}
return undefined;
}
function formatDuration(milliseconds) {
if (!Number.isFinite(milliseconds)) {
return "未知";
}
if (milliseconds <= 0) {
return "即将重置";
}
const totalMinutes = Math.max(1, Math.ceil(milliseconds / 60000));
const days = Math.floor(totalMinutes / 1440);
const hours = Math.floor((totalMinutes % 1440) / 60);
const minutes = totalMinutes % 60;
if (days > 0) {
return hours > 0 ? `${days}天 ${hours}小时` : `${days}天`;
}
if (hours > 0) {
return minutes > 0 ? `${hours}小时 ${minutes}分钟` : `${hours}小时`;
}
return `${minutes}分钟`;
}
function formatAbsoluteTime(milliseconds) {
if (!Number.isFinite(milliseconds)) {
return "";
}
try {
return new Intl.DateTimeFormat("zh-CN", {
month: "numeric",
day: "numeric",
hour: "2-digit",
minute: "2-digit"
}).format(new Date(milliseconds));
} catch {
return new Date(milliseconds).toLocaleString();
}
}
function formatUpdatedTime(milliseconds) {
if (!Number.isFinite(milliseconds)) {
return "尚未读取额度";
}
const elapsed = Math.max(0, Date.now() - milliseconds);
if (elapsed < 60000) {
return "刚刚更新";
}
const minutes = Math.floor(elapsed / 60000);
if (minutes < 60) {
return `${minutes} 分钟前更新`;
}
const hours = Math.floor(minutes / 60);
if (hours < 24) {
return `${hours} 小时前更新`;
}
return `${Math.floor(hours / 24)} 天前更新`;
}
function normalizePlanName(value) {
const text = String(value || "").trim();
if (!text) {
return "";
}
const key = text.toLowerCase().replace(/[\s-]+/g, "_");
const names = {
guest: "Guest",
free: "Free",
go: "Go",
plus: "Plus",
pro: "Pro",
prolite: "Pro Lite",
pro_lite: "Pro Lite",
free_workspace: "Free Workspace",
team: "Team",
business: "Business",
enterprise: "Enterprise",
education: "Education",
edu: "Edu"
};
return names[key] || text;
}
function normalizeLimitStatus(rawStatus, spendControlReached, usedPercent) {
const raw = String(rawStatus || "").trim();
const key = raw.toLowerCase().replace(/[\s-]+/g, "_");
if (spendControlReached === true) {
return {
text: "支出受限",
level: "danger",
title: raw ? `支出控制已触发;服务端状态:${raw}` : "支出控制已触发"
};
}
if (raw) {
const weeklyKeys = ["secondary", "weekly", "week", "secondary_window"];
const primaryKeys = ["primary", "five_hour", "5_hour", "5h", "primary_window"];
if (weeklyKeys.some(item => key.includes(item))) {
return { text: "周额度已用尽", level: "danger", title: raw };
}
if (primaryKeys.some(item => key.includes(item))) {
return { text: "短时额度受限", level: "warning", title: raw };
}
return { text: "额度受限", level: "danger", title: raw };
}
if (Number(usedPercent) >= 100) {
return { text: "周额度已用尽", level: "danger", title: "周额度剩余 0%" };
}
if (Number(usedPercent) >= 90) {
return { text: "额度偏低", level: "warning", title: "周额度剩余不足 10%" };
}
return { text: "额度正常", level: "normal", title: "当前未触发额度限制" };
}
function render() {
authPathText.textContent = authPath
? `${fileNameFromPath(authPath)} · ${authPath}`
: defaultAuthPath
? `未找到:${defaultAuthPath}`
: "未找到";
authPathText.title = authPath || defaultAuthPath || "";
autoRefreshInput.checked = autoRefresh;
refreshButton.disabled = busy || !authPath;
detectButton.disabled = busy;
selectAuthButton.disabled = busy;
detectSettingsButton.disabled = busy;
changeAuthButton.disabled = busy;
retryButton.disabled = busy;
settingsView.hidden = !settingsOpen;
settingsButton.setAttribute("aria-expanded", String(settingsOpen));
if (settingsOpen) {
quotaView.hidden = true;
emptyView.hidden = true;
return;
}
if (!snapshot) {
quotaView.hidden = true;
emptyView.hidden = false;
planBadge.hidden = true;
statusBadge.hidden = true;
if (authPath) {
emptyText.textContent = busy ? "正在读取 Codex 周额度…" : "尚未取得周额度数据。";
updatedText.textContent = busy ? "正在读取额度" : "等待读取额度";
} else {
emptyText.textContent = defaultAuthPath
? `未找到默认登录文件:${defaultAuthPath}`
: "无法确定 Codex 登录文件位置。";
updatedText.textContent = "未找到 Codex 登录信息";
}
return;
}
quotaView.hidden = false;
emptyView.hidden = true;
const used = clampPercent(snapshot.usedPercent);
const remaining = used === null ? null : clampPercent(100 - used);
const roundedRemaining = remaining === null ? null : Math.round(remaining);
remainingNumber.textContent = roundedRemaining === null ? "—" : String(roundedRemaining);
usedText.textContent = used === null ? "已用 —%" : `已用 ${Math.round(used)}%`;
progressFill.style.width = `${roundedRemaining ?? 0}%`;
progressElement.setAttribute("aria-valuenow", String(roundedRemaining ?? 0));
progressFill.classList.toggle("warning", remaining !== null && remaining <= 25 && remaining > 10);
progressFill.classList.toggle("danger", remaining !== null && remaining <= 10);
const planName = normalizePlanName(snapshot.planType);
planBadge.textContent = planName;
planBadge.hidden = !planName;
const status = normalizeLimitStatus(snapshot.limitStatus, snapshot.spendControlReached, used);
statusBadge.textContent = status.text;
statusBadge.title = status.title;
statusBadge.classList.remove("normal", "warning", "danger");
statusBadge.classList.add(status.level);
statusBadge.hidden = false;
const availableCount = Number(snapshot.resetCredits?.availableCount);
if (Number.isFinite(availableCount) && availableCount > 0) {
resetCredit.textContent = `可重置 ×${Math.floor(availableCount)}`;
resetCredit.hidden = false;
const firstCredit = Array.isArray(snapshot.resetCredits?.credits)
? snapshot.resetCredits.credits.find(item => item && typeof item === "object")
: null;
const title = String(firstCredit?.title || firstCredit?.description || "").trim();
const expiresAt = epochToMilliseconds(firstCredit?.expiresAt);
const expiryText = expiresAt ? `;${formatAbsoluteTime(expiresAt)} 过期` : "";
resetCredit.title = `${title || "有可用的额度重置次数"}${expiryText}`;
} else {
resetCredit.textContent = "";
resetCredit.title = "";
resetCredit.hidden = true;
}
updatedText.textContent = busy ? "正在刷新额度" : formatUpdatedTime(Number(snapshot.updatedAt));
updateResetTime();
}
function updateResetTime() {
if (!snapshot) {
return;
}
const resetAt = Number(snapshot.resetAt);
if (!Number.isFinite(resetAt)) {
resetCountdown.textContent = "重置时间未知";
resetCountdown.title = "";
absoluteReset.textContent = "重置时间未知";
absoluteReset.title = "";
return;
}
const duration = formatDuration(resetAt - Date.now());
const absolute = formatAbsoluteTime(resetAt);
resetCountdown.textContent = `距重置 ${duration}`;
resetCountdown.title = absolute ? `预计 ${absolute} 重置` : "";
absoluteReset.textContent = absolute ? `预计 ${absolute} 重置` : "重置时间未知";
absoluteReset.title = absoluteReset.textContent;
if (!busy) {
updatedText.textContent = formatUpdatedTime(Number(snapshot.updatedAt));
}
}
function setBusy(value) {
busy = value;
refreshButton.classList.toggle("spinning", value);
render();
}
function parseAuthFile(text) {
let data;
try {
data = JSON.parse(text);
} catch {
throw new ComponentError("auth.json 不是有效的 JSON 文件");
}
const containers = [data?.tokens, data?.auth, data?.credentials, data]
.filter(value => value && typeof value === "object");
let accessToken = "";
let accountId = "";
for (const container of containers) {
accessToken ||= String(
container.access_token ??
container.accessToken ??
container.token ??
""
).trim();
accountId ||= String(
container.account_id ??
container.accountId ??
container.chatgpt_account_id ??
container.chatgptAccountId ??
""
).trim();
}
if (!accessToken) {
const hasApiKey = Boolean(data?.OPENAI_API_KEY || data?.openai_api_key);
throw new ComponentError(
hasApiKey
? "当前 auth.json 使用 API Key,无法读取 ChatGPT 周额度"
: "auth.json 中没有找到 access_token"
);
}
if (!accountId) {
throw new ComponentError("auth.json 中没有找到 account_id");
}
return { accessToken, accountId };
}
function getHeader(headers, name) {
if (!headers) {
return null;
}
const target = name.toLowerCase();
if (Array.isArray(headers)) {
for (const item of headers) {
if (Array.isArray(item) && String(item[0]).toLowerCase() === target) {
return item[1];
}
if (item && typeof item === "object") {
const key = String(item.name ?? item.key ?? "").toLowerCase();
if (key === target) {
return item.value;
}
}
}
return null;
}
if (typeof headers === "object") {
for (const [key, value] of Object.entries(headers)) {
if (key.toLowerCase() === target) {
return Array.isArray(value) ? value[0] : value;
}
}
}
return null;
}
function normalizeWindow(windowValue, path, preferredSecondary) {
if (!windowValue || typeof windowValue !== "object") {
return null;
}
const usedPercent = clampPercent(firstDefined(windowValue, [
"used_percent", "usedPercent", "percent_used", "percentUsed"
]));
if (usedPercent === null) {
return null;
}
let resetAt = epochToMilliseconds(firstDefined(windowValue, [
"reset_at", "resets_at", "resetAt", "resetsAt"
]));
const resetAfterSeconds = Number(firstDefined(windowValue, [
"reset_after_seconds", "resetAfterSeconds"
]));
if (resetAt === null && Number.isFinite(resetAfterSeconds) && resetAfterSeconds >= 0) {
resetAt = Date.now() + resetAfterSeconds * 1000;
}
if (resetAt === null) {
return null;
}
const durationSeconds = Number(firstDefined(windowValue, [
"limit_window_seconds", "window_seconds", "limitWindowSeconds", "windowDurationSeconds"
]));
const durationMinutes = Number(firstDefined(windowValue, [
"window_minutes", "windowDurationMins", "window_duration_mins"
]));
let durationMs = null;
if (Number.isFinite(durationSeconds) && durationSeconds > 0) {
durationMs = durationSeconds * 1000;
} else if (Number.isFinite(durationMinutes) && durationMinutes > 0) {
durationMs = durationMinutes * 60000;
}
const lowerPath = path.toLowerCase();
let score = 0;
if (preferredSecondary || lowerPath.includes("secondary")) {
score += 100;
}
if (lowerPath.includes("codex")) {
score += 30;
}
if (lowerPath.includes("additional_rate_limits") || lowerPath.includes("additionalratelimits")) {
score -= 35;
}
if (durationMs !== null) {
const days = durationMs / 86400000;
score += days >= 5 && days <= 10 ? 80 : Math.min(50, days);
}
return { usedPercent, resetAt, durationMs, score, path };
}
function collectKnownWindows(root, basePath, candidates) {
if (!root || typeof root !== "object") {
return;
}
const rateLimit = root.rate_limit ?? root.rateLimit;
if (rateLimit && typeof rateLimit === "object") {
const primary = rateLimit.primary_window ?? rateLimit.primaryWindow ?? rateLimit.primary;
const secondary = rateLimit.secondary_window ?? rateLimit.secondaryWindow ?? rateLimit.secondary;
const primaryCandidate = normalizeWindow(primary, `${basePath}.rate_limit.primary`, false);
const secondaryCandidate = normalizeWindow(secondary, `${basePath}.rate_limit.secondary`, true);
if (primaryCandidate) candidates.push(primaryCandidate);
if (secondaryCandidate) candidates.push(secondaryCandidate);
}
const rateSnapshots = [
root.rate_limits,
root.rateLimits,
root.rate_limits_by_limit_id?.codex,
root.rateLimitsByLimitId?.codex
];
for (const rateSnapshot of rateSnapshots) {
if (!rateSnapshot || typeof rateSnapshot !== "object") {
continue;
}
const primary = rateSnapshot.primary_window ?? rateSnapshot.primaryWindow ?? rateSnapshot.primary;
const secondary = rateSnapshot.secondary_window ?? rateSnapshot.secondaryWindow ?? rateSnapshot.secondary;
const primaryCandidate = normalizeWindow(primary, `${basePath}.rateLimits.primary`, false);
const secondaryCandidate = normalizeWindow(secondary, `${basePath}.rateLimits.secondary`, true);
if (primaryCandidate) candidates.push(primaryCandidate);
if (secondaryCandidate) candidates.push(secondaryCandidate);
}
const directPrimary = root.primary_window ?? root.primaryWindow ?? root.primary;
const directSecondary = root.secondary_window ?? root.secondaryWindow ?? root.secondary;
const directPrimaryCandidate = normalizeWindow(directPrimary, `${basePath}.primary`, false);
const directSecondaryCandidate = normalizeWindow(directSecondary, `${basePath}.secondary`, true);
if (directPrimaryCandidate) candidates.push(directPrimaryCandidate);
if (directSecondaryCandidate) candidates.push(directSecondaryCandidate);
const additional = root.additional_rate_limits ?? root.additionalRateLimits;
if (Array.isArray(additional)) {
additional.forEach((item, index) => {
collectKnownWindows(item, `${basePath}.additional_rate_limits[${index}]`, candidates);
});
}
}
function recursivelyCollectWindows(value, path, depth, seen, candidates) {
if (!value || typeof value !== "object" || depth > 6 || seen.has(value)) {
return;
}
seen.add(value);
const direct = normalizeWindow(value, path, path.toLowerCase().includes("secondary"));
if (direct) {
candidates.push(direct);
}
if (Array.isArray(value)) {
value.forEach((item, index) => {
recursivelyCollectWindows(item, `${path}[${index}]`, depth + 1, seen, candidates);
});
return;
}
for (const [key, child] of Object.entries(value)) {
if (child && typeof child === "object") {
recursivelyCollectWindows(child, `${path}.${key}`, depth + 1, seen, candidates);
}
}
}
function parseHeaderWindow(headers) {
const usedPercent = clampPercent(getHeader(headers, "x-codex-secondary-used-percent"));
const resetAt = epochToMilliseconds(getHeader(headers, "x-codex-secondary-reset-at"));
if (usedPercent === null || resetAt === null) {
return null;
}
const windowMinutes = Number(getHeader(headers, "x-codex-secondary-window-minutes"));
return {
usedPercent,
resetAt,
durationMs: Number.isFinite(windowMinutes) ? windowMinutes * 60000 : null,
score: 200,
path: "response.headers.secondary"
};
}
function traverseObjects(root, visitor, maxDepth = 7) {
const seen = new WeakSet();
function visit(value, depth) {
if (!value || typeof value !== "object" || depth > maxDepth || seen.has(value)) {
return;
}
seen.add(value);
visitor(value);
if (Array.isArray(value)) {
value.forEach(item => visit(item, depth + 1));
} else {
Object.values(value).forEach(item => visit(item, depth + 1));
}
}
visit(root, 0);
}
function findPlanType(data) {
let result = "";
traverseObjects(data, object => {
if (result) return;
const value = firstDefined(object, ["plan_type", "planType"])
?? firstDefined(object.account, ["plan_type", "planType"]);
if (value) result = String(value);
});
return result;
}
function findLimitMetadata(data) {
let limitStatus = "";
let spendControlReached = null;
traverseObjects(data, object => {
if (!limitStatus) {
const statusValue = firstDefined(object, [
"rate_limit_reached_type", "rateLimitReachedType", "limit_reached_type", "limitReachedType"
]);
const status = statusValue && typeof statusValue === "object"
? firstDefined(statusValue, ["type", "name", "value"])
: statusValue;
if (status !== undefined && status !== null && String(status).trim()) {
limitStatus = String(status).trim();
}
}
if (!limitStatus) {
const rateLimit = object.rate_limit ?? object.rateLimit;
if (rateLimit && typeof rateLimit === "object") {
const limitReached = firstDefined(rateLimit, ["limit_reached", "limitReached"]);
const allowed = firstDefined(rateLimit, ["allowed"]);
if (limitReached === true) {
limitStatus = "limit_reached";
} else if (allowed === false) {
limitStatus = "not_allowed";
}
}
}
if (spendControlReached === null) {
const spend = firstDefined(object, ["spend_control_reached", "spendControlReached"]);
if (typeof spend === "boolean") {
spendControlReached = spend;
}
}
});
return { limitStatus, spendControlReached };
}
function normalizeResetCreditItem(item) {
if (!item || typeof item !== "object") {
return null;
}
const status = String(firstDefined(item, ["status"]) || "").trim();
return {
id: String(firstDefined(item, ["id", "credit_id", "creditId"]) || "").trim(),
resetType: String(firstDefined(item, ["reset_type", "resetType"]) || "").trim(),
status,
grantedAt: epochToMilliseconds(firstDefined(item, ["granted_at", "grantedAt"])),
expiresAt: epochToMilliseconds(firstDefined(item, ["expires_at", "expiresAt"])),
title: String(firstDefined(item, ["title"]) || "").trim(),
description: String(firstDefined(item, ["description"]) || "").trim()
};
}
function findResetCredits(data) {
let best = null;
traverseObjects(data, object => {
const candidate = object.rate_limit_reset_credits ?? object.rateLimitResetCredits;
if (!candidate || typeof candidate !== "object") {
return;
}
const availableCountValue = firstDefined(candidate, ["available_count", "availableCount", "count"]);
const rawCredits = Array.isArray(candidate.credits) ? candidate.credits : [];
const credits = rawCredits
.map(normalizeResetCreditItem)
.filter(item => item && (!item.status || item.status.toLowerCase() === "available"));
const availableCount = Number(availableCountValue);
const normalizedCount = Number.isFinite(availableCount)
? Math.max(0, Math.floor(availableCount))
: credits.length;
if (!best || normalizedCount > best.availableCount) {
best = { availableCount: normalizedCount, credits };
}
});
return best || { availableCount: 0, credits: [] };
}
function parseUsageResponse(data, headers) {
const roots = [
{ value: data, path: "root" },
{ value: data?.result, path: "root.result" },
{ value: data?.data, path: "root.data" },
{ value: data?.usage, path: "root.usage" }
].filter(item => item.value && typeof item.value === "object");
const candidates = [];
for (const root of roots) {
collectKnownWindows(root.value, root.path, candidates);
}
recursivelyCollectWindows(data, "root", 0, new WeakSet(), candidates);
const headerCandidate = parseHeaderWindow(headers);
if (headerCandidate) candidates.push(headerCandidate);
const uniqueCandidates = [];
const candidateKeys = new Set();
for (const candidate of candidates) {
const key = `${candidate.usedPercent}|${candidate.resetAt}|${candidate.path}`;
if (!candidateKeys.has(key)) {
candidateKeys.add(key);
uniqueCandidates.push(candidate);
}
}
uniqueCandidates.sort((a, b) => {
if (b.score !== a.score) return b.score - a.score;
return (b.durationMs ?? 0) - (a.durationMs ?? 0);
});
const weekly = uniqueCandidates[0];
if (!weekly) {
const topKeys = data && typeof data === "object"
? Object.keys(data).slice(0, 20).join(", ")
: typeof data;
throw new ComponentError(
"服务器响应中没有找到可识别的周额度",
`响应顶层字段:${topKeys || "无"}`
);
}
const metadata = findLimitMetadata(data);
return {
usedPercent: weekly.usedPercent,
resetAt: weekly.resetAt,
planType: findPlanType(data),
updatedAt: Date.now(),
sourcePath: weekly.path,
limitStatus: metadata.limitStatus,
spendControlReached: metadata.spendControlReached,
resetCredits: findResetCredits(data)
};
}
function getHttpError(response) {
let serverMessage = "";
try {
const parsed = JSON.parse(response?.text || "");
serverMessage = String(
parsed?.error?.message ??
parsed?.message ??
parsed?.detail ??
""
);
} catch {
serverMessage = "";
}
const status = Number(response?.status || 0);
if (status === 401) {
return new ComponentError(
"Codex 登录已过期",
serverMessage || "请先打开 Codex 完成登录,再返回组件刷新。"
);
}
if (status === 403) {
return new ComponentError(
"当前登录没有读取额度的权限",
serverMessage || `HTTP ${status} ${response?.statusText || ""}`.trim()
);
}
return new ComponentError(
serverMessage || `读取失败:HTTP ${status || "未知"}`,
`HTTP ${status || "未知"} ${response?.statusText || ""}`.trim()
);
}
async function saveSnapshot() {
try {
await host.state.write(STATE_SNAPSHOT, snapshot);
} catch (error) {
showError(error, "额度已读取,但缓存保存失败");
}
}
async function saveAuthPath() {
try {
await host.state.write(STATE_AUTH_PATH, authPath);
} catch (error) {
throw new ComponentError("登录文件路径保存失败", errorText(error));
}
}
async function pathExists(path) {
let result;
try {
result = await host.fs.exists({ path });
} catch (error) {
throw new ComponentError("无法检查文件是否存在", errorText(error));
}
return Boolean(result?.exists && result.type === "file");
}
async function detectDefaultAuth(refreshAfterDetection = true) {
if (busy) return;
setBusy(true);
clearError();
try {
let knownFolders;
try {
knownFolders = await host.fs.getKnownFolders();
} catch (error) {
throw new ComponentError("无法读取 Windows 用户目录", errorText(error));
}
const userProfile = String(knownFolders?.userProfile || "").trim();
if (!userProfile) {
throw new ComponentError("宿主没有返回用户目录");
}
codexDirectory = joinWindowsPath(userProfile, ".codex");
defaultAuthPath = joinWindowsPath(codexDirectory, "auth.json");
if (await pathExists(defaultAuthPath)) {
authPath = defaultAuthPath;
await saveAuthPath();
render();
if (refreshAfterDetection) {
setBusy(false);
await refreshUsage();
return;
}
} else {
authPath = "";
snapshot = null;
try {
await host.state.remove(STATE_AUTH_PATH);
} catch (error) {
throw new ComponentError("旧路径清理失败", errorText(error));
}
try {
await host.state.remove(STATE_SNAPSHOT);
} catch (error) {
throw new ComponentError("旧额度缓存清理失败", errorText(error));
}
}
} catch (error) {
showError(error, "自动检测失败");
} finally {
if (busy) setBusy(false);
restartAutoRefresh();
}
}
async function refreshUsage() {
if (busy || !authPath) return;
setBusy(true);
clearError();
try {
if (!(await pathExists(authPath))) {
if (authPath !== defaultAuthPath && defaultAuthPath && await pathExists(defaultAuthPath)) {
authPath = defaultAuthPath;
await saveAuthPath();
} else {
throw new ComponentError("Codex 登录文件已不存在", authPath);
}
}
let fileResult;
try {
fileResult = await host.fs.readText({
path: authPath,
maxBytes: 1024 * 1024,
encoding: "utf-8"
});
} catch (error) {
throw new ComponentError("读取 auth.json 失败", errorText(error));
}
if (fileResult?.truncated) {
throw new ComponentError("auth.json 文件过大,未能完整读取");
}
const credentials = parseAuthFile(fileResult?.text || "");
let response;
try {
response = await host.http.get({
url: USAGE_URL,
headers: {
"Accept": "application/json",
"Authorization": `Bearer ${credentials.accessToken}`,
"ChatGPT-Account-Id": credentials.accountId
},
timeoutMs: 15000,
maxBytes: 512 * 1024
});
} catch (error) {
throw new ComponentError("连接 Codex 额度服务失败", errorText(error));
}
if (response?.truncated) {
throw new ComponentError("服务器响应过大,未能完整读取");
}
if (!response?.ok) {
throw getHttpError(response || {});
}
let data;
try {
data = JSON.parse(response.text || "");
} catch (error) {
throw new ComponentError("服务器返回了无效的 JSON", errorText(error));
}
snapshot = parseUsageResponse(data, response.headers);
await saveSnapshot();
} catch (error) {
showError(error);
} finally {
setBusy(false);
restartAutoRefresh();
}
}
async function chooseAuthFile() {
if (busy) return;
clearError();
let selected;
try {
selected = await host.fs.selectFile({
title: "选择 Codex auth.json",
filter: "JSON 文件|*.json|所有文件|*.*",
defaultExtension: ".json",
defaultFileName: "auth.json",
initialDirectory: codexDirectory || undefined
});
} catch (error) {
showError(error, "选择登录文件失败");
return;
}
if (!selected) return;
const selectedPath = String(selected.path || "").trim();
if (!selectedPath) {
showError(new ComponentError("没有取得所选文件路径"));
return;
}
authPath = selectedPath;
try {
await saveAuthPath();
} catch (error) {
showError(error);
return;
}
settingsOpen = false;
render();
await refreshUsage();
}
async function changeAutoRefresh() {
const previous = autoRefresh;
autoRefresh = autoRefreshInput.checked;
try {
await host.state.write(STATE_AUTO_REFRESH, autoRefresh);
} catch (error) {
autoRefresh = previous;
autoRefreshInput.checked = previous;
showError(error, "自动刷新设置保存失败");
return;
}
restartAutoRefresh();
}
function stopAutoRefresh() {
if (autoRefreshTimer !== null) {
clearInterval(autoRefreshTimer);
autoRefreshTimer = null;
}
}
function restartAutoRefresh() {
stopAutoRefresh();
if (!autoRefresh || !authPath) return;
autoRefreshTimer = setInterval(() => {
refreshUsage().catch(error => showError(error));
}, REFRESH_INTERVAL_MS);
}
function startClock() {
if (clockTimer !== null) clearInterval(clockTimer);
clockTimer = setInterval(updateResetTime, 30000);
}
async function initialize() {
if (!host) {
throw new ComponentError("当前环境没有提供 widgetHost");
}
let storedPath;
let storedAutoRefresh;
let storedSnapshot;
try {
storedPath = await host.state.read(STATE_AUTH_PATH, "");
} catch (error) {
throw new ComponentError("读取登录文件设置失败", errorText(error));
}
try {
storedAutoRefresh = await host.state.read(STATE_AUTO_REFRESH, true);
} catch (error) {
throw new ComponentError("读取自动刷新设置失败", errorText(error));
}
try {
storedSnapshot = await host.state.read(STATE_SNAPSHOT, null);
} catch (error) {
throw new ComponentError("读取额度缓存失败", errorText(error));
}
authPath = typeof storedPath === "string" ? storedPath : "";
autoRefresh = storedAutoRefresh !== false;
if (
storedSnapshot &&
typeof storedSnapshot === "object" &&
clampPercent(storedSnapshot.usedPercent) !== null &&
Number.isFinite(Number(storedSnapshot.resetAt))
) {
snapshot = {
...storedSnapshot,
limitStatus: String(storedSnapshot.limitStatus || ""),
spendControlReached: typeof storedSnapshot.spendControlReached === "boolean"
? storedSnapshot.spendControlReached
: null,
resetCredits: storedSnapshot.resetCredits && typeof storedSnapshot.resetCredits === "object"
? storedSnapshot.resetCredits
: { availableCount: 0, credits: [] }
};
}
let knownFolders;
try {
knownFolders = await host.fs.getKnownFolders();
} catch (error) {
throw new ComponentError("无法读取 Windows 用户目录", errorText(error));
}
const userProfile = String(knownFolders?.userProfile || "").trim();
if (userProfile) {
codexDirectory = joinWindowsPath(userProfile, ".codex");
defaultAuthPath = joinWindowsPath(codexDirectory, "auth.json");
}
if (authPath && !(await pathExists(authPath))) {
authPath = "";
}
if (!authPath && defaultAuthPath && await pathExists(defaultAuthPath)) {
authPath = defaultAuthPath;
try {
await host.state.write(STATE_AUTH_PATH, authPath);
} catch (error) {
showError(error, "默认登录文件已找到,但路径保存失败");
}
}
render();
startClock();
restartAutoRefresh();
if (authPath) {
await refreshUsage();
}
}
refreshButton.addEventListener("click", () => {
refreshUsage().catch(error => showError(error));
});
settingsButton.addEventListener("click", () => {
settingsOpen = !settingsOpen;
clearError();
render();
});
detectButton.addEventListener("click", () => {
detectDefaultAuth(true).catch(error => showError(error));
});
detectSettingsButton.addEventListener("click", () => {
settingsOpen = false;
detectDefaultAuth(true).catch(error => showError(error));
});
selectAuthButton.addEventListener("click", () => {
chooseAuthFile().catch(error => showError(error));
});
changeAuthButton.addEventListener("click", () => {
chooseAuthFile().catch(error => showError(error));
});
autoRefreshInput.addEventListener("change", () => {
changeAutoRefresh().catch(error => showError(error));
});
retryButton.addEventListener("click", () => {
if (authPath) {
refreshUsage().catch(error => showError(error));
} else {
detectDefaultAuth(true).catch(error => showError(error));
}
});
initialize().catch(error => {
showError(error, "组件初始化失败");
render();
});
</script>
</body>
</html>