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
看见每一次增长
一个容器内完成用量洞察、重置提醒与账户管理。
);
}
function Login({ version }: { version: string }) {
const { login } = useAuth();
const [u, setU] = useState("admin"),
[p, setP] = useState(""),
[e, setE] = useState("");
return (
);
}
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 (
);
}
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;
}) => (
);
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 (
);
})}
>
);
}
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 (
);
}
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 账号。
{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 (
);
}
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 (
);
}
function Header({
title,
sub,
children,
}: {
title: string;
sub?: string;
children?: React.ReactNode;
}) {
return (
);
}
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(
,
);