mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-04 16:37:46 +08:00
fix(pool): align status filters with visible key state
This commit is contained in:
@@ -943,6 +943,10 @@ fn admin_pool_scheduling_payload(
|
|||||||
account_status_reason: Option<&str>,
|
account_status_reason: Option<&str>,
|
||||||
account_status_source: Option<&str>,
|
account_status_source: Option<&str>,
|
||||||
account_quota_exhausted: bool,
|
account_quota_exhausted: bool,
|
||||||
|
oauth_status_code: Option<&str>,
|
||||||
|
oauth_status_label: Option<&str>,
|
||||||
|
oauth_status_reason: Option<&str>,
|
||||||
|
oauth_status_source: Option<&str>,
|
||||||
) -> (String, String, String, Vec<serde_json::Value>) {
|
) -> (String, String, String, Vec<serde_json::Value>) {
|
||||||
if !key.is_active {
|
if !key.is_active {
|
||||||
return (
|
return (
|
||||||
@@ -989,6 +993,26 @@ fn admin_pool_scheduling_payload(
|
|||||||
})],
|
})],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if matches!(oauth_status_code, Some("invalid" | "expired")) {
|
||||||
|
let label = oauth_status_label.unwrap_or(if oauth_status_code == Some("expired") {
|
||||||
|
"已过期"
|
||||||
|
} else {
|
||||||
|
"已失效"
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
"blocked".to_string(),
|
||||||
|
oauth_status_code.unwrap_or("invalid").to_string(),
|
||||||
|
label.to_string(),
|
||||||
|
vec![json!({
|
||||||
|
"code": oauth_status_code.unwrap_or("invalid"),
|
||||||
|
"label": label,
|
||||||
|
"blocking": true,
|
||||||
|
"source": oauth_status_source.unwrap_or("oauth"),
|
||||||
|
"ttl_seconds": serde_json::Value::Null,
|
||||||
|
"detail": oauth_status_reason,
|
||||||
|
})],
|
||||||
|
);
|
||||||
|
}
|
||||||
if let Some(reason) = cooldown_reason {
|
if let Some(reason) = cooldown_reason {
|
||||||
return (
|
return (
|
||||||
"degraded".to_string(),
|
"degraded".to_string(),
|
||||||
@@ -1105,6 +1129,13 @@ pub(super) fn build_admin_pool_key_payload(
|
|||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
let account_status_source =
|
let account_status_source =
|
||||||
admin_pool_trimmed_string(account_snapshot.and_then(|item| item.get("source")));
|
admin_pool_trimmed_string(account_snapshot.and_then(|item| item.get("source")));
|
||||||
|
let oauth_status_code = admin_pool_trimmed_string_from_map(oauth_snapshot, "code");
|
||||||
|
let oauth_status_label =
|
||||||
|
admin_pool_trimmed_string(oauth_snapshot.and_then(|item| item.get("label")));
|
||||||
|
let oauth_status_reason =
|
||||||
|
admin_pool_trimmed_string(oauth_snapshot.and_then(|item| item.get("reason")));
|
||||||
|
let oauth_status_source =
|
||||||
|
admin_pool_trimmed_string(oauth_snapshot.and_then(|item| item.get("source")));
|
||||||
let (scheduling_status, scheduling_reason, scheduling_label, scheduling_reasons) =
|
let (scheduling_status, scheduling_reason, scheduling_label, scheduling_reasons) =
|
||||||
admin_pool_scheduling_payload(
|
admin_pool_scheduling_payload(
|
||||||
key,
|
key,
|
||||||
@@ -1116,6 +1147,10 @@ pub(super) fn build_admin_pool_key_payload(
|
|||||||
account_status_reason.as_deref(),
|
account_status_reason.as_deref(),
|
||||||
account_status_source.as_deref(),
|
account_status_source.as_deref(),
|
||||||
account_quota_exhausted,
|
account_quota_exhausted,
|
||||||
|
oauth_status_code.as_deref(),
|
||||||
|
oauth_status_label.as_deref(),
|
||||||
|
oauth_status_reason.as_deref(),
|
||||||
|
oauth_status_source.as_deref(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut payload = serde_json::Map::new();
|
let mut payload = serde_json::Map::new();
|
||||||
|
|||||||
@@ -2,13 +2,15 @@ use super::{
|
|||||||
admin_pool_provider_id_from_path, admin_provider_pool_config, build_admin_pool_error_response,
|
admin_pool_provider_id_from_path, admin_provider_pool_config, build_admin_pool_error_response,
|
||||||
parse_admin_pool_key_sort, parse_admin_pool_page, parse_admin_pool_page_size,
|
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,
|
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,
|
pool_payloads, pool_selection, read_admin_provider_pool_runtime_state, AdminPoolKeySort,
|
||||||
read_admin_provider_pool_runtime_state, AdminPoolKeySort, AdminPoolKeySortDirection,
|
AdminPoolKeySortDirection, AdminPoolKeySortField, AdminProviderPoolRuntimeState,
|
||||||
AdminPoolKeySortField, AdminProviderPoolRuntimeState, ProviderCatalogKeyListOrder,
|
ProviderCatalogKeyListOrder, ProviderCatalogKeyListQuery,
|
||||||
ProviderCatalogKeyListQuery, ADMIN_POOL_PROVIDER_CATALOG_READER_UNAVAILABLE_DETAIL,
|
ADMIN_POOL_PROVIDER_CATALOG_READER_UNAVAILABLE_DETAIL,
|
||||||
};
|
};
|
||||||
use crate::ai_serving::{provider_key_pool_score_id, provider_key_pool_score_scope};
|
use crate::ai_serving::{provider_key_pool_score_id, provider_key_pool_score_scope};
|
||||||
|
use crate::handlers::admin::provider::shared::support::AdminProviderPoolConfig;
|
||||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||||
|
use crate::handlers::admin::shared::provider_key_status_snapshot_payload;
|
||||||
use crate::GatewayError;
|
use crate::GatewayError;
|
||||||
use aether_admin::provider::pool as admin_provider_pool_pure;
|
use aether_admin::provider::pool as admin_provider_pool_pure;
|
||||||
use aether_data_contracts::repository::pool_scores::{
|
use aether_data_contracts::repository::pool_scores::{
|
||||||
@@ -24,7 +26,7 @@ use axum::{
|
|||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
Json,
|
Json,
|
||||||
};
|
};
|
||||||
use serde_json::json;
|
use serde_json::{json, Value};
|
||||||
use std::{
|
use std::{
|
||||||
cmp::Ordering,
|
cmp::Ordering,
|
||||||
collections::BTreeMap,
|
collections::BTreeMap,
|
||||||
@@ -290,6 +292,163 @@ fn admin_pool_repository_key_order(sort: AdminPoolKeySort) -> ProviderCatalogKey
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn admin_pool_trimmed_string(value: Option<&Value>) -> Option<String> {
|
||||||
|
value
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
.map(ToOwned::to_owned)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn admin_pool_account_code_status_filter(code: &str) -> Option<&'static str> {
|
||||||
|
match code.trim().to_ascii_lowercase().as_str() {
|
||||||
|
"oauth_token_invalid" => Some("invalid"),
|
||||||
|
"account_banned" | "account_suspended" => Some("account_banned"),
|
||||||
|
"account_disabled" => Some("account_disabled"),
|
||||||
|
"workspace_deactivated" => Some("workspace_deactivated"),
|
||||||
|
"account_forbidden" => Some("account_forbidden"),
|
||||||
|
"account_verification" => Some("account_verification"),
|
||||||
|
"account_quarantined" => Some("account_quarantined"),
|
||||||
|
"account_blocked" => Some("account_blocked"),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn admin_pool_label_status_filter(label: &str) -> Option<&'static str> {
|
||||||
|
match label.trim() {
|
||||||
|
"已失效" | "Token 失效" | "Token失效" => Some("invalid"),
|
||||||
|
"已过期" => Some("expired"),
|
||||||
|
"账号封禁" | "账号已封禁" | "封禁" => Some("account_banned"),
|
||||||
|
"额度耗尽" => Some("quota_exhausted"),
|
||||||
|
"访问受限" | "账号访问受限" => Some("account_forbidden"),
|
||||||
|
"账号停用" => Some("account_disabled"),
|
||||||
|
"工作区停用" | "工作区已停用" => Some("workspace_deactivated"),
|
||||||
|
"需要验证" => Some("account_verification"),
|
||||||
|
"账号隔离" => Some("account_quarantined"),
|
||||||
|
"账号异常" => Some("account_blocked"),
|
||||||
|
"速率受限" => Some("rate_limited"),
|
||||||
|
"超限" => Some("cost_exhausted"),
|
||||||
|
"冷却中" => Some("cooldown"),
|
||||||
|
"已禁用" | "禁用" | "停用" => Some("inactive"),
|
||||||
|
"可用" => Some("available"),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn admin_pool_oauth_status_filter(
|
||||||
|
oauth_snapshot: Option<&serde_json::Map<String, Value>>,
|
||||||
|
) -> Option<&'static str> {
|
||||||
|
let oauth_snapshot = oauth_snapshot?;
|
||||||
|
let code = admin_pool_trimmed_string(oauth_snapshot.get("code"))
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_ascii_lowercase();
|
||||||
|
match code.as_str() {
|
||||||
|
"invalid" => Some("invalid"),
|
||||||
|
"expired" => Some("expired"),
|
||||||
|
_ => admin_pool_trimmed_string(oauth_snapshot.get("label"))
|
||||||
|
.as_deref()
|
||||||
|
.and_then(admin_pool_label_status_filter),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn admin_pool_account_status_filter(
|
||||||
|
account_snapshot: Option<&serde_json::Map<String, Value>>,
|
||||||
|
) -> Option<&'static str> {
|
||||||
|
let account_snapshot = account_snapshot?;
|
||||||
|
let code = admin_pool_trimmed_string(account_snapshot.get("code"));
|
||||||
|
let label = admin_pool_trimmed_string(account_snapshot.get("label"));
|
||||||
|
let blocked = account_snapshot
|
||||||
|
.get("blocked")
|
||||||
|
.and_then(Value::as_bool)
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if let Some(code) = code
|
||||||
|
.as_deref()
|
||||||
|
.and_then(admin_pool_account_code_status_filter)
|
||||||
|
{
|
||||||
|
return Some(code);
|
||||||
|
}
|
||||||
|
if let Some(label) = label.as_deref().and_then(admin_pool_label_status_filter) {
|
||||||
|
return Some(label);
|
||||||
|
}
|
||||||
|
blocked.then_some("account_blocked")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn admin_pool_quota_status_filter(
|
||||||
|
quota_snapshot: Option<&serde_json::Map<String, Value>>,
|
||||||
|
) -> Option<&'static str> {
|
||||||
|
let quota_snapshot = quota_snapshot?;
|
||||||
|
let code = admin_pool_trimmed_string(quota_snapshot.get("code"))
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_ascii_lowercase();
|
||||||
|
match code.as_str() {
|
||||||
|
"banned" => Some("account_banned"),
|
||||||
|
"forbidden" => Some("account_forbidden"),
|
||||||
|
"quarantined" => Some("account_quarantined"),
|
||||||
|
"rate_limited" => Some("rate_limited"),
|
||||||
|
"exhausted" => Some("quota_exhausted"),
|
||||||
|
_ => admin_pool_trimmed_string(quota_snapshot.get("label"))
|
||||||
|
.as_deref()
|
||||||
|
.and_then(admin_pool_label_status_filter),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn admin_pool_key_cost_exhausted(
|
||||||
|
key: &StoredProviderCatalogKey,
|
||||||
|
pool_config: Option<&AdminProviderPoolConfig>,
|
||||||
|
runtime: &AdminProviderPoolRuntimeState,
|
||||||
|
) -> bool {
|
||||||
|
let Some(limit) = pool_config
|
||||||
|
.and_then(|config| config.cost_limit_per_key_tokens)
|
||||||
|
.filter(|limit| *limit > 0)
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
runtime
|
||||||
|
.cost_window_usage_by_key
|
||||||
|
.get(&key.id)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or(0)
|
||||||
|
>= limit
|
||||||
|
}
|
||||||
|
|
||||||
|
fn admin_pool_key_visible_status_filter(
|
||||||
|
key: &StoredProviderCatalogKey,
|
||||||
|
provider_type: &str,
|
||||||
|
pool_config: Option<&AdminProviderPoolConfig>,
|
||||||
|
runtime: &AdminProviderPoolRuntimeState,
|
||||||
|
) -> &'static str {
|
||||||
|
let status_snapshot = provider_key_status_snapshot_payload(key, provider_type);
|
||||||
|
let account_snapshot = status_snapshot.get("account").and_then(Value::as_object);
|
||||||
|
let quota_snapshot = status_snapshot.get("quota").and_then(Value::as_object);
|
||||||
|
let oauth_snapshot = status_snapshot.get("oauth").and_then(Value::as_object);
|
||||||
|
|
||||||
|
if let Some(status) = admin_pool_account_status_filter(account_snapshot) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
if let Some(status) = admin_pool_quota_status_filter(quota_snapshot) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
if let Some(status) = admin_pool_oauth_status_filter(oauth_snapshot) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
if pool_config.is_some_and(|config| config.skip_exhausted_accounts)
|
||||||
|
&& admin_provider_pool_pure::admin_pool_key_account_quota_exhausted(key, provider_type)
|
||||||
|
{
|
||||||
|
return "quota_exhausted";
|
||||||
|
}
|
||||||
|
if !key.is_active {
|
||||||
|
return "inactive";
|
||||||
|
}
|
||||||
|
if runtime.cooldown_reason_by_key.contains_key(&key.id) {
|
||||||
|
return "cooldown";
|
||||||
|
}
|
||||||
|
if admin_pool_key_cost_exhausted(key, pool_config, runtime) {
|
||||||
|
return "cost_exhausted";
|
||||||
|
}
|
||||||
|
"available"
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) async fn build_admin_pool_list_keys_response(
|
pub(super) async fn build_admin_pool_list_keys_response(
|
||||||
state: &AdminAppState<'_>,
|
state: &AdminAppState<'_>,
|
||||||
request_context: &AdminRequestContext<'_>,
|
request_context: &AdminRequestContext<'_>,
|
||||||
@@ -365,16 +524,12 @@ pub(super) async fn build_admin_pool_list_keys_response(
|
|||||||
let page_offset = page.saturating_sub(1).saturating_mul(page_size);
|
let page_offset = page.saturating_sub(1).saturating_mul(page_size);
|
||||||
let sort_by_score = matches!(sort.field, AdminPoolKeySortField::Score);
|
let sort_by_score = matches!(sort.field, AdminPoolKeySortField::Score);
|
||||||
|
|
||||||
let (keys, total, preloaded_pool_scores_by_key_id) = if status == "cooldown" {
|
let (keys, total, preloaded_pool_scores_by_key_id) = if status != "all" {
|
||||||
let cooldown_key_ids =
|
let mut keys = state
|
||||||
read_admin_provider_pool_cooldown_key_ids(state.runtime_state(), &provider.id).await;
|
.list_provider_catalog_keys_by_provider_ids(std::slice::from_ref(&provider.id))
|
||||||
let mut keys = if cooldown_key_ids.is_empty() {
|
.await?
|
||||||
Vec::new()
|
.into_iter()
|
||||||
} else {
|
.collect::<Vec<_>>();
|
||||||
state
|
|
||||||
.read_provider_catalog_keys_by_ids(&cooldown_key_ids)
|
|
||||||
.await?
|
|
||||||
};
|
|
||||||
if let Some(keyword) = search.as_ref() {
|
if let Some(keyword) = search.as_ref() {
|
||||||
keys.retain(|key| {
|
keys.retain(|key| {
|
||||||
pool_selection::admin_pool_matches_search(
|
pool_selection::admin_pool_matches_search(
|
||||||
@@ -397,6 +552,30 @@ pub(super) async fn build_admin_pool_list_keys_response(
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(),
|
||||||
|
};
|
||||||
|
keys.retain(|key| {
|
||||||
|
admin_pool_key_visible_status_filter(
|
||||||
|
key,
|
||||||
|
&provider.provider_type,
|
||||||
|
pool_config.as_ref(),
|
||||||
|
&runtime,
|
||||||
|
) == status
|
||||||
|
});
|
||||||
|
|
||||||
let preloaded_pool_scores_by_key_id = if sort_by_score {
|
let preloaded_pool_scores_by_key_id = if sort_by_score {
|
||||||
let key_ids = keys.iter().map(|key| key.id.clone()).collect::<Vec<_>>();
|
let key_ids = keys.iter().map(|key| key.id.clone()).collect::<Vec<_>>();
|
||||||
let scores = read_admin_pool_scores_by_key_id(state, &provider.id, &key_ids)
|
let scores = read_admin_pool_scores_by_key_id(state, &provider.id, &key_ids)
|
||||||
@@ -420,11 +599,6 @@ pub(super) async fn build_admin_pool_list_keys_response(
|
|||||||
.list_provider_catalog_keys_by_provider_ids(std::slice::from_ref(&provider.id))
|
.list_provider_catalog_keys_by_provider_ids(std::slice::from_ref(&provider.id))
|
||||||
.await?
|
.await?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|key| match status.as_str() {
|
|
||||||
"active" => key.is_active,
|
|
||||||
"inactive" => !key.is_active,
|
|
||||||
_ => true,
|
|
||||||
})
|
|
||||||
.filter(|key| {
|
.filter(|key| {
|
||||||
pool_selection::admin_pool_matches_search(
|
pool_selection::admin_pool_matches_search(
|
||||||
state,
|
state,
|
||||||
@@ -467,11 +641,7 @@ pub(super) async fn build_admin_pool_list_keys_response(
|
|||||||
.list_provider_catalog_key_page(&ProviderCatalogKeyListQuery {
|
.list_provider_catalog_key_page(&ProviderCatalogKeyListQuery {
|
||||||
provider_id: provider.id.clone(),
|
provider_id: provider.id.clone(),
|
||||||
search: search.clone(),
|
search: search.clone(),
|
||||||
is_active: match status.as_str() {
|
is_active: None,
|
||||||
"active" => Some(true),
|
|
||||||
"inactive" => Some(false),
|
|
||||||
_ => None,
|
|
||||||
},
|
|
||||||
offset: page_offset,
|
offset: page_offset,
|
||||||
limit: page_size,
|
limit: page_size,
|
||||||
order: admin_pool_repository_key_order(sort),
|
order: admin_pool_repository_key_order(sort),
|
||||||
|
|||||||
@@ -107,8 +107,29 @@ pub(crate) fn parse_admin_pool_status_filter(query: Option<&str>) -> Result<Stri
|
|||||||
.trim()
|
.trim()
|
||||||
.to_ascii_lowercase();
|
.to_ascii_lowercase();
|
||||||
match value.as_str() {
|
match value.as_str() {
|
||||||
"all" | "active" | "inactive" | "cooldown" => Ok(value),
|
"all"
|
||||||
_ => Err("status must be one of: all, active, cooldown, inactive".to_string()),
|
| "available"
|
||||||
|
| "cooldown"
|
||||||
|
| "inactive"
|
||||||
|
| "invalid"
|
||||||
|
| "expired"
|
||||||
|
| "account_banned"
|
||||||
|
| "quota_exhausted"
|
||||||
|
| "account_forbidden"
|
||||||
|
| "account_disabled"
|
||||||
|
| "workspace_deactivated"
|
||||||
|
| "account_verification"
|
||||||
|
| "account_quarantined"
|
||||||
|
| "account_blocked"
|
||||||
|
| "rate_limited"
|
||||||
|
| "cost_exhausted" => Ok(value),
|
||||||
|
// Backward compatibility for older links/localStorage. This filter used to
|
||||||
|
// be called "active", but the table status column displays the exact
|
||||||
|
// scheduling/account state. Treat it as the visible "可用" bucket.
|
||||||
|
"active" => Ok("available".to_string()),
|
||||||
|
_ => Err(
|
||||||
|
"status must be one of: all, available, cooldown, inactive, invalid, expired, account_banned, quota_exhausted, account_forbidden, account_disabled, workspace_deactivated, account_verification, account_quarantined, account_blocked, rate_limited, cost_exhausted".to_string(),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -271,7 +271,23 @@ export interface PoolKeysQuery {
|
|||||||
page?: number
|
page?: number
|
||||||
page_size?: number
|
page_size?: number
|
||||||
search?: string
|
search?: string
|
||||||
status?: 'all' | 'active' | 'cooldown' | 'inactive'
|
status?:
|
||||||
|
| 'all'
|
||||||
|
| 'available'
|
||||||
|
| 'cooldown'
|
||||||
|
| 'inactive'
|
||||||
|
| 'invalid'
|
||||||
|
| 'expired'
|
||||||
|
| 'account_banned'
|
||||||
|
| 'quota_exhausted'
|
||||||
|
| 'account_forbidden'
|
||||||
|
| 'account_disabled'
|
||||||
|
| 'workspace_deactivated'
|
||||||
|
| 'account_verification'
|
||||||
|
| 'account_quarantined'
|
||||||
|
| 'account_blocked'
|
||||||
|
| 'rate_limited'
|
||||||
|
| 'cost_exhausted'
|
||||||
quick_selectors?: string[]
|
quick_selectors?: string[]
|
||||||
search_scope?: 'name' | 'full'
|
search_scope?: 'name' | 'full'
|
||||||
sort_by?: 'imported_at' | 'last_used_at' | 'score'
|
sort_by?: 'imported_at' | 'last_used_at' | 'score'
|
||||||
|
|||||||
@@ -1,4 +1,20 @@
|
|||||||
export type PoolManagementStatus = 'all' | 'active' | 'cooldown' | 'inactive'
|
export type PoolManagementStatus =
|
||||||
|
| 'all'
|
||||||
|
| 'available'
|
||||||
|
| 'cooldown'
|
||||||
|
| 'inactive'
|
||||||
|
| 'invalid'
|
||||||
|
| 'expired'
|
||||||
|
| 'account_banned'
|
||||||
|
| 'quota_exhausted'
|
||||||
|
| 'account_forbidden'
|
||||||
|
| 'account_disabled'
|
||||||
|
| 'workspace_deactivated'
|
||||||
|
| 'account_verification'
|
||||||
|
| 'account_quarantined'
|
||||||
|
| 'account_blocked'
|
||||||
|
| 'rate_limited'
|
||||||
|
| 'cost_exhausted'
|
||||||
export type PoolManagementSortBy = 'imported_at' | 'last_used_at' | 'score'
|
export type PoolManagementSortBy = 'imported_at' | 'last_used_at' | 'score'
|
||||||
export type PoolManagementSortOrder = 'asc' | 'desc'
|
export type PoolManagementSortOrder = 'asc' | 'desc'
|
||||||
export type PoolManagementStatsMode = 'current_cycle' | 'account_total'
|
export type PoolManagementStatsMode = 'current_cycle' | 'account_total'
|
||||||
@@ -58,7 +74,26 @@ function normalizeSearch(value: unknown): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizeStatus(value: unknown): PoolManagementStatus {
|
function normalizeStatus(value: unknown): PoolManagementStatus {
|
||||||
if (value === 'active' || value === 'cooldown' || value === 'inactive') {
|
if (value === 'active') {
|
||||||
|
return 'available'
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
value === 'available'
|
||||||
|
|| value === 'cooldown'
|
||||||
|
|| value === 'inactive'
|
||||||
|
|| value === 'invalid'
|
||||||
|
|| value === 'expired'
|
||||||
|
|| value === 'account_banned'
|
||||||
|
|| value === 'quota_exhausted'
|
||||||
|
|| value === 'account_forbidden'
|
||||||
|
|| value === 'account_disabled'
|
||||||
|
|| value === 'workspace_deactivated'
|
||||||
|
|| value === 'account_verification'
|
||||||
|
|| value === 'account_quarantined'
|
||||||
|
|| value === 'account_blocked'
|
||||||
|
|| value === 'rate_limited'
|
||||||
|
|| value === 'cost_exhausted'
|
||||||
|
) {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
return 'all'
|
return 'all'
|
||||||
|
|||||||
@@ -52,17 +52,12 @@
|
|||||||
<SelectValue placeholder="状态" />
|
<SelectValue placeholder="状态" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="all">
|
<SelectItem
|
||||||
全部
|
v-for="item in poolKeyStatusFilterOptions"
|
||||||
</SelectItem>
|
:key="`mobile-${item.value}`"
|
||||||
<SelectItem value="active">
|
:value="item.value"
|
||||||
可调度
|
>
|
||||||
</SelectItem>
|
{{ item.label }}
|
||||||
<SelectItem value="cooldown">
|
|
||||||
冷却中
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="inactive">
|
|
||||||
禁用
|
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
@@ -1695,9 +1690,21 @@ const showDemandMetricsDialog = ref(false)
|
|||||||
const providerDemandMetricSamples = ref<PoolDemandMetricSample[]>([])
|
const providerDemandMetricSamples = ref<PoolDemandMetricSample[]>([])
|
||||||
const poolKeyStatusFilterOptions: Array<{ value: PoolManagementViewState['status'], label: string }> = [
|
const poolKeyStatusFilterOptions: Array<{ value: PoolManagementViewState['status'], label: string }> = [
|
||||||
{ value: 'all', label: '全部状态' },
|
{ value: 'all', label: '全部状态' },
|
||||||
{ value: 'active', label: '可调度' },
|
{ value: 'available', label: '可用' },
|
||||||
{ value: 'cooldown', label: '冷却中' },
|
{ value: 'cooldown', label: '冷却中' },
|
||||||
{ value: 'inactive', label: '禁用' },
|
{ value: 'inactive', label: '已禁用' },
|
||||||
|
{ value: 'invalid', label: '已失效' },
|
||||||
|
{ value: 'expired', label: '已过期' },
|
||||||
|
{ value: 'account_banned', label: '账号封禁' },
|
||||||
|
{ value: 'quota_exhausted', label: '额度耗尽' },
|
||||||
|
{ value: 'account_forbidden', label: '访问受限' },
|
||||||
|
{ value: 'account_disabled', label: '账号停用' },
|
||||||
|
{ value: 'workspace_deactivated', label: '工作区停用' },
|
||||||
|
{ value: 'account_verification', label: '需要验证' },
|
||||||
|
{ value: 'account_quarantined', label: '账号隔离' },
|
||||||
|
{ value: 'account_blocked', label: '账号异常' },
|
||||||
|
{ value: 'rate_limited', label: '速率受限' },
|
||||||
|
{ value: 'cost_exhausted', label: '超限' },
|
||||||
]
|
]
|
||||||
const poolScoreHardStateOptions = [
|
const poolScoreHardStateOptions = [
|
||||||
{ value: 'all', label: '全部状态' },
|
{ value: 'all', label: '全部状态' },
|
||||||
@@ -2667,7 +2674,7 @@ async function loadKeys(options: { cacheTtlMs?: number } = {}) {
|
|||||||
const page = currentPage.value
|
const page = currentPage.value
|
||||||
const pageSizeValue = pageSize.value
|
const pageSizeValue = pageSize.value
|
||||||
const search = searchQuery.value || undefined
|
const search = searchQuery.value || undefined
|
||||||
const status = statusFilter.value as 'all' | 'active' | 'cooldown' | 'inactive'
|
const status = statusFilter.value
|
||||||
const sortByValue = sortBy.value || undefined
|
const sortByValue = sortBy.value || undefined
|
||||||
keysLoading.value = true
|
keysLoading.value = true
|
||||||
try {
|
try {
|
||||||
@@ -3427,6 +3434,7 @@ function getVisibleSchedulingReasons(key: PoolKeyDetail) {
|
|||||||
|
|
||||||
function getSchedulingStatus(key: PoolKeyDetail): 'available' | 'degraded' | 'blocked' {
|
function getSchedulingStatus(key: PoolKeyDetail): 'available' | 'degraded' | 'blocked' {
|
||||||
if (getAccountAlertLabel(key)) return 'blocked'
|
if (getAccountAlertLabel(key)) return 'blocked'
|
||||||
|
if (getBlockingOAuthStatusLabel(key)) return 'blocked'
|
||||||
|
|
||||||
const status = key.scheduling_status
|
const status = key.scheduling_status
|
||||||
if (
|
if (
|
||||||
@@ -3472,9 +3480,23 @@ function getOAuthStatusBadgeLabel(status: ReturnType<typeof getVisibleOAuthState
|
|||||||
return '有效'
|
return '有效'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getBlockingOAuthStatusLabel(key: PoolKeyDetail): string | null {
|
||||||
|
const oauthState = getVisibleOAuthState(key)
|
||||||
|
if (!oauthState?.isInvalid && !oauthState?.isExpired) return null
|
||||||
|
return getOAuthStatusBadgeLabel(oauthState)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPoolKeyCostExhausted(key: PoolKeyDetail): boolean {
|
||||||
|
return key.cost_limit != null
|
||||||
|
&& key.cost_limit > 0
|
||||||
|
&& key.cost_window_usage >= key.cost_limit
|
||||||
|
}
|
||||||
|
|
||||||
function getSchedulingBadgeLabel(key: PoolKeyDetail): string {
|
function getSchedulingBadgeLabel(key: PoolKeyDetail): string {
|
||||||
const accountAlert = getAccountAlertLabel(key)
|
const accountAlert = getAccountAlertLabel(key)
|
||||||
if (accountAlert) return compactPoolStatusLabel(accountAlert) || accountAlert
|
if (accountAlert) return compactPoolStatusLabel(accountAlert) || accountAlert
|
||||||
|
const oauthAlert = getBlockingOAuthStatusLabel(key)
|
||||||
|
if (oauthAlert) return oauthAlert
|
||||||
|
|
||||||
const rawLabel = String(key.scheduling_label || '').trim()
|
const rawLabel = String(key.scheduling_label || '').trim()
|
||||||
if (
|
if (
|
||||||
@@ -3482,24 +3504,28 @@ function getSchedulingBadgeLabel(key: PoolKeyDetail): string {
|
|||||||
&& !isHealthDerivedSchedulingReason(key.scheduling_reason)
|
&& !isHealthDerivedSchedulingReason(key.scheduling_reason)
|
||||||
&& !isHealthDerivedSchedulingLabel(rawLabel)
|
&& !isHealthDerivedSchedulingLabel(rawLabel)
|
||||||
) {
|
) {
|
||||||
|
if ((rawLabel === '可用' || key.scheduling_reason === 'available') && isPoolKeyCostExhausted(key)) {
|
||||||
|
return '超限'
|
||||||
|
}
|
||||||
if (rawLabel === '禁用' || rawLabel === '停用') return '禁用'
|
if (rawLabel === '禁用' || rawLabel === '停用') return '禁用'
|
||||||
return compactPoolStatusLabel(rawLabel) || rawLabel
|
return compactPoolStatusLabel(rawLabel) || rawLabel
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!key.is_active) return '禁用'
|
if (!key.is_active) return '已禁用'
|
||||||
if (key.cooldown_reason) return '冷却中'
|
if (key.cooldown_reason) return '冷却中'
|
||||||
if (key.cost_limit != null && key.cost_limit > 0 && key.cost_window_usage >= key.cost_limit) return '超限'
|
|
||||||
return '可用'
|
return '可用'
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSchedulingBadgeVariant(key: PoolKeyDetail): PoolStatusVariant {
|
function getSchedulingBadgeVariant(key: PoolKeyDetail): PoolStatusVariant {
|
||||||
if (getAccountAlertLabel(key)) return 'destructive'
|
if (getAccountAlertLabel(key)) return 'destructive'
|
||||||
|
if (getBlockingOAuthStatusLabel(key)) return 'destructive'
|
||||||
|
|
||||||
const reason = getVisibleSchedulingReason(key)
|
const reason = getVisibleSchedulingReason(key)
|
||||||
if (reason === 'manual_disabled' || reason === 'inactive') return 'secondary'
|
if (reason === 'manual_disabled' || reason === 'inactive') return 'secondary'
|
||||||
if (reason === 'account_blocked' || reason === 'account_quota_exhausted' || reason === 'cost_exhausted') return 'destructive'
|
if (reason === 'account_blocked' || reason === 'account_quota_exhausted' || reason === 'cost_exhausted') return 'destructive'
|
||||||
if (reason === 'cooldown') return 'warning'
|
if (reason === 'cooldown') return 'warning'
|
||||||
if (reason === 'cost_soft' || reason === 'cost') return 'warning'
|
if (reason === 'cost_soft' || reason === 'cost') return 'warning'
|
||||||
|
if (isPoolKeyCostExhausted(key)) return 'destructive'
|
||||||
if (reason === 'available') return 'default'
|
if (reason === 'available') return 'default'
|
||||||
if (!reason && !key.is_active) return 'secondary'
|
if (!reason && !key.is_active) return 'secondary'
|
||||||
|
|
||||||
@@ -3512,6 +3538,7 @@ function getSchedulingBadgeVariant(key: PoolKeyDetail): PoolStatusVariant {
|
|||||||
function getSchedulingTitle(key: PoolKeyDetail): string {
|
function getSchedulingTitle(key: PoolKeyDetail): string {
|
||||||
const accountAlertTitle = getAccountAlertTitle(key)
|
const accountAlertTitle = getAccountAlertTitle(key)
|
||||||
if (accountAlertTitle) return accountAlertTitle
|
if (accountAlertTitle) return accountAlertTitle
|
||||||
|
if (getBlockingOAuthStatusLabel(key)) return getOAuthStatusTitle(key)
|
||||||
|
|
||||||
const reasons = getVisibleSchedulingReasons(key)
|
const reasons = getVisibleSchedulingReasons(key)
|
||||||
if (reasons.length > 0) {
|
if (reasons.length > 0) {
|
||||||
@@ -3526,6 +3553,7 @@ function getSchedulingTitle(key: PoolKeyDetail): string {
|
|||||||
const ttl = key.cooldown_ttl_seconds ? ` (${formatTTL(key.cooldown_ttl_seconds)})` : ''
|
const ttl = key.cooldown_ttl_seconds ? ` (${formatTTL(key.cooldown_ttl_seconds)})` : ''
|
||||||
return `${formatCooldownReason(key.cooldown_reason)}${ttl}`
|
return `${formatCooldownReason(key.cooldown_reason)}${ttl}`
|
||||||
}
|
}
|
||||||
|
if (isPoolKeyCostExhausted(key)) return '超限'
|
||||||
return getSchedulingBadgeLabel(key)
|
return getSchedulingBadgeLabel(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user