feat: add Token Plan quota query for Kimi, Zhipu GLM, and MiniMax

Add a new "Token Plan" template type in the usage query panel that
natively queries quota/usage from Chinese coding plan providers
(Kimi For Coding, Zhipu GLM, MiniMax) without requiring custom scripts.

- Rust backend: new coding_plan service with provider-specific API
  queries (Kimi /v1/usages, Zhipu /api/monitor/usage/quota/limit,
  MiniMax /coding_plan/remains) normalized into UsageResult
- Frontend: Token Plan template in UsageScriptModal with auto-detection
  of provider based on ANTHROPIC_BASE_URL pattern matching
- Follows the same pattern as GitHub Copilot template (dedicated API
  path in queryProviderUsage, no JS script needed)
This commit is contained in:
Jason
2026-04-05 11:39:29 +08:00
parent 8d38f0ee4f
commit bfdac2a22a
16 changed files with 761 additions and 98 deletions
+9
View File
@@ -0,0 +1,9 @@
use crate::services::subscription::SubscriptionQuota;
#[tauri::command]
pub async fn get_coding_plan_quota(
base_url: String,
api_key: String,
) -> Result<SubscriptionQuota, String> {
crate::services::coding_plan::get_coding_plan_quota(&base_url, &api_key).await
}
+2
View File
@@ -1,6 +1,7 @@
#![allow(non_snake_case)]
mod auth;
mod coding_plan;
mod config;
mod copilot;
mod deeplink;
@@ -30,6 +31,7 @@ mod webdav_sync;
mod workspace;
pub use auth::*;
pub use coding_plan::*;
pub use config::*;
pub use copilot::*;
pub use deeplink::*;
+77 -19
View File
@@ -13,6 +13,7 @@ use std::str::FromStr;
// 常量定义
const TEMPLATE_TYPE_GITHUB_COPILOT: &str = "github_copilot";
const TEMPLATE_TYPE_TOKEN_PLAN: &str = "token_plan";
const COPILOT_UNIT_PREMIUM: &str = "requests";
/// 获取所有供应商
@@ -158,29 +159,25 @@ pub async fn queryProviderUsage(
) -> Result<crate::provider::UsageResult, String> {
let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?;
// 检查是否为 GitHub Copilot 模板类型,并解析绑定账号
let (is_copilot_template, copilot_account_id) = {
let providers = state
.db
.get_all_providers(app_type.as_str())
.map_err(|e| format!("Failed to get providers: {e}"))?;
// 从数据库读取供应商信息,检查特殊模板类型
let providers = state
.db
.get_all_providers(app_type.as_str())
.map_err(|e| format!("Failed to get providers: {e}"))?;
let provider = providers.get(&providerId);
let usage_script = provider
.and_then(|p| p.meta.as_ref())
.and_then(|m| m.usage_script.as_ref());
let template_type = usage_script
.and_then(|s| s.template_type.as_deref())
.unwrap_or("");
let provider = providers.get(&providerId);
let is_copilot = provider
.and_then(|p| p.meta.as_ref())
.and_then(|m| m.usage_script.as_ref())
.and_then(|s| s.template_type.as_ref())
.map(|t| t == TEMPLATE_TYPE_GITHUB_COPILOT)
.unwrap_or(false);
let account_id = provider
// ── GitHub Copilot 专用路径 ──
if template_type == TEMPLATE_TYPE_GITHUB_COPILOT {
let copilot_account_id = provider
.and_then(|p| p.meta.as_ref())
.and_then(|m| m.managed_account_id_for(TEMPLATE_TYPE_GITHUB_COPILOT));
(is_copilot, account_id)
};
if is_copilot_template {
// 使用 Copilot 专用 API
let auth_manager = copilot_state.0.read().await;
let usage = match copilot_account_id.as_deref() {
Some(account_id) => auth_manager
@@ -211,6 +208,67 @@ pub async fn queryProviderUsage(
});
}
// ── Coding Plan 专用路径 ──
if template_type == TEMPLATE_TYPE_TOKEN_PLAN {
// 从供应商配置中提取 API Key 和 Base URL
let settings_config = provider
.map(|p| &p.settings_config)
.cloned()
.unwrap_or_default();
let env = settings_config.get("env");
let base_url = env
.and_then(|e| e.get("ANTHROPIC_BASE_URL"))
.and_then(|v| v.as_str())
.unwrap_or("");
let api_key = env
.and_then(|e| {
e.get("ANTHROPIC_AUTH_TOKEN")
.or_else(|| e.get("ANTHROPIC_API_KEY"))
})
.and_then(|v| v.as_str())
.unwrap_or("");
let quota = crate::services::coding_plan::get_coding_plan_quota(base_url, api_key)
.await
.map_err(|e| format!("Failed to query coding plan: {e}"))?;
// 将 SubscriptionQuota 转换为 UsageResult
if !quota.success {
return Ok(crate::provider::UsageResult {
success: false,
data: None,
error: quota.error,
});
}
let data: Vec<crate::provider::UsageData> = quota
.tiers
.iter()
.map(|tier| {
let total = 100.0;
let used = tier.utilization;
let remaining = total - used;
crate::provider::UsageData {
plan_name: Some(tier.name.clone()),
remaining: Some(remaining),
total: Some(total),
used: Some(used),
unit: Some("%".to_string()),
is_valid: Some(true),
invalid_message: None,
extra: tier.resets_at.clone(),
}
})
.collect();
return Ok(crate::provider::UsageResult {
success: true,
data: if data.is_empty() { None } else { Some(data) },
error: None,
});
}
// ── 通用 JS 脚本路径 ──
ProviderService::query_usage(state.inner(), app_type, &providerId)
.await
.map_err(|e| e.to_string())