mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-09 18:59:50 +08:00
Fix OAuth token import and table filters
This commit is contained in:
@@ -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('/');
|
||||
|
||||
Reference in New Issue
Block a user