diff --git a/docs/USAGE-QUERY-ADAPTATION.md b/docs/USAGE-QUERY-ADAPTATION.md index 712b082..f97215e 100644 --- a/docs/USAGE-QUERY-ADAPTATION.md +++ b/docs/USAGE-QUERY-ADAPTATION.md @@ -253,6 +253,17 @@ KimiSwitch 现有 `usageKinds` 枚举是**每家中转站一个 Rust 函数**的 ## 5. 新增能力实现清单(改动点) +> 实施状态更新(2026-09-13):**Sub2API 兼容已实现(`balance:sub2api`)**。 +> 实测澄清:`codingplan.site`(主域)**不是** NewAPI 而是 Wei-Shaw/sub2api 面板 +> (NewAPI 端点 `/api/status`、`/api/user/self` 全 404;`/api/v1/settings/public` +> 返回 `data.affiliate_enabled` 指纹命中;`/v1/usage`、`/v1/sub2api/billing` 为 +> API-key 网关端点)。`ai.codingplan.site` 才是 NewAPI 实例。detect_provider +> 已按此拆分家族规则。Sub2API 查询复用推理 sk- key(`GET {base}/v1/usage`, +> Bearer),解析三种 mode:quota_limited(quota + 5h/1d/7d rate_limits)、 +> unrestricted 订阅组(daily/weekly/monthly usage/limit USD)、钱包余额。 +> UsageConfigModal 模板加 "sub2api"(无需凭据,可选 Base URL 覆盖)。 +> 验证:cargo test 124 通过(含 6 个 sub2api 解析用例)、tsc 零错误。 + > 实施状态(2026-07-31):**P0 已实现,未验证、未提交**。 > 展示方案已确认为 **A(供应商卡片底部 UsageFooter,零前端改动)**。 > 改动文件:`src-tauri/src/services/balance.rs`(query_kimi/parse_kimi + 单测)、`src-tauri/src/services/mod.rs`(枚举/路由/detect + 单测)、`src/config/providerPresets.ts`(UsageKind/SUPPORTED_USAGE_KINDS + moonshot 预设挂 `balance:kimi`)。 diff --git a/package.json b/package.json index d040d05..f942035 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "kimiswitch", "private": true, - "version": "0.7.16", + "version": "0.7.17", "type": "module", "scripts": { "dev": "vite", diff --git a/release-notes-v0.7.17.md b/release-notes-v0.7.17.md new file mode 100644 index 0000000..80cc288 --- /dev/null +++ b/release-notes-v0.7.17.md @@ -0,0 +1,16 @@ +# KimiSwitch v0.7.17 + +## 新增 + +- **账单查询适配 Sub2API 中转站(`balance:sub2api`)**:支持 Wei-Shaw/sub2api 面板的余额/额度查询(`GET {base}/v1/usage`),自动复用推理 `sk-` API Key,**无需**网页后台 Access Token;解析三种模式——单 Key 总额度(quota)、5 小时/每日/7 天速率窗口(含重置倒计时)、订阅组日/周/月额度与钱包余额(USD) + - 用量配置面板新增「Sub2API 中转站」模板,可选覆盖查询地址(Base URL) + - `detect_provider` 家族规则拆分:`codingplan.site` 主域识别为 Sub2API,`ai.codingplan.site` 识别为 NewAPI(修正此前主域被误判为 NewAPI 导致查询 404 的问题) +- **模型映射「获取模型列表」新增批量选择**:发现列表标题右侧新增「全选 / 取消全选 / 反选」三个按钮,配合已有的已添加标记与「添加选中的」,批量接入中转站模型列表更省事 + +## 修复 + +- 修复 `codingplan.site`(Sub2API 面板)走 NewAPI 模板查询必然失败的问题——NewAPI 模板(Access Token + User ID)仍适用于 NewAPI / OneAPI 站点,两者互不影响 + +## 备注 + +- Sub2API 查询的金额单位由上游硬编码为 USD;速率窗口标签沿用现有本地化(5 小时 / 每日 / 7 天) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index c98a683..2f9eb6a 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1978,7 +1978,7 @@ dependencies = [ [[package]] name = "kimiswitch" -version = "0.7.16" +version = "0.7.17" dependencies = [ "anyhow", "chrono", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 9e51189..855154b 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kimiswitch" -version = "0.7.16" +version = "0.7.17" description = "Kimi Switch - model config manager" authors = ["codingplan.site"] edition = "2021" diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index f99c0b7..8e6116a 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -690,19 +690,20 @@ pub async fn query_provider_usage( let base_url = resolve_base_url(provider); - // NewAPI template: query the gateway's own usage endpoints with its - // dedicated credentials, bypassing usage_kinds entirely. + // NewAPI / Sub2API template: query the gateway's own usage endpoints. + // NewAPI needs web-console credentials (accessToken + userId); Sub2API + // reuses the inference API key directly — both bypass usage_kinds. if let Some(cfg) = &provider.usage_config { - if cfg.template_type == UsageConfig::TEMPLATE_NEWAPI { + let template_kind = match cfg.template_type.as_str() { + UsageConfig::TEMPLATE_NEWAPI => Some(UsageKind::BalanceNewapi), + UsageConfig::TEMPLATE_SUB2API => Some(UsageKind::BalanceSub2Api), + _ => None, + }; + if let Some(kind) = template_kind { // Err = transient (network) → propagate for retry, same semantics // as the kinds loop below. Config errors surface as Ok(failure). - let result = services::query_kind( - UsageKind::BalanceNewapi, - &base_url, - &api_key, - Some(cfg), - ) - .await?; + let result = + services::query_kind(kind, &base_url, &api_key, Some(cfg)).await?; if result.success { usage_cache() .lock() diff --git a/src-tauri/src/models.rs b/src-tauri/src/models.rs index 0aa2785..694bdee 100644 --- a/src-tauri/src/models.rs +++ b/src-tauri/src/models.rs @@ -290,6 +290,7 @@ pub struct UsageConfig { impl UsageConfig { pub const TEMPLATE_AUTO: &'static str = "auto"; pub const TEMPLATE_NEWAPI: &'static str = "newapi"; + pub const TEMPLATE_SUB2API: &'static str = "sub2api"; } impl std::fmt::Debug for Config { diff --git a/src-tauri/src/services/balance.rs b/src-tauri/src/services/balance.rs index 8c8ed11..45eabc4 100644 --- a/src-tauri/src/services/balance.rs +++ b/src-tauri/src/services/balance.rs @@ -521,6 +521,159 @@ fn parse_newapi( }]) } +// ── Sub2API (Wei-Shaw/sub2api) 中转站 ──────────────────────── +// GET {base_url}/v1/usage +// 头:Authorization: Bearer (就是推理用的 sk- key,无需额外凭据) +// Response(三种 mode): +// - quota_limited:key 设了总额度/速率限制 → quota{limit,used,remaining} + +// rate_limits[{window: 5h|1d|7d, limit, used, remaining, reset_at}] +// - unrestricted + subscription:订阅组 → planName + subscription 的 +// daily/weekly/monthly usage/limit(USD) +// - unrestricted + 钱包:planName="钱包余额" → balance / remaining(USD) +// +// 站点指纹(可选探测):GET /api/v1/settings/public 返回 data.affiliate_enabled +// 即为 sub2api 面板。注意 NewAPI 的 /api/user/self、/api/status 在 sub2api 上 +// 全部 404 —— 两套中转站协议不互通。 + +/// 把供应商 base_url 归一成站点根(去掉结尾 `/` 与 `/v1`),网关路由挂在根路径。 +fn sub2api_site_root(base_url: &str) -> String { + let mut base = base_url.trim_end_matches('/'); + if let Some(stripped) = base.strip_suffix("/v1") { + base = stripped; + } + base.to_string() +} + +pub async fn query_sub2api( + base_url: &str, + api_key: &str, + timeout: Duration, +) -> Result { + let url = format!("{}/v1/usage", sub2api_site_root(base_url)); + match get_json(&url, api_key, AuthStyle::Bearer, timeout).await? { + Fetched::Body(body) => Ok(parse_sub2api(&body)), + Fetched::Failed(err) => Ok(err), + } +} + +/// 速率窗口名 → 本地化 tier 名(前端 planLabel 会翻译;five_hour / +/// weekly_limit 已有映射,daily_limit 新增)。 +fn sub2api_window_tier(window: &str) -> &'static str { + match window { + "5h" => "five_hour", + "1d" => "daily_limit", + "7d" => "weekly_limit", + _ => "window", + } +} + +fn parse_sub2api(body: &serde_json::Value) -> UsageResult { + let is_valid = body + .get("isValid") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + let mode = body.get("mode").and_then(|v| v.as_str()).unwrap_or(""); + if mode.is_empty() { + return UsageResult::failure( + "Missing 'mode' field in response (not a sub2api gateway?)".to_string(), + ); + } + + let mut out: Vec = Vec::new(); + + // key 总额度(quota_limited 模式才有) + if let Some(quota) = body.get("quota") { + if let Some(limit) = parse_f64_field(quota, "limit").filter(|&l| l > 0.0) { + let used = parse_f64_field(quota, "used").unwrap_or(0.0); + let remaining = parse_f64_field(quota, "remaining").unwrap_or(limit - used); + out.push(UsageData { + plan_name: Some("Sub2API".to_string()), + remaining: Some(remaining), + total: Some(limit), + used: Some(used), + unit: Some("USD".to_string()), + is_valid: Some(is_valid && remaining > 0.0), + ..Default::default() + }); + } + } + + // 速率窗口(5h / 1d / 7d;reset_at 为 RFC3339,窗口过期时缺省) + if let Some(rates) = body.get("rate_limits").and_then(|v| v.as_array()) { + for w in rates { + let window = w.get("window").and_then(|v| v.as_str()).unwrap_or(""); + let Some(limit) = parse_f64_field(w, "limit").filter(|&l| l > 0.0) else { + continue; + }; + let used = parse_f64_field(w, "used").unwrap_or(0.0); + let remaining = parse_f64_field(w, "remaining").unwrap_or(limit - used); + out.push(UsageData { + plan_name: Some(sub2api_window_tier(window).to_string()), + remaining: Some(remaining), + total: Some(limit), + used: Some(used), + unit: Some("USD".to_string()), + is_valid: Some(is_valid && remaining > 0.0), + resets_at: w + .get("reset_at") + .and_then(|v| v.as_str()) + .map(str::to_string), + }); + } + } + + // unrestricted 订阅组:daily/weekly/monthly 三窗口(usage_usd vs limit_usd) + if mode == "unrestricted" { + if let Some(sub) = body.get("subscription") { + for (prefix, tier) in [ + ("daily", "daily_limit"), + ("weekly", "weekly_limit"), + ("monthly", "monthly_limit"), + ] { + let Some(usage) = parse_f64_field(sub, &format!("{prefix}_usage_usd")) else { + continue; + }; + let Some(limit) = parse_f64_field(sub, &format!("{prefix}_limit_usd")) + .filter(|&l| l > 0.0) + else { + continue; + }; + let remaining = limit - usage; + out.push(UsageData { + plan_name: Some(tier.to_string()), + remaining: Some(remaining), + total: Some(limit), + used: Some(usage), + unit: Some("USD".to_string()), + is_valid: Some(is_valid && remaining > 0.0), + resets_at: None, + }); + } + } + } + + // 钱包余额(unrestricted 非订阅)或以上全部缺失时的兜底 + if out.is_empty() { + if let Some(balance) = parse_f64_field(body, "balance") + .or_else(|| parse_f64_field(body, "remaining")) + { + out.push(UsageData { + plan_name: Some("Sub2API".to_string()), + remaining: Some(balance), + unit: Some("USD".to_string()), + is_valid: Some(is_valid), + ..Default::default() + }); + } + } + + if out.is_empty() { + UsageResult::failure("No quota/rate-limit/balance fields in response".to_string()) + } else { + UsageResult::ok(out) + } +} + #[cfg(test)] mod tests { use super::*; @@ -688,4 +841,105 @@ mod tests { assert_eq!(data[0].remaining, Some(1.0)); assert_eq!(data[0].used, Some(0.2)); } + + #[test] + fn sub2api_site_root_strips_v1_and_slash() { + assert_eq!(sub2api_site_root("https://x.com"), "https://x.com"); + assert_eq!(sub2api_site_root("https://x.com/"), "https://x.com"); + assert_eq!(sub2api_site_root("https://x.com/v1"), "https://x.com"); + assert_eq!(sub2api_site_root("https://x.com/v1/"), "https://x.com"); + } + + #[test] + fn sub2api_quota_limited_parses_quota_and_windows() { + let body = json!({ + "mode": "quota_limited", + "isValid": true, + "quota": { "limit": 50.0, "used": 20.0, "remaining": 30.0, "unit": "USD" }, + "remaining": 30.0, + "unit": "USD", + "rate_limits": [ + { "window": "5h", "limit": 10.0, "used": 4.0, "remaining": 6.0, + "window_start": "2026-09-13T00:00:00Z", "reset_at": "2026-09-13T05:00:00Z" }, + { "window": "1d", "limit": 30.0, "used": 10.0, "remaining": 20.0, + "window_start": "2026-09-13T00:00:00Z" } + ] + }); + let result = parse_sub2api(&body); + assert!(result.success); + let data = result.data.unwrap(); + assert_eq!(data.len(), 3); + assert_eq!(data[0].plan_name.as_deref(), Some("Sub2API")); + assert_eq!(data[0].remaining, Some(30.0)); + assert_eq!(data[0].total, Some(50.0)); + assert_eq!(data[0].used, Some(20.0)); + assert_eq!(data[0].unit.as_deref(), Some("USD")); + assert_eq!(data[1].plan_name.as_deref(), Some("five_hour")); + assert_eq!(data[1].remaining, Some(6.0)); + assert_eq!(data[1].resets_at.as_deref(), Some("2026-09-13T05:00:00Z")); + assert_eq!(data[2].plan_name.as_deref(), Some("daily_limit")); + assert_eq!(data[2].resets_at, None); + } + + #[test] + fn sub2api_subscription_windows() { + let body = json!({ + "mode": "unrestricted", + "isValid": true, + "planName": "claude-pro", + "unit": "USD", + "subscription": { + "daily_usage_usd": 1.0, "daily_limit_usd": 10.0, + "weekly_usage_usd": 8.0, "weekly_limit_usd": 30.0, + "monthly_usage_usd": 55.0, "monthly_limit_usd": 60.0 + } + }); + let result = parse_sub2api(&body); + assert!(result.success); + let data = result.data.unwrap(); + assert_eq!(data.len(), 3); + assert_eq!(data[0].plan_name.as_deref(), Some("daily_limit")); + assert_eq!(data[0].remaining, Some(9.0)); + assert_eq!(data[1].plan_name.as_deref(), Some("weekly_limit")); + assert_eq!(data[1].remaining, Some(22.0)); + assert_eq!(data[2].plan_name.as_deref(), Some("monthly_limit")); + assert_eq!(data[2].remaining, Some(5.0)); + assert_eq!(data[2].is_valid, Some(true)); + } + + #[test] + fn sub2api_wallet_balance() { + let body = json!({ + "mode": "unrestricted", + "isValid": true, + "planName": "钱包余额", + "remaining": 12.5, + "unit": "USD", + "balance": 12.5 + }); + let data = parse_sub2api(&body).data.unwrap(); + assert_eq!(data.len(), 1); + assert_eq!(data[0].plan_name.as_deref(), Some("Sub2API")); + assert_eq!(data[0].remaining, Some(12.5)); + assert_eq!(data[0].unit.as_deref(), Some("USD")); + } + + #[test] + fn sub2api_missing_mode_is_deterministic_failure() { + let body = json!({ "foo": 1 }); + let result = parse_sub2api(&body); + assert!(!result.success); + assert!(result.error.unwrap().contains("Missing 'mode'")); + } + + #[test] + fn sub2api_exhausted_window_is_invalid() { + let body = json!({ + "mode": "quota_limited", + "isValid": true, + "quota": { "limit": 10.0, "used": 10.0, "remaining": 0.0, "unit": "USD" } + }); + let data = parse_sub2api(&body).data.unwrap(); + assert_eq!(data[0].is_valid, Some(false)); + } } diff --git a/src-tauri/src/services/mod.rs b/src-tauri/src/services/mod.rs index 5f7ed0e..f1bc7a4 100644 --- a/src-tauri/src/services/mod.rs +++ b/src-tauri/src/services/mod.rs @@ -23,6 +23,7 @@ pub enum UsageKind { BalanceNovita, BalanceKimi, BalanceNewapi, + BalanceSub2Api, PlanKimiCoding, PlanZhipu, PlanMinimax, @@ -40,6 +41,7 @@ impl UsageKind { UsageKind::BalanceNovita => "balance:novita", UsageKind::BalanceKimi => "balance:kimi", UsageKind::BalanceNewapi => "balance:newapi", + UsageKind::BalanceSub2Api => "balance:sub2api", UsageKind::PlanKimiCoding => "plan:kimi_coding", UsageKind::PlanZhipu => "plan:zhipu", UsageKind::PlanMinimax => "plan:minimax", @@ -47,7 +49,7 @@ impl UsageKind { } } - pub const ALL: [UsageKind; 11] = [ + pub const ALL: [UsageKind; 12] = [ UsageKind::BalanceDeepseek, UsageKind::BalanceSiliconflow, UsageKind::BalanceOpenrouter, @@ -55,6 +57,7 @@ impl UsageKind { UsageKind::BalanceNovita, UsageKind::BalanceKimi, UsageKind::BalanceNewapi, + UsageKind::BalanceSub2Api, UsageKind::PlanKimiCoding, UsageKind::PlanZhipu, UsageKind::PlanMinimax, @@ -74,6 +77,7 @@ impl std::str::FromStr for UsageKind { "balance:novita" => UsageKind::BalanceNovita, "balance:kimi" => UsageKind::BalanceKimi, "balance:newapi" => UsageKind::BalanceNewapi, + "balance:sub2api" => UsageKind::BalanceSub2Api, "plan:kimi_coding" => UsageKind::PlanKimiCoding, "plan:zhipu" => UsageKind::PlanZhipu, "plan:minimax" => UsageKind::PlanMinimax, @@ -97,10 +101,13 @@ pub fn detect_provider(base_url: &str) -> Vec { if url.contains("openrouter.ai") { kinds.push(UsageKind::BalanceOpenrouter); } - // codingplan.site 检测为 NewAPI 实例(本工具仅支持 NewAPI 查询); - // 家族匹配意味着 ai. 子域也自然命中该规则。 - if url.contains("codingplan.site") { + // codingplan.site 家族:主域(含任意子域除 ai. 外)是 Sub2API 面板 + // (/api/v1/settings/public 指纹命中,NewAPI 端点全 404); + // ai.codingplan.site 才是 NewAPI 实例(/api/status、/api/user/self 可用)。 + if url.contains("ai.codingplan.site") { kinds.push(UsageKind::BalanceNewapi); + } else if url.contains("codingplan.site") { + kinds.push(UsageKind::BalanceSub2Api); } if url.contains("api.stepfun.com") { kinds.push(UsageKind::BalanceStepfun); @@ -186,6 +193,14 @@ pub async fn query_kind( .unwrap_or(base_url); balance::query_newapi(url, token, uid, timeout).await } + UsageKind::BalanceSub2Api => { + // sub2api 复用推理 API Key,无需额外凭据;base_url 同样允许面板覆盖。 + let url = usage_config + .and_then(|c| c.base_url.as_deref()) + .filter(|s| !s.is_empty()) + .unwrap_or(base_url); + balance::query_sub2api(url, api_key, timeout).await + } UsageKind::PlanKimiCoding => { coding_plan::query_kimi_coding(base_url, api_key, timeout).await } @@ -279,15 +294,15 @@ mod tests { #[test] fn detect_provider_codingplan_site_family() { - // codingplan.site 是 NewAPI 实例(本工具仅支持 NewAPI 查询): - // 主域与 ai. 子域都命中家族规则,且只映射到 BalanceNewapi。 + // codingplan.site 家族分家:主域是 Sub2API 面板(推理 key 直查 /v1/usage), + // ai. 子域才是 NewAPI 实例。 assert_eq!( detect_provider("https://codingplan.site"), - vec![UsageKind::BalanceNewapi] + vec![UsageKind::BalanceSub2Api] ); assert_eq!( detect_provider("https://codingplan.site/v1"), - vec![UsageKind::BalanceNewapi] + vec![UsageKind::BalanceSub2Api] ); assert_eq!( detect_provider("https://ai.codingplan.site"), diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 9c6c5eb..ad7d478 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,6 +1,6 @@ { "productName": "Kimi Switch", - "version": "0.7.16", + "version": "0.7.17", "identifier": "com.kimiswitch.app", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/components/ProviderEdit.tsx b/src/components/ProviderEdit.tsx index 6fbc796..dc3b50e 100644 --- a/src/components/ProviderEdit.tsx +++ b/src/components/ProviderEdit.tsx @@ -527,6 +527,18 @@ function ModelMapping({ }); }; + const selectAllModels = () => { + if (!discovered) return; + setSelected(new Set(discovered.map((m) => m.id))); + }; + + const deselectAllModels = () => setSelected(new Set()); + + const invertSelection = () => { + if (!discovered) return; + setSelected((prev) => new Set(discovered.filter((m) => !prev.has(m.id)).map((m) => m.id))); + }; + const handleAddSelected = () => { if (!discovered || selected.size === 0) return; const existingIds = new Set(models.filter((m) => m.model).map((m) => m.model)); @@ -596,8 +608,33 @@ function ModelMapping({ {discovered && (
-
- {t("discoveredModels", { count: discovered.length })} +
+
+ {t("discoveredModels", { count: discovered.length })} +
+
+ + + +
@@ -344,6 +347,21 @@ export function UsageConfigModal({ open, agent, provider, onClose, onSave }: Usa
)} + {/* sub2api: no extra credentials (reuses the inference key), optional gateway override */} + {!isManaged && cfg.templateType === "sub2api" && ( +
+
+ + setCfg({ ...cfg, baseUrl: e.target.value || undefined })} + /> +
+
+ )} {/* timeout + auto query interval */} diff --git a/src/config/providerPresets.ts b/src/config/providerPresets.ts index 2fbcfd9..0bed075 100644 --- a/src/config/providerPresets.ts +++ b/src/config/providerPresets.ts @@ -15,6 +15,7 @@ export type UsageKind = | "balance:novita" | "balance:kimi" | "balance:newapi" + | "balance:sub2api" | "plan:kimi_coding" | "plan:zhipu" | "plan:minimax" @@ -32,6 +33,7 @@ export const SUPPORTED_USAGE_KINDS: ReadonlySet = new Set([ "balance:novita", "balance:kimi", "balance:newapi", + "balance:sub2api", "plan:kimi_coding", "plan:zhipu", "plan:minimax", diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 2f4acf4..42cadcb 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -190,6 +190,7 @@ export const enTranslations: Record = { statusAll: "All", selectAll: "Select all", deselectAll: "Deselect all", + invertSelection: "Invert selection", selectedCount: "{n} selected", archive: "Archive", unarchive: "Unarchive", @@ -408,8 +409,10 @@ export const enTranslations: Record = { usageTemplate: "Query template", usageTemplateAuto: "Auto-detect", usageTemplateNewapi: "NewAPI gateway", + usageTemplateSub2api: "Sub2API gateway", usageAutoDetectHint: "Query balance / plan with the provider's API Key and Base URL", usageNewapiHint: "Query a NewAPI / OneAPI gateway balance (requires a web-console Access Token, not the sk- inference key)", + usageSub2apiHint: "Query a Sub2API (Wei-Shaw/sub2api) gateway balance / quota (GET /v1/usage). Reuses the inference sk- API key — no extra credentials; the query URL is optionally overridable", usageOAuthManagedHint: "Official OAuth managed: queries the subscription with your Kimi Code login credentials", usageDetectedKinds: "Detected query kinds", usageNoKinds: "Not detected (switch to the NewAPI template to configure manually)", @@ -430,6 +433,7 @@ export const enTranslations: Record = { usageSwitchToNewapi: "Switch to NewAPI template", usageUnsupportedProvider: "This provider is not supported by auto query", usageTier5h: "5-hour", + usageTierDaily: "Daily", usageTierWeekly: "Weekly", usageTierMonthly: "Monthly", usageErrNoKey: "No API key configured", diff --git a/src/i18n/zh.ts b/src/i18n/zh.ts index 1595f9e..8c54db0 100644 --- a/src/i18n/zh.ts +++ b/src/i18n/zh.ts @@ -187,6 +187,7 @@ export const zhTranslations = { statusAll: "全部", selectAll: "全选", deselectAll: "取消全选", + invertSelection: "反选", selectedCount: "已选 {n} 项", archive: "归档", unarchive: "取消归档", @@ -403,8 +404,10 @@ export const zhTranslations = { usageTemplate: "查询模板", usageTemplateAuto: "自动检测", usageTemplateNewapi: "NewAPI 中转站", + usageTemplateSub2api: "Sub2API 中转站", usageAutoDetectHint: "自动使用供应商的 API Key 和 Base URL 查询余额 / 套餐", usageNewapiHint: "查询 NewAPI / OneAPI 中转站的账号余额(需网页后台 Access Token,不是 sk- 推理 Key)", + usageSub2apiHint: "查询 Sub2API (Wei-Shaw/sub2api) 面板的余额 / 额度(GET /v1/usage),自动复用推理 sk- API Key,无需额外凭据;可选覆盖查询地址", usageOAuthManagedHint: "官方 OAuth 托管:自动使用 Kimi Code 登录凭据查询订阅用量", usageDetectedKinds: "检测到的查询类型", usageNoKinds: "未识别(可切换到 NewAPI 模板手动配置)", @@ -425,6 +428,7 @@ export const zhTranslations = { usageSwitchToNewapi: "切换到 NewAPI 模板", usageUnsupportedProvider: "此供应商暂不支持自动查询", usageTier5h: "5小时", + usageTierDaily: "每日", usageTierWeekly: "7天", usageTierMonthly: "30天", usageErrNoKey: "未配置 API Key", diff --git a/src/lib/usage-display.ts b/src/lib/usage-display.ts index adf35e6..ac05ad8 100644 --- a/src/lib/usage-display.ts +++ b/src/lib/usage-display.ts @@ -19,6 +19,8 @@ export function planLabel(name: string, t: TranslateFn): string { switch (name) { case "five_hour": return t("usageTier5h"); + case "daily_limit": + return t("usageTierDaily"); case "weekly_limit": return t("usageTierWeekly"); case "monthly_limit": diff --git a/src/types/index.ts b/src/types/index.ts index 208e00f..b9b6e80 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -75,11 +75,12 @@ export interface Config { /** * Usage query configuration (mirrors the Rust `UsageConfig`, camelCase). * "auto" = query kinds from usageKinds/host detect; "newapi" = query a - * NewAPI/OneAPI gateway with accessToken + userId. + * NewAPI/OneAPI gateway with accessToken + userId; "sub2api" = query a + * Sub2API (Wei-Shaw/sub2api) gateway, reusing the inference API key. */ export interface UsageConfig { enabled: boolean; - templateType: "auto" | "newapi"; + templateType: "auto" | "newapi" | "sub2api"; baseUrl?: string; /** NewAPI web-console access token (NOT the sk- inference key). */ accessToken?: string;