mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 21:30:17 +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>
|
||||
)}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { TEMPLATE_TYPES } from "@/config/constants";
|
||||
|
||||
export interface CodingPlanProviderEntry {
|
||||
/** 与后端 QuotaTier 的 `codingPlanProvider` 取值对齐 */
|
||||
id: "kimi" | "zhipu" | "minimax";
|
||||
id: "kimi" | "zhipu" | "minimax" | "zenmux";
|
||||
/** UsageScriptModal 下拉显示用 */
|
||||
label: string;
|
||||
/** base_url 匹配规则 */
|
||||
@@ -30,6 +30,11 @@ export const CODING_PLAN_PROVIDERS: readonly CodingPlanProviderEntry[] = [
|
||||
label: "MiniMax",
|
||||
pattern: /api\.minimaxi?\.com|api\.minimax\.io/i,
|
||||
},
|
||||
{
|
||||
id: "zenmux",
|
||||
label: "ZenMux",
|
||||
pattern: /zenmux\./i,
|
||||
},
|
||||
] as const;
|
||||
|
||||
/** 根据 Base URL 自动检测 Coding Plan 供应商;未命中返回 null */
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -306,6 +306,13 @@ export const iconMetadata: Record<string, IconMetadata> = {
|
||||
keywords: ["minimax"],
|
||||
defaultColor: "#FF6B6B",
|
||||
},
|
||||
zenmux: {
|
||||
name: "zenmux",
|
||||
displayName: "ZenMux",
|
||||
category: "ai-provider",
|
||||
keywords: ["zenmux", "zen", "mux"],
|
||||
defaultColor: "#6366F1",
|
||||
},
|
||||
mistral: {
|
||||
name: "mistral",
|
||||
displayName: "Mistral",
|
||||
|
||||
@@ -8,6 +8,9 @@ export interface QuotaTier {
|
||||
name: string;
|
||||
utilization: number; // 0-100
|
||||
resetsAt: string | null;
|
||||
usedValueUsd?: number | null;
|
||||
maxValueUsd?: number | null;
|
||||
planLabel?: string | null;
|
||||
}
|
||||
|
||||
export interface ExtraUsage {
|
||||
|
||||
Reference in New Issue
Block a user