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:
Jason
2026-04-05 16:54:51 +08:00
parent 24555275bb
commit f1fb3351c1
12 changed files with 561 additions and 2 deletions
+103 -2
View File
@@ -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 = () => {
// CopilotCoding Plan 模板不需要脚本验证
// CopilotCoding 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">
+1
View File
@@ -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];
+2
View File
@@ -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",
+2
View File
@@ -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": "認証情報",
+2
View File
@@ -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": "凭证配置",
+5
View File
@@ -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 }),
};