From ac17425ed9131fd7a58b40f2ac3f67144effc3c6 Mon Sep 17 00:00:00 2001 From: LB7666 Date: Fri, 19 Jun 2026 02:22:40 +0800 Subject: [PATCH 1/2] Lazy proxy traffic refresh --- crates/frontend/src/api.rs | 64 +++++++ .../frontend/src/pages/admin_llm_gateway.rs | 179 +++++++++++++----- crates/llm-access-core/src/store/empty.rs | 11 +- crates/llm-access-core/src/store/mod.rs | 3 +- crates/llm-access-core/src/store/proxy.rs | 63 +++++- crates/llm-access-core/src/store/traits.rs | 12 +- .../migrations/postgres/0001_init.sql | 12 ++ .../0027_proxy_config_traffic_snapshots.sql | 11 ++ crates/llm-access-migrations/src/lib.rs | 23 +++ crates/llm-access-store/src/postgres.rs | 81 +++++++- .../llm-access-store/src/postgres/decode.rs | 1 + crates/llm-access-store/src/postgres/proxy.rs | 137 +++++++++++++- crates/llm-access/src/admin.rs | 163 ++++++++++++++++ crates/llm-access/src/lib.rs | 4 + 14 files changed, 705 insertions(+), 59 deletions(-) create mode 100644 crates/llm-access-migrations/migrations/postgres/0027_proxy_config_traffic_snapshots.sql diff --git a/crates/frontend/src/api.rs b/crates/frontend/src/api.rs index 92392e5e..e7844b73 100644 --- a/crates/frontend/src/api.rs +++ b/crates/frontend/src/api.rs @@ -6474,6 +6474,16 @@ pub struct AdminLlmGatewayProxyTrafficTotalsView { pub total_bytes: u64, } +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] +#[serde(default)] +pub struct AdminProxyTrafficSnapshotView { + pub refreshed_at_ms: i64, + pub window_start_ms: i64, + pub window_end_ms: i64, + pub retention_days: u64, + pub totals: AdminLlmGatewayProxyTrafficTotalsView, +} + #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] #[serde(default)] pub struct AdminLlmGatewayProxyTrafficPointView { @@ -7091,6 +7101,7 @@ pub struct AdminUpstreamProxyConfigView { pub can_edit_slot_metadata: bool, pub latest_codex_check: Option, pub latest_kiro_check: Option, + pub traffic_snapshot: Option, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] @@ -7112,6 +7123,14 @@ pub struct AdminUpstreamProxyConfigsResponse { pub generated_at: i64, } +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] +#[serde(default)] +pub struct AdminProxyTrafficRefreshResponse { + pub proxy_config_id: String, + pub traffic_snapshot: AdminProxyTrafficSnapshotView, + pub generated_at: i64, +} + #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[serde(default)] pub struct AdminUpstreamProxyConfigScopeView { @@ -8073,6 +8092,7 @@ pub async fn create_admin_llm_gateway_proxy_config( can_edit_slot_metadata: true, latest_codex_check: None, latest_kiro_check: None, + traffic_snapshot: None, }) } @@ -8121,6 +8141,7 @@ pub async fn patch_admin_llm_gateway_proxy_config( can_edit_slot_metadata: true, latest_codex_check: None, latest_kiro_check: None, + traffic_snapshot: None, }) } @@ -8174,6 +8195,49 @@ pub async fn delete_admin_llm_gateway_proxy_config(proxy_id: &str) -> Result<(), } } +pub async fn refresh_admin_llm_gateway_proxy_traffic( + proxy_id: &str, +) -> Result { + #[cfg(feature = "mock")] + { + Ok(AdminProxyTrafficRefreshResponse { + proxy_config_id: proxy_id.to_string(), + traffic_snapshot: AdminProxyTrafficSnapshotView { + retention_days: 7, + totals: AdminLlmGatewayProxyTrafficTotalsView { + event_count: 12, + request_bytes: 1024 * 1024, + response_bytes: 3 * 1024 * 1024, + total_bytes: 4 * 1024 * 1024, + }, + ..AdminProxyTrafficSnapshotView::default() + }, + generated_at: 0, + }) + } + + #[cfg(not(feature = "mock"))] + { + let url = format!( + "{}/admin/llm-gateway/proxy-configs/{}/traffic-refresh", + llm_access_admin_base(), + urlencoding::encode(proxy_id) + ); + let response = api_post(&url) + .send() + .await + .map_err(|e| format!("Network error: {:?}", e))?; + if !response.ok() { + let text = response.text().await.unwrap_or_default(); + return Err(format!("Failed: {text}")); + } + response + .json() + .await + .map_err(|e| format!("Parse error: {:?}", e)) + } +} + pub async fn reset_admin_llm_gateway_proxy_config_override( proxy_id: &str, ) -> Result { diff --git a/crates/frontend/src/pages/admin_llm_gateway.rs b/crates/frontend/src/pages/admin_llm_gateway.rs index e05ac281..c0d81207 100644 --- a/crates/frontend/src/pages/admin_llm_gateway.rs +++ b/crates/frontend/src/pages/admin_llm_gateway.rs @@ -28,31 +28,31 @@ use crate::{ fetch_admin_llm_gateway_accounts_page_with_query, fetch_admin_llm_gateway_config, fetch_admin_llm_gateway_keys, fetch_admin_llm_gateway_keys_page, fetch_admin_llm_gateway_keys_page_with_query, fetch_admin_llm_gateway_proxy_bindings, - fetch_admin_llm_gateway_proxy_configs, fetch_admin_llm_gateway_proxy_traffic, - fetch_admin_llm_gateway_sponsor_requests, fetch_admin_llm_gateway_token_requests, - fetch_admin_llm_gateway_usage_event_detail, fetch_admin_llm_gateway_usage_events, - fetch_admin_llm_gateway_usage_filter_options, fetch_admin_usage_journal_preview, - fetch_admin_usage_journal_status, import_admin_legacy_kiro_proxy_configs, - import_admin_llm_gateway_account, patch_admin_llm_gateway_account, - patch_admin_llm_gateway_account_group, patch_admin_llm_gateway_key, - patch_admin_llm_gateway_proxy_config, probe_admin_llm_gateway_account_models, - refresh_admin_llm_gateway_account_auth, refresh_admin_llm_gateway_account_usage, + fetch_admin_llm_gateway_proxy_configs, fetch_admin_llm_gateway_sponsor_requests, + fetch_admin_llm_gateway_token_requests, fetch_admin_llm_gateway_usage_event_detail, + fetch_admin_llm_gateway_usage_events, fetch_admin_llm_gateway_usage_filter_options, + fetch_admin_usage_journal_preview, fetch_admin_usage_journal_status, + import_admin_legacy_kiro_proxy_configs, import_admin_llm_gateway_account, + patch_admin_llm_gateway_account, patch_admin_llm_gateway_account_group, + patch_admin_llm_gateway_key, patch_admin_llm_gateway_proxy_config, + probe_admin_llm_gateway_account_models, refresh_admin_llm_gateway_account_auth, + refresh_admin_llm_gateway_account_usage, refresh_admin_llm_gateway_proxy_traffic, reset_admin_llm_gateway_proxy_config_override, update_admin_llm_gateway_config, update_admin_llm_gateway_proxy_binding, AccountSummaryView, AdminAccountGroupOptionView, AdminAccountGroupView, AdminAccountsSummaryView, AdminLlmGatewayAccountContributionRequestView, AdminLlmGatewayAccountContributionRequestsQuery, AdminLlmGatewayAccountPageQuery, AdminLlmGatewayKeyPageQuery, AdminLlmGatewayKeyView, AdminLlmGatewayKeysSummaryView, - AdminLlmGatewayProxyTrafficQuery, AdminLlmGatewayProxyTrafficTotalsView, AdminLlmGatewaySponsorRequestView, AdminLlmGatewaySponsorRequestsQuery, AdminLlmGatewayTokenRequestView, AdminLlmGatewayTokenRequestsQuery, AdminLlmGatewayUsageEventDetailView, AdminLlmGatewayUsageEventView, AdminLlmGatewayUsageEventsQuery, AdminLlmGatewayUsageFilterOptionsResponse, - AdminUpstreamProxyBindingView, AdminUpstreamProxyCheckResponse, - AdminUpstreamProxyCheckTargetView, AdminUpstreamProxyConfigScopeView, - AdminUpstreamProxyConfigView, AdminUpstreamProxyEndpointCheckView, - AdminUsageJournalFileView, AdminUsageJournalPreviewResponse, AdminUsageJournalStatusView, - AdminUsageTotalsView, CodexAccountImportJobDetailView, CodexAccountImportJobSummaryView, + AdminProxyTrafficSnapshotView, AdminUpstreamProxyBindingView, + AdminUpstreamProxyCheckResponse, AdminUpstreamProxyCheckTargetView, + AdminUpstreamProxyConfigScopeView, AdminUpstreamProxyConfigView, + AdminUpstreamProxyEndpointCheckView, AdminUsageJournalFileView, + AdminUsageJournalPreviewResponse, AdminUsageJournalStatusView, AdminUsageTotalsView, + CodexAccountImportJobDetailView, CodexAccountImportJobSummaryView, CreateAdminAccountGroupInput, CreateAdminUpstreamProxyConfigInput, LlmGatewayRuntimeConfig, PatchAdminAccountGroupInput, PatchAdminLlmGatewayAccountInput, PatchAdminLlmGatewayKeyRequest, PatchAdminUpstreamProxyConfigInput, @@ -83,6 +83,7 @@ const USAGE_STATUS_KIND_NON_OK: &str = "non_ok"; const TOKEN_REQUEST_PAGE_SIZE: usize = 20; const ACCOUNT_CONTRIBUTION_REQUEST_PAGE_SIZE: usize = 20; const SPONSOR_REQUEST_PAGE_SIZE: usize = 20; +const PROXY_TRAFFIC_QUERY_WINDOW_DAYS: u64 = 30; const ADMIN_CODEX_IMPORT_JOB_LIST_LIMIT: usize = 10; const ACCOUNT_PAGE_SIZE: usize = 8; const KEY_PAGE_SIZE: usize = 8; @@ -347,6 +348,37 @@ fn format_optional_bytes(bytes: Option) -> String { format_optional_bytes_human(bytes) } +fn proxy_traffic_window_label(retention_days: u64) -> String { + let retained_days = retention_days.clamp(1, PROXY_TRAFFIC_QUERY_WINDOW_DAYS); + if retained_days >= PROXY_TRAFFIC_QUERY_WINDOW_DAYS { + format!("{PROXY_TRAFFIC_QUERY_WINDOW_DAYS}d traffic") + } else { + format!("retained {retained_days}d traffic") + } +} + +fn proxy_traffic_snapshot_badge(snapshot: Option<&AdminProxyTrafficSnapshotView>) -> String { + match snapshot { + Some(snapshot) => format!( + "{} {}", + proxy_traffic_window_label(snapshot.retention_days), + format_optional_bytes(Some(snapshot.totals.total_bytes)) + ), + None => "流量未计算".to_string(), + } +} + +fn proxy_traffic_snapshot_meta(snapshot: Option<&AdminProxyTrafficSnapshotView>) -> String { + match snapshot { + Some(snapshot) => format!( + "traffic refreshed {} · traffic events {}", + format_ms(snapshot.refreshed_at_ms), + snapshot.totals.event_count + ), + None => "traffic not calculated".to_string(), + } +} + fn format_cgroup_memory_usage(memory: &ProcessMemoryRuntimeStats) -> String { match (memory.cgroup_current_bytes, memory.cgroup_max_bytes) { (Some(current), Some(max)) if max > 0 => { @@ -1866,7 +1898,6 @@ fn account_group_editor_card(props: &AccountGroupEditorCardProps) -> Html { #[derive(Properties, PartialEq)] struct ProxyConfigEditorCardProps { proxy_config: AdminUpstreamProxyConfigView, - traffic_30d: Option, on_changed: Callback<()>, on_copy: Callback<(String, String)>, on_flash: Callback<(String, bool)>, @@ -1923,7 +1954,6 @@ impl Default for CreateKeyForm { #[function_component(ProxyConfigEditorCard)] fn proxy_config_editor_card(props: &ProxyConfigEditorCardProps) -> Html { let proxy_config = props.proxy_config.clone(); - let traffic_30d = props.traffic_30d.unwrap_or_default(); let can_edit_slot_metadata = proxy_config.can_edit_slot_metadata; let scope_node_label = proxy_config .scope_node_id @@ -1938,6 +1968,10 @@ fn proxy_config_editor_card(props: &ProxyConfigEditorCardProps) -> Html { let saving = use_state(|| false); let checking = use_state(|| None::); let feedback = use_state(|| None::); + let traffic_snapshot = use_state(|| proxy_config.traffic_snapshot.clone()); + let refreshing_traffic = use_state(|| false); + let traffic_badge = proxy_traffic_snapshot_badge((*traffic_snapshot).as_ref()); + let traffic_meta = proxy_traffic_snapshot_meta((*traffic_snapshot).as_ref()); { let form = form.clone(); @@ -1947,6 +1981,14 @@ fn proxy_config_editor_card(props: &ProxyConfigEditorCardProps) -> Html { }); } + { + let traffic_snapshot = traffic_snapshot.clone(); + use_effect_with(props.proxy_config.traffic_snapshot.clone(), move |snapshot| { + traffic_snapshot.set(snapshot.clone()); + || () + }); + } + let on_save = { let proxy_id = proxy_config.id.clone(); let form = form.clone(); @@ -2124,6 +2166,39 @@ fn proxy_config_editor_card(props: &ProxyConfigEditorCardProps) -> Html { }) }; + let on_refresh_traffic = { + let proxy_id = proxy_config.id.clone(); + let traffic_snapshot = traffic_snapshot.clone(); + let refreshing_traffic = refreshing_traffic.clone(); + let feedback = feedback.clone(); + let on_flash = props.on_flash.clone(); + Callback::from(move |_| { + if *refreshing_traffic { + return; + } + let proxy_id = proxy_id.clone(); + let traffic_snapshot = traffic_snapshot.clone(); + let refreshing_traffic = refreshing_traffic.clone(); + let feedback = feedback.clone(); + let on_flash = on_flash.clone(); + wasm_bindgen_futures::spawn_local(async move { + refreshing_traffic.set(true); + match refresh_admin_llm_gateway_proxy_traffic(&proxy_id).await { + Ok(response) => { + traffic_snapshot.set(Some(response.traffic_snapshot)); + feedback.set(Some("流量已刷新".to_string())); + on_flash.emit(("已刷新代理流量".to_string(), false)); + }, + Err(err) => { + feedback.set(Some(err.clone())); + on_flash.emit((format!("刷新代理流量失败\n{err}"), true)); + }, + } + refreshing_traffic.set(false); + }); + }) + }; + html! {
@@ -2141,11 +2216,11 @@ fn proxy_config_editor_card(props: &ProxyConfigEditorCardProps) -> Html { { format!("scope: {}", scope_node_label) } - { format!("30d traffic {}", format_optional_bytes(Some(traffic_30d.total_bytes))) } + { traffic_badge }

- { format!("created {} · updated {} · traffic events {}", format_ms(props.proxy_config.created_at), format_ms(props.proxy_config.updated_at), traffic_30d.event_count) } + { format!("created {} · updated {} · {}", format_ms(props.proxy_config.created_at), format_ms(props.proxy_config.updated_at), traffic_meta) }

@@ -2284,6 +2359,13 @@ fn proxy_config_editor_card(props: &ProxyConfigEditorCardProps) -> Html {
+