feat(usage): CodingPlan.site 自动识别 NewAPI + 账单查询体验修复
- detect_provider 新增 codingplan.site → balance:newapi(该站为 NewAPI 实例,实测确认),保存补齐 TS 侧缺失的 balance:newapi 断言 - 凭据缺失等确定性错误改走 Ok(failure)(原 Err 被前端一律显示为 「网络异常」),错误消息按 kind 前缀包裹后仍可本地化映射 - formatAmount 收口到 usage-display 统一复用:¤ 占位符号不再渲染、 金额统一两位小数(配置弹窗此前直接拼原始数值)
This commit is contained in:
1 parent
81699e757d
commit
1a11cfbaaf
6 files changed
+198
-31
No files matched your search
@@ -97,6 +97,11 @@ pub fn detect_provider(base_url: &str) -> Vec<UsageKind> {
|
||||
if url.contains("openrouter.ai") {
|
||||
kinds.push(UsageKind::BalanceOpenrouter);
|
||||
}
|
||||
// codingplan.site 检测为 NewAPI 实例(本工具仅支持 NewAPI 查询);
|
||||
// 家族匹配意味着 ai. 子域也自然命中该规则。
|
||||
if url.contains("codingplan.site") {
|
||||
kinds.push(UsageKind::BalanceNewapi);
|
||||
}
|
||||
if url.contains("api.stepfun.com") {
|
||||
kinds.push(UsageKind::BalanceStepfun);
|
||||
}
|
||||
@@ -126,6 +131,19 @@ pub fn detect_provider(base_url: &str) -> Vec<UsageKind> {
|
||||
kinds
|
||||
}
|
||||
|
||||
/// BalanceNewapi 模板所需凭据(web 控制台 access token + user id)。
|
||||
/// 两者都必须存在且非空;返回 None 表示缺少凭据(确定性配置错误)。
|
||||
fn newapi_creds(
|
||||
usage_config: Option<&crate::models::UsageConfig>,
|
||||
) -> Option<(&str, &str)> {
|
||||
usage_config.and_then(|c| {
|
||||
c.access_token
|
||||
.as_deref()
|
||||
.zip(c.user_id.as_deref())
|
||||
.filter(|(t, u)| !t.is_empty() && !u.is_empty())
|
||||
})
|
||||
}
|
||||
|
||||
/// 按 kind 路由到对应查询实现。`base_url` 用于消歧同一家供应商的
|
||||
/// 国内/海外站(SiliconFlow .cn/.com、MiniMax .com/.io、智谱 bigmodel/z.ai)。
|
||||
/// `usage_config` 仅 BalanceNewapi 分支读取(access_token / user_id),
|
||||
@@ -155,16 +173,13 @@ pub async fn query_kind(
|
||||
balance::query_kimi(api_key, !lower.contains("moonshot.ai"), timeout).await
|
||||
}
|
||||
UsageKind::BalanceNewapi => {
|
||||
let (token, uid) = usage_config
|
||||
.and_then(|c| {
|
||||
c.access_token
|
||||
.as_deref()
|
||||
.zip(c.user_id.as_deref())
|
||||
.filter(|(t, u)| !t.is_empty() && !u.is_empty())
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
"newapi template requires accessToken and userId".to_string()
|
||||
})?;
|
||||
// 缺凭据是确定性配置错误 → Ok(failure)(前端按前缀本地化提示),
|
||||
// 而非 Err(Err 语义 = 瞬时网络错误,见 commands.rs newapi 分支)。
|
||||
let Some((token, uid)) = newapi_creds(usage_config) else {
|
||||
return Ok(UsageResult::failure(
|
||||
"newapi template requires accessToken and userId".to_string(),
|
||||
));
|
||||
};
|
||||
let url = usage_config
|
||||
.and_then(|c| c.base_url.as_deref())
|
||||
.filter(|s| !s.is_empty())
|
||||
@@ -262,6 +277,28 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_provider_codingplan_site_family() {
|
||||
// codingplan.site 是 NewAPI 实例(本工具仅支持 NewAPI 查询):
|
||||
// 主域与 ai. 子域都命中家族规则,且只映射到 BalanceNewapi。
|
||||
assert_eq!(
|
||||
detect_provider("https://codingplan.site"),
|
||||
vec![UsageKind::BalanceNewapi]
|
||||
);
|
||||
assert_eq!(
|
||||
detect_provider("https://codingplan.site/v1"),
|
||||
vec![UsageKind::BalanceNewapi]
|
||||
);
|
||||
assert_eq!(
|
||||
detect_provider("https://ai.codingplan.site"),
|
||||
vec![UsageKind::BalanceNewapi]
|
||||
);
|
||||
assert_eq!(
|
||||
detect_provider("https://ai.codingplan.site/v1"),
|
||||
vec![UsageKind::BalanceNewapi]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usage_kind_string_roundtrip() {
|
||||
use std::str::FromStr;
|
||||
@@ -272,4 +309,30 @@ mod tests {
|
||||
assert!(UsageKind::from_str("balance:unknown").is_err());
|
||||
assert!(UsageKind::from_str("").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn newapi_creds_requires_both_and_nonempty() {
|
||||
use crate::models::UsageConfig;
|
||||
let cfg = |token: Option<&str>, uid: Option<&str>| UsageConfig {
|
||||
enabled: true,
|
||||
template_type: "newapi".to_string(),
|
||||
base_url: None,
|
||||
access_token: token.map(str::to_string),
|
||||
user_id: uid.map(str::to_string),
|
||||
auto_query_interval_minutes: None,
|
||||
timeout_seconds: None,
|
||||
};
|
||||
// 缺 config / 缺任一项 / 空字符串 → None(确定性配置错误)
|
||||
assert_eq!(newapi_creds(None), None);
|
||||
assert_eq!(newapi_creds(Some(&cfg(None, None))), None);
|
||||
assert_eq!(newapi_creds(Some(&cfg(Some("tok"), None))), None);
|
||||
assert_eq!(newapi_creds(Some(&cfg(None, Some("uid")))), None);
|
||||
assert_eq!(newapi_creds(Some(&cfg(Some(""), Some("uid")))), None);
|
||||
assert_eq!(newapi_creds(Some(&cfg(Some("tok"), Some("")))), None);
|
||||
// 两项齐全 → Some
|
||||
assert_eq!(
|
||||
newapi_creds(Some(&cfg(Some("tok"), Some("uid")))),
|
||||
Some(("tok", "uid"))
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user