feat: 新增部分套餐账单查询(v0.6.4)
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

- 账单/用量查询:balance:kimi(Moonshot 开放平台余额)、NewAPI 中转站余额(accessToken+userId)、Kimi/智谱/MiniMax 套餐档位(5小时/7天)
- managed:kimi-code 走 Kimi Code OAuth 登录凭据查询订阅用量,access token 过期自动 refresh 并写回凭据(单 flight 锁 + 写回前重读防 CLI 竞态)
- 全屏「配置用量查询」页面:启用开关 / 自动检测 vs NewAPI 模板 / 超时与自动查询间隔 / 测试查询
- 供应商卡片紧凑用量行(切换使用左侧)+ 多档明细行;managed 供应商列表置顶(仅展示层)
- 用量显示中英文适配(five_hour→5小时、weekly_limit→7天,Rust 错误串本地化映射)
- 修复:usage_kinds serde rename 导致账单入口不显示;测试查询因 save 触发 loading 翻页卸载面板导致结果丢失(silent save)
This commit is contained in:
KimiSwitch Dev committed 2026-08-01 09:44:36 +08:00
1 parent 12dcb17771
commit ace9842556
26 files changed
+1822 -60

No files matched your search

+66 -5
View File
@@ -1,5 +1,5 @@
use crate::db;
use crate::models::{Agent, Config, DiscoveredModel, Model, Provider, ProviderType};
use crate::models::{Agent, Config, DiscoveredModel, Model, Provider, ProviderType, UsageConfig};
use crate::pi_io;
use crate::services::{self, UsageKind, UsageResult};
use indexmap::IndexMap;
@@ -27,6 +27,10 @@ fn usage_kinds_key(provider_name: &str) -> String {
format!("usage_kinds:{provider_name}")
}
fn usage_config_key(provider_name: &str) -> String {
format!("usage_config:{provider_name}")
}
/// Merge per-provider `usage_kinds` into a loaded config: explicit SQLite
/// settings first, host-based detection as fallback so existing installs get
/// billing support automatically. The field never enters config.toml.
@@ -45,6 +49,11 @@ fn merge_usage_kinds(config: &mut Config) {
Some(kinds.iter().map(|k| k.as_str().to_string()).collect())
}
});
// Same pattern for the panel-edited usage config.
p.usage_config = db::get_setting_pub(&usage_config_key(&p.name))
.ok()
.flatten()
.and_then(|s| serde_json::from_str::<UsageConfig>(&s).ok());
}
}
@@ -158,6 +167,14 @@ pub fn save_agent_config_command(agent: Agent, config: Config) -> Result<(), Str
}
_ => db::delete_setting_pub(&key).map_err(fmt_anyhow)?,
}
let cfg_key = usage_config_key(&provider.name);
match &provider.usage_config {
Some(cfg) => {
let json = serde_json::to_string(cfg).map_err(|e| e.to_string())?;
db::set_setting_pub(&cfg_key, &json).map_err(fmt_anyhow)?;
}
None => db::delete_setting_pub(&cfg_key).map_err(fmt_anyhow)?,
}
}
Ok(())
}
@@ -614,9 +631,20 @@ pub async fn query_provider_usage(
)));
};
// Panel toggle: user disabled usage queries for this provider.
if let Some(cfg) = &provider.usage_config {
if !cfg.enabled {
return Ok(UsageResult::failure(
"usage query disabled in config panel".to_string(),
));
}
}
// The api_key only ever goes into request headers — never into logs,
// error messages, or the cache key.
let api_key = provider
// error messages, or the cache key. Managed (OAuth-login) providers have
// no static key; their credential comes from the Kimi Code OAuth session
// file (refreshed on demand when the 15-min access token expires).
let mut api_key = provider
.api_key
.clone()
.filter(|s| !s.trim().is_empty())
@@ -627,15 +655,48 @@ pub async fn query_provider_usage(
.cloned()
.filter(|s| !s.is_empty())
});
let mut oauth_err: Option<String> = None;
if api_key.is_none() && provider.managed {
match crate::oauth::get_valid_access_token().await {
Ok(token) => api_key = Some(token),
Err(e) => oauth_err = Some(e),
}
}
let Some(api_key) = api_key else {
return Ok(UsageResult::failure(if provider.managed {
"provider uses managed OAuth; usage query requires an API key".to_string()
oauth_err.unwrap_or_else(|| {
"no Kimi Code OAuth credentials found; run `kimi login` first".to_string()
})
} else {
"no API key configured".to_string()
}));
};
let base_url = resolve_base_url(provider);
// NewAPI template: query the gateway's own /api/user/self with the
// web-console access token, bypassing usage_kinds entirely.
if let Some(cfg) = &provider.usage_config {
if cfg.template_type == UsageConfig::TEMPLATE_NEWAPI {
// 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?;
if result.success {
usage_cache()
.lock()
.unwrap()
.insert(cache_key, (Instant::now(), result.clone()));
}
return Ok(result);
}
}
let kinds: Vec<UsageKind> = provider
.usage_kinds
.as_ref()
@@ -660,7 +721,7 @@ pub async fn query_provider_usage(
let mut transient: Vec<String> = Vec::new();
let mut any_success = false;
for kind in kinds {
match services::query_kind(kind, &base_url, &api_key).await {
match services::query_kind(kind, &base_url, &api_key, provider.usage_config.as_ref()).await {
Ok(result) if result.success => {
any_success = true;
if let Some(d) = result.data {