mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-10 08:06:03 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
acf574129f | ||
|
|
6426e0d3de |
@@ -169,6 +169,8 @@ pub struct AccountPage {
|
||||
}
|
||||
|
||||
/// 统一账号目录的全局状态计数,不受当前筛选和分页影响。
|
||||
///
|
||||
/// 配额耗尽是可恢复状态,始终单列;`unavailable` 只包含过期、禁用和封禁账号。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct AccountSummary {
|
||||
pub total: u64,
|
||||
|
||||
@@ -460,6 +460,8 @@ pub struct RequestMetricPoint {
|
||||
}
|
||||
|
||||
/// 账号池容量统计。
|
||||
///
|
||||
/// 配额耗尽单列为可恢复状态;`unavailable` 只包含过期、禁用和封禁账号。
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct AccountPoolMetrics {
|
||||
pub total: u64,
|
||||
|
||||
@@ -2234,6 +2234,9 @@ fn dashboard_view(result: domain::DashboardResult, kind: TrendKind) -> Dashboard
|
||||
account_usage,
|
||||
recent_requests,
|
||||
} = observation;
|
||||
// 概览卡只有可用和不可用两桶:额度耗尽可恢复,保留在可用;不可用只含终态账号。
|
||||
let available_accounts =
|
||||
dashboard_available_account_count(provider_accounts.total, provider_accounts.unavailable);
|
||||
let mut account_usage_views = Vec::with_capacity(account_usage.len());
|
||||
let mut credential_usage_views = Vec::with_capacity(account_usage.len());
|
||||
for credential in account_usage {
|
||||
@@ -2282,8 +2285,8 @@ fn dashboard_view(result: domain::DashboardResult, kind: TrendKind) -> Dashboard
|
||||
credentials: DashboardCredentialsCardView {
|
||||
total: format_compact_number(provider_accounts.total),
|
||||
total_value: provider_accounts.total,
|
||||
available: format_compact_number(provider_accounts.active),
|
||||
available_value: provider_accounts.active,
|
||||
available: format_compact_number(available_accounts),
|
||||
available_value: available_accounts,
|
||||
unavailable: format_compact_number(provider_accounts.unavailable),
|
||||
unavailable_value: provider_accounts.unavailable,
|
||||
},
|
||||
@@ -2339,6 +2342,10 @@ fn dashboard_view(result: domain::DashboardResult, kind: TrendKind) -> Dashboard
|
||||
}
|
||||
}
|
||||
|
||||
fn dashboard_available_account_count(total: u64, unavailable: u64) -> u64 {
|
||||
total.saturating_sub(unavailable)
|
||||
}
|
||||
|
||||
fn usage_summary_view(summary: domain::UsageSummary) -> UsageSummaryView {
|
||||
let overview = summary.overview;
|
||||
UsageSummaryView {
|
||||
|
||||
@@ -79,6 +79,7 @@ pub(super) struct AdminTestFixture {
|
||||
pub auth: Arc<MemoryAuthStore>,
|
||||
pub settings: Arc<MemorySettingsStore>,
|
||||
pub usage_records: Arc<Mutex<Vec<UsageRecord>>>,
|
||||
pub dashboard_observation: Arc<Mutex<Option<DashboardObservation>>>,
|
||||
}
|
||||
|
||||
impl AdminTestFixture {
|
||||
@@ -92,8 +93,10 @@ impl AdminTestFixture {
|
||||
let settings = Arc::new(MemorySettingsStore::new(api_key));
|
||||
let client_keys = Arc::new(MemoryClientKeyStore);
|
||||
let usage_records = Arc::new(Mutex::new(Vec::new()));
|
||||
let dashboard_observation = Arc::new(Mutex::new(None));
|
||||
let unused = Arc::new(UnusedStore {
|
||||
usage_records: Arc::clone(&usage_records),
|
||||
dashboard_observation: Arc::clone(&dashboard_observation),
|
||||
});
|
||||
let stores = AdminStorePorts::new(
|
||||
unused.clone(),
|
||||
@@ -125,6 +128,7 @@ impl AdminTestFixture {
|
||||
auth,
|
||||
settings,
|
||||
usage_records,
|
||||
dashboard_observation,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -392,6 +396,7 @@ impl ClientKeyStore for MemoryClientKeyStore {
|
||||
|
||||
struct UnusedStore {
|
||||
usage_records: Arc<Mutex<Vec<UsageRecord>>>,
|
||||
dashboard_observation: Arc<Mutex<Option<DashboardObservation>>>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -503,7 +508,11 @@ impl AccountStore for UnusedStore {
|
||||
#[async_trait]
|
||||
impl ObservabilityStore for UnusedStore {
|
||||
async fn dashboard_summary(&self, _: TimeRange) -> AdminStoreResult<DashboardObservation> {
|
||||
Err(unavailable("dashboard"))
|
||||
self.dashboard_observation
|
||||
.lock()
|
||||
.expect("dashboard observation")
|
||||
.clone()
|
||||
.ok_or_else(|| unavailable("dashboard"))
|
||||
}
|
||||
|
||||
async fn dashboard_trend(&self, _: TimeRange) -> AdminStoreResult<Vec<RequestMetricPoint>> {
|
||||
|
||||
@@ -260,6 +260,80 @@ mod response {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dashboard_summary_should_count_quota_exhausted_accounts_as_available_in_headline_card() {
|
||||
use axum::{
|
||||
body::{Body, to_bytes},
|
||||
http::{Request, StatusCode, header},
|
||||
};
|
||||
use chrono::{Duration, Utc};
|
||||
use gateway_admin::model::observability::{
|
||||
AccountPoolMetrics, AttemptMetrics, DashboardObservation, RequestMetrics, TimeRange,
|
||||
};
|
||||
use gateway_api::admin::observability;
|
||||
use tower::ServiceExt as _;
|
||||
|
||||
use super::{AdminTestFixture, AdminTestState};
|
||||
|
||||
let fixture = AdminTestFixture::new().await;
|
||||
fixture.auth.insert_session("valid-session");
|
||||
let now = Utc::now();
|
||||
*fixture
|
||||
.dashboard_observation
|
||||
.lock()
|
||||
.expect("dashboard observation") = Some(DashboardObservation {
|
||||
range: TimeRange::new(now - Duration::hours(1), now).expect("dashboard range"),
|
||||
requests: RequestMetrics::default(),
|
||||
attempts: AttemptMetrics::default(),
|
||||
provider_accounts: AccountPoolMetrics {
|
||||
total: 807,
|
||||
enabled: 804,
|
||||
unavailable: 7,
|
||||
active: 15,
|
||||
expired: 2,
|
||||
quota_exhausted: 785,
|
||||
refreshing: None,
|
||||
disabled: 3,
|
||||
banned: 2,
|
||||
},
|
||||
trend: Vec::new(),
|
||||
account_usage: Vec::new(),
|
||||
recent_requests: Vec::new(),
|
||||
});
|
||||
|
||||
let response = observability::router::<AdminTestState>()
|
||||
.with_state(fixture.state())
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/api/admin/dashboard/summary")
|
||||
.header(header::COOKIE, "cpr_admin_session=valid-session")
|
||||
.header("x-request-id", "req_dashboard_account_summary")
|
||||
.body(Body::empty())
|
||||
.expect("dashboard request"),
|
||||
)
|
||||
.await
|
||||
.expect("dashboard response");
|
||||
let status = response.status();
|
||||
let body = to_bytes(response.into_body(), 1024 * 1024)
|
||||
.await
|
||||
.expect("dashboard response body");
|
||||
let value: serde_json::Value = serde_json::from_slice(&body).expect("dashboard response JSON");
|
||||
assert_eq!(status, StatusCode::OK, "{value}");
|
||||
|
||||
assert_eq!(
|
||||
(
|
||||
&value["data"]["cards"]["credentials"]["totalValue"],
|
||||
&value["data"]["cards"]["credentials"]["availableValue"],
|
||||
&value["data"]["cards"]["credentials"]["unavailableValue"],
|
||||
),
|
||||
(
|
||||
&serde_json::json!(807),
|
||||
&serde_json::json!(800),
|
||||
&serde_json::json!(7)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn usage_route_should_forward_a_bounded_unknown_outcome_filter() {
|
||||
use axum::{
|
||||
|
||||
@@ -2013,7 +2013,7 @@ async fn provider_account_metrics(
|
||||
)
|
||||
select count(*)::bigint as total,
|
||||
count(*) filter (where enabled)::bigint as enabled,
|
||||
count(*) filter (where status <> 'active')::bigint
|
||||
count(*) filter (where status in ('expired', 'disabled', 'banned'))::bigint
|
||||
as unavailable,
|
||||
count(*) filter (where status = 'active')::bigint as active,
|
||||
count(*) filter (where status = 'expired')::bigint as expired,
|
||||
|
||||
@@ -1610,36 +1610,26 @@ fn admin_account_summary(
|
||||
accounts: &[ProviderAccountSummary],
|
||||
now: DateTime<Utc>,
|
||||
) -> AccountSummary {
|
||||
let total = u64::try_from(accounts.len()).unwrap_or(u64::MAX);
|
||||
let active = u64::try_from(
|
||||
accounts
|
||||
.iter()
|
||||
.filter(|account| admin_account_status(account, now) == AdminAccountStatus::Active)
|
||||
.count(),
|
||||
)
|
||||
.unwrap_or(u64::MAX);
|
||||
let quota_exhausted = u64::try_from(
|
||||
accounts
|
||||
.iter()
|
||||
.filter(|account| {
|
||||
admin_account_status(account, now) == AdminAccountStatus::QuotaExhausted
|
||||
})
|
||||
.count(),
|
||||
)
|
||||
.unwrap_or(u64::MAX);
|
||||
let unavailable = u64::try_from(
|
||||
accounts
|
||||
.iter()
|
||||
.filter(|account| admin_account_status(account, now) != AdminAccountStatus::Active)
|
||||
.count(),
|
||||
)
|
||||
.unwrap_or(u64::MAX);
|
||||
AccountSummary {
|
||||
total,
|
||||
active,
|
||||
quota_exhausted,
|
||||
unavailable,
|
||||
let mut summary = AccountSummary {
|
||||
total: u64::try_from(accounts.len()).unwrap_or(u64::MAX),
|
||||
active: 0,
|
||||
quota_exhausted: 0,
|
||||
unavailable: 0,
|
||||
};
|
||||
for account in accounts {
|
||||
match admin_account_status(account, now) {
|
||||
AdminAccountStatus::Active => summary.active = summary.active.saturating_add(1),
|
||||
AdminAccountStatus::QuotaExhausted => {
|
||||
summary.quota_exhausted = summary.quota_exhausted.saturating_add(1);
|
||||
}
|
||||
AdminAccountStatus::Expired
|
||||
| AdminAccountStatus::Disabled
|
||||
| AdminAccountStatus::Banned => {
|
||||
summary.unavailable = summary.unavailable.saturating_add(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
summary
|
||||
}
|
||||
|
||||
fn sort_admin_account_items(items: &mut [AdminAccountListItem], sort: Option<AdminAccountSort>) {
|
||||
|
||||
@@ -112,7 +112,7 @@ async fn observability_preserves_and_filters_opaque_response_ids() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dashboard_account_metrics_should_partition_available_and_unavailable_accounts() {
|
||||
async fn dashboard_account_metrics_should_partition_account_statuses() {
|
||||
let Some(database) = TestDatabase::create("dashboard_account_metrics").await else {
|
||||
return;
|
||||
};
|
||||
@@ -169,9 +169,12 @@ async fn dashboard_account_metrics_should_partition_available_and_unavailable_ac
|
||||
metrics.disabled,
|
||||
metrics.banned,
|
||||
),
|
||||
(6, 5, 2, 4, 1, 1, 1, 1),
|
||||
(6, 5, 2, 3, 1, 1, 1, 1),
|
||||
);
|
||||
assert_eq!(
|
||||
metrics.total,
|
||||
metrics.active + metrics.quota_exhausted + metrics.unavailable
|
||||
);
|
||||
assert_eq!(metrics.total, metrics.active + metrics.unavailable);
|
||||
database.close().await;
|
||||
}
|
||||
|
||||
|
||||
@@ -130,10 +130,13 @@ async fn terminal_admin_list_filters_and_sorts_before_pagination_with_retained_u
|
||||
let mut invalid = account("acct_invalid", "user-invalid");
|
||||
invalid.email = Some("invalid@example.invalid".to_owned());
|
||||
invalid.availability = AccountAvailability::Invalid;
|
||||
let mut disabled = account("acct_disabled", "user-disabled");
|
||||
disabled.email = Some("disabled@example.invalid".to_owned());
|
||||
disabled.enabled = false;
|
||||
let mut quota_exhausted = account("acct_quota_exhausted", "user-quota-exhausted");
|
||||
quota_exhausted.email = Some("quota-exhausted@example.invalid".to_owned());
|
||||
quota_exhausted.availability = AccountAvailability::QuotaExhausted;
|
||||
for account in [alpha, beta, charlie, invalid, quota_exhausted] {
|
||||
for account in [alpha, beta, charlie, invalid, disabled, quota_exhausted] {
|
||||
repository
|
||||
.insert_provider_account(account)
|
||||
.await
|
||||
@@ -213,14 +216,16 @@ async fn terminal_admin_list_filters_and_sorts_before_pagination_with_retained_u
|
||||
.await
|
||||
.expect("sort accounts by retained usage");
|
||||
assert_eq!(usage_page.config_revision.get(), 1);
|
||||
assert_eq!(usage_page.total, 5);
|
||||
assert_eq!(usage_page.summary.total, 5);
|
||||
assert_eq!(usage_page.total, 6);
|
||||
assert_eq!(usage_page.summary.total, 6);
|
||||
assert_eq!(usage_page.summary.active, 2);
|
||||
assert_eq!(usage_page.summary.quota_exhausted, 1);
|
||||
assert_eq!(usage_page.summary.unavailable, 3);
|
||||
assert_eq!(
|
||||
usage_page.summary.total,
|
||||
usage_page.summary.active + usage_page.summary.unavailable
|
||||
usage_page.summary.active
|
||||
+ usage_page.summary.quota_exhausted
|
||||
+ usage_page.summary.unavailable
|
||||
);
|
||||
assert_eq!(
|
||||
usage_page
|
||||
|
||||
@@ -1 +1 @@
|
||||
version: "3.0.27"
|
||||
version: "3.0.28"
|
||||
|
||||
Reference in New Issue
Block a user