mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-05-24 23:10:39 +08:00
feat: add official balance query for DeepSeek, StepFun, SiliconFlow, OpenRouter, Novita AI
Add a new "Official" (官方) template type in the usage query panel that queries account balance via each provider's native API endpoint. Follows the same zero-script pattern as Token Plan — Rust handles the HTTP call, frontend auto-detects the provider from base URL. Supported providers and endpoints: - DeepSeek: GET /user/balance - StepFun: GET /v1/accounts - SiliconFlow: GET /v1/user/info (cn + com) - OpenRouter: GET /api/v1/credits - Novita AI: GET /v3/user/balance
This commit is contained in:
@@ -98,6 +98,9 @@ const generatePresetTemplates = (
|
||||
|
||||
// Coding Plan 模板不需要脚本,使用专用 Rust 查询
|
||||
[TEMPLATE_TYPES.TOKEN_PLAN]: "",
|
||||
|
||||
// 官方余额查询模板不需要脚本,使用专用 Rust 查询
|
||||
[TEMPLATE_TYPES.BALANCE]: "",
|
||||
});
|
||||
|
||||
// 模板名称国际化键映射
|
||||
@@ -107,6 +110,7 @@ const TEMPLATE_NAME_KEYS: Record<string, string> = {
|
||||
[TEMPLATE_TYPES.NEW_API]: "usageScript.templateNewAPI",
|
||||
[TEMPLATE_TYPES.GITHUB_COPILOT]: "usageScript.templateCopilot",
|
||||
[TEMPLATE_TYPES.TOKEN_PLAN]: "usageScript.templateTokenPlan",
|
||||
[TEMPLATE_TYPES.BALANCE]: "usageScript.templateBalance",
|
||||
};
|
||||
|
||||
/** Coding Plan 供应商选项 */
|
||||
@@ -124,6 +128,25 @@ const TOKEN_PLAN_PROVIDERS = [
|
||||
},
|
||||
] as const;
|
||||
|
||||
/** 官方余额查询供应商检测 */
|
||||
const BALANCE_PROVIDERS = [
|
||||
{ id: "deepseek", label: "DeepSeek", pattern: /api\.deepseek\.com/i },
|
||||
{ id: "stepfun", label: "StepFun", pattern: /api\.stepfun\.(ai|com)/i },
|
||||
{
|
||||
id: "siliconflow",
|
||||
label: "SiliconFlow",
|
||||
pattern: /api\.siliconflow\.(cn|com)/i,
|
||||
},
|
||||
{ id: "openrouter", label: "OpenRouter", pattern: /openrouter\.ai/i },
|
||||
{ id: "novita", label: "Novita AI", pattern: /api\.novita\.ai/i },
|
||||
] as const;
|
||||
|
||||
/** 根据 Base URL 自动检测余额查询供应商 */
|
||||
function detectBalanceProvider(baseUrl: string | undefined): boolean {
|
||||
if (!baseUrl) return false;
|
||||
return BALANCE_PROVIDERS.some((bp) => bp.pattern.test(baseUrl));
|
||||
}
|
||||
|
||||
/** 根据 Base URL 自动检测 Coding Plan 供应商 */
|
||||
function detectTokenPlanProvider(baseUrl: string | undefined): string | null {
|
||||
if (!baseUrl) return null;
|
||||
@@ -219,6 +242,16 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
||||
};
|
||||
}
|
||||
|
||||
// 新配置:如果 URL 匹配官方余额查询供应商,自动初始化
|
||||
if (detectBalanceProvider(providerCredentials.baseUrl)) {
|
||||
return {
|
||||
enabled: false,
|
||||
language: "javascript" as const,
|
||||
code: "",
|
||||
timeout: 10,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
enabled: false,
|
||||
language: "javascript" as const,
|
||||
@@ -300,6 +333,10 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
||||
if (detectTokenPlanProvider(providerCredentials.baseUrl)) {
|
||||
return TEMPLATE_TYPES.TOKEN_PLAN;
|
||||
}
|
||||
// 新配置:如果 URL 匹配官方余额查询供应商,自动选择 Balance 模板
|
||||
if (detectBalanceProvider(providerCredentials.baseUrl)) {
|
||||
return TEMPLATE_TYPES.BALANCE;
|
||||
}
|
||||
// 默认使用 GENERAL(与默认代码模板一致)
|
||||
return TEMPLATE_TYPES.GENERAL;
|
||||
},
|
||||
@@ -331,10 +368,11 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
// Copilot 和 Coding Plan 模板不需要脚本验证
|
||||
// Copilot、Coding Plan、Balance 模板不需要脚本验证
|
||||
if (
|
||||
selectedTemplate !== TEMPLATE_TYPES.GITHUB_COPILOT &&
|
||||
selectedTemplate !== TEMPLATE_TYPES.TOKEN_PLAN
|
||||
selectedTemplate !== TEMPLATE_TYPES.TOKEN_PLAN &&
|
||||
selectedTemplate !== TEMPLATE_TYPES.BALANCE
|
||||
) {
|
||||
if (script.enabled && !script.code.trim()) {
|
||||
toast.error(t("usageScript.scriptEmpty"));
|
||||
@@ -354,6 +392,7 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
||||
| "newapi"
|
||||
| "github_copilot"
|
||||
| "token_plan"
|
||||
| "balance"
|
||||
| undefined,
|
||||
};
|
||||
onSave(scriptWithTemplate);
|
||||
@@ -363,6 +402,37 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
||||
const handleTest = async () => {
|
||||
setTesting(true);
|
||||
try {
|
||||
// 官方余额查询模板使用专用 API
|
||||
if (selectedTemplate === TEMPLATE_TYPES.BALANCE) {
|
||||
const config = provider.settingsConfig as Record<string, any>;
|
||||
const baseUrl: string = config?.env?.ANTHROPIC_BASE_URL ?? "";
|
||||
const apiKey: string =
|
||||
config?.env?.ANTHROPIC_AUTH_TOKEN ??
|
||||
config?.env?.ANTHROPIC_API_KEY ??
|
||||
"";
|
||||
const { subscriptionApi } = await import("@/lib/api/subscription");
|
||||
const result = await subscriptionApi.getBalance(baseUrl, apiKey);
|
||||
if (result.success && result.data && result.data.length > 0) {
|
||||
const summary = result.data
|
||||
.map((d) => {
|
||||
const name = d.planName ? `[${d.planName}] ` : "";
|
||||
return `${name}${t("usage.remaining")} ${d.remaining?.toFixed(2)} ${d.unit || ""}`;
|
||||
})
|
||||
.join(", ");
|
||||
toast.success(`${t("usageScript.testSuccess")}${summary}`, {
|
||||
duration: 3000,
|
||||
closeButton: true,
|
||||
});
|
||||
queryClient.setQueryData(["usage", provider.id, appId], result);
|
||||
} else {
|
||||
toast.error(
|
||||
`${t("usageScript.testFailed")}: ${result.error || t("endpointTest.noResult")}`,
|
||||
{ duration: 5000 },
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Coding Plan 模板使用专用 API
|
||||
if (selectedTemplate === TEMPLATE_TYPES.TOKEN_PLAN) {
|
||||
const config = provider.settingsConfig as Record<string, any>;
|
||||
@@ -558,6 +628,16 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
||||
codingPlanProvider:
|
||||
script.codingPlanProvider || autoDetected || "kimi",
|
||||
});
|
||||
} else if (presetName === TEMPLATE_TYPES.BALANCE) {
|
||||
// 官方余额查询模板不需要脚本,使用 Rust 原生查询
|
||||
setScript({
|
||||
...script,
|
||||
code: "",
|
||||
apiKey: undefined,
|
||||
baseUrl: undefined,
|
||||
accessToken: undefined,
|
||||
userId: undefined,
|
||||
});
|
||||
}
|
||||
setSelectedTemplate(presetName);
|
||||
}
|
||||
@@ -746,6 +826,27 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 官方余额查询模式:自动提示 */}
|
||||
{selectedTemplate === TEMPLATE_TYPES.BALANCE && (
|
||||
<div className="space-y-3 border-t border-white/10 pt-3">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("usageScript.balanceHint")}
|
||||
</p>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{BALANCE_PROVIDERS.filter((bp) =>
|
||||
bp.pattern.test(providerCredentials.baseUrl || ""),
|
||||
).map((bp) => (
|
||||
<span
|
||||
key={bp.id}
|
||||
className="inline-flex items-center px-2.5 py-1 rounded-md bg-primary/10 text-primary text-xs font-medium"
|
||||
>
|
||||
{bp.label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Coding Plan 模式:供应商选择 */}
|
||||
{selectedTemplate === TEMPLATE_TYPES.TOKEN_PLAN && (
|
||||
<div className="space-y-3 border-t border-white/10 pt-3">
|
||||
|
||||
@@ -10,6 +10,7 @@ export const TEMPLATE_TYPES = {
|
||||
NEW_API: "newapi",
|
||||
GITHUB_COPILOT: "github_copilot",
|
||||
TOKEN_PLAN: "token_plan",
|
||||
BALANCE: "balance",
|
||||
} as const;
|
||||
|
||||
export type TemplateType = (typeof TEMPLATE_TYPES)[keyof typeof TEMPLATE_TYPES];
|
||||
|
||||
@@ -1091,8 +1091,10 @@
|
||||
"templateNewAPI": "NewAPI",
|
||||
"templateCopilot": "GitHub Copilot",
|
||||
"templateTokenPlan": "Token Plan",
|
||||
"templateBalance": "Official",
|
||||
"copilotAutoAuth": "Auto OAuth authentication, no manual credentials needed",
|
||||
"tokenPlanHint": "Automatically uses the provider's API Key and Base URL to query Token Plan quota",
|
||||
"balanceHint": "Automatically uses the provider's API Key to query account balance",
|
||||
"resetDate": "Reset date",
|
||||
"premiumRequests": "Premium Requests",
|
||||
"credentialsConfig": "Credentials",
|
||||
|
||||
@@ -1091,8 +1091,10 @@
|
||||
"templateNewAPI": "NewAPI",
|
||||
"templateCopilot": "GitHub Copilot",
|
||||
"templateTokenPlan": "Token Plan",
|
||||
"templateBalance": "公式",
|
||||
"copilotAutoAuth": "OAuth 認証を自動使用、手動設定不要",
|
||||
"tokenPlanHint": "プロバイダーのAPI KeyとBase URLを使用してToken Planクォータを自動クエリ",
|
||||
"balanceHint": "プロバイダーのAPI Keyを使用してアカウント残高を自動クエリ",
|
||||
"resetDate": "リセット日",
|
||||
"premiumRequests": "Premium リクエスト",
|
||||
"credentialsConfig": "認証情報",
|
||||
|
||||
@@ -1091,8 +1091,10 @@
|
||||
"templateNewAPI": "NewAPI",
|
||||
"templateCopilot": "GitHub Copilot",
|
||||
"templateTokenPlan": "Token Plan",
|
||||
"templateBalance": "官方",
|
||||
"copilotAutoAuth": "自动使用 OAuth 认证,无需手动配置凭证",
|
||||
"tokenPlanHint": "自动使用供应商的 API Key 和 Base URL 查询 Token Plan 额度",
|
||||
"balanceHint": "自动使用供应商的 API Key 查询账户余额",
|
||||
"resetDate": "重置日期",
|
||||
"premiumRequests": "Premium 请求",
|
||||
"credentialsConfig": "凭证配置",
|
||||
|
||||
@@ -9,4 +9,9 @@ export const subscriptionApi = {
|
||||
apiKey: string,
|
||||
): Promise<SubscriptionQuota> =>
|
||||
invoke("get_coding_plan_quota", { baseUrl, apiKey }),
|
||||
getBalance: (
|
||||
baseUrl: string,
|
||||
apiKey: string,
|
||||
): Promise<import("@/types").UsageResult> =>
|
||||
invoke("get_balance", { baseUrl, apiKey }),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user