diff --git a/src-tauri/src/services/coding_plan.rs b/src-tauri/src/services/coding_plan.rs index 1798b10c5..39c3309f2 100644 --- a/src-tauri/src/services/coding_plan.rs +++ b/src-tauri/src/services/coding_plan.rs @@ -239,12 +239,31 @@ fn parse_zhipu_token_tiers(data: &serde_json::Value) -> Vec { .collect() } -async fn query_zhipu(api_key: &str) -> SubscriptionQuota { - let client = crate::proxy::http_client::get(); +/// Resolve the Zhipu quota endpoint from the user's configured `base_url`. +/// +/// Zhipu ships as two distinct presets (Zhipu GLM = `open.bigmodel.cn`, +/// Zhipu GLM en = `api.z.ai`) that share the same quota path and JSON shape. +/// The quota endpoint lives on the same host as the user's coding endpoint, +/// so we route by `base_url` and let the caller's existing reachability +/// (they're already using this host to run coding) determine success — no +/// cross-host fallback, no auth-error heuristics. +fn zhipu_quota_base(base_url: &str) -> &'static str { + if base_url.to_lowercase().contains("bigmodel.cn") { + "https://open.bigmodel.cn" + } else { + "https://api.z.ai" + } +} + +async fn query_zhipu(base_url: &str, api_key: &str) -> SubscriptionQuota { + let client = crate::proxy::http_client::get(); + let url = format!( + "{}/api/monitor/usage/quota/limit", + zhipu_quota_base(base_url) + ); - // 统一走 api.z.ai 国际站(中国站 bigmodel.cn 有反爬机制) let resp = client - .get("https://api.z.ai/api/monitor/usage/quota/limit") + .get(&url) .header("Authorization", api_key) // 注意:智谱不加 Bearer 前缀 .header("Content-Type", "application/json") .header("Accept-Language", "en-US,en") @@ -629,7 +648,9 @@ pub async fn get_coding_plan_quota( let quota = match provider { CodingPlanProvider::Kimi => query_kimi(api_key).await, - CodingPlanProvider::ZhipuCn | CodingPlanProvider::ZhipuEn => query_zhipu(api_key).await, + CodingPlanProvider::ZhipuCn | CodingPlanProvider::ZhipuEn => { + query_zhipu(base_url, api_key).await + } CodingPlanProvider::MiniMaxCn => query_minimax(api_key, true).await, CodingPlanProvider::MiniMaxEn => query_minimax(api_key, false).await, CodingPlanProvider::ZenMux => query_zenmux(base_url, api_key).await, @@ -640,7 +661,10 @@ pub async fn get_coding_plan_quota( #[cfg(test)] mod tests { - use super::{parse_minimax_tiers, parse_zhipu_token_tiers, TIER_FIVE_HOUR, TIER_WEEKLY_LIMIT}; + use super::{ + parse_minimax_tiers, parse_zhipu_token_tiers, zhipu_quota_base, TIER_FIVE_HOUR, + TIER_WEEKLY_LIMIT, + }; use serde_json::json; #[test] @@ -940,4 +964,42 @@ mod tests { assert_eq!(tiers[0].name, TIER_FIVE_HOUR); assert_eq!(tiers[0].utilization, 20.0); } + + #[test] + fn zhipu_quota_base_routes_bigmodel_url_to_cn_endpoint() { + assert_eq!( + zhipu_quota_base("https://open.bigmodel.cn/api/paas/v4"), + "https://open.bigmodel.cn" + ); + } + + #[test] + fn zhipu_quota_base_routes_z_ai_url_to_en_endpoint() { + assert_eq!( + zhipu_quota_base("https://api.z.ai/api/paas/v4"), + "https://api.z.ai" + ); + } + + #[test] + fn zhipu_quota_base_defaults_to_en_for_unknown_url() { + // 没有明显 Zhipu 域名特征时,默认走国际站(更通用的入口) + assert_eq!( + zhipu_quota_base("https://example.com/zhipu"), + "https://api.z.ai" + ); + } + + #[test] + fn zhipu_quota_base_routes_uppercase_cn_url_to_cn_endpoint() { + // 大小写不敏感:与 detect_provider 保持一致的约定,避免大写 preset URL 静默路由到国际站 + assert_eq!( + zhipu_quota_base("HTTPS://OPEN.BIGMODEL.CN/api/paas/v4"), + "https://open.bigmodel.cn" + ); + assert_eq!( + zhipu_quota_base("https://Open.BigModel.cn/api/paas/v4"), + "https://open.bigmodel.cn" + ); + } }