mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-07 00:05:49 +00:00
refactor(api): dedupe admin presenter helpers and page metadata
- extract format_number/format_compact_number into admin/presenter.rs - use gateway_admin model china_day_start instead of local copy - unify PageMeta onto admin::wire, drop observability duplicate - accounts reuse map_admin_service_error instead of local match Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,7 @@ use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
|
||||
use chrono::{DateTime, FixedOffset, Utc};
|
||||
use futures::{Stream, StreamExt as _};
|
||||
use gateway_admin::model::{
|
||||
AdminError as AdminServiceError, AdminErrorKind, PageSize,
|
||||
AdminError as AdminServiceError, PageSize,
|
||||
accounts::{
|
||||
AccountConnectionTestEvent as DomainConnectionTestEvent, AccountCost, AccountListQuery,
|
||||
AccountModelUsage, AccountSort, AccountSortField, AccountStatus as DomainAccountStatus,
|
||||
@@ -37,6 +37,7 @@ use gateway_core::{
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
use super::presenter::{format_compact_number, format_number};
|
||||
use super::{
|
||||
AdminAuth, AdminEnvelope, AdminError, AdminResponse, AdminSessionState, PageMeta,
|
||||
WireValidationError,
|
||||
@@ -1468,7 +1469,7 @@ fn quota_local_usage(usage: &AccountUsage) -> Value {
|
||||
"imageRequestCount": usage.image_request_count,
|
||||
"imageRequestFailedCount": usage.image_request_failed_count,
|
||||
"totalTokens": total_tokens,
|
||||
"totalTokensDisplay": format_tokens(total_tokens),
|
||||
"totalTokensDisplay": format_compact_number(total_tokens),
|
||||
"requestBuckets": usage.request_buckets.iter().map(|bucket| serde_json::json!({
|
||||
"bucketStart": bucket.bucket_start,
|
||||
"requestCount": bucket.request_count,
|
||||
@@ -1601,7 +1602,7 @@ fn account_currency_cost_view(cost: &AccountCost) -> CurrencyCostView {
|
||||
}
|
||||
|
||||
fn display_optional_tokens(value: Option<u64>) -> String {
|
||||
value.map_or_else(|| "—".to_owned(), format_tokens)
|
||||
value.map_or_else(|| "—".to_owned(), format_compact_number)
|
||||
}
|
||||
|
||||
fn empty_account_usage() -> AccountUsageView {
|
||||
@@ -1719,37 +1720,6 @@ fn account_status_name(status: DomainAccountStatus) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
fn format_number(value: u64) -> String {
|
||||
let text = value.to_string();
|
||||
let mut output = String::with_capacity(text.len() + text.len() / 3);
|
||||
for (index, character) in text.chars().rev().enumerate() {
|
||||
if index > 0 && index % 3 == 0 {
|
||||
output.push(',');
|
||||
}
|
||||
output.push(character);
|
||||
}
|
||||
output.chars().rev().collect()
|
||||
}
|
||||
|
||||
fn format_tokens(value: u64) -> String {
|
||||
if value < 1_000 {
|
||||
return format_number(value);
|
||||
}
|
||||
for (suffix, threshold) in [
|
||||
("P", 1_000_000_000_000_000_u64),
|
||||
("T", 1_000_000_000_000_u64),
|
||||
("B", 1_000_000_000_u64),
|
||||
("M", 1_000_000_u64),
|
||||
("K", 1_000_u64),
|
||||
] {
|
||||
if value >= threshold {
|
||||
let scaled = value as f64 / threshold as f64;
|
||||
return format!("{scaled:.1}{suffix}").replace(".0", "");
|
||||
}
|
||||
}
|
||||
format_number(value)
|
||||
}
|
||||
|
||||
fn relative_time(value: DateTime<Utc>, now: DateTime<Utc>) -> String {
|
||||
let elapsed = now.signed_duration_since(value);
|
||||
if elapsed.num_seconds() < 0 {
|
||||
@@ -1787,16 +1757,5 @@ fn map_wire_error(error: WireValidationError) -> AdminError {
|
||||
}
|
||||
|
||||
fn map_service_error(error: AdminServiceError) -> AdminError {
|
||||
match error.kind() {
|
||||
AdminErrorKind::Invalid => AdminError::bad_request(error.to_string()),
|
||||
AdminErrorKind::Unauthorized => AdminError::admin_session_required(),
|
||||
AdminErrorKind::NotFound => AdminError::not_found(error.to_string()),
|
||||
AdminErrorKind::Conflict => AdminError::conflict(error.to_string()),
|
||||
AdminErrorKind::RateLimited => AdminError::too_many_login_attempts(),
|
||||
AdminErrorKind::BadGateway => AdminError::bad_gateway(error.to_string()),
|
||||
AdminErrorKind::Unavailable => {
|
||||
AdminError::service_unavailable("Account directory unavailable")
|
||||
}
|
||||
AdminErrorKind::Internal => AdminError::internal(error.to_string()),
|
||||
}
|
||||
super::wire::map_admin_service_error(error, "Account directory unavailable")
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ pub mod auth;
|
||||
pub mod backups;
|
||||
pub mod client_keys;
|
||||
pub mod observability;
|
||||
pub mod presenter;
|
||||
pub mod settings;
|
||||
pub mod system;
|
||||
pub mod wire;
|
||||
|
||||
@@ -10,14 +10,15 @@ use axum::{
|
||||
routing::get,
|
||||
};
|
||||
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
|
||||
use chrono::{DateTime, Duration, Timelike as _, Utc};
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use gateway_admin::model::{PageSize as DomainPageSize, observability as domain};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use super::presenter::{format_compact_number, format_number};
|
||||
use super::{
|
||||
AdminAuth, AdminEnvelope, AdminError, AdminResponse, AdminSessionState, WireValidationError,
|
||||
wire::map_admin_service_error,
|
||||
AdminAuth, AdminEnvelope, AdminError, AdminResponse, AdminSessionState, PageMeta,
|
||||
WireValidationError, wire::map_admin_service_error,
|
||||
};
|
||||
|
||||
/// 观测列表默认页大小。
|
||||
@@ -329,7 +330,8 @@ fn dashboard_range(
|
||||
end: Option<&str>,
|
||||
) -> Result<domain::TimeRange, WireValidationError> {
|
||||
let end = parse_datetime(end)?.unwrap_or_else(Utc::now);
|
||||
let start = parse_datetime(start)?.unwrap_or_else(|| china_day_start(end) - Duration::days(1));
|
||||
let start =
|
||||
parse_datetime(start)?.unwrap_or_else(|| domain::china_day_start(end) - Duration::days(1));
|
||||
domain::TimeRange::new(start, end).map_err(|_| WireValidationError::new("timeRange"))
|
||||
}
|
||||
|
||||
@@ -338,7 +340,7 @@ fn dashboard_today_range(
|
||||
end: Option<&str>,
|
||||
) -> Result<domain::TimeRange, WireValidationError> {
|
||||
let end = parse_datetime(end)?.unwrap_or_else(Utc::now);
|
||||
let start = parse_datetime(start)?.unwrap_or_else(|| china_day_start(end));
|
||||
let start = parse_datetime(start)?.unwrap_or_else(|| domain::china_day_start(end));
|
||||
domain::TimeRange::new(start, end).map_err(|_| WireValidationError::new("timeRange"))
|
||||
}
|
||||
|
||||
@@ -481,29 +483,6 @@ pub struct CursorWire {
|
||||
pub stable_id: String,
|
||||
}
|
||||
|
||||
/// 观测列表分页元数据。
|
||||
#[derive(Debug, Clone, Copy, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PageMeta {
|
||||
pub page: u32,
|
||||
pub page_size: u16,
|
||||
pub total: u64,
|
||||
pub total_pages: u32,
|
||||
}
|
||||
|
||||
impl PageMeta {
|
||||
/// 由应用层已校验的分页事实构造响应元数据。
|
||||
#[must_use]
|
||||
pub const fn new(page: u32, page_size: u16, total: u64, total_pages: u32) -> Self {
|
||||
Self {
|
||||
page,
|
||||
page_size,
|
||||
total,
|
||||
total_pages,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 观测列表响应数据。
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -1443,37 +1422,6 @@ where
|
||||
Ok(AdminResponse::new(StatusCode::OK, AdminEnvelope::ok(data)))
|
||||
}
|
||||
|
||||
fn format_number(value: u64) -> String {
|
||||
let text = value.to_string();
|
||||
let mut output = String::with_capacity(text.len() + text.len() / 3);
|
||||
for (index, character) in text.chars().rev().enumerate() {
|
||||
if index > 0 && index % 3 == 0 {
|
||||
output.push(',');
|
||||
}
|
||||
output.push(character);
|
||||
}
|
||||
output.chars().rev().collect()
|
||||
}
|
||||
|
||||
fn format_compact_number(value: u64) -> String {
|
||||
if value < 1_000 {
|
||||
return format_number(value);
|
||||
}
|
||||
for (suffix, threshold) in [
|
||||
("P", 1_000_000_000_000_000_u64),
|
||||
("T", 1_000_000_000_000_u64),
|
||||
("B", 1_000_000_000_u64),
|
||||
("M", 1_000_000_u64),
|
||||
("K", 1_000_u64),
|
||||
] {
|
||||
if value >= threshold {
|
||||
let scaled = value as f64 / threshold as f64;
|
||||
return format!("{scaled:.1}{suffix}").replace(".0", "");
|
||||
}
|
||||
}
|
||||
format_number(value)
|
||||
}
|
||||
|
||||
fn display_duration(value: Option<u64>) -> String {
|
||||
let Some(value) = value.and_then(|value| i64::try_from(value).ok()) else {
|
||||
return "—".to_owned();
|
||||
@@ -1497,12 +1445,6 @@ fn display_rate(value: f64) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn china_day_start(value: DateTime<Utc>) -> DateTime<Utc> {
|
||||
const CHINA_OFFSET_SECONDS: i64 = 8 * 60 * 60;
|
||||
let elapsed = (value.timestamp() + CHINA_OFFSET_SECONDS).rem_euclid(24 * 60 * 60);
|
||||
value - Duration::seconds(elapsed) - Duration::nanoseconds(i64::from(value.nanosecond()))
|
||||
}
|
||||
|
||||
fn china_datetime(value: &DateTime<Utc>) -> String {
|
||||
(*value + Duration::hours(8))
|
||||
.format("%Y-%m-%d %H:%M:%S")
|
||||
@@ -1908,7 +1850,7 @@ fn page_meta(page: u32, page_size: u16, total: u64) -> PageMeta {
|
||||
let total_pages = total.saturating_add(page_size_u64 - 1) / page_size_u64;
|
||||
PageMeta::new(
|
||||
page,
|
||||
page_size,
|
||||
u32::from(page_size),
|
||||
total,
|
||||
u32::try_from(total_pages).unwrap_or(u32::MAX),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
//! Admin API 的展示层辅助:数值格式化。
|
||||
//!
|
||||
//! 按 docs/project-redundancy-boundary-audit.md BE-02 的目标,display 字段最终
|
||||
//! 迁移到前端;过渡期统一由本模块提供,避免 accounts/observability 各自维护。
|
||||
|
||||
/// 千分位格式化。
|
||||
#[must_use]
|
||||
pub fn format_number(value: u64) -> String {
|
||||
let text = value.to_string();
|
||||
let mut output = String::with_capacity(text.len() + text.len() / 3);
|
||||
for (index, character) in text.chars().rev().enumerate() {
|
||||
if index > 0 && index % 3 == 0 {
|
||||
output.push(',');
|
||||
}
|
||||
output.push(character);
|
||||
}
|
||||
output.chars().rev().collect()
|
||||
}
|
||||
|
||||
/// 紧凑格式化:≥1000 时用 K/M/B/T/P 后缀,否则千分位。
|
||||
#[must_use]
|
||||
pub fn format_compact_number(value: u64) -> String {
|
||||
if value < 1_000 {
|
||||
return format_number(value);
|
||||
}
|
||||
for (suffix, threshold) in [
|
||||
("P", 1_000_000_000_000_000_u64),
|
||||
("T", 1_000_000_000_000_u64),
|
||||
("B", 1_000_000_000_u64),
|
||||
("M", 1_000_000_u64),
|
||||
("K", 1_000_u64),
|
||||
] {
|
||||
if value >= threshold {
|
||||
let scaled = value as f64 / threshold as f64;
|
||||
return format!("{scaled:.1}{suffix}").replace(".0", "");
|
||||
}
|
||||
}
|
||||
format_number(value)
|
||||
}
|
||||
@@ -66,11 +66,12 @@ mod query {
|
||||
mod response {
|
||||
use chrono::{TimeZone, Utc};
|
||||
use gateway_admin::model::observability::DesktopReleaseStatus;
|
||||
use gateway_api::admin::PageMeta;
|
||||
use gateway_api::admin::observability::{
|
||||
BillingView, CostCoverageView, CursorWire, DashboardAccountRequestBucketView,
|
||||
DashboardAccountUsageView, DashboardDesktopReleaseStatusView, DashboardWireAttributeView,
|
||||
DashboardWireProfileView, DashboardWireTargetView, PageData, PageMeta, TokenDetailsView,
|
||||
TrendData, TrendKind, TrendPointView, TrendSummaryView,
|
||||
DashboardWireProfileView, DashboardWireTargetView, PageData, TokenDetailsView, TrendData,
|
||||
TrendKind, TrendPointView, TrendSummaryView,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ fn source_tree_should_match_frozen_machine_manifest() {
|
||||
"src/admin/client_keys.rs",
|
||||
"src/admin/mod.rs",
|
||||
"src/admin/observability.rs",
|
||||
"src/admin/presenter.rs",
|
||||
"src/admin/settings.rs",
|
||||
"src/admin/system.rs",
|
||||
"src/admin/wire.rs",
|
||||
|
||||
Reference in New Issue
Block a user