mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-25 13:45:03 +08:00
feat: 新增 ZenMux Token Plan 供应商,支持手动凭证与 USD 额度富展示 (#2709)
* feat(Token plan): 增加 ZenMux 支持 * chore: format code with prettier * chore: format code with cargo fmt --------- Co-authored-by: 明桓 <jihaodong.jhd@oceanbase.com> Co-authored-by: Jason <farion1231@gmail.com>
This commit is contained in:
@@ -309,6 +309,8 @@ export const TierBadge: React.FC<{
|
||||
: tier.name;
|
||||
const countdown = countdownStr(tier.resetsAt);
|
||||
|
||||
const hasUsd = tier.usedValueUsd != null && tier.maxValueUsd != null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-0.5">
|
||||
<span className="text-gray-500 dark:text-gray-400">{label}:</span>
|
||||
@@ -317,6 +319,11 @@ export const TierBadge: React.FC<{
|
||||
>
|
||||
{t("subscription.utilization", { value: Math.round(tier.utilization) })}
|
||||
</span>
|
||||
{hasUsd && (
|
||||
<span className="text-muted-foreground/60">
|
||||
(${tier.usedValueUsd!.toFixed(2)}/${tier.maxValueUsd!.toFixed(2)})
|
||||
</span>
|
||||
)}
|
||||
{countdown && (
|
||||
<span className="text-muted-foreground/60 ml-0.5 flex items-center gap-px">
|
||||
<Clock size={10} />
|
||||
|
||||
@@ -19,10 +19,26 @@ interface UsageFooterProps {
|
||||
|
||||
/** UsageData → QuotaTier 转换(Token Plan 使用) */
|
||||
function toQuotaTier(data: UsageData): QuotaTier {
|
||||
const extra = data.extra;
|
||||
if (extra && extra.startsWith("{")) {
|
||||
try {
|
||||
const parsed = JSON.parse(extra);
|
||||
return {
|
||||
name: data.planName || "",
|
||||
utilization: data.used || 0,
|
||||
resetsAt: parsed.resetsAt || null,
|
||||
usedValueUsd: parsed.usedValueUsd ?? null,
|
||||
maxValueUsd: parsed.maxValueUsd ?? null,
|
||||
planLabel: parsed.planLabel ?? null,
|
||||
};
|
||||
} catch {
|
||||
// fall through to plain string
|
||||
}
|
||||
}
|
||||
return {
|
||||
name: data.planName || "",
|
||||
utilization: data.used || 0,
|
||||
resetsAt: data.extra || null,
|
||||
resetsAt: extra || null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -147,9 +163,22 @@ const UsageFooter: React.FC<UsageFooterProps> = ({
|
||||
</div>
|
||||
{/* 第二行:tier 徽章(复用官方订阅的 TierBadge) */}
|
||||
<div className="flex items-center gap-2">
|
||||
{usageDataList.map((data, index) => (
|
||||
<TierBadge key={index} tier={toQuotaTier(data)} t={t} />
|
||||
))}
|
||||
{(() => {
|
||||
const tiers = usageDataList.map((d) => toQuotaTier(d));
|
||||
const planLabel = tiers[0]?.planLabel;
|
||||
return (
|
||||
<>
|
||||
{planLabel && (
|
||||
<span className="font-semibold text-muted-foreground">
|
||||
💰 {planLabel}
|
||||
</span>
|
||||
)}
|
||||
{tiers.map((tier, index) => (
|
||||
<TierBadge key={index} tier={tier} t={t} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -444,8 +444,14 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
||||
|
||||
// Coding Plan 模板使用专用 API
|
||||
if (selectedTemplate === TEMPLATE_TYPES.TOKEN_PLAN) {
|
||||
const baseUrl = providerCredentials.baseUrl ?? "";
|
||||
const apiKey = providerCredentials.apiKey ?? "";
|
||||
// ZenMux 使用用户在脚本配置中手动填入的 API Key 和 Base URL
|
||||
const isZenMux = script.codingPlanProvider === "zenmux";
|
||||
const baseUrl = isZenMux
|
||||
? (script.baseUrl ?? "")
|
||||
: (providerCredentials.baseUrl ?? "");
|
||||
const apiKey = isZenMux
|
||||
? (script.apiKey ?? "")
|
||||
: (providerCredentials.apiKey ?? "");
|
||||
const { subscriptionApi } = await import("@/lib/api/subscription");
|
||||
const quota = await subscriptionApi.getCodingPlanQuota(baseUrl, apiKey);
|
||||
if (quota.success && quota.tiers.length > 0) {
|
||||
@@ -626,15 +632,17 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
||||
const autoDetected = detectCodingPlanProvider(
|
||||
providerCredentials.baseUrl,
|
||||
);
|
||||
const provider = script.codingPlanProvider || autoDetected || "kimi";
|
||||
// ZenMux 允许手动填写 API Key 和 Base URL,不清除
|
||||
const isZenMux = provider === "zenmux";
|
||||
setScript({
|
||||
...script,
|
||||
code: "",
|
||||
apiKey: undefined,
|
||||
baseUrl: undefined,
|
||||
apiKey: isZenMux ? script.apiKey : undefined,
|
||||
baseUrl: isZenMux ? script.baseUrl : undefined,
|
||||
accessToken: undefined,
|
||||
userId: undefined,
|
||||
codingPlanProvider:
|
||||
script.codingPlanProvider || autoDetected || "kimi",
|
||||
codingPlanProvider: provider,
|
||||
});
|
||||
} else if (presetName === TEMPLATE_TYPES.BALANCE) {
|
||||
// 官方余额查询模板不需要脚本,使用 Rust 原生查询
|
||||
@@ -653,7 +661,9 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
||||
|
||||
const shouldShowCredentialsConfig =
|
||||
selectedTemplate === TEMPLATE_TYPES.GENERAL ||
|
||||
selectedTemplate === TEMPLATE_TYPES.NEW_API;
|
||||
selectedTemplate === TEMPLATE_TYPES.NEW_API ||
|
||||
(selectedTemplate === TEMPLATE_TYPES.TOKEN_PLAN &&
|
||||
script.codingPlanProvider === "zenmux");
|
||||
|
||||
const footer = (
|
||||
<>
|
||||
@@ -1050,6 +1060,66 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{selectedTemplate === TEMPLATE_TYPES.TOKEN_PLAN &&
|
||||
script.codingPlanProvider === "zenmux" && (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="usage-zenmux-base-url">
|
||||
{t("usageScript.baseUrl")}
|
||||
</Label>
|
||||
<Input
|
||||
id="usage-zenmux-base-url"
|
||||
type="text"
|
||||
value={script.baseUrl || ""}
|
||||
onChange={(e) =>
|
||||
setScript({ ...script, baseUrl: e.target.value })
|
||||
}
|
||||
placeholder="https://api.zenmux.com/v1/..."
|
||||
autoComplete="off"
|
||||
className="border-white/10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="usage-zenmux-api-key">API Key</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="usage-zenmux-api-key"
|
||||
type={showApiKey ? "text" : "password"}
|
||||
value={script.apiKey || ""}
|
||||
onChange={(e) =>
|
||||
setScript({
|
||||
...script,
|
||||
apiKey: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="sk-..."
|
||||
autoComplete="off"
|
||||
className="border-white/10"
|
||||
/>
|
||||
{script.apiKey && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowApiKey(!showApiKey)}
|
||||
className="absolute inset-y-0 right-0 flex items-center pr-3 text-muted-foreground hover:text-foreground transition-colors"
|
||||
aria-label={
|
||||
showApiKey
|
||||
? t("apiKeyInput.hide")
|
||||
: t("apiKeyInput.show")
|
||||
}
|
||||
>
|
||||
{showApiKey ? (
|
||||
<EyeOff size={16} />
|
||||
) : (
|
||||
<Eye size={16} />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user