mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
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:
@@ -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
|
||||
}
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -229,6 +229,7 @@ fn build_provider_meta(request: &DeepLinkImportRequest) -> Result<Option<Provide
|
||||
user_id: request.usage_user_id.clone(),
|
||||
template_type: None, // Deeplink providers don't specify template type (will use backward compatibility logic)
|
||||
auto_query_interval: request.usage_auto_interval,
|
||||
coding_plan_provider: None,
|
||||
};
|
||||
|
||||
Ok(Some(ProviderMeta {
|
||||
|
||||
@@ -892,6 +892,7 @@ pub fn run() {
|
||||
commands::testUsageScript,
|
||||
// subscription quota
|
||||
commands::get_subscription_quota,
|
||||
commands::get_coding_plan_quota,
|
||||
// New MCP via config.json (SSOT)
|
||||
commands::get_mcp_config,
|
||||
commands::upsert_mcp_server_in_config,
|
||||
|
||||
@@ -106,6 +106,10 @@ pub struct UsageScript {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(rename = "autoQueryInterval")]
|
||||
pub auto_query_interval: Option<u64>,
|
||||
/// Coding Plan 供应商标识(如 "kimi", "zhipu", "minimax")
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(rename = "codingPlanProvider")]
|
||||
pub coding_plan_provider: Option<String>,
|
||||
}
|
||||
|
||||
/// 用量数据
|
||||
|
||||
@@ -0,0 +1,425 @@
|
||||
//! 国产 Token Plan 额度查询服务
|
||||
//!
|
||||
//! 支持 Kimi For Coding、智谱 GLM、MiniMax 的 Token Plan 额度查询。
|
||||
//! 复用 subscription 模块的 SubscriptionQuota / QuotaTier 类型。
|
||||
|
||||
use super::subscription::{CredentialStatus, QuotaTier, SubscriptionQuota};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
// ── 供应商检测 ──────────────────────────────────────────────
|
||||
|
||||
enum CodingPlanProvider {
|
||||
Kimi,
|
||||
ZhipuCn,
|
||||
ZhipuEn,
|
||||
MiniMaxCn,
|
||||
MiniMaxEn,
|
||||
}
|
||||
|
||||
fn detect_provider(base_url: &str) -> Option<CodingPlanProvider> {
|
||||
let url = base_url.to_lowercase();
|
||||
if url.contains("api.kimi.com/coding") {
|
||||
Some(CodingPlanProvider::Kimi)
|
||||
} else if url.contains("open.bigmodel.cn") || url.contains("bigmodel.cn") {
|
||||
Some(CodingPlanProvider::ZhipuCn)
|
||||
} else if url.contains("api.z.ai") {
|
||||
Some(CodingPlanProvider::ZhipuEn)
|
||||
} else if url.contains("api.minimaxi.com") {
|
||||
Some(CodingPlanProvider::MiniMaxCn)
|
||||
} else if url.contains("api.minimax.io") {
|
||||
Some(CodingPlanProvider::MiniMaxEn)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn now_millis() -> i64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as i64
|
||||
}
|
||||
|
||||
fn millis_to_iso8601(ms: i64) -> Option<String> {
|
||||
let secs = ms / 1000;
|
||||
let nsecs = ((ms % 1000) * 1_000_000) as u32;
|
||||
chrono::DateTime::from_timestamp(secs, nsecs).map(|dt| dt.to_rfc3339())
|
||||
}
|
||||
|
||||
fn make_error(msg: String) -> SubscriptionQuota {
|
||||
SubscriptionQuota {
|
||||
tool: "coding_plan".to_string(),
|
||||
credential_status: CredentialStatus::Valid,
|
||||
credential_message: None,
|
||||
success: false,
|
||||
tiers: vec![],
|
||||
extra_usage: None,
|
||||
error: Some(msg),
|
||||
queried_at: Some(now_millis()),
|
||||
}
|
||||
}
|
||||
|
||||
// ── Kimi For Coding ─────────────────────────────────────────
|
||||
|
||||
async fn query_kimi(api_key: &str) -> SubscriptionQuota {
|
||||
let client = crate::proxy::http_client::get();
|
||||
|
||||
let resp = client
|
||||
.get("https://api.kimi.com/coding/v1/usages")
|
||||
.header("Authorization", format!("Bearer {api_key}"))
|
||||
.header("Accept", "application/json")
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.send()
|
||||
.await;
|
||||
|
||||
let resp = match resp {
|
||||
Ok(r) => r,
|
||||
Err(e) => return make_error(format!("Network error: {e}")),
|
||||
};
|
||||
|
||||
let status = resp.status();
|
||||
if status == reqwest::StatusCode::UNAUTHORIZED || status == reqwest::StatusCode::FORBIDDEN {
|
||||
return SubscriptionQuota {
|
||||
tool: "coding_plan".to_string(),
|
||||
credential_status: CredentialStatus::Expired,
|
||||
credential_message: Some("Invalid API key".to_string()),
|
||||
success: false,
|
||||
tiers: vec![],
|
||||
extra_usage: None,
|
||||
error: Some(format!("Authentication failed (HTTP {status})")),
|
||||
queried_at: Some(now_millis()),
|
||||
};
|
||||
}
|
||||
|
||||
if !status.is_success() {
|
||||
let body = resp.text().await.unwrap_or_default();
|
||||
return make_error(format!("API error (HTTP {status}): {body}"));
|
||||
}
|
||||
|
||||
let body: serde_json::Value = match resp.json().await {
|
||||
Ok(v) => v,
|
||||
Err(e) => return make_error(format!("Failed to parse response: {e}")),
|
||||
};
|
||||
|
||||
let mut tiers = Vec::new();
|
||||
|
||||
// 总体用量(周限额)
|
||||
if let Some(usage) = body.get("usage") {
|
||||
let used = usage.get("used").and_then(|v| v.as_f64()).unwrap_or(0.0);
|
||||
let limit = usage.get("limit").and_then(|v| v.as_f64()).unwrap_or(1.0);
|
||||
let resets_at = usage
|
||||
.get("reset_at")
|
||||
.or_else(|| usage.get("resetAt"))
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let utilization = if limit > 0.0 {
|
||||
(used / limit) * 100.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
tiers.push(QuotaTier {
|
||||
name: "weekly_limit".to_string(),
|
||||
utilization,
|
||||
resets_at,
|
||||
});
|
||||
}
|
||||
|
||||
// 会话限额(5 小时窗口)
|
||||
if let Some(limits) = body.get("limits").and_then(|v| v.as_array()) {
|
||||
for limit_item in limits {
|
||||
if let Some(detail) = limit_item.get("detail") {
|
||||
let used = detail.get("used").and_then(|v| v.as_f64()).unwrap_or(0.0);
|
||||
let limit = detail.get("limit").and_then(|v| v.as_f64()).unwrap_or(1.0);
|
||||
let resets_at = detail
|
||||
.get("reset_at")
|
||||
.or_else(|| detail.get("resetAt"))
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let utilization = if limit > 0.0 {
|
||||
(used / limit) * 100.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
tiers.push(QuotaTier {
|
||||
name: "session_limit".to_string(),
|
||||
utilization,
|
||||
resets_at,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SubscriptionQuota {
|
||||
tool: "coding_plan".to_string(),
|
||||
credential_status: CredentialStatus::Valid,
|
||||
credential_message: None,
|
||||
success: true,
|
||||
tiers,
|
||||
extra_usage: None,
|
||||
error: None,
|
||||
queried_at: Some(now_millis()),
|
||||
}
|
||||
}
|
||||
|
||||
// ── 智谱 GLM ────────────────────────────────────────────────
|
||||
|
||||
async fn query_zhipu(api_key: &str) -> SubscriptionQuota {
|
||||
let client = crate::proxy::http_client::get();
|
||||
|
||||
// 统一走 api.z.ai 国际站(中国站 bigmodel.cn 有反爬机制)
|
||||
let resp = client
|
||||
.get("https://api.z.ai/api/monitor/usage/quota/limit")
|
||||
.header("Authorization", api_key) // 注意:智谱不加 Bearer 前缀
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Accept-Language", "en-US,en")
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.send()
|
||||
.await;
|
||||
|
||||
let resp = match resp {
|
||||
Ok(r) => r,
|
||||
Err(e) => return make_error(format!("Network error: {e}")),
|
||||
};
|
||||
|
||||
let status = resp.status();
|
||||
if status == reqwest::StatusCode::UNAUTHORIZED || status == reqwest::StatusCode::FORBIDDEN {
|
||||
return SubscriptionQuota {
|
||||
tool: "coding_plan".to_string(),
|
||||
credential_status: CredentialStatus::Expired,
|
||||
credential_message: Some("Invalid API key".to_string()),
|
||||
success: false,
|
||||
tiers: vec![],
|
||||
extra_usage: None,
|
||||
error: Some(format!("Authentication failed (HTTP {status})")),
|
||||
queried_at: Some(now_millis()),
|
||||
};
|
||||
}
|
||||
|
||||
if !status.is_success() {
|
||||
let body = resp.text().await.unwrap_or_default();
|
||||
return make_error(format!("API error (HTTP {status}): {body}"));
|
||||
}
|
||||
|
||||
let body: serde_json::Value = match resp.json().await {
|
||||
Ok(v) => v,
|
||||
Err(e) => return make_error(format!("Failed to parse response: {e}")),
|
||||
};
|
||||
|
||||
// 检查业务级别错误
|
||||
if body.get("success").and_then(|v| v.as_bool()) == Some(false) {
|
||||
let msg = body
|
||||
.get("msg")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("Unknown error");
|
||||
return make_error(format!("API error: {msg}"));
|
||||
}
|
||||
|
||||
let data = match body.get("data") {
|
||||
Some(d) => d,
|
||||
None => return make_error("Missing 'data' field in response".to_string()),
|
||||
};
|
||||
|
||||
let mut tiers = Vec::new();
|
||||
|
||||
if let Some(limits) = data.get("limits").and_then(|v| v.as_array()) {
|
||||
for limit_item in limits {
|
||||
let limit_type = limit_item
|
||||
.get("type")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("");
|
||||
let percentage = limit_item
|
||||
.get("percentage")
|
||||
.and_then(|v| v.as_f64())
|
||||
.unwrap_or(0.0);
|
||||
let next_reset = limit_item
|
||||
.get("nextResetTime")
|
||||
.and_then(|v| v.as_i64())
|
||||
.and_then(millis_to_iso8601);
|
||||
|
||||
let tier_name = match limit_type {
|
||||
"TOKENS_LIMIT" => "tokens_limit",
|
||||
"TIME_LIMIT" => "mcp_limit",
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
tiers.push(QuotaTier {
|
||||
name: tier_name.to_string(),
|
||||
utilization: percentage,
|
||||
resets_at: next_reset,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 套餐等级存入 credential_message
|
||||
let level = data
|
||||
.get("level")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
SubscriptionQuota {
|
||||
tool: "coding_plan".to_string(),
|
||||
credential_status: CredentialStatus::Valid,
|
||||
credential_message: level,
|
||||
success: true,
|
||||
tiers,
|
||||
extra_usage: None,
|
||||
error: None,
|
||||
queried_at: Some(now_millis()),
|
||||
}
|
||||
}
|
||||
|
||||
// ── MiniMax ─────────────────────────────────────────────────
|
||||
|
||||
async fn query_minimax(api_key: &str, is_cn: bool) -> SubscriptionQuota {
|
||||
let client = crate::proxy::http_client::get();
|
||||
|
||||
let api_domain = if is_cn {
|
||||
"api.minimaxi.com"
|
||||
} else {
|
||||
"api.minimax.io"
|
||||
};
|
||||
let url = format!("https://{api_domain}/v1/api/openplatform/coding_plan/remains");
|
||||
|
||||
let resp = client
|
||||
.get(&url)
|
||||
.header("Authorization", format!("Bearer {api_key}"))
|
||||
.header("Content-Type", "application/json")
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.send()
|
||||
.await;
|
||||
|
||||
let resp = match resp {
|
||||
Ok(r) => r,
|
||||
Err(e) => return make_error(format!("Network error: {e}")),
|
||||
};
|
||||
|
||||
let status = resp.status();
|
||||
if status == reqwest::StatusCode::UNAUTHORIZED || status == reqwest::StatusCode::FORBIDDEN {
|
||||
return SubscriptionQuota {
|
||||
tool: "coding_plan".to_string(),
|
||||
credential_status: CredentialStatus::Expired,
|
||||
credential_message: Some("Invalid API key".to_string()),
|
||||
success: false,
|
||||
tiers: vec![],
|
||||
extra_usage: None,
|
||||
error: Some(format!("Authentication failed (HTTP {status})")),
|
||||
queried_at: Some(now_millis()),
|
||||
};
|
||||
}
|
||||
|
||||
if !status.is_success() {
|
||||
let body = resp.text().await.unwrap_or_default();
|
||||
return make_error(format!("API error (HTTP {status}): {body}"));
|
||||
}
|
||||
|
||||
let body: serde_json::Value = match resp.json().await {
|
||||
Ok(v) => v,
|
||||
Err(e) => return make_error(format!("Failed to parse response: {e}")),
|
||||
};
|
||||
|
||||
// 检查业务级别错误
|
||||
if let Some(base_resp) = body.get("base_resp") {
|
||||
let status_code = base_resp
|
||||
.get("status_code")
|
||||
.and_then(|v| v.as_i64())
|
||||
.unwrap_or(-1);
|
||||
if status_code != 0 {
|
||||
let msg = base_resp
|
||||
.get("status_msg")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("Unknown error");
|
||||
return make_error(format!("API error (code {status_code}): {msg}"));
|
||||
}
|
||||
}
|
||||
|
||||
let mut tiers = Vec::new();
|
||||
|
||||
if let Some(model_remains) = body.get("model_remains").and_then(|v| v.as_array()) {
|
||||
for item in model_remains {
|
||||
let model_name = item
|
||||
.get("model_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("unknown");
|
||||
let total = item
|
||||
.get("current_interval_total_count")
|
||||
.and_then(|v| v.as_f64())
|
||||
.unwrap_or(0.0);
|
||||
// 注意:current_interval_usage_count 名字有误导,实际是"剩余量"
|
||||
let remaining = item
|
||||
.get("current_interval_usage_count")
|
||||
.and_then(|v| v.as_f64())
|
||||
.unwrap_or(0.0);
|
||||
let end_time = item.get("end_time").and_then(|v| v.as_i64());
|
||||
|
||||
let utilization = if total > 0.0 {
|
||||
((total - remaining) / total) * 100.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
tiers.push(QuotaTier {
|
||||
name: model_name.to_string(),
|
||||
utilization,
|
||||
resets_at: end_time.and_then(millis_to_iso8601),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
SubscriptionQuota {
|
||||
tool: "coding_plan".to_string(),
|
||||
credential_status: CredentialStatus::Valid,
|
||||
credential_message: None,
|
||||
success: true,
|
||||
tiers,
|
||||
extra_usage: None,
|
||||
error: None,
|
||||
queried_at: Some(now_millis()),
|
||||
}
|
||||
}
|
||||
|
||||
// ── 公开入口 ────────────────────────────────────────────────
|
||||
|
||||
pub async fn get_coding_plan_quota(
|
||||
base_url: &str,
|
||||
api_key: &str,
|
||||
) -> Result<SubscriptionQuota, String> {
|
||||
if api_key.trim().is_empty() {
|
||||
return Ok(SubscriptionQuota {
|
||||
tool: "coding_plan".to_string(),
|
||||
credential_status: CredentialStatus::NotFound,
|
||||
credential_message: None,
|
||||
success: false,
|
||||
tiers: vec![],
|
||||
extra_usage: None,
|
||||
error: None,
|
||||
queried_at: None,
|
||||
});
|
||||
}
|
||||
|
||||
let provider = match detect_provider(base_url) {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
return Ok(SubscriptionQuota {
|
||||
tool: "coding_plan".to_string(),
|
||||
credential_status: CredentialStatus::NotFound,
|
||||
credential_message: None,
|
||||
success: false,
|
||||
tiers: vec![],
|
||||
extra_usage: None,
|
||||
error: None,
|
||||
queried_at: None,
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
let quota = match provider {
|
||||
CodingPlanProvider::Kimi => query_kimi(api_key).await,
|
||||
CodingPlanProvider::ZhipuCn | CodingPlanProvider::ZhipuEn => query_zhipu(api_key).await,
|
||||
CodingPlanProvider::MiniMaxCn => query_minimax(api_key, true).await,
|
||||
CodingPlanProvider::MiniMaxEn => query_minimax(api_key, false).await,
|
||||
};
|
||||
|
||||
Ok(quota)
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod coding_plan;
|
||||
pub mod config;
|
||||
pub mod env_checker;
|
||||
pub mod env_manager;
|
||||
|
||||
@@ -95,6 +95,9 @@ const generatePresetTemplates = (
|
||||
|
||||
// GitHub Copilot 模板不需要脚本,使用专用 API
|
||||
[TEMPLATE_TYPES.GITHUB_COPILOT]: "",
|
||||
|
||||
// Coding Plan 模板不需要脚本,使用专用 Rust 查询
|
||||
[TEMPLATE_TYPES.TOKEN_PLAN]: "",
|
||||
});
|
||||
|
||||
// 模板名称国际化键映射
|
||||
@@ -103,8 +106,33 @@ const TEMPLATE_NAME_KEYS: Record<string, string> = {
|
||||
[TEMPLATE_TYPES.GENERAL]: "usageScript.templateGeneral",
|
||||
[TEMPLATE_TYPES.NEW_API]: "usageScript.templateNewAPI",
|
||||
[TEMPLATE_TYPES.GITHUB_COPILOT]: "usageScript.templateCopilot",
|
||||
[TEMPLATE_TYPES.TOKEN_PLAN]: "usageScript.templateTokenPlan",
|
||||
};
|
||||
|
||||
/** Coding Plan 供应商选项 */
|
||||
const TOKEN_PLAN_PROVIDERS = [
|
||||
{ id: "kimi", label: "Kimi For Coding", pattern: /api\.kimi\.com\/coding/i },
|
||||
{
|
||||
id: "zhipu",
|
||||
label: "Zhipu GLM (智谱)",
|
||||
pattern: /bigmodel\.cn|api\.z\.ai/i,
|
||||
},
|
||||
{
|
||||
id: "minimax",
|
||||
label: "MiniMax",
|
||||
pattern: /api\.minimaxi?\.com|api\.minimax\.io/i,
|
||||
},
|
||||
] as const;
|
||||
|
||||
/** 根据 Base URL 自动检测 Coding Plan 供应商 */
|
||||
function detectTokenPlanProvider(baseUrl: string | undefined): string | null {
|
||||
if (!baseUrl) return null;
|
||||
for (const cp of TOKEN_PLAN_PROVIDERS) {
|
||||
if (cp.pattern.test(baseUrl)) return cp.id;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
||||
provider,
|
||||
appId,
|
||||
@@ -164,18 +192,39 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
||||
|
||||
const [script, setScript] = useState<UsageScript>(() => {
|
||||
const savedScript = provider.meta?.usage_script;
|
||||
const defaultScript = {
|
||||
if (savedScript) {
|
||||
// 已有配置:如果是 coding_plan 但没有 codingPlanProvider,自动检测填充
|
||||
if (
|
||||
savedScript.templateType === TEMPLATE_TYPES.TOKEN_PLAN &&
|
||||
!savedScript.codingPlanProvider
|
||||
) {
|
||||
return {
|
||||
...savedScript,
|
||||
codingPlanProvider:
|
||||
detectTokenPlanProvider(providerCredentials.baseUrl) || "kimi",
|
||||
};
|
||||
}
|
||||
return savedScript;
|
||||
}
|
||||
|
||||
// 新配置:如果 URL 匹配 Coding Plan,自动初始化
|
||||
const autoDetected = detectTokenPlanProvider(providerCredentials.baseUrl);
|
||||
if (autoDetected) {
|
||||
return {
|
||||
enabled: false,
|
||||
language: "javascript" as const,
|
||||
code: "",
|
||||
timeout: 10,
|
||||
codingPlanProvider: autoDetected,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
enabled: false,
|
||||
language: "javascript" as const,
|
||||
code: PRESET_TEMPLATES[TEMPLATE_TYPES.GENERAL],
|
||||
timeout: 10,
|
||||
};
|
||||
|
||||
if (!savedScript) {
|
||||
return defaultScript;
|
||||
}
|
||||
|
||||
return savedScript;
|
||||
});
|
||||
|
||||
const [testing, setTesting] = useState(false);
|
||||
@@ -236,7 +285,7 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
||||
}
|
||||
// 优先使用保存的 templateType
|
||||
if (existingScript?.templateType) {
|
||||
return existingScript.templateType;
|
||||
return existingScript.templateType as string;
|
||||
}
|
||||
// 向后兼容:根据字段推断模板类型
|
||||
// 检测 NEW_API 模板(有 accessToken 或 userId)
|
||||
@@ -247,7 +296,11 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
||||
if (existingScript?.apiKey || existingScript?.baseUrl) {
|
||||
return TEMPLATE_TYPES.GENERAL;
|
||||
}
|
||||
// 新配置或无凭证:默认使用 GENERAL(与默认代码模板一致)
|
||||
// 新配置:如果 URL 匹配 Coding Plan 供应商,自动选择 Coding Plan 模板
|
||||
if (detectTokenPlanProvider(providerCredentials.baseUrl)) {
|
||||
return TEMPLATE_TYPES.TOKEN_PLAN;
|
||||
}
|
||||
// 默认使用 GENERAL(与默认代码模板一致)
|
||||
return TEMPLATE_TYPES.GENERAL;
|
||||
},
|
||||
);
|
||||
@@ -278,8 +331,11 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
// Copilot 模板不需要脚本验证
|
||||
if (selectedTemplate !== TEMPLATE_TYPES.GITHUB_COPILOT) {
|
||||
// Copilot 和 Coding Plan 模板不需要脚本验证
|
||||
if (
|
||||
selectedTemplate !== TEMPLATE_TYPES.GITHUB_COPILOT &&
|
||||
selectedTemplate !== TEMPLATE_TYPES.TOKEN_PLAN
|
||||
) {
|
||||
if (script.enabled && !script.code.trim()) {
|
||||
toast.error(t("usageScript.scriptEmpty"));
|
||||
return;
|
||||
@@ -297,6 +353,7 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
||||
| "general"
|
||||
| "newapi"
|
||||
| "github_copilot"
|
||||
| "token_plan"
|
||||
| undefined,
|
||||
};
|
||||
onSave(scriptWithTemplate);
|
||||
@@ -306,6 +363,45 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
||||
const handleTest = async () => {
|
||||
setTesting(true);
|
||||
try {
|
||||
// Coding Plan 模板使用专用 API
|
||||
if (selectedTemplate === TEMPLATE_TYPES.TOKEN_PLAN) {
|
||||
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 quota = await subscriptionApi.getCodingPlanQuota(baseUrl, apiKey);
|
||||
if (quota.success && quota.tiers.length > 0) {
|
||||
const summary = quota.tiers
|
||||
.map((tier) => `${tier.name}: ${Math.round(tier.utilization)}%`)
|
||||
.join(", ");
|
||||
toast.success(`${t("usageScript.testSuccess")}${summary}`, {
|
||||
duration: 3000,
|
||||
closeButton: true,
|
||||
});
|
||||
// 将结果转换为 UsageResult 格式更新缓存
|
||||
const usageData = quota.tiers.map((tier) => ({
|
||||
planName: tier.name,
|
||||
remaining: 100 - tier.utilization,
|
||||
total: 100,
|
||||
used: tier.utilization,
|
||||
unit: "%",
|
||||
}));
|
||||
queryClient.setQueryData(["usage", provider.id, appId], {
|
||||
success: true,
|
||||
data: usageData,
|
||||
});
|
||||
} else {
|
||||
toast.error(
|
||||
`${t("usageScript.testFailed")}: ${quota.error || t("endpointTest.noResult")}`,
|
||||
{ duration: 5000 },
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Copilot 模板使用专用 API
|
||||
if (selectedTemplate === TEMPLATE_TYPES.GITHUB_COPILOT) {
|
||||
const accountId = resolveManagedAccountId(
|
||||
@@ -410,7 +506,7 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
||||
|
||||
const handleUsePreset = (presetName: string) => {
|
||||
const preset = PRESET_TEMPLATES[presetName];
|
||||
if (preset) {
|
||||
if (preset !== undefined) {
|
||||
if (presetName === TEMPLATE_TYPES.CUSTOM) {
|
||||
// 🔧 自定义模式:用户应该在脚本中直接写完整 URL 和凭证,而不是依赖变量替换
|
||||
// 这样可以避免同源检查导致的问题
|
||||
@@ -447,6 +543,21 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
||||
accessToken: undefined,
|
||||
userId: undefined,
|
||||
});
|
||||
} else if (presetName === TEMPLATE_TYPES.TOKEN_PLAN) {
|
||||
// Coding Plan 模板不需要脚本,使用 Rust 原生查询
|
||||
const autoDetected = detectTokenPlanProvider(
|
||||
providerCredentials.baseUrl,
|
||||
);
|
||||
setScript({
|
||||
...script,
|
||||
code: "",
|
||||
apiKey: undefined,
|
||||
baseUrl: undefined,
|
||||
accessToken: undefined,
|
||||
userId: undefined,
|
||||
codingPlanProvider:
|
||||
script.codingPlanProvider || autoDetected || "kimi",
|
||||
});
|
||||
}
|
||||
setSelectedTemplate(presetName);
|
||||
}
|
||||
@@ -529,10 +640,11 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
||||
.filter((name) => {
|
||||
const isCopilotProvider =
|
||||
provider.meta?.providerType === "github_copilot";
|
||||
// Copilot 供应商只显示 copilot 模板,其他供应商不显示 copilot 模板
|
||||
// Copilot 供应商只显示 copilot 模板
|
||||
if (isCopilotProvider) {
|
||||
return name === TEMPLATE_TYPES.GITHUB_COPILOT;
|
||||
}
|
||||
// 非 Copilot 供应商不显示 copilot 模板
|
||||
return name !== TEMPLATE_TYPES.GITHUB_COPILOT;
|
||||
})
|
||||
.map((name) => {
|
||||
@@ -634,6 +746,43 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Coding Plan 模式:供应商选择 */}
|
||||
{selectedTemplate === TEMPLATE_TYPES.TOKEN_PLAN && (
|
||||
<div className="space-y-3 border-t border-white/10 pt-3">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("usageScript.tokenPlanHint")}
|
||||
</p>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{TOKEN_PLAN_PROVIDERS.map((cp) => (
|
||||
<Button
|
||||
key={cp.id}
|
||||
type="button"
|
||||
variant={
|
||||
script.codingPlanProvider === cp.id
|
||||
? "default"
|
||||
: "outline"
|
||||
}
|
||||
size="sm"
|
||||
className={cn(
|
||||
"rounded-lg border",
|
||||
script.codingPlanProvider === cp.id
|
||||
? "shadow-sm"
|
||||
: "bg-background text-muted-foreground hover:bg-accent hover:text-accent-foreground",
|
||||
)}
|
||||
onClick={() =>
|
||||
setScript({
|
||||
...script,
|
||||
codingPlanProvider: cp.id,
|
||||
})
|
||||
}
|
||||
>
|
||||
{cp.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 凭证配置 */}
|
||||
{shouldShowCredentialsConfig && (
|
||||
<div className="space-y-4">
|
||||
@@ -860,40 +1009,42 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
||||
</div>
|
||||
|
||||
{/* 提取器代码 - Copilot 模板不需要 */}
|
||||
{selectedTemplate !== TEMPLATE_TYPES.GITHUB_COPILOT && (
|
||||
<div className="space-y-4 glass rounded-xl border border-white/10 p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-base font-medium">
|
||||
{t("usageScript.extractorCode")}
|
||||
</Label>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t("usageScript.extractorHint")}
|
||||
{selectedTemplate !== TEMPLATE_TYPES.GITHUB_COPILOT &&
|
||||
selectedTemplate !== TEMPLATE_TYPES.TOKEN_PLAN && (
|
||||
<div className="space-y-4 glass rounded-xl border border-white/10 p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-base font-medium">
|
||||
{t("usageScript.extractorCode")}
|
||||
</Label>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t("usageScript.extractorHint")}
|
||||
</div>
|
||||
</div>
|
||||
<JsonEditor
|
||||
id="usage-code"
|
||||
value={script.code || ""}
|
||||
onChange={(value) =>
|
||||
setScript((prev) => ({ ...prev, code: value }))
|
||||
}
|
||||
height={480}
|
||||
language="javascript"
|
||||
showMinimap={false}
|
||||
/>
|
||||
</div>
|
||||
<JsonEditor
|
||||
id="usage-code"
|
||||
value={script.code || ""}
|
||||
onChange={(value) =>
|
||||
setScript((prev) => ({ ...prev, code: value }))
|
||||
}
|
||||
height={480}
|
||||
language="javascript"
|
||||
showMinimap={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{/* 帮助信息 - Copilot 模板不需要 */}
|
||||
{selectedTemplate !== TEMPLATE_TYPES.GITHUB_COPILOT && (
|
||||
<div className="glass rounded-xl border border-white/10 p-6 text-sm text-foreground/90">
|
||||
<h4 className="font-medium mb-2">
|
||||
{t("usageScript.scriptHelp")}
|
||||
</h4>
|
||||
<div className="space-y-3 text-xs">
|
||||
<div>
|
||||
<strong>{t("usageScript.configFormat")}</strong>
|
||||
<pre className="mt-1 p-2 bg-black/20 text-foreground rounded border border-white/10 text-[10px] overflow-x-auto">
|
||||
{`({
|
||||
{selectedTemplate !== TEMPLATE_TYPES.GITHUB_COPILOT &&
|
||||
selectedTemplate !== TEMPLATE_TYPES.TOKEN_PLAN && (
|
||||
<div className="glass rounded-xl border border-white/10 p-6 text-sm text-foreground/90">
|
||||
<h4 className="font-medium mb-2">
|
||||
{t("usageScript.scriptHelp")}
|
||||
</h4>
|
||||
<div className="space-y-3 text-xs">
|
||||
<div>
|
||||
<strong>{t("usageScript.configFormat")}</strong>
|
||||
<pre className="mt-1 p-2 bg-black/20 text-foreground rounded border border-white/10 text-[10px] overflow-x-auto">
|
||||
{`({
|
||||
request: {
|
||||
url: "{{baseUrl}}/api/usage",
|
||||
method: "POST",
|
||||
@@ -910,39 +1061,39 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
||||
};
|
||||
}
|
||||
})`}
|
||||
</pre>
|
||||
</div>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<strong>{t("usageScript.extractorFormat")}</strong>
|
||||
<ul className="mt-1 space-y-0.5 ml-2">
|
||||
<li>{t("usageScript.fieldIsValid")}</li>
|
||||
<li>{t("usageScript.fieldInvalidMessage")}</li>
|
||||
<li>{t("usageScript.fieldRemaining")}</li>
|
||||
<li>{t("usageScript.fieldUnit")}</li>
|
||||
<li>{t("usageScript.fieldPlanName")}</li>
|
||||
<li>{t("usageScript.fieldTotal")}</li>
|
||||
<li>{t("usageScript.fieldUsed")}</li>
|
||||
<li>{t("usageScript.fieldExtra")}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<strong>{t("usageScript.extractorFormat")}</strong>
|
||||
<ul className="mt-1 space-y-0.5 ml-2">
|
||||
<li>{t("usageScript.fieldIsValid")}</li>
|
||||
<li>{t("usageScript.fieldInvalidMessage")}</li>
|
||||
<li>{t("usageScript.fieldRemaining")}</li>
|
||||
<li>{t("usageScript.fieldUnit")}</li>
|
||||
<li>{t("usageScript.fieldPlanName")}</li>
|
||||
<li>{t("usageScript.fieldTotal")}</li>
|
||||
<li>{t("usageScript.fieldUsed")}</li>
|
||||
<li>{t("usageScript.fieldExtra")}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="text-muted-foreground">
|
||||
<strong>{t("usageScript.tips")}</strong>
|
||||
<ul className="mt-1 space-y-0.5 ml-2">
|
||||
<li>
|
||||
{t("usageScript.tip1", {
|
||||
apiKey: "{{apiKey}}",
|
||||
baseUrl: "{{baseUrl}}",
|
||||
})}
|
||||
</li>
|
||||
<li>{t("usageScript.tip2")}</li>
|
||||
<li>{t("usageScript.tip3")}</li>
|
||||
</ul>
|
||||
<div className="text-muted-foreground">
|
||||
<strong>{t("usageScript.tips")}</strong>
|
||||
<ul className="mt-1 space-y-0.5 ml-2">
|
||||
<li>
|
||||
{t("usageScript.tip1", {
|
||||
apiKey: "{{apiKey}}",
|
||||
baseUrl: "{{baseUrl}}",
|
||||
})}
|
||||
</li>
|
||||
<li>{t("usageScript.tip2")}</li>
|
||||
<li>{t("usageScript.tip3")}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -343,9 +343,7 @@ export function ProviderCard({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex items-center ml-auto min-w-0 gap-3"
|
||||
>
|
||||
<div className="flex items-center ml-auto min-w-0 gap-3">
|
||||
<div className="ml-auto">
|
||||
<div className="flex items-center gap-1">
|
||||
{isOfficial ? (
|
||||
@@ -393,9 +391,7 @@ export function ProviderCard({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex items-center gap-1.5 flex-shrink-0 opacity-0 pointer-events-none group-hover:opacity-100 group-focus-within:opacity-100 group-hover:pointer-events-auto group-focus-within:pointer-events-auto transition-opacity duration-200"
|
||||
>
|
||||
<div className="flex items-center gap-1.5 flex-shrink-0 opacity-0 pointer-events-none group-hover:opacity-100 group-focus-within:opacity-100 group-hover:pointer-events-auto group-focus-within:pointer-events-auto transition-opacity duration-200">
|
||||
<ProviderActions
|
||||
appId={appId}
|
||||
isCurrent={isCurrent}
|
||||
@@ -406,7 +402,9 @@ export function ProviderCard({
|
||||
onSwitch={() => onSwitch(provider)}
|
||||
onEdit={() => onEdit(provider)}
|
||||
onDuplicate={() => onDuplicate(provider)}
|
||||
onTest={onTest && !isOfficial ? () => onTest(provider) : undefined}
|
||||
onTest={
|
||||
onTest && !isOfficial ? () => onTest(provider) : undefined
|
||||
}
|
||||
onConfigureUsage={
|
||||
isOfficial ? undefined : () => onConfigureUsage(provider)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ export const TEMPLATE_TYPES = {
|
||||
GENERAL: "general",
|
||||
NEW_API: "newapi",
|
||||
GITHUB_COPILOT: "github_copilot",
|
||||
TOKEN_PLAN: "token_plan",
|
||||
} as const;
|
||||
|
||||
export type TemplateType = (typeof TEMPLATE_TYPES)[keyof typeof TEMPLATE_TYPES];
|
||||
|
||||
@@ -1090,7 +1090,9 @@
|
||||
"templateGeneral": "General",
|
||||
"templateNewAPI": "NewAPI",
|
||||
"templateCopilot": "GitHub Copilot",
|
||||
"templateTokenPlan": "Token Plan",
|
||||
"copilotAutoAuth": "Auto OAuth authentication, no manual credentials needed",
|
||||
"tokenPlanHint": "Automatically uses the provider's API Key and Base URL to query Token Plan quota",
|
||||
"resetDate": "Reset date",
|
||||
"premiumRequests": "Premium Requests",
|
||||
"credentialsConfig": "Credentials",
|
||||
|
||||
@@ -1090,7 +1090,9 @@
|
||||
"templateGeneral": "General",
|
||||
"templateNewAPI": "NewAPI",
|
||||
"templateCopilot": "GitHub Copilot",
|
||||
"templateTokenPlan": "Token Plan",
|
||||
"copilotAutoAuth": "OAuth 認証を自動使用、手動設定不要",
|
||||
"tokenPlanHint": "プロバイダーのAPI KeyとBase URLを使用してToken Planクォータを自動クエリ",
|
||||
"resetDate": "リセット日",
|
||||
"premiumRequests": "Premium リクエスト",
|
||||
"credentialsConfig": "認証情報",
|
||||
|
||||
@@ -1090,7 +1090,9 @@
|
||||
"templateGeneral": "通用模板",
|
||||
"templateNewAPI": "NewAPI",
|
||||
"templateCopilot": "GitHub Copilot",
|
||||
"templateTokenPlan": "Token Plan",
|
||||
"copilotAutoAuth": "自动使用 OAuth 认证,无需手动配置凭证",
|
||||
"tokenPlanHint": "自动使用供应商的 API Key 和 Base URL 查询 Token Plan 额度",
|
||||
"resetDate": "重置日期",
|
||||
"premiumRequests": "Premium 请求",
|
||||
"credentialsConfig": "凭证配置",
|
||||
|
||||
@@ -4,4 +4,9 @@ import type { SubscriptionQuota } from "@/types/subscription";
|
||||
export const subscriptionApi = {
|
||||
getQuota: (tool: string): Promise<SubscriptionQuota> =>
|
||||
invoke("get_subscription_quota", { tool }),
|
||||
getCodingPlanQuota: (
|
||||
baseUrl: string,
|
||||
apiKey: string,
|
||||
): Promise<SubscriptionQuota> =>
|
||||
invoke("get_coding_plan_quota", { baseUrl, apiKey }),
|
||||
};
|
||||
|
||||
@@ -62,6 +62,7 @@ export interface UsageScript {
|
||||
baseUrl?: string; // 用量查询专用的 Base URL(通用和 NewAPI 模板使用)
|
||||
accessToken?: string; // 访问令牌(NewAPI 模板使用)
|
||||
userId?: string; // 用户ID(NewAPI 模板使用)
|
||||
codingPlanProvider?: string; // Coding Plan 供应商标识(如 "kimi", "zhipu", "minimax")
|
||||
autoQueryInterval?: number; // 自动查询间隔(单位:分钟,0 表示禁用)
|
||||
autoIntervalMinutes?: number; // 自动查询间隔(分钟)- 别名字段
|
||||
request?: {
|
||||
|
||||
Reference in New Issue
Block a user