Codex周额度

经验创意 · 139 次浏览
困困君 创建于 2026-07-18 17:40

<!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>


回复内容
暂无回复
回复主贴