mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-31 02:51:21 +08:00
feat: add Grok official subscription quota query
Add SuperGrok subscription usage display, following the existing Claude Code / Codex official-subscription pattern (protocol ported from steipete/CodexBar): - New subscription_grok service: reads Grok CLI credentials from ~/.grok/auth.json, calls the grok.com GrokBuildBilling gRPC-web endpoint, and parses the response via heuristic protobuf scanning (used percent, reset time, zero-usage special case) - Transient failures (network errors, HTTP 408, gRPC deadline/ cancelled) propagate as Err so the frontend retries and keeps the last good value; auth failures map to Expired with a re-login hint - Tier naming by reset distance: weekly limit, monthly, or a new "credits" tier (i18n added for zh/en/ja/zh-TW; tray shows "c") - New get_xai_oauth_quota command: xai_oauth providers (managed SuperGrok OAuth accounts) query the same billing endpoint with their bound account token; ProviderCard auto-renders the quota footer for them and hides the usage-script entry, and the tray / usage-script path routes xai_oauth providers to the managed account instead of the host app's CLI credentials - UsageScriptModal: drop the config-content heuristic for official detection; category === "official" is the single source of truth Claude-Session: https://claude.ai/code/session_01LSNvhEfuoJHaQLZcYQgBU5
This commit is contained in:
@@ -37,6 +37,8 @@ export const TIER_I18N_KEYS: Record<string, string> = {
|
||||
weekly_limit: "subscription.sevenDay",
|
||||
// 火山方舟 Agent Plan / Coding Plan 的月窗口
|
||||
monthly: "subscription.monthly",
|
||||
// Grok credit 额度的兜底窗口(重置距离可识别时归入 weekly_limit/monthly)
|
||||
credits: "subscription.credits",
|
||||
// GitHub Copilot
|
||||
premium: "subscription.copilotPremium",
|
||||
};
|
||||
@@ -422,7 +424,8 @@ const SubscriptionQuotaFooter: React.FC<SubscriptionQuotaFooterProps> = ({
|
||||
quota={quota}
|
||||
loading={loading}
|
||||
refetch={refetch}
|
||||
appIdForExpiredHint={appId}
|
||||
// expiredHint 里的 {tool} 是 CLI 命令名:Grok 的命令是 `grok` 而非 appId
|
||||
appIdForExpiredHint={appId === "grokbuild" ? "grok" : appId}
|
||||
inline={inline}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -160,7 +160,7 @@ function detectBalanceProvider(baseUrl: string | undefined): boolean {
|
||||
}
|
||||
|
||||
function isOfficialSubscriptionProvider(provider: Provider, appId: AppId) {
|
||||
if (!["claude", "codex", "gemini"].includes(appId)) return false;
|
||||
if (!["claude", "codex", "gemini", "grokbuild"].includes(appId)) return false;
|
||||
if (provider.category === "official") return true;
|
||||
|
||||
const config = provider.settingsConfig as Record<string, any>;
|
||||
@@ -188,6 +188,12 @@ function isOfficialSubscriptionProvider(provider: Provider, appId: AppId) {
|
||||
(!baseUrl || (typeof baseUrl === "string" && baseUrl.trim() === ""))
|
||||
);
|
||||
}
|
||||
// grokbuild 不做配置启发式,只认上方的 category === "official":官方态判定
|
||||
// 在后端是 TOML 解析(grok_config::is_official_live_config),正则无法忠实
|
||||
// 镜像(引号键/inline table/非法 TOML 均会误判为官方),误判会让本组件的
|
||||
// state 初始化丢弃已保存的非官方脚本。claude/codex/gemini 的启发式建立在
|
||||
// 已解析的 JSON 字段上是精确的,不受此限。官方判定以 category 为 SSOT 的
|
||||
// 理由见 ProviderCard 中的注释。
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import React from "react";
|
||||
import type { ProviderMeta } from "@/types";
|
||||
import { useXaiOauthQuota } from "@/lib/query/subscription";
|
||||
import { SubscriptionQuotaView } from "@/components/SubscriptionQuotaFooter";
|
||||
|
||||
interface XaiOauthQuotaFooterProps {
|
||||
meta?: ProviderMeta;
|
||||
inline?: boolean;
|
||||
/** 是否为当前激活的供应商 */
|
||||
isCurrent?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* xAI OAuth (SuperGrok 反代) 订阅额度 footer
|
||||
*
|
||||
* 复用 SubscriptionQuotaView 的全部渲染逻辑(5 状态 × inline/expanded)。
|
||||
* 数据源为 cc-switch 自管的 xAI OAuth token,与 Grok Build 分区读 Grok CLI
|
||||
* 凭据的路径查同一个 grok.com 账单端点,展示同一份订阅额度。
|
||||
*/
|
||||
const XaiOauthQuotaFooter: React.FC<XaiOauthQuotaFooterProps> = ({
|
||||
meta,
|
||||
inline = false,
|
||||
isCurrent = false,
|
||||
}) => {
|
||||
const {
|
||||
data: quota,
|
||||
isFetching: loading,
|
||||
refetch,
|
||||
} = useXaiOauthQuota(meta, { enabled: true, autoQuery: isCurrent });
|
||||
|
||||
return (
|
||||
<SubscriptionQuotaView
|
||||
quota={quota}
|
||||
loading={loading}
|
||||
refetch={refetch}
|
||||
appIdForExpiredHint="xai_oauth"
|
||||
inline={inline}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default XaiOauthQuotaFooter;
|
||||
@@ -14,6 +14,7 @@ import UsageFooter from "@/components/UsageFooter";
|
||||
import SubscriptionQuotaFooter from "@/components/SubscriptionQuotaFooter";
|
||||
import CopilotQuotaFooter from "@/components/CopilotQuotaFooter";
|
||||
import CodexOauthQuotaFooter from "@/components/CodexOauthQuotaFooter";
|
||||
import XaiOauthQuotaFooter from "@/components/XaiOauthQuotaFooter";
|
||||
import { PROVIDER_TYPES, TEMPLATE_TYPES } from "@/config/constants";
|
||||
import { isHermesReadOnlyProvider } from "@/config/hermesProviderPresets";
|
||||
import { ProviderHealthBadge } from "@/components/providers/ProviderHealthBadge";
|
||||
@@ -196,7 +197,7 @@ export function ProviderCard({
|
||||
const usageEnabled = provider.meta?.usage_script?.enabled ?? false;
|
||||
const isOfficial = isOfficialProvider(provider, appId);
|
||||
const supportsOfficialSubscription =
|
||||
isOfficial && ["claude", "codex", "gemini"].includes(appId);
|
||||
isOfficial && ["claude", "codex", "gemini", "grokbuild"].includes(appId);
|
||||
const isOfficialSubscriptionUsage =
|
||||
provider.meta?.usage_script?.templateType ===
|
||||
TEMPLATE_TYPES.OFFICIAL_SUBSCRIPTION;
|
||||
@@ -227,6 +228,8 @@ export function ProviderCard({
|
||||
appId === "hermes" && isHermesReadOnlyProvider(provider.settingsConfig);
|
||||
const isCodexOauth =
|
||||
provider.meta?.providerType === PROVIDER_TYPES.CODEX_OAUTH;
|
||||
// xAI OAuth (SuperGrok 反代):额度经自管 OAuth token 自动显示,与 codex_oauth 同构
|
||||
const isXaiOauth = provider.meta?.providerType === PROVIDER_TYPES.XAI_OAUTH;
|
||||
// 统一权威谓词(详见 providerNeedsRouting):以 providerType 为准,不受
|
||||
// apiFormat 被改动/缺省影响。此 badge 仅在 Codex 视图渲染,故加 appId 守卫。
|
||||
const codexNeedsRouting =
|
||||
@@ -493,6 +496,12 @@ export function ProviderCard({
|
||||
inline={true}
|
||||
isCurrent={isCurrent}
|
||||
/>
|
||||
) : isXaiOauth ? (
|
||||
<XaiOauthQuotaFooter
|
||||
meta={provider.meta}
|
||||
inline={true}
|
||||
isCurrent={isCurrent}
|
||||
/>
|
||||
) : isOfficial ? (
|
||||
officialSubscriptionEnabled ? (
|
||||
<SubscriptionQuotaFooter
|
||||
@@ -573,7 +582,8 @@ export function ProviderCard({
|
||||
onConfigureUsage={
|
||||
(isOfficial && !supportsOfficialSubscription) ||
|
||||
isCopilot ||
|
||||
isCodexOauth
|
||||
isCodexOauth ||
|
||||
isXaiOauth
|
||||
? undefined
|
||||
: () => onConfigureUsage(provider)
|
||||
}
|
||||
|
||||
@@ -3006,6 +3006,7 @@
|
||||
"geminiFlashLite": "Flash Lite",
|
||||
"weeklyLimit": "Weekly",
|
||||
"monthly": "Monthly",
|
||||
"credits": "Credits",
|
||||
"copilotPremium": "Premium",
|
||||
"utilization": "{{value}}%",
|
||||
"resetsIn": "Resets in {{time}}",
|
||||
|
||||
@@ -3006,6 +3006,7 @@
|
||||
"geminiFlashLite": "Flash Lite",
|
||||
"weeklyLimit": "週間",
|
||||
"monthly": "月間",
|
||||
"credits": "クレジット",
|
||||
"copilotPremium": "プレミアム",
|
||||
"utilization": "{{value}}%",
|
||||
"resetsIn": "{{time}}後にリセット",
|
||||
|
||||
@@ -2978,6 +2978,7 @@
|
||||
"geminiFlashLite": "Flash Lite",
|
||||
"weeklyLimit": "每週",
|
||||
"monthly": "每月",
|
||||
"credits": "額度",
|
||||
"copilotPremium": "進階請求",
|
||||
"utilization": "{{value}}%",
|
||||
"resetsIn": "{{time}} 後重設",
|
||||
|
||||
@@ -3006,6 +3006,7 @@
|
||||
"geminiFlashLite": "Flash Lite",
|
||||
"weeklyLimit": "每周",
|
||||
"monthly": "每月",
|
||||
"credits": "额度",
|
||||
"copilotPremium": "高级请求",
|
||||
"utilization": "{{value}}%",
|
||||
"resetsIn": "{{time}}后重置",
|
||||
|
||||
@@ -6,6 +6,8 @@ export const subscriptionApi = {
|
||||
invoke("get_subscription_quota", { tool }),
|
||||
getCodexOauthQuota: (accountId: string | null): Promise<SubscriptionQuota> =>
|
||||
invoke("get_codex_oauth_quota", { accountId }),
|
||||
getXaiOauthQuota: (accountId: string | null): Promise<SubscriptionQuota> =>
|
||||
invoke("get_xai_oauth_quota", { accountId }),
|
||||
getCodingPlanQuota: (
|
||||
baseUrl: string,
|
||||
apiKey: string,
|
||||
|
||||
@@ -90,7 +90,8 @@ export function useSubscriptionQuota(
|
||||
const query = useQuery({
|
||||
queryKey: subscriptionKeys.quota(appId),
|
||||
queryFn: () => subscriptionApi.getQuota(appId),
|
||||
enabled: enabled && ["claude", "codex", "gemini"].includes(appId),
|
||||
enabled:
|
||||
enabled && ["claude", "codex", "gemini", "grokbuild"].includes(appId),
|
||||
refetchInterval,
|
||||
refetchIntervalInBackground: Boolean(refetchInterval),
|
||||
refetchOnWindowFocus: Boolean(refetchInterval),
|
||||
@@ -138,3 +139,30 @@ export function useCodexOauthQuota(
|
||||
|
||||
return useQuotaKeepLastGood(query, accountId ?? "default");
|
||||
}
|
||||
|
||||
/**
|
||||
* xAI OAuth (SuperGrok 反代) 订阅额度查询 hook
|
||||
*
|
||||
* 与 `useCodexOauthQuota` 平行:数据走 cc-switch 自管的 xAI OAuth token,
|
||||
* 而不是 Grok CLI 的 ~/.grok/auth.json;后端复用同一个 grok.com 账单端点,
|
||||
* 因此与 Grok Build 分区的官方订阅显示同一份额度。
|
||||
*/
|
||||
export function useXaiOauthQuota(
|
||||
meta: ProviderMeta | undefined,
|
||||
options: UseCodexOauthQuotaOptions = {},
|
||||
) {
|
||||
const { enabled = true, autoQuery = false } = options;
|
||||
const accountId = resolveManagedAccountId(meta, PROVIDER_TYPES.XAI_OAUTH);
|
||||
const query = useQuery({
|
||||
queryKey: ["xai_oauth", "quota", accountId ?? "default"],
|
||||
queryFn: () => subscriptionApi.getXaiOauthQuota(accountId),
|
||||
enabled,
|
||||
refetchInterval: autoQuery ? REFETCH_INTERVAL : false,
|
||||
refetchIntervalInBackground: autoQuery,
|
||||
refetchOnWindowFocus: autoQuery,
|
||||
staleTime: REFETCH_INTERVAL,
|
||||
retry: 1,
|
||||
});
|
||||
|
||||
return useQuotaKeepLastGood(query, accountId ?? "default");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user