feat(usage): v0.7.6 — OpenCode Go 套餐用量查询(5h/7天/30天三窗口)+ 插件市场入口暂时隐藏(功能保留)
Release / Version consistency (push) Canceled after 0s
Release / Build (macos-latest) (push) Canceled after 0s
Release / Build (ubuntu-latest) (push) Canceled after 0s
Release / Build (windows-latest) (push) Canceled after 0s
Release / Attach macOS install script (push) Canceled after 0s
Release / Version consistency (push) Canceled after 0s
Release / Build (macos-latest) (push) Canceled after 0s
Release / Build (ubuntu-latest) (push) Canceled after 0s
Release / Build (windows-latest) (push) Canceled after 0s
Release / Attach macOS install script (push) Canceled after 0s
This commit is contained in:
1 parent
9a3875d990
commit
6b761251bc
24 files changed
+3353
-77
No files matched your search
Generated
+52
-1
@@ -59,6 +59,15 @@ version = "1.0.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
||||
dependencies = [
|
||||
"derive_arbitrary",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-broadcast"
|
||||
version = "0.7.2"
|
||||
@@ -710,6 +719,17 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "2.1.1"
|
||||
@@ -1958,7 +1978,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kimiswitch"
|
||||
version = "0.7.5"
|
||||
version = "0.7.6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -1975,9 +1995,11 @@ dependencies = [
|
||||
"tauri-build",
|
||||
"tauri-plugin-opener",
|
||||
"tauri-plugin-single-instance",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"toml 0.8.23",
|
||||
"walkdir",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5484,12 +5506,41 @@ dependencies = [
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "2.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"crc32fast",
|
||||
"crossbeam-utils",
|
||||
"displaydoc",
|
||||
"flate2",
|
||||
"indexmap 2.14.0",
|
||||
"memchr",
|
||||
"thiserror 2.0.18",
|
||||
"zopfli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
|
||||
[[package]]
|
||||
name = "zopfli"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"crc32fast",
|
||||
"log",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant"
|
||||
version = "5.12.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "kimiswitch"
|
||||
version = "0.7.5"
|
||||
version = "0.7.6"
|
||||
description = "Kimi Switch - model config manager"
|
||||
authors = ["codingplan.site"]
|
||||
edition = "2021"
|
||||
@@ -25,7 +25,13 @@ futures-util = "0.3"
|
||||
dirs = "5.0"
|
||||
toml = "0.8"
|
||||
rusqlite = { version = "0.32", features = ["bundled", "chrono"] }
|
||||
tokio = { version = "1", features = ["sync"] }
|
||||
tokio = { version = "1", features = ["sync", "fs", "io-util"] }
|
||||
# Plugin marketplace: zip extraction (deflate-only; no extra compression
|
||||
# backends needed for plugin archives).
|
||||
zip = { version = "2", default-features = false, features = ["deflate"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
||||
[lib]
|
||||
name = "kimiswitch_lib"
|
||||
|
||||
@@ -345,7 +345,10 @@ pub struct UsageRecord {
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn resolve_kimi_home(override_path: Option<String>) -> PathBuf {
|
||||
/// Resolve the Kimi Code home directory: explicit override, then
|
||||
/// `KIMI_CODE_HOME`, then the default `~/.kimi-code`. Shared with the plugin
|
||||
/// marketplace module (`crate::plugins`).
|
||||
pub(crate) fn resolve_kimi_home(override_path: Option<String>) -> PathBuf {
|
||||
if let Some(p) = override_path {
|
||||
if !p.trim().is_empty() {
|
||||
return PathBuf::from(p);
|
||||
|
||||
@@ -6,6 +6,7 @@ pub mod kimi_code_io;
|
||||
pub mod models;
|
||||
pub mod oauth;
|
||||
pub mod pi_io;
|
||||
pub mod plugins;
|
||||
pub mod services;
|
||||
|
||||
use tauri::menu::{Menu, MenuItem, PredefinedMenuItem};
|
||||
@@ -124,6 +125,11 @@ pub fn run() {
|
||||
dashboard::delete_session,
|
||||
dashboard::delete_workspace,
|
||||
dashboard::get_session_preview,
|
||||
plugins::get_plugin_marketplace,
|
||||
plugins::list_installed_plugins,
|
||||
plugins::install_plugin,
|
||||
plugins::set_plugin_enabled,
|
||||
plugins::remove_plugin,
|
||||
])
|
||||
.manage(commands::KimiWebState::default())
|
||||
.build(tauri::generate_context!())
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -37,16 +37,35 @@ pub(crate) enum Fetched {
|
||||
///
|
||||
/// 先 `bytes()` 再解析:读体失败(超时/连接中断)是瞬时 → Err;拿到完整响应体
|
||||
/// 后解析失败才是确定性。reqwest 的 `.json()` 把读体错误也包成 decode,无法区分。
|
||||
///
|
||||
/// 默认不带 User-Agent;需要自定义 UA 的供应商(如 opencode.ai 有 Cloudflare
|
||||
/// 1010 拦截,必须带浏览器 UA)走 [`get_json_with_ua`]。
|
||||
pub(crate) async fn get_json(
|
||||
url: &str,
|
||||
api_key: &str,
|
||||
auth: AuthStyle,
|
||||
timeout: Duration,
|
||||
) -> Result<Fetched, String> {
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(timeout)
|
||||
.build()
|
||||
.map_err(|e| format!("Failed to build HTTP client: {e}"))?;
|
||||
get_json_with_ua(url, api_key, auth, timeout, None).await
|
||||
}
|
||||
|
||||
/// [`get_json`] 的 UA 变体:`user_agent` 为 `Some` 时设到 client 上。
|
||||
/// 仅新增链路需要,现有调用方不受影响。
|
||||
pub(crate) async fn get_json_with_ua(
|
||||
url: &str,
|
||||
api_key: &str,
|
||||
auth: AuthStyle,
|
||||
timeout: Duration,
|
||||
user_agent: Option<&str>,
|
||||
) -> Result<Fetched, String> {
|
||||
let mut builder = reqwest::Client::builder().timeout(timeout);
|
||||
if let Some(ua) = user_agent {
|
||||
builder = builder.user_agent(ua);
|
||||
}
|
||||
let client = match builder.build() {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(format!("Failed to build HTTP client: {e}")),
|
||||
};
|
||||
|
||||
// 注意:api_key 只允许进请求头,严禁拼进 URL / 日志 / 错误信息。
|
||||
let req = client.get(url).header("Accept", "application/json");
|
||||
|
||||
@@ -3,23 +3,24 @@
|
||||
|
||||
//! Token Plan 套餐额度查询服务
|
||||
//!
|
||||
//! 支持 Kimi For Coding、智谱 GLM、MiniMax 的套餐额度查询。
|
||||
//! 支持 Kimi For Coding、智谱 GLM、MiniMax、OpenCode Go 的套餐额度查询。
|
||||
//! cc-switch 的 SubscriptionQuota/tiers 结构在此展平为 `Vec<UsageData>`:
|
||||
//! 每个窗口(tier)一条 UsageData,`plan_name` = tier 名("five_hour" /
|
||||
//! "weekly_limit"),`used` = 已用百分比(0-100),`total` = 100,
|
||||
//! "weekly_limit" / "monthly_limit"),`used` = 已用百分比(0-100),`total` = 100,
|
||||
//! `remaining` = 剩余百分比,`resets_at` 为 ISO 8601 字符串。
|
||||
//!
|
||||
//! 错误通道语义与 balance.rs 一致(Err = 瞬时,Ok(success:false) = 确定性)。
|
||||
|
||||
use super::balance::{get_json, AuthStyle, Fetched};
|
||||
use super::balance::{get_json, get_json_with_ua, AuthStyle, Fetched};
|
||||
use super::usage_types::{UsageData, UsageResult};
|
||||
use std::time::Duration;
|
||||
|
||||
// 套餐类 tier id 的唯一来源:所有套餐供应商(Kimi/智谱/MiniMax 及未来新增)
|
||||
// 都只用这两个 id。前端 src/lib/usage-display.ts 的 planLabel() 依赖此约定
|
||||
// 做本地化映射——新增 tier id 时必须同步加映射。
|
||||
// 套餐类 tier id 的唯一来源:所有套餐供应商(Kimi/智谱/MiniMax/OpenCode Go 及
|
||||
// 未来新增)都只用这三个 id。前端 src/lib/usage-display.ts 的 planLabel() 依赖
|
||||
// 此约定做本地化映射——新增 tier id 时必须同步加映射。
|
||||
const TIER_FIVE_HOUR: &str = "five_hour";
|
||||
const TIER_WEEKLY_LIMIT: &str = "weekly_limit";
|
||||
const TIER_MONTHLY_LIMIT: &str = "monthly_limit";
|
||||
|
||||
/// 套餐条目的统一构造:按百分比表示用量。
|
||||
fn percent_tier(name: &str, used_percent: f64, resets_at: Option<String>) -> UsageData {
|
||||
@@ -315,6 +316,81 @@ fn parse_minimax(body: &serde_json::Value) -> Result<Vec<UsageData>, UsageResult
|
||||
Ok(tiers)
|
||||
}
|
||||
|
||||
// ── OpenCode Go ─────────────────────────────────────────────
|
||||
// GET https://opencode.ai/zen/go/v1/usage
|
||||
// Response: { usage: { rolling: { status, percent, resetsAt },
|
||||
// weekly: { status, percent, resetsAt },
|
||||
// monthly: { status, percent, resetsAt } } }
|
||||
// percent = 已用百分比(与 percent_tier 语义一致);status 仅作展示参考,缺失容忍;
|
||||
// resetsAt 为 ISO 8601 字符串。
|
||||
//
|
||||
// 坑位:opencode.ai 有 Cloudflare 1010 拦截——reqwest 默认不带 User-Agent 的
|
||||
// 裸请求会被 403(error code: 1010)。必须显式设置浏览器 UA。
|
||||
|
||||
const BROWSER_USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) \
|
||||
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36";
|
||||
|
||||
pub async fn query_opencode_go(
|
||||
api_key: &str,
|
||||
timeout: Duration,
|
||||
) -> Result<UsageResult, String> {
|
||||
match get_json_with_ua(
|
||||
"https://opencode.ai/zen/go/v1/usage",
|
||||
api_key,
|
||||
AuthStyle::Bearer,
|
||||
timeout,
|
||||
Some(BROWSER_USER_AGENT),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
Fetched::Body(body) => {
|
||||
let tiers = parse_opencode_go(&body);
|
||||
if tiers.is_empty() {
|
||||
// 响应里没有可解析的用量窗口(usage 缺失或字段变了)。
|
||||
// 把原始响应(仅用量数字,无密钥)透出,方便对照接口结构修复。
|
||||
return Ok(opencode_go_empty_failure(&body));
|
||||
}
|
||||
Ok(UsageResult::ok(tiers))
|
||||
}
|
||||
Fetched::Failed(err) => Ok(err),
|
||||
}
|
||||
}
|
||||
|
||||
/// usage 全缺时的确定性失败,附 400 字原始响应预览。抽成纯函数便于离线单测。
|
||||
fn opencode_go_empty_failure(body: &serde_json::Value) -> UsageResult {
|
||||
let preview = serde_json::to_string(body)
|
||||
.unwrap_or_else(|_| "<unserializable body>".into());
|
||||
let trimmed: String = preview.chars().take(400).collect();
|
||||
UsageResult::failure(format!(
|
||||
"opencode go usage 响应无套餐数据,原始返回: {trimmed}"
|
||||
))
|
||||
}
|
||||
|
||||
/// 三个窗口(5 小时滚动 / 周 / 月)各解析为一条 `percent_tier`;
|
||||
/// 缺失或 percent 不可解析的窗口跳过(status 字段可缺)。
|
||||
fn parse_opencode_go(body: &serde_json::Value) -> Vec<UsageData> {
|
||||
let mut tiers = Vec::new();
|
||||
let Some(usage) = body.get("usage") else {
|
||||
return tiers;
|
||||
};
|
||||
let windows = [
|
||||
(TIER_FIVE_HOUR, "rolling"),
|
||||
(TIER_WEEKLY_LIMIT, "weekly"),
|
||||
(TIER_MONTHLY_LIMIT, "monthly"),
|
||||
];
|
||||
for (name, key) in windows {
|
||||
let Some(window) = usage.get(key) else {
|
||||
continue;
|
||||
};
|
||||
let Some(percent) = window.get("percent").and_then(parse_f64) else {
|
||||
continue;
|
||||
};
|
||||
let resets_at = window.get("resetsAt").and_then(extract_reset_time);
|
||||
tiers.push(percent_tier(name, percent, resets_at));
|
||||
}
|
||||
tiers
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -429,4 +505,77 @@ mod tests {
|
||||
assert!(!err.success);
|
||||
assert!(err.error.unwrap().contains("invalid key"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opencode_go_parses_three_windows() {
|
||||
// 真实 key 实测样例(2026-08):percent 为已用百分比,直接透传
|
||||
let body = json!({
|
||||
"usage": {
|
||||
"rolling": { "status": "ok", "percent": 9, "resetsAt": "2026-08-18T06:09:19.735Z" },
|
||||
"weekly": { "status": "ok", "percent": 5, "resetsAt": "2026-08-24T00:00:00.735Z" },
|
||||
"monthly": { "status": "ok", "percent": 59, "resetsAt": "2026-08-27T03:33:50.735Z" }
|
||||
}
|
||||
});
|
||||
let tiers = parse_opencode_go(&body);
|
||||
assert_eq!(tiers.len(), 3);
|
||||
assert_eq!(tiers[0].plan_name.as_deref(), Some("five_hour"));
|
||||
assert_eq!(tiers[0].used, Some(9.0));
|
||||
assert_eq!(tiers[0].total, Some(100.0));
|
||||
assert_eq!(tiers[0].remaining, Some(91.0));
|
||||
assert_eq!(
|
||||
tiers[0].resets_at.as_deref(),
|
||||
Some("2026-08-18T06:09:19.735Z")
|
||||
);
|
||||
assert_eq!(tiers[1].plan_name.as_deref(), Some("weekly_limit"));
|
||||
assert_eq!(tiers[1].used, Some(5.0));
|
||||
assert_eq!(tiers[1].remaining, Some(95.0));
|
||||
assert_eq!(tiers[2].plan_name.as_deref(), Some("monthly_limit"));
|
||||
assert_eq!(tiers[2].used, Some(59.0));
|
||||
assert_eq!(tiers[2].remaining, Some(41.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opencode_go_tolerates_missing_status_field() {
|
||||
// status 属展示字段,缺失照常解析(percent/resetsAt 都只要求本身存在)
|
||||
let body = json!({
|
||||
"usage": {
|
||||
"rolling": { "percent": 9, "resetsAt": "2026-08-18T06:09:19.735Z" },
|
||||
"weekly": { "percent": 5, "resetsAt": "2026-08-24T00:00:00.735Z" },
|
||||
"monthly": { "percent": 59, "resetsAt": "2026-08-27T03:33:50.735Z" }
|
||||
}
|
||||
});
|
||||
let tiers = parse_opencode_go(&body);
|
||||
assert_eq!(tiers.len(), 3);
|
||||
assert_eq!(tiers[1].plan_name.as_deref(), Some("weekly_limit"));
|
||||
assert_eq!(tiers[2].used, Some(59.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opencode_go_partial_windows_only_emit_present_ones() {
|
||||
// 单窗口缺失只出两条;percent 缺失 / 非数字的窗口整窗跳过
|
||||
let body = json!({
|
||||
"usage": {
|
||||
"rolling": { "status": "ok", "percent": "9", "resetsAt": "2026-08-18T06:09:19.735Z" },
|
||||
"monthly": { "status": "ok", "resetsAt": "2026-08-27T03:33:50.735Z" }
|
||||
}
|
||||
});
|
||||
let tiers = parse_opencode_go(&body);
|
||||
assert_eq!(tiers.len(), 1);
|
||||
// percent 以字符串给出也兼容(parse_f64)
|
||||
assert_eq!(tiers[0].plan_name.as_deref(), Some("five_hour"));
|
||||
assert_eq!(tiers[0].used, Some(9.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opencode_go_missing_usage_is_deterministic_failure_with_preview() {
|
||||
// usage 全缺:确定性失败,附原始响应预览(400 字内,无密钥)
|
||||
let body = json!({ "error": { "message": "boom" } });
|
||||
assert!(parse_opencode_go(&body).is_empty());
|
||||
let err = opencode_go_empty_failure(&body);
|
||||
assert!(!err.success);
|
||||
assert!(err.data.is_none());
|
||||
let msg = err.error.unwrap();
|
||||
assert!(msg.contains("原始返回"), "msg: {msg}");
|
||||
assert!(msg.contains(r#""message":"boom""#), "msg: {msg}");
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
//! 供应商账单/用量查询统一入口。
|
||||
//!
|
||||
//! - [`UsageKind`]:8 种查询类型,字符串形式与前端 / SQLite settings 约定一致
|
||||
//! - [`UsageKind`]:11 种查询类型,字符串形式与前端 / SQLite settings 约定一致
|
||||
//! (如 `"balance:deepseek"`、`"plan:kimi_coding"`)。
|
||||
//! - [`detect_provider`]:按 base_url host 子串匹配,旧用户无显式配置时自动识别。
|
||||
//! - [`query_kind`]:按 kind 路由到 balance / coding_plan 的具体实现。
|
||||
@@ -26,6 +26,7 @@ pub enum UsageKind {
|
||||
PlanKimiCoding,
|
||||
PlanZhipu,
|
||||
PlanMinimax,
|
||||
PlanOpencodeGo,
|
||||
}
|
||||
|
||||
impl UsageKind {
|
||||
@@ -42,10 +43,11 @@ impl UsageKind {
|
||||
UsageKind::PlanKimiCoding => "plan:kimi_coding",
|
||||
UsageKind::PlanZhipu => "plan:zhipu",
|
||||
UsageKind::PlanMinimax => "plan:minimax",
|
||||
UsageKind::PlanOpencodeGo => "plan:opencode_go",
|
||||
}
|
||||
}
|
||||
|
||||
pub const ALL: [UsageKind; 10] = [
|
||||
pub const ALL: [UsageKind; 11] = [
|
||||
UsageKind::BalanceDeepseek,
|
||||
UsageKind::BalanceSiliconflow,
|
||||
UsageKind::BalanceOpenrouter,
|
||||
@@ -56,6 +58,7 @@ impl UsageKind {
|
||||
UsageKind::PlanKimiCoding,
|
||||
UsageKind::PlanZhipu,
|
||||
UsageKind::PlanMinimax,
|
||||
UsageKind::PlanOpencodeGo,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -74,6 +77,7 @@ impl std::str::FromStr for UsageKind {
|
||||
"plan:kimi_coding" => UsageKind::PlanKimiCoding,
|
||||
"plan:zhipu" => UsageKind::PlanZhipu,
|
||||
"plan:minimax" => UsageKind::PlanMinimax,
|
||||
"plan:opencode_go" => UsageKind::PlanOpencodeGo,
|
||||
_ => return Err(()),
|
||||
})
|
||||
}
|
||||
@@ -113,6 +117,10 @@ pub fn detect_provider(base_url: &str) -> Vec<UsageKind> {
|
||||
if url.contains("api.minimaxi.com") {
|
||||
kinds.push(UsageKind::PlanMinimax);
|
||||
}
|
||||
// OpenCode Go 套餐:只认 /zen/go 路径;/zen/v1(OpenCode Zen 按量付费)不得命中。
|
||||
if url.contains("/zen/go") {
|
||||
kinds.push(UsageKind::PlanOpencodeGo);
|
||||
}
|
||||
kinds
|
||||
}
|
||||
|
||||
@@ -166,6 +174,7 @@ pub async fn query_kind(
|
||||
UsageKind::PlanMinimax => {
|
||||
coding_plan::query_minimax(api_key, !lower.contains("minimax.io"), timeout).await
|
||||
}
|
||||
UsageKind::PlanOpencodeGo => coding_plan::query_opencode_go(api_key, timeout).await,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,7 +184,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn detect_provider_maps_known_hosts() {
|
||||
let cases: [(&str, UsageKind); 9] = [
|
||||
let cases: [(&str, UsageKind); 10] = [
|
||||
("https://api.deepseek.com/v1", UsageKind::BalanceDeepseek),
|
||||
("https://api.siliconflow.cn/v1", UsageKind::BalanceSiliconflow),
|
||||
("https://openrouter.ai/api/v1", UsageKind::BalanceOpenrouter),
|
||||
@@ -188,6 +197,7 @@ mod tests {
|
||||
UsageKind::PlanZhipu,
|
||||
),
|
||||
("https://api.minimaxi.com/v1", UsageKind::PlanMinimax),
|
||||
("https://opencode.ai/zen/go/v1", UsageKind::PlanOpencodeGo),
|
||||
];
|
||||
for (url, expected) in cases {
|
||||
assert_eq!(
|
||||
@@ -212,6 +222,22 @@ mod tests {
|
||||
assert!(detect_provider("https://api.kimi.com/v1").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_provider_opencode_go_excludes_payg_zen() {
|
||||
// /zen/go 命中套餐查询
|
||||
assert_eq!(
|
||||
detect_provider("https://opencode.ai/zen/go/v1"),
|
||||
vec![UsageKind::PlanOpencodeGo]
|
||||
);
|
||||
// /zen/v1(OpenCode Zen 按量付费)不得命中
|
||||
assert!(detect_provider("https://opencode.ai/zen/v1").is_empty());
|
||||
// 裸域名也命中(go 套餐 base 无 v1 后缀时)
|
||||
assert_eq!(
|
||||
detect_provider("https://opencode.ai/zen/go"),
|
||||
vec![UsageKind::PlanOpencodeGo]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usage_kind_string_roundtrip() {
|
||||
use std::str::FromStr;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"productName": "Kimi Switch",
|
||||
"version": "0.7.5",
|
||||
"version": "0.7.6",
|
||||
"identifier": "com.kimiswitch.app",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
|
||||
Reference in new issue
Block a user