mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +08:00
fix: 修复账号批量操作快捷多选不生效和今日 Token 不准确 (#328)
* fix(pool): 修复账号批量操作快捷多选不生效 - 解析号池账号列表接口的 quick_selectors 查询参数 - 列表查询复用现有快捷筛选匹配逻辑 - 支持冷却列表与普通列表的快捷筛选后分页 - 更新架构测试中的 pool admin helper 边界断言 * fix(dashboard): 今日 Token 改为按分项汇总口径展示 - /api/dashboard/stats 的 today.tokens 改为 输入+输出+写缓存+读缓存 - “今日 Token”卡片 value 与 subValue 口径保持一致 - 同步更新 dashboard 相关测试断言
This commit is contained in:
@@ -35,8 +35,8 @@ pub(crate) use self::batch_shared::{
|
||||
};
|
||||
pub(crate) use self::support::{
|
||||
admin_pool_provider_id_from_path, parse_admin_pool_page, parse_admin_pool_page_size,
|
||||
parse_admin_pool_search, parse_admin_pool_status_filter, AdminPoolResolveSelectionRequest,
|
||||
ADMIN_POOL_BANNED_KEY_CLEANUP_EMPTY_MESSAGE,
|
||||
parse_admin_pool_quick_selectors, parse_admin_pool_search, parse_admin_pool_status_filter,
|
||||
AdminPoolResolveSelectionRequest, ADMIN_POOL_BANNED_KEY_CLEANUP_EMPTY_MESSAGE,
|
||||
ADMIN_POOL_PROVIDER_CATALOG_READER_UNAVAILABLE_DETAIL,
|
||||
ADMIN_POOL_PROVIDER_CATALOG_WRITER_UNAVAILABLE_DETAIL,
|
||||
};
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
use super::{
|
||||
admin_pool_provider_id_from_path, admin_provider_pool_config, build_admin_pool_error_response,
|
||||
parse_admin_pool_page, parse_admin_pool_page_size, parse_admin_pool_search,
|
||||
parse_admin_pool_status_filter, pool_payloads, pool_selection,
|
||||
parse_admin_pool_page, parse_admin_pool_page_size, parse_admin_pool_quick_selectors,
|
||||
parse_admin_pool_search, parse_admin_pool_status_filter, pool_payloads, pool_selection,
|
||||
read_admin_provider_pool_cooldown_key_ids, read_admin_provider_pool_runtime_state,
|
||||
AdminProviderPoolRuntimeState, ProviderCatalogKeyListOrder, ProviderCatalogKeyListQuery,
|
||||
ADMIN_POOL_PROVIDER_CATALOG_READER_UNAVAILABLE_DETAIL,
|
||||
};
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||
use crate::GatewayError;
|
||||
use aether_admin::provider::pool as admin_provider_pool_pure;
|
||||
use axum::{
|
||||
body::Body,
|
||||
http,
|
||||
@@ -53,6 +54,9 @@ pub(super) async fn build_admin_pool_list_keys_response(
|
||||
}
|
||||
};
|
||||
let search = parse_admin_pool_search(query).map(|value| value.to_ascii_lowercase());
|
||||
let quick_selectors = admin_provider_pool_pure::admin_pool_sanitize_quick_selectors(
|
||||
parse_admin_pool_quick_selectors(query),
|
||||
);
|
||||
let status = match parse_admin_pool_status_filter(query) {
|
||||
Ok(value) => value,
|
||||
Err(detail) => {
|
||||
@@ -93,10 +97,63 @@ pub(super) async fn build_admin_pool_list_keys_response(
|
||||
};
|
||||
if let Some(keyword) = search.as_ref() {
|
||||
keys.retain(|key| {
|
||||
key.name.to_ascii_lowercase().contains(keyword)
|
||||
|| key.id.to_ascii_lowercase().contains(keyword)
|
||||
pool_selection::admin_pool_matches_search(
|
||||
state,
|
||||
key,
|
||||
&provider.provider_type,
|
||||
Some(keyword),
|
||||
)
|
||||
});
|
||||
}
|
||||
if !quick_selectors.is_empty() {
|
||||
keys.retain(|key| {
|
||||
quick_selectors.iter().all(|selector| {
|
||||
pool_selection::admin_pool_matches_quick_selector(
|
||||
state,
|
||||
key,
|
||||
&provider.provider_type,
|
||||
selector,
|
||||
)
|
||||
})
|
||||
});
|
||||
}
|
||||
pool_selection::admin_pool_sort_keys(&mut keys);
|
||||
let total = keys.len();
|
||||
let keys = keys
|
||||
.into_iter()
|
||||
.skip(page_offset)
|
||||
.take(page_size)
|
||||
.collect::<Vec<_>>();
|
||||
(keys, total)
|
||||
} else if !quick_selectors.is_empty() {
|
||||
let mut keys = state
|
||||
.list_provider_catalog_keys_by_provider_ids(std::slice::from_ref(&provider.id))
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|key| match status.as_str() {
|
||||
"active" => key.is_active,
|
||||
"inactive" => !key.is_active,
|
||||
_ => true,
|
||||
})
|
||||
.filter(|key| {
|
||||
pool_selection::admin_pool_matches_search(
|
||||
state,
|
||||
key,
|
||||
&provider.provider_type,
|
||||
search.as_deref(),
|
||||
)
|
||||
})
|
||||
.filter(|key| {
|
||||
quick_selectors.iter().all(|selector| {
|
||||
pool_selection::admin_pool_matches_quick_selector(
|
||||
state,
|
||||
key,
|
||||
&provider.provider_type,
|
||||
selector,
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
pool_selection::admin_pool_sort_keys(&mut keys);
|
||||
let total = keys.len();
|
||||
let keys = keys
|
||||
|
||||
@@ -60,6 +60,18 @@ pub(crate) fn parse_admin_pool_search(query: Option<&str>) -> Option<String> {
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
pub(crate) fn parse_admin_pool_quick_selectors(query: Option<&str>) -> Vec<String> {
|
||||
query_param_value(query, "quick_selectors")
|
||||
.map(|value| {
|
||||
value
|
||||
.split(',')
|
||||
.map(|item| item.trim().to_string())
|
||||
.filter(|item| !item.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub(crate) fn parse_admin_pool_status_filter(query: Option<&str>) -> Result<String, String> {
|
||||
let value = query_param_value(query, "status")
|
||||
.unwrap_or_else(|| "all".to_string())
|
||||
|
||||
@@ -213,6 +213,14 @@ fn dashboard_format_today_token_subvalue(totals: &DashboardUsageTotals) -> Strin
|
||||
)
|
||||
}
|
||||
|
||||
fn dashboard_today_token_value_from_subvalue(totals: &DashboardUsageTotals) -> u64 {
|
||||
totals
|
||||
.effective_input_tokens
|
||||
.saturating_add(totals.output_tokens)
|
||||
.saturating_add(totals.cache_creation_tokens)
|
||||
.saturating_add(totals.cache_read_tokens)
|
||||
}
|
||||
|
||||
fn dashboard_parse_tz_offset_minutes(query: Option<&str>) -> Result<i32, String> {
|
||||
query_param_value(query, "tz_offset_minutes")
|
||||
.map(|value| {
|
||||
@@ -1591,9 +1599,10 @@ pub(super) async fn handle_dashboard_stats_get(
|
||||
"cache_creation": period_totals.cache_creation_tokens,
|
||||
"cache_read": period_totals.cache_read_tokens,
|
||||
});
|
||||
let today_token_value = dashboard_today_token_value_from_subvalue(&today_totals);
|
||||
let today_payload = json!({
|
||||
"requests": today_totals.requests,
|
||||
"tokens": today_totals.total_tokens,
|
||||
"tokens": today_token_value,
|
||||
"cost": dashboard_round_f64(today_totals.total_cost_usd, 4),
|
||||
"actual_cost": dashboard_round_f64(today_totals.actual_total_cost_usd, 4),
|
||||
"cache_creation_tokens": today_totals.cache_creation_tokens,
|
||||
@@ -1634,7 +1643,7 @@ pub(super) async fn handle_dashboard_stats_get(
|
||||
},
|
||||
{
|
||||
"name": "今日 Token",
|
||||
"value": dashboard_format_token_compact(today_totals.total_tokens),
|
||||
"value": dashboard_format_token_compact(today_token_value),
|
||||
"subValue": dashboard_format_today_token_subvalue(&today_totals),
|
||||
"icon": "Zap",
|
||||
},
|
||||
|
||||
@@ -858,6 +858,7 @@ fn admin_provider_pool_admin_mod_stays_thin() {
|
||||
"struct AdminPoolResolveSelectionRequest",
|
||||
"fn parse_admin_pool_page(",
|
||||
"fn parse_admin_pool_page_size(",
|
||||
"fn parse_admin_pool_quick_selectors(",
|
||||
"fn parse_admin_pool_search(",
|
||||
"fn parse_admin_pool_status_filter(",
|
||||
"fn admin_pool_provider_id_from_path(",
|
||||
@@ -880,6 +881,7 @@ fn admin_provider_pool_admin_mod_stays_thin() {
|
||||
"pub(crate) use aether_admin::provider::pool::AdminPoolResolveSelectionRequest;",
|
||||
"pub(crate) fn parse_admin_pool_page(",
|
||||
"pub(crate) fn parse_admin_pool_page_size(",
|
||||
"pub(crate) fn parse_admin_pool_quick_selectors(",
|
||||
"pub(crate) fn parse_admin_pool_search(",
|
||||
"pub(crate) fn parse_admin_pool_status_filter(",
|
||||
"pub(crate) fn admin_pool_provider_id_from_path(",
|
||||
|
||||
@@ -157,7 +157,7 @@ async fn gateway_handles_dashboard_stats_locally_without_proxying_upstream() {
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let payload: serde_json::Value = response.json().await.expect("json body should parse");
|
||||
assert_eq!(payload["today"]["requests"], 1);
|
||||
assert_eq!(payload["today"]["tokens"], 150);
|
||||
assert_eq!(payload["today"]["tokens"], 160);
|
||||
assert_eq!(payload["api_keys"]["total"], 2);
|
||||
assert_eq!(payload["api_keys"]["active"], 1);
|
||||
assert_eq!(payload["stats"][3]["subValue"], json!("输入 240 / 输出 60"));
|
||||
@@ -405,10 +405,10 @@ async fn gateway_handles_admin_dashboard_stats_locally_without_proxying_upstream
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let payload: serde_json::Value = response.json().await.expect("json body should parse");
|
||||
assert_eq!(payload["today"]["requests"], 2);
|
||||
assert_eq!(payload["today"]["tokens"], 16_000);
|
||||
assert_eq!(payload["today"]["tokens"], 17_450);
|
||||
assert_eq!(payload["today"]["cost"], json!(2.5));
|
||||
assert_eq!(payload["stats"][0]["value"], json!("2"));
|
||||
assert_eq!(payload["stats"][1]["value"], json!("16K"));
|
||||
assert_eq!(payload["stats"][1]["value"], json!("17.4K"));
|
||||
assert_eq!(
|
||||
payload["stats"][1]["subValue"],
|
||||
json!("输入 12.1K / 输出 3.1K · 写缓存 1.25K / 读缓存 1K")
|
||||
|
||||
Reference in New Issue
Block a user