import React, { lazy, Suspense, useEffect, useRef, useState } from "react"; import { createRoot } from "react-dom/client"; import { BrowserRouter, Link, Navigate, Route, Routes, useLocation, useNavigate, } from "react-router-dom"; import { Activity, Bell, ChevronDown, Clock, Coins, Github, Gauge, LogIn, Mail, LogOut, Moon, MoreHorizontal, Settings, Sun, Ticket, Trash2, TrendingUp, Zap, Wifi, WifiOff, } from "lucide-react"; import { api, del, get, post, put, toErrorMessage } from "./api"; import { AuthProvider, useAuth } from "./auth"; import { ThemeProvider, useTheme } from "./theme"; import { decodeAccounts, decodeAction, decodeCode, decodeDashboard, decodeGeneral, decodeOK, decodeSMTP, decodeTelegram, type Account, type Dashboard as Dash, type GeneralSettings, type Limit, type SMTPSettingsForm, type TelegramSettingsForm, } from "./types"; import "./styles.css"; const UsageChart = lazy(() => import("./usage-chart")); const repositoryURL = "https://github.com/zhoujun0601/codex-helper"; function GitHubLink({ className = "" }: { className?: string }) { return ( GitHub ); } function App() { const { status, authenticated } = useAuth(); if (!status.initialized) return ; return ( {authenticated ? ( } /> ) : ( <> } /> } /> )} ); } const Splash = () => (
Codex Helper
); function Brand({ version }: { version?: string }) { return (
Codex Helper {version && v{version}}
); } function Setup({ version }: { version: string }) { const { setup } = useAuth(); const [form, set] = useState({ username: "admin", password: "", timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, }); const [err, setErr] = useState(""); async function submit(e: React.FormEvent) { e.preventDefault(); try { await setup(form.username, form.password, form.timezone); } catch (x) { setErr(toErrorMessage(x)); } } return (

连接你的 Codex
看见每一次增长

一个容器内完成用量洞察、重置提醒与账户管理。

首次初始化 · 1 / 1

创建管理员

{err &&

{err}

}

Codex、Telegram 与 SMTP 可在进入系统后通过设置中心完成。

); } function Login({ version }: { version: string }) { const { login } = useAuth(); const [u, setU] = useState("admin"), [p, setP] = useState(""), [e, setE] = useState(""); return (
{ x.preventDefault(); try { await login(u, p); } catch (q) { setE(toErrorMessage(q)); } }} >

欢迎回来

{e &&

{e}

}

查看公开总览

); } function PublicShell({ version }: { version: string }) { const nav = useNavigate(); return (
} /> } />
); } function Shell({ version }: { version: string }) { const { logout } = useAuth(); const nav = useNavigate(); const location = useLocation(); const { theme, saveTheme, error: themeError } = useTheme(); const [shellError, setShellError] = useState(""); const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const mobileMenuRef = useRef(null); const mobileMenuButtonRef = useRef(null); useEffect(() => setMobileMenuOpen(false), [location.pathname]); useEffect(() => { if (!mobileMenuOpen) return; const close = (event: PointerEvent) => { if (!mobileMenuRef.current?.contains(event.target as Node)) setMobileMenuOpen(false); }; const escape = (event: KeyboardEvent) => { if (event.key !== "Escape") return; setMobileMenuOpen(false); mobileMenuButtonRef.current?.focus(); }; document.addEventListener("pointerdown", close); document.addEventListener("keydown", escape); return () => { document.removeEventListener("pointerdown", close); document.removeEventListener("keydown", escape); }; }, [mobileMenuOpen]); const changeTheme = () => void saveTheme(theme === "dark" ? "light" : "dark").catch(() => undefined); const signOut = async () => { if (!confirm("确定要退出 Codex Helper 管理后台吗?")) return; try { setShellError(""); await logout(); } catch (error) { setShellError(toErrorMessage(error)); } }; return (
{mobileMenuOpen && (
{(themeError || shellError) && ( {themeError || shellError} )}
)}
} /> } /> } />
); } type DashboardCardState = { dashboard: Dash | null; loading: boolean; error: string; }; const emptyDashboardCardState = (): DashboardCardState => ({ dashboard: null, loading: false, error: "", }); function Dashboard({ publicView = false }: { publicView?: boolean }) { const [accounts, setAccounts] = useState(null), [cards, setCards] = useState>({}), [expanded, setExpanded] = useState>({}), [error, setError] = useState(""); const requestRef = useRef(new Map()), activeControllersRef = useRef(new Map()); const updateCard = ( accountId: number, patch: Partial, ) => { setCards((current) => ({ ...current, [accountId]: { ...(current[accountId] || emptyDashboardCardState()), ...patch, }, })); }; const loadDashboard = async (accountId: number, signal?: AbortSignal) => { const request = (requestRef.current.get(accountId) || 0) + 1; requestRef.current.set(accountId, request); let ownController: AbortController | undefined; if (!signal) { activeControllersRef.current.get(accountId)?.abort(); ownController = new AbortController(); activeControllersRef.current.set(accountId, ownController); signal = ownController.signal; } updateCard(accountId, { loading: true, error: "", }); try { const dashboard = await get( "dashboard?accountId=" + accountId, decodeDashboard, signal, ); if ( requestRef.current.get(accountId) !== request || dashboard.accountId !== accountId ) return; updateCard(accountId, { dashboard, loading: false, error: "", }); } catch (value) { if (signal.aborted || requestRef.current.get(accountId) !== request) return; updateCard(accountId, { loading: false, error: toErrorMessage(value), }); } finally { if ( ownController && activeControllersRef.current.get(accountId) === ownController ) activeControllersRef.current.delete(accountId); } }; const loadAccounts = async () => { try { const values = await get("accounts", decodeAccounts); setAccounts(values); setError(""); setExpanded((current) => { const valid = new Set(values.map((account) => account.id)); return Object.fromEntries( Object.entries(current).filter(([id]) => valid.has(+id)), ); }); } catch (value) { setError(toErrorMessage(value)); } }; useEffect(() => { void loadAccounts(); }, []); useEffect(() => { if (!accounts?.length) return; const controller = new AbortController(); let timer = 0; const poll = async () => { await Promise.allSettled( accounts.map((account) => loadDashboard(account.id, controller.signal)), ); if (!controller.signal.aborted) timer = window.setTimeout(poll, 30_000); }; void poll(); return () => { controller.abort(); window.clearTimeout(timer); }; }, [accounts]); useEffect( () => () => { activeControllersRef.current.forEach((controller) => controller.abort()); }, [], ); if (accounts === null) return ( <>
{error ? (

{error}

) : ( )} ); if (!accounts.length) return ( <>
尚未添加 Codex 账号
); return ( <>
{error &&
{error}
}
{accounts.map((account) => ( setExpanded((current) => ({ ...current, [account.id]: !current[account.id], })) } /> ))}
); } function AccountOverviewCard({ account, state, publicView, expanded, onToggle, }: { account: Account; state: DashboardCardState; publicView: boolean; expanded: boolean; onToggle: () => void; }) { const dashboard = state.dashboard; const email = dashboard?.account.email ?? account.email; const plan = dashboard?.account.planType ?? account.planType; const connected = dashboard?.account.connected ?? account.connected; const title = dashboard?.displayName || account.displayName; return (
{expanded && (
{dashboard ? ( ) : (
{state.loading &&
}

{state.error || "正在读取账号用量…"}

)}
)}
); } function AccountQuickSummary({ dashboard, state, }: { dashboard: Dash | null; state: DashboardCardState; }) { if (!dashboard) return ( {state.loading ? "读取中…" : state.error ? "读取失败,展开查看" : "等待数据"} ); const limits = quickSummaryLimits(dashboard.limits); if (!limits.length) return 限额暂无; return ( {limits.map(({ kind, limit }) => { const remaining = Math.round(remainingPercent(limit)); return ( {quickLimitLabel(kind, limit)} {remaining}% 剩余 {limit.resetsAt ? `${relative(limit.resetsAt)}重置` : "重置时间暂无"} ); })} ); } function AccountDashboardDetails({ dashboard, state, publicView, }: { dashboard: Dash; state: DashboardCardState; publicView: boolean; }) { return ( <>
{dashboard.account.connected ? : }
Codex Account

{dashboard.displayName}

更新于{" "} {dashboard.fetchedAt ? new Date(dashboard.fetchedAt * 1000).toLocaleString() : "尚未同步"}
{(state.error || dashboard.lastError) && (
{state.error || dashboard.lastError}
)}
} label="登录邮箱" value={ publicView ? "登录后查看" : dashboard.account.email ? maskEmail(dashboard.account.email) : "尚未连接" } /> : } label="连接状态" value={dashboard.account.connected ? "已连接" : "未连接"} /> } label="套餐" value={planLabel(dashboard.account.planType)} /> } label="数据状态" value={dashboard.stale ? "数据可能已过期" : "数据已更新"} />
{dashboard.resetCredits && dashboard.resetCredits.availableCount > 0 && (
重置卡 可用 {dashboard.resetCredits.availableCount} 张
到期时间 {formatResetCreditExpiry(dashboard.resetCredits.expiresAt)}
)}
{dashboard.limits.map((limit, index) => ( ))}
{!dashboard.limits.length && (
{publicView ? "该连接暂无限额数据" : "该连接暂无限额数据,服务器会自动同步。"}
)} {dashboard.currentCycle && (
本周期:{cycleTimestamp(dashboard.currentCycle.startedAt)} 至{" "} {cycleTimestamp(dashboard.currentCycle.resetsAt)}( {cycleDuration(dashboard.currentCycle.windowDurationMinutes)} ,按每日数据汇总)
)}
} label="本周期 Tokens" v={num(dashboard.currentCycle?.totalTokens)} /> } label="本周期单日峰值" v={num(dashboard.summary.peakDailyTokens)} />
}> ); } function AccountInfo({ icon, label, value, }: { icon: React.ReactNode; label: string; value: string; }) { return (
{icon}
{label} {value}
); } function LimitCard({ x }: { x: Limit }) { const used = Math.min(100, Math.max(0, x.usedPercent)), left = 100 - used, usedLabel = Math.round(used), leftLabel = Math.round(left), leftColor = `hsl(${left * 1.2} 70% 45%)`; return (
{limitLabel(x)}
{leftLabel} % 剩余
下次重置
{x.resetsAt ? new Date(x.resetsAt * 1000).toLocaleString() : "暂未提供"} {x.resetsAt ? relative(x.resetsAt) : ""}
); } const Stat = ({ icon, label, v, muted = false, }: { icon?: React.ReactNode; label: string; v: string; muted?: boolean; }) => (
{icon} {label}
{v}
); type SettingsTab = "general" | "codex" | "telegram" | "smtp"; const settingsTabs: Array<{ id: SettingsTab; label: string; icon: React.ReactNode; content: React.ReactNode; }> = [ { id: "general", label: "通用", icon: , content: }, { id: "codex", label: "Codex", icon: , content: }, { id: "telegram", label: "Telegram", icon: , content: , }, { id: "smtp", label: "SMTP 邮件", icon: , content: }, ]; function SettingsPage() { const [tab, setTab] = useState("general"); const selectAdjacentTab = ( event: React.KeyboardEvent, currentIndex: number, ) => { let nextIndex: number | undefined; if (event.key === "ArrowRight" || event.key === "ArrowDown") nextIndex = (currentIndex + 1) % settingsTabs.length; if (event.key === "ArrowLeft" || event.key === "ArrowUp") nextIndex = (currentIndex - 1 + settingsTabs.length) % settingsTabs.length; if (event.key === "Home") nextIndex = 0; if (event.key === "End") nextIndex = settingsTabs.length - 1; if (nextIndex === undefined) return; event.preventDefault(); const nextTab = settingsTabs[nextIndex].id; setTab(nextTab); document.getElementById(`settings-tab-${nextTab}`)?.focus(); }; return ( <>
{settingsTabs.map((item, index) => ( ))}
{settingsTabs.map((item) => { const active = tab === item.id; return (
{item.content}
); })}
); } const SettingsLoading = () => (
); function General() { const [v, setV] = useState(null), [msg, setMsg] = useState(""), [loadError, setLoadError] = useState(""); const { applyTheme } = useTheme(); const loadGeneral = async () => { try { setLoadError(""); const value = await get("settings/general", decodeGeneral); setV(value); applyTheme(value.theme); } catch (error) { setLoadError(toErrorMessage(error)); } }; useEffect(() => { void loadGeneral(); }, []); if (!v) return loadError ? (

{loadError}

) : ( ); return (
{ e.preventDefault(); try { const saved = await api("settings/general", decodeGeneral, { method: "PUT", body: JSON.stringify(v), }); setV(saved); applyTheme(saved.theme); setMsg("设置已保存"); } catch (x) { setMsg(toErrorMessage(x)); } }} >

通用设置

自动同步 服务器每 5 分钟自动刷新所有账号
{msg &&

{msg}

}
); } function CodexSettings() { const [xs, setXs] = useState([]), [active, setActive] = useState(0), [newAuthIndex, setNewAuthIndex] = useState(""), [newKind, setNewKind] = useState<"personal" | "team">("team"), [newPublicVisible, setNewPublicVisible] = useState(false), [busy, setBusy] = useState(false), [err, setErr] = useState(""); const load = async (signal?: AbortSignal) => { const accounts = await get("accounts", decodeAccounts, signal); setXs(accounts); return accounts; }; useEffect(() => { void load().catch((error) => setErr(toErrorMessage(error))); }, []); const updateAccount = async ( account: Account, changes: Partial< Pick< Account, "displayName" | "authIndex" | "expectedKind" | "publicVisible" > >, ) => { try { setErr(""); await put( `accounts/${account.id}`, decodeOK, { displayName: changes.displayName ?? account.displayName, authIndex: changes.authIndex ?? account.authIndex, expectedKind: changes.expectedKind ?? account.expectedKind, publicVisible: changes.publicVisible ?? account.publicVisible, }, changes.authIndex === undefined ? 30_000 : 60_000, ); await load(); return true; } catch (error) { setErr(toErrorMessage(error)); return false; } }; const add = async () => { const authIndex = newAuthIndex.trim(); if (!authIndex) { setErr("请输入 CPA authIndex"); return; } try { setBusy(true); setActive(0); setErr(""); const account = await post( "accounts", (value) => decodeAccounts([value])[0], { displayName: "", authIndex, expectedKind: newKind, publicVisible: newPublicVisible, }, undefined, 60_000, ); setNewAuthIndex(""); await load(); setActive(account.id); } catch (error) { setErr(toErrorMessage(error)); } finally { setBusy(false); } }; return (

Codex 账户与工作区

账号凭据和 OAuth 刷新由 CLIProxyAPI 管理;这里通过 CPA authIndex 绑定需要展示的 Codex 账号。

添加 CPA 账号

authIndex 可从 CLIProxyAPI 的认证文件列表中取得。

{xs.length === 0 && (
尚未绑定 CPA Codex 账号,请在上方输入 authIndex。
)} {xs.map((account) => (
{ const input = event.currentTarget; const displayName = input.value.trim(); if ( displayName && displayName !== account.displayName && !(await updateAccount(account, { displayName })) ) input.value = account.displayName; }} />
{account.email ? maskEmail(account.email) : "账号信息暂不可用"} {planLabel(account.planType)} {validationLabel(account)}
{account.possibleDuplicate && ( 同一邮箱已有相同类型绑定,请确认 authIndex 没有指向重复凭据 )}
))}
{err &&

{err}

}
); } function Telegram() { const [v, setV] = useState(null), [msg, setMsg] = useState(""), [code, setCode] = useState(""), [loadError, setLoadError] = useState(""); const loadTelegram = async () => { try { setLoadError(""); const value = await get("settings/telegram", decodeTelegram); setV({ ...value, token: "" }); } catch (error) { setLoadError(toErrorMessage(error)); } }; useEffect(() => { void loadTelegram(); }, []); if (!v) return loadError ? (

{loadError}

) : ( ); return (
{ e.preventDefault(); try { const x = await api("settings/telegram", decodeTelegram, { method: "PUT", body: JSON.stringify(v), }); setV({ ...x, token: "" }); setMsg(x.warning || "Bot 已验证并保存"); } catch (x) { setMsg(toErrorMessage(x)); } }} >

Telegram Bot

Bot 绑定后会自动启用额度提醒和查询菜单。

{code && (
向 Bot 发送 /bind {code}
)} {msg &&

{msg}

}
); } function SMTP() { const [v, setV] = useState(null), [msg, setMsg] = useState(""), [loadError, setLoadError] = useState(""); const loadSMTP = async () => { try { setLoadError(""); const value = await get("settings/smtp", decodeSMTP); setV({ ...value, password: "" }); } catch (error) { setLoadError(toErrorMessage(error)); } }; useEffect(() => { void loadSMTP(); }, []); if (!v) return loadError ? (

{loadError}

) : ( ); const save = async (e: React.FormEvent) => { e.preventDefault(); try { const saved = await api("settings/smtp", decodeSMTP, { method: "PUT", body: JSON.stringify(v), }); setV({ ...saved, password: "" }); setMsg("SMTP 设置已保存"); } catch (x) { setMsg(toErrorMessage(x)); } }; return (

SMTP 邮件

{msg &&

{msg}

}
); } function Header({ title, sub, children, }: { title: string; sub?: string; children?: React.ReactNode; }) { return (

{title}

{sub &&

{sub}

}
{children}
); } const num = (v?: number | null) => v == null ? "暂无" : new Intl.NumberFormat("en", { notation: "compact", maximumFractionDigits: 1, }).format(v); const cycleTimestamp = (value: number) => new Date(value * 1000).toLocaleString(); const formatResetCreditExpiry = (values: number[]) => values.length ? values.map(cycleTimestamp).join("、") : "服务端未提供"; const cycleDuration = (minutes: number) => minutes % (24 * 60) === 0 ? `${minutes / (24 * 60)} 天窗口` : minutes % 60 === 0 ? `${minutes / 60} 小时窗口` : `${minutes} 分钟窗口`; const remainingPercent = (limit: Limit) => 100 - Math.min(100, Math.max(0, limit.usedPercent)); type QuickLimitKind = "hour" | "week" | "other"; const quickSummaryLimits = ( limits: Limit[], ): Array<{ kind: QuickLimitKind; limit: Limit }> => { const sorted = [...limits].sort( (a, b) => a.windowDurationMinutes - b.windowDurationMinutes, ); const hour = sorted.find( (limit) => limit.windowDurationMinutes > 0 && limit.windowDurationMinutes < 24 * 60, ); const week = sorted.find( (limit) => limit.limitId.toLowerCase() === "codex" && limit.windowDurationMinutes === 168 * 60, ) ?? sorted.find( (limit) => limit.limitName == null && limit.windowDurationMinutes === 168 * 60, ); const selected: Array<{ kind: QuickLimitKind; limit: Limit }> = []; if (hour) selected.push({ kind: "hour", limit: hour }); if (week && week !== hour) selected.push({ kind: "week", limit: week }); if (selected.length) return selected; return sorted.slice(0, 2).map((limit) => ({ kind: "other", limit })); }; const quickLimitLabel = (kind: QuickLimitKind, limit: Limit) => { if (kind === "week") return "周额度"; if (kind === "hour") { const hours = limit.windowDurationMinutes / 60; return `${Number.isInteger(hours) ? hours : hours.toFixed(1)} 小时额度`; } return limitLabel(limit).replace(/限额$/, "额度"); }; const limitLabel = (limit: Limit) => { if (limit.limitName) return limit.limitName; if (limit.windowDurationMinutes >= 60) { const hours = limit.windowDurationMinutes / 60; return `${Number.isInteger(hours) ? hours : hours.toFixed(1)} 小时限额`; } if (limit.windowDurationMinutes > 0) return `${limit.windowDurationMinutes} 分钟限额`; return limit.windowType || "额度窗口"; }; const maskEmailPart = (part: string) => { if (part.length <= 1) return "*"; if (part.length === 2) return part[0] + "*"; return part[0] + "*".repeat(part.length - 2) + part.at(-1); }; const maskEmail = (email: string) => { const at = email.lastIndexOf("@"); if (at < 1 || at === email.length - 1) return maskEmailPart(email); const local = email.slice(0, at); const domain = email.slice(at + 1).split("."); return `${maskEmailPart(local)}@${domain .map((part, index) => index === domain.length - 1 ? part : maskEmailPart(part), ) .join(".")}`; }; const planLabel = (plan?: string | null) => { if (!plan) return "未识别套餐"; const labels: Record = { free: "Free", go: "Go", plus: "Plus", pro: "Pro", prolite: "Pro Lite", team: "Team", business: "Business", self_serve_business_prolite: "Business Pro Lite", self_serve_business_usage_based: "Business(按量)", enterprise: "Enterprise", edu: "Edu", }; return labels[plan.toLowerCase()] || plan; }; const validationLabel = (account: Account) => { if (account.expectedKind === "any") return "未启用套餐类型校验"; if (account.validationStatus === "pending") return `等待验证${account.expectedKind === "team" ? " Team / Business" : "个人订阅"}`; if (account.validationStatus === "matched") return account.actualKind === "team" ? "已连接 Team / Business 工作区" : "已连接个人订阅"; if (account.validationStatus === "mismatch") return `类型不匹配:实际为 ${planLabel(account.planType)},请退出后重新授权`; return `无法验证工作区类型:${planLabel(account.planType)}`; }; const relative = (ts: number) => { const s = Math.max(0, ts - Math.floor(Date.now() / 1000)); if (!s) return "即将重置"; const d = Math.floor(s / 86400), h = Math.floor((s % 86400) / 3600), m = Math.floor((s % 3600) / 60); return d ? `${d} 天 ${h} 小时后` : h ? `${h} 小时 ${m} 分后` : `${m} 分钟后`; }; const root = document.getElementById("root"); if (!root) throw new Error("缺少 #root 挂载节点"); createRoot(root).render( , );