fix(pool): align status filters with visible key state

This commit is contained in:
cym
2026-05-30 18:49:26 +08:00
parent 0d76a8e478
commit ce1d532e3c
6 changed files with 351 additions and 46 deletions
@@ -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(),
),
} }
} }
+17 -1
View File
@@ -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'
+44 -16
View File
@@ -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)
} }