mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-31 11:01:36 +08:00
feat: display subscription quota for Codex OAuth provider cards
Codex OAuth (ChatGPT Plus/Pro) providers previously fell through to the
default UsageFooter branch and showed no quota at all, while Copilot and
official Codex providers already had a wham/usage-backed quota footer.
This wires up the same five-hour / seven-day tier badges for codex_oauth
provider cards by reusing the existing query_codex_quota function and
SubscriptionQuotaFooter rendering, parameterized to keep both the CLI
credential path ("codex") and the cc-switch managed OAuth path
("codex_oauth") working from a single source of truth.
- Parameterize services::subscription::query_codex_quota with tool_label
and expired_message; promote SubscriptionQuota constructors to
pub(crate). The CLI path keeps its existing "codex" label and the
"re-login with Codex CLI" message; the new path passes "codex_oauth"
and a cc-switch-specific re-login hint.
- Add a new get_codex_oauth_quota Tauri command in commands/codex_oauth.rs
that resolves the ChatGPT account (explicit binding > default account
> not_found), pulls a valid access_token from CodexOAuthManager
(auto-refresh handled), and delegates to query_codex_quota.
- Extract SubscriptionQuotaFooter's render body into a pure
SubscriptionQuotaView component (props: quota / loading / refetch /
appIdForExpiredHint / inline). The existing SubscriptionQuotaFooter
becomes a thin wrapper with identical props and behavior, so
CopilotQuotaFooter and the official Claude/Codex/Gemini paths are
untouched. This avoids duplicating ~280 lines of five-state rendering.
- Add CodexOauthQuotaFooter, a 38-line wrapper that calls the new
useCodexOauthQuota hook and forwards to SubscriptionQuotaView.
- ProviderCard inserts an isCodexOauth branch between isCopilot and
isOfficial, keyed off PROVIDER_TYPES.CODEX_OAUTH (newly added to
config/constants.ts to centralize the previously scattered string).
- Frontend hook caches per (codex_oauth, accountId) so multiple cards
bound to the same ChatGPT account share one fetch via react-query
dedup; cards bound to different accounts get independent fetches.
- No new i18n keys: existing subscription.fiveHour / sevenDay / expired /
refresh / queryFailed / expiredHint are reused.
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
import React from "react";
|
||||
import type { ProviderMeta } from "@/types";
|
||||
import { useCodexOauthQuota } from "@/lib/query/subscription";
|
||||
import { SubscriptionQuotaView } from "@/components/SubscriptionQuotaFooter";
|
||||
|
||||
interface CodexOauthQuotaFooterProps {
|
||||
meta?: ProviderMeta;
|
||||
inline?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Codex OAuth (ChatGPT Plus/Pro 反代) 订阅额度 footer
|
||||
*
|
||||
* 复用 SubscriptionQuotaView 的全部渲染逻辑(5 状态 × inline/expanded)。
|
||||
* 数据源切换为 cc-switch 自管的 OAuth token 而非 Codex CLI 凭据。
|
||||
*/
|
||||
const CodexOauthQuotaFooter: React.FC<CodexOauthQuotaFooterProps> = ({
|
||||
meta,
|
||||
inline = false,
|
||||
}) => {
|
||||
const {
|
||||
data: quota,
|
||||
isFetching: loading,
|
||||
refetch,
|
||||
} = useCodexOauthQuota(meta, true);
|
||||
|
||||
return (
|
||||
<SubscriptionQuotaView
|
||||
quota={quota}
|
||||
loading={loading}
|
||||
refetch={refetch}
|
||||
appIdForExpiredHint="codex_oauth"
|
||||
inline={inline}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodexOauthQuotaFooter;
|
||||
@@ -3,13 +3,22 @@ import { RefreshCw, AlertCircle, Clock } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { AppId } from "@/lib/api";
|
||||
import { useSubscriptionQuota } from "@/lib/query/subscription";
|
||||
import type { QuotaTier } from "@/types/subscription";
|
||||
import type { QuotaTier, SubscriptionQuota } from "@/types/subscription";
|
||||
|
||||
interface SubscriptionQuotaFooterProps {
|
||||
appId: AppId;
|
||||
inline?: boolean;
|
||||
}
|
||||
|
||||
interface SubscriptionQuotaViewProps {
|
||||
quota: SubscriptionQuota | undefined;
|
||||
loading: boolean;
|
||||
refetch: () => void;
|
||||
/** 用于 `subscription.expiredHint` 的 {tool} 插值;解耦了 hook 的 appId */
|
||||
appIdForExpiredHint: string;
|
||||
inline?: boolean;
|
||||
}
|
||||
|
||||
/** 已知 tier 名称的显示映射(官方订阅 + Token Plan 共用) */
|
||||
export const TIER_I18N_KEYS: Record<string, string> = {
|
||||
five_hour: "subscription.fiveHour",
|
||||
@@ -78,16 +87,22 @@ function formatRelativeTime(
|
||||
return t("usage.daysAgo", { count: Math.floor(diff / 86400) });
|
||||
}
|
||||
|
||||
const SubscriptionQuotaFooter: React.FC<SubscriptionQuotaFooterProps> = ({
|
||||
appId,
|
||||
/**
|
||||
* 纯展示组件:渲染 SubscriptionQuota 的 5 种状态(not_found / parse_error /
|
||||
* expired / API 失败 / 成功),支持 inline / expanded 两种布局。
|
||||
*
|
||||
* 数据源由调用方 hook 注入,方便不同的额度后端复用同一套渲染逻辑:
|
||||
* - `SubscriptionQuotaFooter`(CLI 凭据路径,by appId)
|
||||
* - `CodexOauthQuotaFooter`(cc-switch 自管 OAuth 路径,by ChatGPT account)
|
||||
*/
|
||||
export const SubscriptionQuotaView: React.FC<SubscriptionQuotaViewProps> = ({
|
||||
quota,
|
||||
loading,
|
||||
refetch,
|
||||
appIdForExpiredHint,
|
||||
inline = false,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
data: quota,
|
||||
isFetching: loading,
|
||||
refetch,
|
||||
} = useSubscriptionQuota(appId, true);
|
||||
|
||||
// 定期更新相对时间显示
|
||||
const [now, setNow] = React.useState(Date.now());
|
||||
@@ -131,7 +146,7 @@ const SubscriptionQuotaFooter: React.FC<SubscriptionQuotaFooterProps> = ({
|
||||
<div>
|
||||
<span className="font-medium">{t("subscription.expired")}</span>
|
||||
<span className="ml-2 text-amber-500/70 dark:text-amber-400/70">
|
||||
{t("subscription.expiredHint", { tool: appId })}
|
||||
{t("subscription.expiredHint", { tool: appIdForExpiredHint })}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -364,4 +379,29 @@ const TierBar: React.FC<{
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* CLI 凭据路径下的薄 wrapper:通过 useSubscriptionQuota(appId) 自取数据
|
||||
* 后转发到 SubscriptionQuotaView。对外 props/行为与重构前完全一致。
|
||||
*/
|
||||
const SubscriptionQuotaFooter: React.FC<SubscriptionQuotaFooterProps> = ({
|
||||
appId,
|
||||
inline = false,
|
||||
}) => {
|
||||
const {
|
||||
data: quota,
|
||||
isFetching: loading,
|
||||
refetch,
|
||||
} = useSubscriptionQuota(appId, true);
|
||||
|
||||
return (
|
||||
<SubscriptionQuotaView
|
||||
quota={quota}
|
||||
loading={loading}
|
||||
refetch={refetch}
|
||||
appIdForExpiredHint={appId}
|
||||
inline={inline}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SubscriptionQuotaFooter;
|
||||
|
||||
@@ -13,6 +13,7 @@ import { ProviderIcon } from "@/components/ProviderIcon";
|
||||
import UsageFooter from "@/components/UsageFooter";
|
||||
import SubscriptionQuotaFooter from "@/components/SubscriptionQuotaFooter";
|
||||
import CopilotQuotaFooter from "@/components/CopilotQuotaFooter";
|
||||
import CodexOauthQuotaFooter from "@/components/CodexOauthQuotaFooter";
|
||||
import { PROVIDER_TYPES } from "@/config/constants";
|
||||
import { ProviderHealthBadge } from "@/components/providers/ProviderHealthBadge";
|
||||
import { FailoverPriorityBadge } from "@/components/providers/FailoverPriorityBadge";
|
||||
@@ -177,6 +178,8 @@ export function ProviderCard({
|
||||
const isCopilot =
|
||||
provider.meta?.providerType === PROVIDER_TYPES.GITHUB_COPILOT ||
|
||||
provider.meta?.usage_script?.templateType === "github_copilot";
|
||||
const isCodexOauth =
|
||||
provider.meta?.providerType === PROVIDER_TYPES.CODEX_OAUTH;
|
||||
|
||||
// 获取用量数据以判断是否有多套餐
|
||||
// 累加模式应用(OpenCode/OpenClaw):使用 isInConfig 代替 isCurrent
|
||||
@@ -355,6 +358,8 @@ export function ProviderCard({
|
||||
<div className="flex items-center gap-1">
|
||||
{isCopilot ? (
|
||||
<CopilotQuotaFooter meta={provider.meta} inline={true} />
|
||||
) : isCodexOauth ? (
|
||||
<CodexOauthQuotaFooter meta={provider.meta} inline={true} />
|
||||
) : isOfficial ? (
|
||||
<SubscriptionQuotaFooter appId={appId} inline={true} />
|
||||
) : hasMultiplePlans ? (
|
||||
|
||||
Reference in New Issue
Block a user