Fix OAuth token import and table filters

This commit is contained in:
fawney19
2026-05-01 02:14:49 +08:00
parent 9570e5c2c1
commit 4fc7cecf30
54 changed files with 3160 additions and 727 deletions
@@ -34,9 +34,11 @@ pub(crate) use self::batch_shared::{
AdminPoolBatchImportRequest,
};
pub(crate) use self::support::{
admin_pool_provider_id_from_path, parse_admin_pool_page, parse_admin_pool_page_size,
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_id_from_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,
ADMIN_POOL_PROVIDER_CATALOG_READER_UNAVAILABLE_DETAIL,
ADMIN_POOL_PROVIDER_CATALOG_WRITER_UNAVAILABLE_DETAIL,
};
@@ -305,17 +305,17 @@ fn admin_pool_quota_window_reset_seconds(
window: &serde_json::Map<String, serde_json::Value>,
now_unix_secs: u64,
) -> Option<f64> {
if let Some(reset_at) = admin_pool_json_to_u64(window.get("reset_at")) {
return Some(reset_at.saturating_sub(now_unix_secs) as f64);
if let Some(remaining) = admin_pool_json_to_f64(window.get("reset_seconds")) {
let observed_at_unix_secs = admin_pool_json_to_u64(quota_snapshot.get("observed_at"))
.or_else(|| admin_pool_json_to_u64(quota_snapshot.get("updated_at")));
let elapsed = observed_at_unix_secs
.map(|observed_at| now_unix_secs.saturating_sub(observed_at) as f64)
.unwrap_or(0.0);
return Some((remaining - elapsed).max(0.0));
}
let remaining = admin_pool_json_to_f64(window.get("reset_seconds"))?;
let observed_at_unix_secs = admin_pool_json_to_u64(quota_snapshot.get("observed_at"))
.or_else(|| admin_pool_json_to_u64(quota_snapshot.get("updated_at")));
let elapsed = observed_at_unix_secs
.map(|observed_at| now_unix_secs.saturating_sub(observed_at) as f64)
.unwrap_or(0.0);
Some((remaining - elapsed).max(0.0))
admin_pool_json_to_u64(window.get("reset_at"))
.map(|reset_at| reset_at.saturating_sub(now_unix_secs) as f64)
}
fn admin_pool_codex_quota_part_from_window(
@@ -323,6 +323,7 @@ fn admin_pool_codex_quota_part_from_window(
window_code: &str,
label: &str,
now_unix_secs: u64,
show_reset_without_consumption: bool,
) -> Option<String> {
let window = admin_pool_quota_window(quota_snapshot, window_code)?;
let used_percent = admin_pool_quota_window_used_percent(window)?;
@@ -338,7 +339,9 @@ fn admin_pool_codex_quota_part_from_window(
"{label}剩余 {}",
admin_pool_format_percent(100.0 - effective_used_percent)
);
if admin_pool_has_quota_consumption(Some(effective_used_percent)) {
if show_reset_without_consumption
|| admin_pool_has_quota_consumption(Some(effective_used_percent))
{
if let Some(reset_text) = reset_seconds.and_then(admin_pool_format_reset_after) {
part.push_str(&format!(" ({reset_text})"));
}
@@ -351,15 +354,27 @@ fn admin_pool_build_codex_account_quota_from_snapshot(
) -> Option<String> {
let now_unix_secs = chrono::Utc::now().timestamp().max(0) as u64;
let mut parts = Vec::new();
let exhausted = quota_snapshot
.get("exhausted")
.and_then(admin_provider_quota_pure::coerce_json_bool)
.unwrap_or(false);
if let Some(part) =
admin_pool_codex_quota_part_from_window(quota_snapshot, "weekly", "周", now_unix_secs)
{
if let Some(part) = admin_pool_codex_quota_part_from_window(
quota_snapshot,
"weekly",
"周",
now_unix_secs,
exhausted,
) {
parts.push(part);
}
if let Some(part) =
admin_pool_codex_quota_part_from_window(quota_snapshot, "5h", "5H", now_unix_secs)
{
if let Some(part) = admin_pool_codex_quota_part_from_window(
quota_snapshot,
"5h",
"5H",
now_unix_secs,
exhausted,
) {
parts.push(part);
}
@@ -798,6 +813,12 @@ pub(super) fn build_admin_pool_key_payload(
} else {
Vec::new()
};
let oauth_temporary = auth_semantics.can_show_oauth_metadata()
&& auth_config
.as_ref()
.and_then(|config| config.get("access_token_import_temporary"))
.and_then(serde_json::Value::as_bool)
.unwrap_or(false);
let account_status_code = admin_pool_trimmed_string_from_map(account_snapshot, "code");
let account_status_label =
admin_pool_trimmed_string(account_snapshot.and_then(|item| item.get("label")));
@@ -879,6 +900,7 @@ pub(super) fn build_admin_pool_key_payload(
"oauth_organizations".to_string(),
serde_json::Value::Array(oauth_organizations),
);
payload.insert("oauth_temporary".to_string(), json!(oauth_temporary));
payload.insert(
"account_status_code".to_string(),
json!(account_status_code),
@@ -1011,6 +1033,10 @@ pub(super) fn build_admin_pool_key_payload(
"created_at".to_string(),
json!(key.created_at_unix_ms.and_then(unix_secs_to_rfc3339)),
);
payload.insert(
"imported_at".to_string(),
json!(key.created_at_unix_ms.and_then(unix_secs_to_rfc3339)),
);
payload.insert(
"last_used_at".to_string(),
json!(key.last_used_at_unix_secs.and_then(unix_secs_to_rfc3339)),
@@ -1,14 +1,16 @@
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_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,
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,
pool_payloads, pool_selection, read_admin_provider_pool_cooldown_key_ids,
read_admin_provider_pool_runtime_state, AdminPoolKeySort, AdminPoolKeySortDirection,
AdminPoolKeySortField, 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 aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKey;
use axum::{
body::Body,
http,
@@ -16,6 +18,51 @@ use axum::{
Json,
};
use serde_json::json;
use std::cmp::Ordering;
fn admin_pool_compare_optional_unix_secs(
left: Option<u64>,
right: Option<u64>,
direction: AdminPoolKeySortDirection,
) -> Ordering {
match (left, right) {
(Some(left), Some(right)) => match direction {
AdminPoolKeySortDirection::Asc => left.cmp(&right),
AdminPoolKeySortDirection::Desc => right.cmp(&left),
},
(Some(_), None) => Ordering::Less,
(None, Some(_)) => Ordering::Greater,
(None, None) => Ordering::Equal,
}
}
fn admin_pool_sort_keys_for_request(keys: &mut [StoredProviderCatalogKey], sort: AdminPoolKeySort) {
match sort.field {
AdminPoolKeySortField::Default => pool_selection::admin_pool_sort_keys(keys),
AdminPoolKeySortField::ImportedAt => {
keys.sort_by(|left, right| {
admin_pool_compare_optional_unix_secs(
left.created_at_unix_ms,
right.created_at_unix_ms,
sort.direction,
)
.then(left.name.cmp(&right.name))
.then(left.id.cmp(&right.id))
});
}
AdminPoolKeySortField::LastUsedAt => {
keys.sort_by(|left, right| {
admin_pool_compare_optional_unix_secs(
left.last_used_at_unix_secs,
right.last_used_at_unix_secs,
sort.direction,
)
.then(left.name.cmp(&right.name))
.then(left.id.cmp(&right.id))
});
}
}
}
pub(super) async fn build_admin_pool_list_keys_response(
state: &AdminAppState<'_>,
@@ -66,6 +113,15 @@ pub(super) async fn build_admin_pool_list_keys_response(
));
}
};
let sort = match parse_admin_pool_key_sort(query) {
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))
@@ -117,7 +173,7 @@ pub(super) async fn build_admin_pool_list_keys_response(
})
});
}
pool_selection::admin_pool_sort_keys(&mut keys);
admin_pool_sort_keys_for_request(&mut keys, sort);
let total = keys.len();
let keys = keys
.into_iter()
@@ -125,7 +181,7 @@ pub(super) async fn build_admin_pool_list_keys_response(
.take(page_size)
.collect::<Vec<_>>();
(keys, total)
} else if !quick_selectors.is_empty() {
} else if !quick_selectors.is_empty() || sort.field != AdminPoolKeySortField::Default {
let mut keys = state
.list_provider_catalog_keys_by_provider_ids(std::slice::from_ref(&provider.id))
.await?
@@ -154,7 +210,7 @@ pub(super) async fn build_admin_pool_list_keys_response(
})
})
.collect::<Vec<_>>();
pool_selection::admin_pool_sort_keys(&mut keys);
admin_pool_sort_keys_for_request(&mut keys, sort);
let total = keys.len();
let keys = keys
.into_iter()
@@ -15,6 +15,34 @@ pub(crate) const ADMIN_POOL_PROVIDER_CATALOG_WRITER_UNAVAILABLE_DETAIL: &str =
"Admin pool cleanup requires provider catalog writer";
pub(crate) const ADMIN_POOL_BANNED_KEY_CLEANUP_EMPTY_MESSAGE: &str = "未发现可清理的异常账号";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum AdminPoolKeySortField {
Default,
ImportedAt,
LastUsedAt,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum AdminPoolKeySortDirection {
Asc,
Desc,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct AdminPoolKeySort {
pub field: AdminPoolKeySortField,
pub direction: AdminPoolKeySortDirection,
}
impl Default for AdminPoolKeySort {
fn default() -> Self {
Self {
field: AdminPoolKeySortField::Default,
direction: AdminPoolKeySortDirection::Desc,
}
}
}
pub(crate) fn build_admin_pool_error_response(
status: http::StatusCode,
detail: impl Into<String>,
@@ -83,6 +111,31 @@ pub(crate) fn parse_admin_pool_status_filter(query: Option<&str>) -> Result<Stri
}
}
pub(crate) fn parse_admin_pool_key_sort(query: Option<&str>) -> Result<AdminPoolKeySort, String> {
let field = match query_param_value(query, "sort_by")
.map(|value| value.trim().to_ascii_lowercase())
.filter(|value| !value.is_empty())
.as_deref()
{
None | Some("default") | Some("name") => AdminPoolKeySortField::Default,
Some("imported_at") | Some("created_at") => AdminPoolKeySortField::ImportedAt,
Some("last_used_at") | Some("last_used") => AdminPoolKeySortField::LastUsedAt,
Some(_) => {
return Err("sort_by must be one of: name, imported_at, last_used_at".to_string());
}
};
let direction = match query_param_value(query, "sort_order")
.map(|value| value.trim().to_ascii_lowercase())
.filter(|value| !value.is_empty())
.as_deref()
{
None | Some("desc") => AdminPoolKeySortDirection::Desc,
Some("asc") => AdminPoolKeySortDirection::Asc,
Some(_) => return Err("sort_order must be one of: asc, desc".to_string()),
};
Ok(AdminPoolKeySort { field, direction })
}
pub(crate) fn admin_pool_provider_id_from_path(request_path: &str) -> Option<String> {
let raw = request_path.strip_prefix("/api/admin/pool/")?;
let mut segments = raw.split('/');