feat(pool): add table-driven account batch actions

This commit is contained in:
elky
2026-07-19 16:09:20 +08:00
parent 95053f9502
commit 644ae9c1bf
20 changed files with 1403 additions and 1095 deletions
@@ -41,8 +41,9 @@ pub(crate) use self::support::{
admin_pool_provider_id_from_path, admin_pool_provider_id_from_scores_path,
parse_admin_pool_key_sort, parse_admin_pool_page, parse_admin_pool_page_size,
parse_admin_pool_quick_selectors, parse_admin_pool_search, parse_admin_pool_status_filter,
AdminPoolKeySort, AdminPoolKeySortDirection, AdminPoolKeySortField,
AdminPoolResolveSelectionRequest, ADMIN_POOL_BANNED_KEY_CLEANUP_EMPTY_MESSAGE,
parse_admin_pool_status_value, AdminPoolKeySort, AdminPoolKeySortDirection,
AdminPoolKeySortField, AdminPoolResolveSelectionRequest,
ADMIN_POOL_BANNED_KEY_CLEANUP_EMPTY_MESSAGE,
ADMIN_POOL_PROVIDER_CATALOG_READER_UNAVAILABLE_DETAIL,
ADMIN_POOL_PROVIDER_CATALOG_WRITER_UNAVAILABLE_DETAIL,
};
@@ -459,7 +459,7 @@ fn admin_pool_key_cost_exhausted(
>= limit
}
fn admin_pool_key_visible_status_filter(
pub(super) fn admin_pool_key_visible_status_filter(
state: &AdminAppState<'_>,
key: &StoredProviderCatalogKey,
provider_type: &str,
@@ -654,17 +654,22 @@ pub(super) async fn build_admin_pool_list_keys_response(
.collect::<Vec<_>>();
(keys, total, preloaded_pool_scores_by_key_id)
} else if !quick_selectors.is_empty() || sort_by_score {
let use_full_search = !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| {
pool_selection::admin_pool_matches_search(
state,
key,
&provider.provider_type,
search.as_deref(),
)
if use_full_search {
pool_selection::admin_pool_matches_search(
state,
key,
&provider.provider_type,
search.as_deref(),
)
} else {
pool_selection::admin_pool_matches_catalog_search(key, search.as_deref())
}
})
.filter(|key| {
quick_selectors.iter().all(|selector| {
@@ -1,6 +1,9 @@
use super::read_keys::admin_pool_key_visible_status_filter;
use super::{
admin_pool_provider_id_from_path, build_admin_pool_error_response, pool_selection,
AdminPoolResolveSelectionRequest, ADMIN_POOL_PROVIDER_CATALOG_READER_UNAVAILABLE_DETAIL,
admin_pool_provider_id_from_path, admin_provider_pool_config, build_admin_pool_error_response,
parse_admin_pool_status_value, pool_selection, read_admin_provider_pool_runtime_state,
AdminPoolResolveSelectionRequest, AdminProviderPoolRuntimeState,
ADMIN_POOL_PROVIDER_CATALOG_READER_UNAVAILABLE_DETAIL,
};
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
use crate::provider_key_auth::{
@@ -49,6 +52,15 @@ pub(super) async fn build_admin_pool_resolve_selection_response(
}
},
};
let status = match parse_admin_pool_status_value(payload.status.as_deref()) {
Ok(value) => value,
Err(detail) => {
return Ok(build_admin_pool_error_response(
http::StatusCode::BAD_REQUEST,
detail,
));
}
};
let Some(provider) = state
.read_provider_catalog_providers_by_ids(std::slice::from_ref(&provider_id))
@@ -66,13 +78,18 @@ pub(super) async fn build_admin_pool_resolve_selection_response(
let search = payload.search.trim();
let quick_selectors =
admin_provider_pool_pure::admin_pool_sanitize_quick_selectors(payload.quick_selectors);
let use_full_search = status != "all" || !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| {
pool_selection::admin_pool_matches_search(state, key, &provider_type, Some(search))
if use_full_search {
pool_selection::admin_pool_matches_search(state, key, &provider_type, Some(search))
} else {
pool_selection::admin_pool_matches_catalog_search(key, Some(search))
}
})
.filter(|key| {
quick_selectors.is_empty()
@@ -87,6 +104,35 @@ pub(super) async fn build_admin_pool_resolve_selection_response(
})
.collect::<Vec<_>>();
if status != "all" {
let pool_config = admin_provider_pool_config(&provider);
let key_ids = keys.iter().map(|key| key.id.clone()).collect::<Vec<_>>();
let runtime = match pool_config.as_ref() {
Some(pool_config) if !key_ids.is_empty() => {
read_admin_provider_pool_runtime_state(
state.runtime_state(),
&provider.id,
&key_ids,
pool_config,
None,
)
.await
}
_ => AdminProviderPoolRuntimeState::default(),
};
let now_unix_secs = admin_provider_pool_pure::admin_pool_now_unix_secs();
keys.retain(|key| {
admin_pool_key_visible_status_filter(
state,
key,
&provider_type,
pool_config.as_ref(),
&runtime,
now_unix_secs,
) == status
});
}
keys.sort_by(|left, right| {
left.internal_priority
.cmp(&right.internal_priority)
@@ -60,6 +60,19 @@ pub(super) fn admin_pool_matches_search(
admin_provider_pool_pure::admin_pool_matches_search(key, search, oauth_plan_type.as_deref())
}
pub(super) fn admin_pool_matches_catalog_search(
key: &StoredProviderCatalogKey,
search: Option<&str>,
) -> bool {
let Some(search) = search else {
return true;
};
let search = admin_pool_normalize_text(search);
search.is_empty()
|| admin_pool_normalize_text(&key.name).contains(&search)
|| admin_pool_normalize_text(&key.id).contains(&search)
}
pub(super) fn admin_pool_key_is_known_banned(key: &StoredProviderCatalogKey) -> bool {
admin_provider_pool_pure::admin_pool_key_is_known_banned(key)
}
@@ -102,10 +102,12 @@ pub(crate) fn parse_admin_pool_quick_selectors(query: Option<&str>) -> Vec<Strin
}
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())
.trim()
.to_ascii_lowercase();
let value = query_param_value(query, "status");
parse_admin_pool_status_value(value.as_deref())
}
pub(crate) fn parse_admin_pool_status_value(value: Option<&str>) -> Result<String, String> {
let value = value.unwrap_or("all").trim().to_ascii_lowercase();
match value.as_str() {
"all"
| "available"