use super::{AdminAppState, ADMIN_SYSTEM_DATA_EXPORT_VERSION}; use crate::ai_serving::build_provider_key_pool_score_upsert; use crate::api::ai::admin_endpoint_signature_parts; use crate::handlers::admin::admin_provider_pool_config; use crate::handlers::admin::model::ADMIN_EXTERNAL_MODELS_PROXY_NODE_CONFIG_KEY; use crate::handlers::admin::provider::endpoints_admin::payloads::AdminProviderEndpointUpdatePatch; use crate::handlers::admin::provider::oauth::provisioning::ensure_codex_credential_generation_rotated; use crate::handlers::admin::provider::shared::payloads::{ AdminProviderCreateRequest, AdminProviderKeyCreateRequest, AdminProviderKeyUpdatePatch, AdminProviderUpdatePatch, }; use crate::handlers::admin::provider::write::keys::build_provider_catalog_key_admin_cas_update; use crate::handlers::admin::shared::{ normalize_json_array, normalize_json_object, normalize_string_list, }; use crate::handlers::admin::system::shared::configs::apply_admin_system_config_update; use crate::handlers::admin::users::{ hash_admin_user_api_key, normalize_admin_feature_settings, normalize_admin_list_policy_mode, normalize_admin_rate_limit_policy_mode, normalize_admin_user_api_formats, normalize_admin_user_ip_rules, normalize_admin_user_string_list, }; use crate::handlers::public::normalize_admin_base_url; use crate::GatewayError; use aether_admin::provider::endpoints as admin_provider_endpoints_pure; use aether_admin::provider::models_write as admin_provider_models_write_pure; use aether_admin::system::{ normalize_admin_system_config_key, parse_admin_system_config_array, parse_admin_system_config_import_request, parse_admin_system_config_nested_array, parse_admin_system_config_optional_object, AdminImportMergeMode, AdminSystemConfigEndpoint as ImportedEndpoint, AdminSystemConfigEntry as ImportedSystemConfig, AdminSystemConfigGlobalModel as ImportedGlobalModel, AdminSystemConfigImportCounter, AdminSystemConfigImportStats, AdminSystemConfigLdap as ImportedLdapConfig, AdminSystemConfigOAuthProvider as ImportedOAuthProvider, AdminSystemConfigProvider as ImportedProvider, AdminSystemConfigProviderKey as ImportedProviderKey, AdminSystemConfigProviderModel as ImportedProviderModel, AdminSystemConfigProxyNode as ImportedProxyNode, ADMIN_SYSTEM_PROVIDER_OPS_SENSITIVE_CREDENTIAL_FIELDS, ADMIN_SYSTEM_USERS_SUPPORTED_VERSIONS, }; use aether_data::repository::auth_modules::StoredLdapModuleConfig; use aether_data::repository::oauth_providers::{ EncryptedSecretUpdate, UpsertOAuthProviderConfigRecord, }; use aether_data::repository::system::{ AdminSystemStatsUserDailyAggregate, AdminSystemUsageAggregateImportMode, AdminSystemUsageAggregateImportSummary, AdminSystemUsageAggregateSnapshot, }; use aether_data::repository::wallet::WalletLookupKey; use aether_data_contracts::repository::global_models::{ AdminGlobalModelListQuery, AdminProviderModelListQuery, CreateAdminGlobalModelRecord, UpdateAdminGlobalModelRecord, UpsertAdminProviderModelRecord, }; use aether_data_contracts::repository::pool_scores::PoolMemberScoreUpsertMode; use axum::{body::Bytes, http}; use serde_json::{json, Map, Value}; use std::collections::{BTreeMap, BTreeSet}; use std::time::{SystemTime, UNIX_EPOCH}; use uuid::Uuid; fn invalid_request(detail: impl Into) -> (http::StatusCode, Value) { ( http::StatusCode::BAD_REQUEST, json!({ "detail": detail.into() }), ) } fn normalize_imported_system_config_key(key: &str) -> String { let normalized = normalize_admin_system_config_key(key); if normalized.eq_ignore_ascii_case(ADMIN_EXTERNAL_MODELS_PROXY_NODE_CONFIG_KEY) { ADMIN_EXTERNAL_MODELS_PROXY_NODE_CONFIG_KEY.to_string() } else { normalized } } fn build_admin_system_data_import_part_body( root: &Map, field_name: &str, merge_mode: AdminImportMergeMode, ) -> Result { let mut part = match root.get(field_name) { Some(Value::Object(map)) => map.clone(), Some(_) => return Err(invalid_request(format!("{field_name} 必须是对象"))), None => return Err(invalid_request(format!("{field_name} 为必填字段"))), }; let merge_mode_value = serde_json::to_value(merge_mode) .map_err(|err| invalid_request(format!("merge_mode 序列化失败: {err}")))?; part.insert("merge_mode".to_string(), merge_mode_value); serde_json::to_vec(&Value::Object(part)) .map(Bytes::from) .map_err(|err| invalid_request(format!("{field_name} 序列化失败: {err}"))) } fn trim_required(value: &str, field_name: &str) -> Result { let trimmed = value.trim(); if trimmed.is_empty() { return Err(format!("{field_name} 不能为空")); } Ok(trimmed.to_string()) } fn normalize_optional_price(value: Option, field_name: &str) -> Result, String> { admin_provider_models_write_pure::normalize_optional_price(value, field_name) } fn normalize_supported_capabilities(value: Option>) -> Option { normalize_string_list(value).map(|items| json!(items)) } fn normalize_import_auth_config(value: Option) -> Result, String> { let Some(value) = value else { return Ok(None); }; match value { Value::Null => Ok(None), Value::String(raw) => { let trimmed = raw.trim(); if trimmed.is_empty() { return Ok(None); } let parsed = serde_json::from_str::(trimmed) .map_err(|_| "auth_config 必须是 JSON 对象或 JSON 字符串".to_string())?; normalize_json_object(Some(parsed), "auth_config") } other => normalize_json_object(Some(other), "auth_config"), } } fn encrypt_imported_provider_config( state: &AdminAppState<'_>, config: Option, ) -> Result, String> { let Some(mut config) = normalize_json_object(config, "config")? else { return Ok(None); }; let Some(credentials) = config .get_mut("provider_ops") .and_then(Value::as_object_mut) .and_then(|provider_ops| provider_ops.get_mut("connector")) .and_then(Value::as_object_mut) .and_then(|connector| connector.get_mut("credentials")) .and_then(Value::as_object_mut) else { return Ok(Some(config)); }; for field in ADMIN_SYSTEM_PROVIDER_OPS_SENSITIVE_CREDENTIAL_FIELDS { let Some(Value::String(raw)) = credentials.get_mut(*field) else { continue; }; if raw.is_empty() { continue; } let encrypted = state .encrypt_catalog_secret_with_fallbacks(raw) .ok_or_else(|| "gateway 未配置 Provider Ops 加密密钥".to_string())?; *raw = encrypted; } Ok(Some(config)) } fn remap_import_proxy( proxy: Option, node_id_map: &BTreeMap, ) -> Option { let proxy = match proxy { Some(Value::Object(map)) if map.is_empty() => return None, Some(Value::Object(map)) => map, _ => return None, }; let Some(Value::String(old_node_id)) = proxy.get("node_id") else { return Some(Value::Object(proxy)); }; let old_node_id = old_node_id.trim(); if old_node_id.is_empty() { return Some(Value::Object(proxy)); } let new_node_id = node_id_map.get(old_node_id)?; let mut remapped = proxy; remapped.insert("node_id".to_string(), json!(new_node_id)); Some(Value::Object(remapped)) } fn normalize_import_endpoint_format(value: &str) -> Result { let normalized = match value.trim().to_ascii_lowercase().as_str() { "openai:cli" => "openai:responses", "openai:compact" => "openai:responses:compact", "openai_image" | "images" | "image" | "/v1/images/generations" | "/v1/images/edits" => { "openai:image" } "claude:chat" | "claude:cli" => "claude:messages", "gemini:chat" | "gemini:cli" => "gemini:generate_content", _ => value.trim(), }; admin_endpoint_signature_parts(normalized) .map(|(signature, _, _)| signature.to_string()) .ok_or_else(|| format!("无效的 api_format: {value}")) } fn fixed_provider_import_endpoint_supported(provider_type: &str, api_format: &str) -> bool { crate::provider_transport::provider_types::fixed_provider_template(provider_type).is_none() || crate::provider_transport::provider_types::fixed_provider_endpoint_template_by_api_format( provider_type, api_format, ) .is_some() } fn normalize_import_key_formats( item: &ImportedProviderKey, provider_endpoint_formats: &BTreeSet, ) -> (Vec, Vec) { let source = item .api_formats .clone() .filter(|items| !items.is_empty()) .or_else(|| { item.supported_endpoints .clone() .filter(|items| !items.is_empty()) }) .unwrap_or_else(|| provider_endpoint_formats.iter().cloned().collect()); let mut normalized = Vec::new(); let mut missing = Vec::new(); let mut seen = BTreeSet::new(); for raw in source { let trimmed = raw.trim(); if trimmed.is_empty() { continue; } let Ok(api_format) = normalize_import_endpoint_format(trimmed) else { missing.push(trimmed.to_string()); continue; }; if !seen.insert(api_format.clone()) { continue; } if !provider_endpoint_formats.is_empty() && !provider_endpoint_formats.contains(&api_format) { missing.push(api_format); continue; } normalized.push(api_format); } (normalized, missing) } fn imported_key_auth_type(item: &ImportedProviderKey) -> String { item.auth_type .as_deref() .unwrap_or("api_key") .trim() .to_ascii_lowercase() } fn imported_service_account_email(config: Option<&Value>) -> Option { match config { Some(Value::Object(map)) => map .get("client_email") .and_then(Value::as_str) .map(str::trim) .filter(|value| !value.is_empty()) .map(ToOwned::to_owned), Some(Value::String(raw)) => serde_json::from_str::(raw) .ok() .and_then(|value| imported_service_account_email(Some(&value))), _ => None, } } fn build_import_key_match_name(item: &ImportedProviderKey) -> Option { item.name .as_deref() .map(str::trim) .filter(|value| !value.is_empty()) .map(ToOwned::to_owned) } fn normalize_selected_import_key_format( value: &str, allowed_formats: &BTreeSet, ) -> Option { let normalized = normalize_import_endpoint_format(value).ok()?; allowed_formats.contains(&normalized).then_some(normalized) } fn normalize_import_key_format_scoped_list( value: Option<&Value>, normalized_api_formats: &[String], ) -> Option { let value = value?; let Value::Array(items) = value else { return Some(value.clone()); }; let allowed_formats = normalized_api_formats .iter() .cloned() .collect::>(); let mut seen = BTreeSet::new(); let mut normalized = Vec::new(); for item in items { let Some(raw) = item .as_str() .map(str::trim) .filter(|value| !value.is_empty()) else { continue; }; let Some(api_format) = normalize_selected_import_key_format(raw, &allowed_formats) else { continue; }; if seen.insert(api_format.clone()) { normalized.push(json!(api_format)); } } Some(Value::Array(normalized)) } fn normalize_import_key_format_scoped_object( value: Option<&Value>, normalized_api_formats: &[String], ) -> Option { let value = value?; let Value::Object(map) = value else { return Some(value.clone()); }; let allowed_formats = normalized_api_formats .iter() .cloned() .collect::>(); let mut normalized = Map::new(); for (key, value) in map { let Some(api_format) = normalize_selected_import_key_format(key, &allowed_formats) else { continue; }; normalized.insert(api_format, value.clone()); } Some(Value::Object(normalized)) } fn normalize_import_key_raw_payload( raw_key: &Map, auth_type: &str, normalized_api_formats: &[String], normalized_auth_config: Option, ) -> Map { let mut payload = raw_key.clone(); if auth_type == "oauth" { payload.remove("api_key"); } payload.insert("api_formats".to_string(), json!(normalized_api_formats)); if let Some(auth_type_by_format) = normalize_import_key_format_scoped_object( raw_key.get("auth_type_by_format"), normalized_api_formats, ) { payload.insert("auth_type_by_format".to_string(), auth_type_by_format); } if let Some(allow_auth_channel_mismatch_formats) = normalize_import_key_format_scoped_list( raw_key.get("allow_auth_channel_mismatch_formats"), normalized_api_formats, ) { payload.insert( "allow_auth_channel_mismatch_formats".to_string(), allow_auth_channel_mismatch_formats, ); } if let Some(auth_config) = normalized_auth_config { payload.insert("auth_config".to_string(), auth_config); } else if raw_key.contains_key("auth_config") { payload.insert("auth_config".to_string(), Value::Null); } payload } fn apply_imported_oauth_key_credentials( state: &AdminAppState<'_>, provider_type: &str, previous_codex_credential_generation: Option<&str>, raw_key: &Map, normalized_auth_config: Option<&Value>, record: &mut aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKey, ) -> Result { let previous_encrypted_api_key = record.encrypted_api_key.clone(); let previous_encrypted_auth_config = record.encrypted_auth_config.clone(); let mut credentials_supplied = false; let mut api_key_supplied = false; if let Some(api_key_value) = raw_key.get("api_key") { let plaintext = api_key_value .as_str() .map(str::trim) .filter(|value| !value.is_empty()); record.encrypted_api_key = match plaintext { Some(plaintext) => { credentials_supplied = true; api_key_supplied = true; Some( state .encrypt_catalog_secret_with_fallbacks(plaintext) .ok_or_else(|| "gateway 未配置 provider key 加密密钥".to_string())?, ) } None => None, }; } if raw_key.contains_key("auth_config") { record.encrypted_auth_config = match normalized_auth_config { Some(auth_config) => { credentials_supplied |= imported_oauth_auth_config_has_credentials(auth_config); let plaintext = serde_json::to_string(auth_config).map_err(|err| err.to_string())?; Some( state .encrypt_catalog_secret_with_fallbacks(&plaintext) .ok_or_else(|| "gateway 未配置 provider key 加密密钥".to_string())?, ) } None => None, }; } record.expires_at_unix_secs = imported_oauth_expiry_after_import( record.expires_at_unix_secs, raw_key.contains_key("auth_config"), normalized_auth_config, api_key_supplied, ); let credential_material_changed = record.encrypted_api_key != previous_encrypted_api_key || record.encrypted_auth_config != previous_encrypted_auth_config; if credentials_supplied { record.oauth_invalid_at_unix_secs = None; record.oauth_invalid_reason = None; } if credential_material_changed { ensure_codex_credential_generation_rotated( record, provider_type, previous_codex_credential_generation, ); } Ok(credentials_supplied) } fn imported_oauth_auth_config_has_credentials(value: &Value) -> bool { const CREDENTIAL_FIELDS: &[&str] = &[ "access_token", "accessToken", "api_key", "apiKey", "auth_token", "authToken", "cf_clearance", "cfClearance", "cf_cookies", "cfCookies", "cookie", "cookieHeader", "cookies", "id_token", "idToken", "refresh_token", "refreshToken", "session_token", "sessionToken", "sso_rw_token", "ssoRwToken", "sso_token", "ssoToken", "token", ]; match value { Value::Object(object) => object.iter().any(|(key, value)| { (CREDENTIAL_FIELDS.contains(&key.as_str()) && imported_credential_value_present(value)) || imported_oauth_auth_config_has_credentials(value) }), Value::Array(items) => items.iter().any(imported_oauth_auth_config_has_credentials), _ => false, } } fn imported_credential_value_present(value: &Value) -> bool { match value { Value::String(value) => !value.trim().is_empty(), Value::Array(items) => !items.is_empty(), Value::Object(object) => !object.is_empty(), _ => false, } } fn imported_oauth_expires_at_unix_secs(normalized_auth_config: Option<&Value>) -> Option { let object = normalized_auth_config?.as_object()?; for field in ["expires_at", "expiresAt", "expiry", "exp"] { let Some(value) = object.get(field) else { continue; }; match value { Value::Number(number) => { if let Some(expires_at) = number.as_u64() { return Some(expires_at); } } Value::String(raw) => { if let Ok(expires_at) = raw.trim().parse::() { return Some(expires_at); } } _ => {} } } None } fn imported_oauth_expiry_after_import( current: Option, auth_config_present: bool, normalized_auth_config: Option<&Value>, api_key_supplied: bool, ) -> Option { if auth_config_present { imported_oauth_expires_at_unix_secs(normalized_auth_config) } else if api_key_supplied { None } else { current } } async fn seed_imported_oauth_pool_score( state: &AdminAppState<'_>, provider_id: &str, key: &aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKey, now_unix_secs: u64, ) -> Result<(), GatewayError> { let provider_id = provider_id.to_string(); let provider = state .read_provider_catalog_providers_by_ids(std::slice::from_ref(&provider_id)) .await? .pop(); let Some(provider) = provider else { return Ok(()); }; let Some(pool_config) = admin_provider_pool_config(&provider) else { return Ok(()); }; if !key.is_active || key.provider_id != provider.id { return Ok(()); } let upsert = build_provider_key_pool_score_upsert( key, provider.provider_type.as_str(), None, now_unix_secs, pool_config.score_rules, ); state .app() .data .upsert_pool_member_score_with_mode(upsert, PoolMemberScoreUpsertMode::OAuthRecovery) .await .map_err(|error| { GatewayError::Internal(format!( "failed to recover OAuth pool score for key '{}': {error}", key.id )) })?; Ok(()) } fn build_import_provider_model_record( provider_id: &str, existing_id: Option<&str>, global_model_id: &str, item: &ImportedProviderModel, ) -> Result { let provider_model_name = trim_required(&item.provider_model_name, "provider_model_name")?; let provider_model_mappings = normalize_json_array( item.provider_model_mappings.clone(), "provider_model_mappings", )?; let price_per_request = normalize_optional_price(item.price_per_request, "price_per_request")?; let tiered_pricing = normalize_json_object(item.tiered_pricing.clone(), "tiered_pricing")?; let config = normalize_json_object(item.config.clone(), "config")?; UpsertAdminProviderModelRecord::new( existing_id .map(ToOwned::to_owned) .unwrap_or_else(|| Uuid::new_v4().to_string()), provider_id.to_string(), global_model_id.to_string(), provider_model_name, provider_model_mappings, price_per_request, tiered_pricing, item.supports_vision, item.supports_function_calling, item.supports_streaming, item.supports_extended_thinking, item.supports_image_generation, item.is_active, true, config, ) .map_err(|err| err.to_string()) } #[derive(Debug, Clone, Default, serde::Serialize)] struct AdminSystemUsersImportStats { user_groups: AdminSystemConfigImportCounter, users: AdminSystemConfigImportCounter, api_keys: AdminSystemConfigImportCounter, standalone_keys: AdminSystemConfigImportCounter, #[serde(skip_serializing_if = "Option::is_none")] usage_aggregates: Option, errors: Vec, } #[derive(Debug, Clone)] struct ImportedWalletTarget { recharge_balance: f64, gift_balance: f64, limit_mode: String, currency: String, status: String, total_recharged: f64, total_consumed: f64, total_refunded: f64, total_adjusted: f64, updated_at_unix_secs: Option, } fn imported_system_export_version(version: Option<&Value>) -> Result<(u32, u32), String> { let Some(Value::String(version)) = version else { return Err("version 必须是 x.y 字符串".to_string()); }; let version = version.trim(); if version.is_empty() { return Err("version 必须是 x.y 字符串".to_string()); } let mut parts = version.split('.'); let Some(major) = parts.next().and_then(|value| value.parse::().ok()) else { return Err("version 必须是 x.y 字符串".to_string()); }; let Some(minor) = parts.next().and_then(|value| value.parse::().ok()) else { return Err("version 必须是 x.y 字符串".to_string()); }; Ok((major, minor)) } fn validate_imported_system_users_export_version(version: Option<&Value>) -> Result<(), String> { let Some(Value::String(raw_version)) = version else { return Err("version 必须是 x.y 字符串".to_string()); }; let normalized = raw_version.trim(); if normalized.is_empty() { return Err("version 必须是 x.y 字符串".to_string()); } let _ = imported_system_export_version(version)?; if !ADMIN_SYSTEM_USERS_SUPPORTED_VERSIONS.contains(&normalized) { return Err(format!( "不支持的用户数据版本: {normalized},支持的版本: {}", ADMIN_SYSTEM_USERS_SUPPORTED_VERSIONS.join(", ") )); } Ok(()) } fn usage_aggregate_import_mode( merge_mode: AdminImportMergeMode, ) -> AdminSystemUsageAggregateImportMode { match merge_mode { AdminImportMergeMode::Skip => AdminSystemUsageAggregateImportMode::Skip, AdminImportMergeMode::Overwrite => AdminSystemUsageAggregateImportMode::Overwrite, AdminImportMergeMode::Error => AdminSystemUsageAggregateImportMode::Error, } } fn imported_object_field<'a>( value: &'a Value, field_name: &str, ) -> Result<&'a Map, String> { value .as_object() .ok_or_else(|| format!("{field_name} 必须是对象")) } fn imported_optional_string(value: Option<&Value>) -> Result, String> { match value { None | Some(Value::Null) => Ok(None), Some(Value::String(raw)) => { let trimmed = raw.trim(); if trimmed.is_empty() { Ok(None) } else { Ok(Some(trimmed.to_string())) } } _ => Err("字段必须是字符串".to_string()), } } fn imported_optional_bool(value: Option<&Value>) -> Result, String> { match value { None | Some(Value::Null) => Ok(None), Some(Value::Bool(value)) => Ok(Some(*value)), _ => Err("字段必须是布尔值".to_string()), } } fn imported_optional_i32(value: Option<&Value>, field_name: &str) -> Result, String> { match value { None | Some(Value::Null) => Ok(None), Some(Value::Number(number)) => number .as_i64() .ok_or_else(|| format!("{field_name} 必须是整数")) .and_then(|value| i32::try_from(value).map_err(|_| format!("{field_name} 超出范围"))) .map(Some), _ => Err(format!("{field_name} 必须是整数")), } } fn imported_optional_u64(value: Option<&Value>, field_name: &str) -> Result, String> { match value { None | Some(Value::Null) => Ok(None), Some(Value::Number(number)) => number .as_u64() .ok_or_else(|| format!("{field_name} 必须是非负整数")) .map(Some), _ => Err(format!("{field_name} 必须是非负整数")), } } fn imported_optional_f64(value: Option<&Value>, field_name: &str) -> Result, String> { match value { None | Some(Value::Null) => Ok(None), Some(Value::Number(number)) => number .as_f64() .filter(|value| value.is_finite()) .ok_or_else(|| format!("{field_name} 必须是有限数值")) .map(Some), Some(Value::String(value)) => value .trim() .parse::() .ok() .filter(|value| value.is_finite()) .ok_or_else(|| format!("{field_name} 必须是有限数值")) .map(Some), _ => Err(format!("{field_name} 必须是有限数值")), } } fn imported_optional_json_object( value: Option<&Value>, field_name: &str, ) -> Result, String> { match value { None | Some(Value::Null) => Ok(None), Some(Value::Object(map)) => Ok(Some(Value::Object(map.clone()))), _ => Err(format!("{field_name} 必须是对象")), } } fn imported_optional_value(value: Option<&Value>) -> Option { value.cloned().filter(|value| !value.is_null()) } fn imported_optional_list_policy_mode( value: Option<&Value>, field_name: &str, ) -> Result, String> { let Some(value) = imported_optional_string(value)? else { return Ok(None); }; let value = value.to_ascii_lowercase(); normalize_admin_list_policy_mode(&value) .map(Some) .map_err(|_| format!("{field_name} 不合法")) } fn imported_optional_rate_limit_policy_mode( value: Option<&Value>, field_name: &str, ) -> Result, String> { let Some(value) = imported_optional_string(value)? else { return Ok(None); }; let value = value.to_ascii_lowercase(); normalize_admin_rate_limit_policy_mode(&value) .map(Some) .map_err(|_| format!("{field_name} 不合法")) } fn legacy_imported_list_policy_mode(values: &Option>) -> String { if values.as_ref().is_some_and(|items| !items.is_empty()) { "specific".to_string() } else { "unrestricted".to_string() } } fn legacy_imported_rate_limit_policy_mode(value: Option) -> String { if value.is_some() { "custom".to_string() } else { "system".to_string() } } fn imported_user_list_policy_mode( object: &Map, mode_field: &str, value_field: &str, values: &Option>, ) -> Result, String> { imported_optional_list_policy_mode(object.get(mode_field), mode_field).map(|mode| { mode.or_else(|| { object .contains_key(value_field) .then(|| legacy_imported_list_policy_mode(values)) }) }) } fn imported_user_rate_limit_policy_mode( object: &Map, mode_field: &str, value_field: &str, value: Option, ) -> Result, String> { imported_optional_rate_limit_policy_mode(object.get(mode_field), mode_field).map(|mode| { mode.or_else(|| { object .contains_key(value_field) .then(|| legacy_imported_rate_limit_policy_mode(value)) }) }) } fn build_imported_user_usage_total_aggregates( users: &[Value], exported_at: Option<&Value>, ) -> Result, String> { let date_unix_secs = imported_export_day_unix_secs(exported_at); let mut rows = Vec::new(); for (index, raw_user) in users.iter().enumerate() { let user = imported_object_field(raw_user, &format!("users[{index}]"))?; let Some(user_id) = imported_optional_string(user.get("id"))? else { continue; }; let request_count = imported_optional_u64(user.get("request_count"), "request_count")?; let total_tokens = imported_optional_u64(user.get("total_tokens"), "total_tokens")?; if request_count.is_none() && total_tokens.is_none() { continue; } let total_requests = request_count.unwrap_or(0); let input_tokens = total_tokens.unwrap_or(0); if total_requests == 0 && input_tokens == 0 { continue; } rows.push(AdminSystemStatsUserDailyAggregate { user_id, username: imported_optional_string(user.get("username"))?, date_unix_secs, total_requests, success_requests: total_requests, error_requests: 0, input_tokens, output_tokens: 0, cache_creation_tokens: 0, cache_read_tokens: 0, total_cost: 0.0, }); } Ok(rows) } fn imported_export_day_unix_secs(exported_at: Option<&Value>) -> u64 { imported_optional_string(exported_at) .ok() .flatten() .and_then(|value| chrono::DateTime::parse_from_rfc3339(&value).ok()) .map(|value| unix_day_start_secs(value.timestamp())) .unwrap_or_else(|| unix_day_start_secs(chrono::Utc::now().timestamp())) } fn unix_day_start_secs(timestamp: i64) -> u64 { let timestamp = timestamp.max(0) as u64; timestamp - (timestamp % 86_400) } fn imported_rfc3339_to_unix_secs( value: Option<&Value>, field_name: &str, ) -> Result, String> { let Some(value) = imported_optional_string(value)? else { return Ok(None); }; let parsed_timestamp = chrono::DateTime::parse_from_rfc3339(&value) .map(|parsed| parsed.timestamp()) .or_else(|_| { chrono::NaiveDateTime::parse_from_str(&value, "%Y-%m-%dT%H:%M:%S%.f") .map(|parsed| parsed.and_utc().timestamp()) }) .map_err(|_| format!("{field_name} 必须是 RFC3339 时间"))?; Ok(Some(parsed_timestamp.max(0) as u64)) } fn imported_string_list_from_value( value: Option<&Value>, field_name: &str, ) -> Result>, String> { let Some(value) = value else { return Ok(None); }; match value { Value::Null => Ok(None), Value::Array(items) => Ok(Some( items .iter() .filter_map(Value::as_str) .map(str::trim) .filter(|value| !value.is_empty()) .map(ToOwned::to_owned) .collect(), )), _ => Err(format!("{field_name} 必须是字符串列表")), } } fn normalize_imported_user_string_list( object: &Map, field_name: &str, ) -> Result>, String> { normalize_admin_user_string_list( imported_string_list_from_value(object.get(field_name), field_name)?, field_name, ) } fn normalize_imported_user_api_formats( object: &Map, field_name: &str, ) -> Result>, String> { normalize_admin_user_api_formats(imported_string_list_from_value( object.get(field_name), field_name, )?) } fn imported_ip_rules_field<'a>( object: &'a Map, ) -> (&'static str, Option<&'a Value>) { if let Some(value) = object.get("ip_rules") { ("ip_rules", Some(value)) } else { ("allowed_ips", object.get("allowed_ips")) } } fn imported_ip_rules_present(object: &Map) -> bool { object.contains_key("ip_rules") || object.contains_key("allowed_ips") } fn normalize_imported_user_ip_rules( object: &Map, ) -> Result>, String> { let (field_name, value) = imported_ip_rules_field(object); normalize_admin_user_ip_rules(imported_string_list_from_value(value, field_name)?) } fn build_imported_user_group_record( group: &Map, field_name: &str, ) -> Result< ( Option, String, aether_data::repository::users::UpsertUserGroupRecord, ), String, > { let export_id = imported_optional_string(group.get("id"))?; let name = imported_optional_string(group.get("name"))? .ok_or_else(|| format!("{field_name}.name 不能为空"))?; let name = aether_data::repository::users::normalize_user_group_name(&name); if name.is_empty() { return Err(format!("{field_name}.name 不能为空")); } let description = imported_optional_string(group.get("description"))?; let allowed_providers = normalize_imported_user_string_list(group, "allowed_providers")?; let allowed_api_formats = normalize_imported_user_api_formats(group, "allowed_api_formats")?; let allowed_models = normalize_imported_user_string_list(group, "allowed_models")?; let rate_limit = imported_optional_i32(group.get("rate_limit"), "rate_limit")?; let allowed_providers_mode = imported_optional_list_policy_mode( group.get("allowed_providers_mode"), "allowed_providers_mode", )? .unwrap_or_else(|| { if group.contains_key("allowed_providers") { legacy_imported_list_policy_mode(&allowed_providers) } else { "inherit".to_string() } }); let allowed_api_formats_mode = imported_optional_list_policy_mode( group.get("allowed_api_formats_mode"), "allowed_api_formats_mode", )? .unwrap_or_else(|| { if group.contains_key("allowed_api_formats") { legacy_imported_list_policy_mode(&allowed_api_formats) } else { "inherit".to_string() } }); let allowed_models_mode = imported_optional_list_policy_mode( group.get("allowed_models_mode"), "allowed_models_mode", )? .unwrap_or_else(|| { if group.contains_key("allowed_models") { legacy_imported_list_policy_mode(&allowed_models) } else { "inherit".to_string() } }); let rate_limit_mode = imported_optional_rate_limit_policy_mode(group.get("rate_limit_mode"), "rate_limit_mode")? .unwrap_or_else(|| { if group.contains_key("rate_limit") { legacy_imported_rate_limit_policy_mode(rate_limit) } else { "inherit".to_string() } }); let normalized_name = name.to_ascii_lowercase(); Ok(( export_id, normalized_name, aether_data::repository::users::UpsertUserGroupRecord { name, description, priority: 0, allowed_providers, allowed_providers_mode, allowed_api_formats, allowed_api_formats_mode, allowed_models, allowed_models_mode, rate_limit, rate_limit_mode, }, )) } fn resolve_imported_user_group_ids( user: &Map, imported_group_id_map: &BTreeMap, imported_group_name_map: &BTreeMap, groups_by_name: &BTreeMap, ) -> Result, String> { let raw_group_ids = imported_string_list_from_value(user.get("group_ids"), "group_ids")?.unwrap_or_default(); let raw_group_names = imported_string_list_from_value(user.get("group_names"), "group_names")? .unwrap_or_default(); let mut group_ids = BTreeSet::new(); for raw_group_id in raw_group_ids { if let Some(group_id) = imported_group_id_map.get(&raw_group_id) { group_ids.insert(group_id.clone()); continue; } group_ids.insert(raw_group_id); } for raw_group_name in raw_group_names { let normalized_name = aether_data::repository::users::normalize_user_group_name(&raw_group_name) .to_ascii_lowercase(); if normalized_name.is_empty() { continue; } if let Some(group_id) = imported_group_name_map.get(&normalized_name) { group_ids.insert(group_id.clone()); continue; } if let Some(group) = groups_by_name.get(&normalized_name) { group_ids.insert(group.id.clone()); } } Ok(group_ids.into_iter().collect()) } fn normalize_imported_wallet_target( wallet: Option<&Map>, unlimited: bool, ) -> Result { let gift_balance = imported_optional_f64( wallet.and_then(|map| map.get("gift_balance")), "wallet.gift_balance", )? .unwrap_or(0.0) .max(0.0); let recharge_balance = if let Some(map) = wallet { if map.contains_key("recharge_balance") { imported_optional_f64(map.get("recharge_balance"), "wallet.recharge_balance")? .unwrap_or(0.0) } else if map.contains_key("refundable_balance") { imported_optional_f64(map.get("refundable_balance"), "wallet.refundable_balance")? .unwrap_or(0.0) } else { let total_balance = imported_optional_f64(map.get("balance"), "wallet.balance")?.unwrap_or(0.0); total_balance - gift_balance } } else { 0.0 }; let limit_mode = if let Some(map) = wallet { if let Some(mode) = imported_optional_string(map.get("limit_mode"))? { match mode.to_ascii_lowercase().as_str() { "finite" => "finite".to_string(), "unlimited" => "unlimited".to_string(), _ => return Err("wallet.limit_mode 仅支持 finite / unlimited".to_string()), } } else if imported_optional_bool(map.get("unlimited"))?.unwrap_or(unlimited) { "unlimited".to_string() } else { "finite".to_string() } } else if unlimited { "unlimited".to_string() } else { "finite".to_string() }; let currency = imported_optional_string(wallet.and_then(|map| map.get("currency")))? .unwrap_or_else(|| "USD".to_string()); let status = imported_optional_string(wallet.and_then(|map| map.get("status")))? .unwrap_or_else(|| "active".to_string()); let total_recharged = imported_optional_f64( wallet.and_then(|map| map.get("total_recharged")), "wallet.total_recharged", )? .unwrap_or(recharge_balance); let total_consumed = imported_optional_f64( wallet.and_then(|map| map.get("total_consumed")), "wallet.total_consumed", )? .unwrap_or(0.0); let total_refunded = imported_optional_f64( wallet.and_then(|map| map.get("total_refunded")), "wallet.total_refunded", )? .unwrap_or(0.0); let total_adjusted = imported_optional_f64( wallet.and_then(|map| map.get("total_adjusted")), "wallet.total_adjusted", )? .unwrap_or(gift_balance); let updated_at_unix_secs = imported_rfc3339_to_unix_secs( wallet.and_then(|map| map.get("updated_at")), "wallet.updated_at", )?; Ok(ImportedWalletTarget { recharge_balance, gift_balance, limit_mode, currency, status, total_recharged, total_consumed, total_refunded, total_adjusted, updated_at_unix_secs, }) } impl<'a> AdminAppState<'a> { pub(crate) async fn import_admin_system_data( &self, request_body: &Bytes, operator_id: Option<&str>, ) -> Result, GatewayError> { if !self.has_global_model_data_reader() || !self.has_global_model_data_writer() || !self.has_provider_catalog_data_reader() || !self.has_provider_catalog_data_writer() || !self.has_auth_user_write_capability() || !self.has_auth_wallet_write_capability() || !self.has_auth_api_key_writer() { return Ok(Err(( http::StatusCode::SERVICE_UNAVAILABLE, json!({ "detail": "Admin system data unavailable" }), ))); } let root = match serde_json::from_slice::(request_body) { Ok(Value::Object(map)) => map, _ => return Ok(Err(invalid_request("请求数据验证失败"))), }; let version = root .get("version") .and_then(Value::as_str) .map(str::trim) .filter(|value| !value.is_empty()) .ok_or_else(|| invalid_request("version 为必填字段")); let version = match version { Ok(value) => value, Err(err) => return Ok(Err(err)), }; if version != ADMIN_SYSTEM_DATA_EXPORT_VERSION { return Ok(Err(invalid_request(format!( "不支持的聚合数据版本: {version},支持的版本: {ADMIN_SYSTEM_DATA_EXPORT_VERSION}" )))); } let merge_mode = match serde_json::from_value::( root.get("merge_mode").cloned().unwrap_or(Value::Null), ) { Ok(value) => value, Err(_) => { return Ok(Err(invalid_request( "merge_mode 仅支持 skip / overwrite / error", ))) } }; let config_body = match build_admin_system_data_import_part_body(&root, "config_data", merge_mode) { Ok(value) => value, Err(err) => return Ok(Err(err)), }; let users_body = match build_admin_system_data_import_part_body(&root, "user_data", merge_mode) { Ok(value) => value, Err(err) => return Ok(Err(err)), }; let config_result = match self.import_admin_system_config(&config_body).await? { Ok(payload) => payload, Err(err) => return Ok(Err(err)), }; let users_result = match self .import_admin_system_users(&users_body, operator_id) .await? { Ok(payload) => payload, Err(err) => return Ok(Err(err)), }; Ok(Ok(json!({ "message": "聚合数据导入成功", "config": config_result, "users": users_result, }))) } pub(crate) async fn import_admin_system_config( &self, request_body: &Bytes, ) -> Result, GatewayError> { macro_rules! invalid { ($expr:expr) => { match $expr { Ok(value) => value, Err(detail) => return Ok(Err(invalid_request(detail))), } }; } macro_rules! routed { ($expr:expr) => { match $expr { Ok(value) => value, Err(err) => return Ok(Err(err)), } }; } if !self.has_global_model_data_reader() || !self.has_global_model_data_writer() || !self.has_provider_catalog_data_reader() || !self.has_provider_catalog_data_writer() { return Ok(Err(( http::StatusCode::SERVICE_UNAVAILABLE, json!({ "detail": "Admin system data unavailable" }), ))); } let parsed = routed!(parse_admin_system_config_import_request(request_body)); let root = parsed.root; let merge_mode = parsed.request.merge_mode; let imported_global_models = routed!( parse_admin_system_config_array::(&root, "global_models") ); let imported_providers = routed!(parse_admin_system_config_array::( &root, "providers" )); let imported_proxy_nodes = routed!(parse_admin_system_config_array::( &root, "proxy_nodes" )); let imported_ldap = routed!(parse_admin_system_config_optional_object::< ImportedLdapConfig, >(&root, "ldap_config")); let imported_oauth_providers = routed!(parse_admin_system_config_array::< ImportedOAuthProvider, >(&root, "oauth_providers",)); let imported_system_configs = routed!(parse_admin_system_config_array::< ImportedSystemConfig, >(&root, "system_configs",)); let mut stats = AdminSystemConfigImportStats::default(); // Proxy nodes are deployment-local resources and are intentionally not imported by the // Rust admin backend. Apply the external catalog selector before importing any other // object, and turn a non-empty exported node reference into direct mode. This keeps a // clean-environment restore portable and prevents a late selector validation failure from // leaving the rest of the document partially imported. let (imported_external_models_configs, imported_system_configs): (Vec<_>, Vec<_>) = imported_system_configs.into_iter().partition(|item| { normalize_imported_system_config_key(&item.value.key) == ADMIN_EXTERNAL_MODELS_PROXY_NODE_CONFIG_KEY }); let mut existing_system_config_keys = self .list_system_config_entries() .await? .into_iter() .map(|entry| normalize_imported_system_config_key(&entry.key)) .collect::>(); for imported_config_item in imported_external_models_configs { let (_, system_config) = imported_config_item.into_parts(); let exists = existing_system_config_keys.contains(ADMIN_EXTERNAL_MODELS_PROXY_NODE_CONFIG_KEY); match (exists, merge_mode) { (true, AdminImportMergeMode::Skip) => { stats.system_configs.skipped += 1; continue; } (true, AdminImportMergeMode::Error) => { return Ok(Err(invalid_request(format!( "SystemConfig '{ADMIN_EXTERNAL_MODELS_PROXY_NODE_CONFIG_KEY}' 已存在" )))); } _ => {} } let imported_proxy_node_id = match system_config.value { Value::Null => None, Value::String(value) => { let value = value.trim(); if value.is_empty() { return Ok(Err(invalid_request( "external_models_proxy_node_id 不能为空", ))); } Some(value.to_string()) } _ => { return Ok(Err(invalid_request( "external_models_proxy_node_id 必须是字符串或 null", ))) } }; let request_bytes = Bytes::from( serde_json::to_vec(&json!({ "proxy_node_id": null })) .map_err(|err| GatewayError::Internal(err.to_string()))?, ); match self .apply_admin_external_models_config_update(&request_bytes) .await? { Ok(_) => { if exists { stats.system_configs.updated += 1; } else { stats.system_configs.created += 1; existing_system_config_keys .insert(ADMIN_EXTERNAL_MODELS_PROXY_NODE_CONFIG_KEY.to_string()); } if let Some(node_id) = imported_proxy_node_id { stats.errors.push(format!( "外部模型目录代理节点 '{node_id}' 是当前部署的本地引用;代理节点未导入,已切换为直连" )); } } Err((status, payload)) => return Ok(Err((status, payload))), } } let mut global_models_by_name = self .list_admin_global_models(&AdminGlobalModelListQuery { offset: 0, limit: 10_000, is_active: None, search: None, }) .await? .items .into_iter() .map(|model| (model.name.clone(), model)) .collect::>(); if !imported_proxy_nodes.is_empty() { let empty_proxy_node_ids = imported_proxy_nodes .iter() .filter(|node| { node.value .id .as_deref() .map(str::trim) .is_none_or(|value| value.is_empty()) }) .count(); stats.proxy_nodes.skipped = imported_proxy_nodes.len() as u64; if empty_proxy_node_ids > 0 { stats.errors.push(format!( "检测到 {empty_proxy_node_ids} 个无效 proxy_nodes 项;当前 Rust 管理后端暂不支持导入代理节点" )); } else { stats.errors.push( "当前 Rust 管理后端暂不支持导入代理节点;仅引用这些节点(node_id)的自动连接代理配置会被清除,手动 URL 代理配置会保留" .to_string(), ); } } let node_id_map = BTreeMap::::new(); for imported_model in imported_global_models { let (_, model) = imported_model.into_parts(); let name = invalid!(trim_required(&model.name, "name")); let display_name = invalid!(trim_required(&model.display_name, "display_name")); let default_price_per_request = invalid!(normalize_optional_price( model.default_price_per_request, "default_price_per_request", )); let default_tiered_pricing = invalid!(normalize_json_object( model.default_tiered_pricing, "default_tiered_pricing", )); let supported_capabilities = normalize_supported_capabilities(model.supported_capabilities); let config = invalid!(normalize_json_object(model.config, "config")); if let Some(existing) = global_models_by_name.get(&name).cloned() { match merge_mode { AdminImportMergeMode::Skip => { stats.global_models.skipped += 1; } AdminImportMergeMode::Error => { return Ok(Err(invalid_request(format!("GlobalModel '{name}' 已存在")))); } AdminImportMergeMode::Overwrite => { let mut record = invalid!(UpdateAdminGlobalModelRecord::new( existing.id.clone(), display_name, model.is_active, default_price_per_request, default_tiered_pricing, supported_capabilities, config, ) .map_err(|err| err.to_string())); record.usage_count = model.usage_count; let Some(updated) = self.update_admin_global_model(&record).await? else { return Ok(Err(invalid_request(format!( "更新 GlobalModel '{name}' 失败" )))); }; global_models_by_name.insert(name, updated); stats.global_models.updated += 1; } } continue; } let mut record = invalid!(CreateAdminGlobalModelRecord::new( Uuid::new_v4().to_string(), name.clone(), display_name, model.is_active, default_price_per_request, default_tiered_pricing, supported_capabilities, config, ) .map_err(|err| err.to_string())); record.usage_count = model.usage_count; let Some(created) = self.create_admin_global_model(&record).await? else { return Ok(Err(invalid_request(format!( "创建 GlobalModel '{name}' 失败" )))); }; global_models_by_name.insert(name, created); stats.global_models.created += 1; } let mut providers_by_name = self .list_provider_catalog_providers(false) .await? .into_iter() .map(|provider| (provider.name.clone(), provider)) .collect::>(); for imported_provider_item in imported_providers { let (raw_provider, imported_provider) = imported_provider_item.into_parts(); let provider_name = invalid!(trim_required(&imported_provider.name, "name")); invalid!( crate::provider_transport::validate_anthropic_compatibility_profile_config( imported_provider.config.as_ref(), ) .map_err(|_| "无效的 Anthropic compatibility profile".to_string()) ); let existing_provider = providers_by_name.get(&provider_name).cloned(); let provider = if let Some(existing) = existing_provider { match merge_mode { AdminImportMergeMode::Skip => { stats.providers.skipped += 1; existing } AdminImportMergeMode::Error => { return Ok(Err(invalid_request(format!( "Provider '{provider_name}' 已存在" )))); } AdminImportMergeMode::Overwrite => { let patch = match AdminProviderUpdatePatch::from_object(raw_provider.clone()) { Ok(patch) => patch, Err(_) => { return Ok(Err(invalid_request(format!( "Provider '{provider_name}' 配置格式无效" )))); } }; let mut updated = invalid!( self.build_admin_update_provider_record(&existing, patch) .await ); updated.proxy = remap_import_proxy(imported_provider.proxy.clone(), &node_id_map); updated.config = invalid!(encrypt_imported_provider_config( self, imported_provider.config.clone(), )); let Some(persisted) = self.update_provider_catalog_provider(&updated).await? else { return Ok(Err(invalid_request(format!( "更新 Provider '{provider_name}' 失败" )))); }; providers_by_name.insert(provider_name.clone(), persisted.clone()); stats.providers.updated += 1; persisted } } } else { let payload = match serde_json::from_value::( Value::Object(raw_provider.clone()), ) { Ok(payload) => payload, Err(_) => { return Ok(Err(invalid_request(format!( "Provider '{provider_name}' 配置格式无效" )))); } }; let (mut record, shift_existing_priorities_from) = invalid!(self.build_admin_create_provider_record(payload).await); if let Some(enable_format_conversion) = imported_provider.enable_format_conversion { record.enable_format_conversion = enable_format_conversion; } record.proxy = remap_import_proxy(imported_provider.proxy.clone(), &node_id_map); record.config = invalid!(encrypt_imported_provider_config( self, imported_provider.config.clone(), )); let Some(created) = self .create_provider_catalog_provider(&record, shift_existing_priorities_from) .await? else { return Ok(Err(invalid_request(format!( "创建 Provider '{provider_name}' 失败" )))); }; providers_by_name.insert(provider_name.clone(), created.clone()); stats.providers.created += 1; created }; let imported_endpoints = routed!(parse_admin_system_config_nested_array::< ImportedEndpoint, >(&raw_provider, "endpoints")); let mut existing_endpoints_by_format = self .list_provider_catalog_endpoints_by_provider_ids(std::slice::from_ref(&provider.id)) .await? .into_iter() .map(|endpoint| (endpoint.api_format.clone(), endpoint)) .collect::>(); for imported_endpoint_item in imported_endpoints { let (raw_endpoint, imported_endpoint) = imported_endpoint_item.into_parts(); let normalized_api_format = invalid!(normalize_import_endpoint_format( &imported_endpoint.api_format )); invalid!( crate::provider_transport::validate_anthropic_compatibility_profile_config( imported_endpoint.config.as_ref(), ) .map_err(|_| "无效的 Anthropic compatibility profile".to_string()) ); if !fixed_provider_import_endpoint_supported( &provider.provider_type, &normalized_api_format, ) { let retired = existing_endpoints_by_format.remove(&normalized_api_format); if let Some(mut retired) = retired { if retired.is_active { retired.is_active = false; retired.updated_at_unix_secs = SystemTime::now() .duration_since(UNIX_EPOCH) .ok() .map(|duration| duration.as_secs()); let Some(_) = self.update_provider_catalog_endpoint(&retired).await? else { return Ok(Err(invalid_request(format!( "停用 Provider '{provider_name}' 的已移除 Endpoint '{normalized_api_format}' 失败" )))); }; stats.endpoints.updated += 1; } else { stats.endpoints.skipped += 1; } } else { stats.endpoints.skipped += 1; } stats.errors.push(format!( "固定 Provider '{provider_name}' 不再支持 Endpoint '{normalized_api_format}',已跳过或停用" )); continue; } let existing_endpoint = existing_endpoints_by_format .get(&normalized_api_format) .cloned(); if let Some(existing_endpoint) = existing_endpoint { match merge_mode { AdminImportMergeMode::Skip => { stats.endpoints.skipped += 1; } AdminImportMergeMode::Error => { return Ok(Err(invalid_request(format!( "Endpoint '{normalized_api_format}' 已存在于 Provider '{provider_name}'" )))); } AdminImportMergeMode::Overwrite => { let Some((normalized_signature, api_family, endpoint_kind)) = admin_endpoint_signature_parts(&normalized_api_format) else { return Ok(Err(invalid_request(format!( "无效的 api_format: {}", imported_endpoint.api_format )))); }; let patch = match AdminProviderEndpointUpdatePatch::from_object( raw_endpoint.clone(), ) { Ok(patch) => patch, Err(_) => { return Ok(Err(invalid_request( "Provider Endpoint 配置格式无效", ))); } }; let (fields, payload) = patch.into_parts(); let normalized_base_url = match payload.base_url.as_deref() { Some(base_url) => { Some(invalid!(normalize_admin_base_url(base_url))) } None => None, }; let update_fields = admin_provider_endpoints_pure::AdminProviderEndpointUpdateFields { base_url: normalized_base_url, custom_path: payload.custom_path, header_rules: payload.header_rules, body_rules: payload.body_rules, max_retries: payload.max_retries, is_active: payload.is_active, config: payload.config, proxy: payload.proxy, format_acceptance_config: payload.format_acceptance_config, }; let mut updated = invalid!( admin_provider_endpoints_pure::apply_admin_provider_endpoint_update_fields( &existing_endpoint, |field| fields.contains(field), |field| fields.is_null(field), &update_fields, ) ); if fields.contains("proxy") { updated.proxy = remap_import_proxy( imported_endpoint.proxy.clone(), &node_id_map, ); } updated.api_format = normalized_signature.to_string(); updated.api_family = Some(api_family.to_string()); updated.endpoint_kind = Some(endpoint_kind.to_string()); updated.updated_at_unix_secs = SystemTime::now() .duration_since(UNIX_EPOCH) .ok() .map(|duration| duration.as_secs()); let Some(persisted) = self.update_provider_catalog_endpoint(&updated).await? else { return Ok(Err(invalid_request(format!( "更新 Endpoint '{normalized_api_format}' 失败" )))); }; existing_endpoints_by_format .insert(normalized_api_format.clone(), persisted); stats.endpoints.updated += 1; } } continue; } let Some((normalized_signature, api_family, endpoint_kind)) = admin_endpoint_signature_parts(&normalized_api_format) else { return Ok(Err(invalid_request(format!( "无效的 api_format: {}", imported_endpoint.api_format )))); }; let now_unix_secs = SystemTime::now() .duration_since(UNIX_EPOCH) .ok() .map(|duration| duration.as_secs()) .unwrap_or(0); let mut record = invalid!( admin_provider_endpoints_pure::build_admin_provider_endpoint_record( Uuid::new_v4().to_string(), provider.id.clone(), normalized_signature.to_string(), api_family.to_string(), endpoint_kind.to_string(), invalid!(normalize_admin_base_url(&imported_endpoint.base_url)), imported_endpoint.custom_path.clone(), imported_endpoint.header_rules.clone(), imported_endpoint.body_rules.clone(), imported_endpoint.max_retries.unwrap_or(2), imported_endpoint.config.clone(), remap_import_proxy(imported_endpoint.proxy.clone(), &node_id_map), imported_endpoint.format_acceptance_config.clone(), now_unix_secs, ) ); record = record.with_health_score(1.0); record.is_active = imported_endpoint.is_active; let Some(created) = self.create_provider_catalog_endpoint(&record).await? else { return Ok(Err(invalid_request(format!( "创建 Endpoint '{normalized_api_format}' 失败" )))); }; existing_endpoints_by_format.insert(normalized_api_format, created); stats.endpoints.created += 1; } let provider_endpoint_formats = existing_endpoints_by_format .keys() .cloned() .collect::>(); let now_unix_secs = SystemTime::now() .duration_since(UNIX_EPOCH) .ok() .map(|duration| duration.as_secs()) .unwrap_or(0); let imported_keys = routed!(parse_admin_system_config_nested_array::< ImportedProviderKey, >(&raw_provider, "api_keys")); let mut existing_keys = self .list_provider_catalog_keys_by_provider_ids(std::slice::from_ref(&provider.id)) .await?; for imported_key_item in imported_keys { let (raw_key, imported_key) = imported_key_item.into_parts(); let (normalized_api_formats, missing_formats) = normalize_import_key_formats(&imported_key, &provider_endpoint_formats); if !missing_formats.is_empty() { stats.errors.push(format!( "Key (Provider: {provider_name}) 的 api_formats 未配置对应 Endpoint,已跳过: {:?}", missing_formats )); } if normalized_api_formats.is_empty() { stats.keys.skipped += 1; continue; } let normalized_auth_config = invalid!(normalize_import_auth_config( imported_key.auth_config.clone() )); let auth_type = imported_key_auth_type(&imported_key); let normalized_raw_key = normalize_import_key_raw_payload( &raw_key, &auth_type, &normalized_api_formats, normalized_auth_config.clone(), ); let existing_key_index = if auth_type == "api_key" { let target_key = imported_key .api_key .as_deref() .map(str::trim) .filter(|value| !value.is_empty()) .map(ToOwned::to_owned); existing_keys.iter().position(|existing_key| { let decrypted_existing = existing_key .encrypted_api_key .as_deref() .and_then(|ciphertext| { self.decrypt_catalog_secret_with_fallbacks(ciphertext) }); target_key .as_deref() .zip(decrypted_existing.as_deref()) .is_some_and(|(target, decrypted)| decrypted == target) }) } else if matches!(auth_type.as_str(), "service_account" | "vertex_ai") { let target_email = imported_service_account_email(normalized_auth_config.as_ref()); existing_keys.iter().position(|existing_key| { target_email.as_deref().is_some_and(|target_email| { self.parse_catalog_auth_config_json(existing_key) .and_then(|config| { config .get("client_email") .and_then(Value::as_str) .map(str::trim) .filter(|value| !value.is_empty()) .map(ToOwned::to_owned) }) .as_deref() == Some(target_email) }) }) } else { build_import_key_match_name(&imported_key).and_then(|target_name| { existing_keys.iter().position(|existing_key| { existing_key .auth_type .trim() .eq_ignore_ascii_case(&auth_type) && existing_key.name == target_name }) }) }; if let Some(existing_index) = existing_key_index { let existing_key = existing_keys[existing_index].clone(); let previous_codex_credential_generation = existing_key .upstream_metadata .as_ref() .and_then(Value::as_object) .and_then(|metadata| metadata.get("codex")) .and_then(|codex| { aether_admin::provider::quota::codex_credential_generation(Some(codex)) }) .map(ToOwned::to_owned); match merge_mode { AdminImportMergeMode::Skip => { stats.keys.skipped += 1; } AdminImportMergeMode::Error => { return Ok(Err(invalid_request(format!( "Provider '{provider_name}' 中存在重复 Key" )))); } AdminImportMergeMode::Overwrite => { let patch = match AdminProviderKeyUpdatePatch::from_object( normalized_raw_key.clone(), ) { Ok(patch) => patch, Err(_) => { return Ok(Err(invalid_request("Provider Key 配置格式无效"))); } }; let mut updated = invalid!( self.build_admin_update_provider_key_record( &provider, &existing_key, patch, ) .await ); let oauth_credentials_supplied = if auth_type == "oauth" { invalid!(apply_imported_oauth_key_credentials( self, &provider.provider_type, previous_codex_credential_generation.as_deref(), &raw_key, normalized_auth_config.as_ref(), &mut updated, )) } else { false }; updated.proxy = remap_import_proxy(imported_key.proxy.clone(), &node_id_map); updated.fingerprint = invalid!(normalize_json_object( imported_key.fingerprint.clone(), "fingerprint", )); let admin_update = build_provider_catalog_key_admin_cas_update( &existing_key, updated.clone(), &provider.provider_type, ); if !self .compare_and_update_provider_catalog_key_admin_state(&admin_update) .await? { return Ok(Err(( http::StatusCode::CONFLICT, json!({ "detail": format!( "Provider '{provider_name}' 的 Key 已被其他请求更新,请重试" ) }), ))); } let Some(mut persisted) = self .read_provider_catalog_keys_by_ids(std::slice::from_ref( &updated.id, )) .await? .into_iter() .next() else { return Ok(Err(invalid_request(format!( "更新 Provider '{provider_name}' 的 Key 失败" )))); }; if updated.learned_rpm_limit != existing_key.learned_rpm_limit { let Some(reloaded) = self .set_provider_catalog_key_learned_rpm_limit( &updated.id, updated.learned_rpm_limit, updated.updated_at_unix_secs, ) .await? else { return Ok(Err(invalid_request(format!( "更新 Provider '{provider_name}' 的 Key 失败" )))); }; persisted = reloaded; } if oauth_credentials_supplied { let Some(reloaded) = self .reset_provider_catalog_key_recovery_state_fenced( &updated.id, updated.encrypted_auth_config.as_deref().ok_or_else(|| { GatewayError::Internal(format!( "OAuth Provider '{provider_name}' imported without auth_config" )) })?, ) .await? else { return Ok(Err(invalid_request(format!( "更新 Provider '{provider_name}' 的 Key 失败" )))); }; persisted = reloaded; let _ = self .app() .invalidate_local_oauth_refresh_entry(&updated.id) .await; seed_imported_oauth_pool_score( self, &provider.id, &persisted, now_unix_secs, ) .await?; } existing_keys[existing_index] = persisted; stats.keys.updated += 1; } } continue; } let payload = match serde_json::from_value::( Value::Object(normalized_raw_key.clone()), ) { Ok(payload) => payload, Err(_) => return Ok(Err(invalid_request("Provider Key 配置格式无效"))), }; let mut record = invalid!( self.build_admin_create_provider_key_record(&provider, payload) .await ); let oauth_credentials_supplied = if auth_type == "oauth" { invalid!(apply_imported_oauth_key_credentials( self, &provider.provider_type, None, &raw_key, normalized_auth_config.as_ref(), &mut record, )) } else { false }; record.is_active = imported_key.is_active; record.global_priority_by_format = invalid!(normalize_json_object( imported_key.global_priority_by_format.clone(), "global_priority_by_format", )); record.proxy = remap_import_proxy(imported_key.proxy.clone(), &node_id_map); record.fingerprint = invalid!(normalize_json_object( imported_key.fingerprint.clone(), "fingerprint", )); let Some(created) = self.create_provider_catalog_key(&record).await? else { return Ok(Err(invalid_request(format!( "创建 Provider '{provider_name}' 的 Key 失败" )))); }; if oauth_credentials_supplied { seed_imported_oauth_pool_score(self, &provider.id, &created, now_unix_secs) .await?; } existing_keys.push(created); stats.keys.created += 1; } let imported_models = routed!(parse_admin_system_config_nested_array::< ImportedProviderModel, >(&raw_provider, "models")); let mut existing_models_by_name = self .list_admin_provider_models(&AdminProviderModelListQuery { provider_id: provider.id.clone(), is_active: None, offset: 0, limit: 10_000, }) .await? .into_iter() .map(|model| (model.provider_model_name.clone(), model)) .collect::>(); for imported_model_item in imported_models { let (_, imported_model) = imported_model_item.into_parts(); let Some(global_model_name) = imported_model .global_model_name .as_deref() .map(str::trim) .filter(|value| !value.is_empty()) else { stats.errors.push(format!( "跳过无 global_model_name 的模型 (Provider: {provider_name})" )); continue; }; let Some(global_model_id) = global_models_by_name .get(global_model_name) .map(|model| model.id.clone()) else { stats.errors.push(format!( "GlobalModel '{global_model_name}' 不存在,跳过模型" )); continue; }; let provider_model_name = invalid!(trim_required( &imported_model.provider_model_name, "provider_model_name" )); let existing_model = existing_models_by_name.get(&provider_model_name).cloned(); if let Some(existing_model) = existing_model { match merge_mode { AdminImportMergeMode::Skip => { stats.models.skipped += 1; } AdminImportMergeMode::Error => { return Ok(Err(invalid_request(format!( "Model '{provider_model_name}' 已存在于 Provider '{provider_name}'" )))); } AdminImportMergeMode::Overwrite => { let record = invalid!(build_import_provider_model_record( &provider.id, Some(&existing_model.id), &global_model_id, &imported_model, )); let Some(updated) = self.update_admin_provider_model(&record).await? else { return Ok(Err(invalid_request(format!( "更新 Provider '{provider_name}' 的模型 '{provider_model_name}' 失败" )))); }; existing_models_by_name.insert(provider_model_name, updated); stats.models.updated += 1; } } continue; } let record = invalid!(build_import_provider_model_record( &provider.id, None, &global_model_id, &imported_model, )); let Some(created) = self.create_admin_provider_model(&record).await? else { return Ok(Err(invalid_request(format!( "创建 Provider '{provider_name}' 的模型 '{provider_model_name}' 失败" )))); }; existing_models_by_name.insert(provider_model_name, created); stats.models.created += 1; } } if let Some(imported_ldap_item) = imported_ldap { let (_, ldap_config) = imported_ldap_item.into_parts(); if !self.has_auth_module_writer() { stats.ldap.skipped += 1; stats .errors .push("当前运行环境不支持写入 LDAP 配置,已跳过 ldap_config".to_string()); } else { let existing = self.get_ldap_module_config().await?; let server_url = invalid!(trim_required(&ldap_config.server_url, "LDAP 服务器地址")); let bind_dn = invalid!(trim_required(&ldap_config.bind_dn, "绑定 DN")); let base_dn = invalid!(trim_required(&ldap_config.base_dn, "Base DN")); let user_search_filter = invalid!(trim_required( ldap_config .user_search_filter .as_deref() .unwrap_or("(uid={username})"), "搜索过滤器", )); let username_attr = invalid!(trim_required( ldap_config.username_attr.as_deref().unwrap_or("uid"), "用户名属性", )); let email_attr = invalid!(trim_required( ldap_config.email_attr.as_deref().unwrap_or("mail"), "邮箱属性", )); let display_name_attr = invalid!(trim_required( ldap_config.display_name_attr.as_deref().unwrap_or("cn"), "显示名称属性", )); let connect_timeout = ldap_config.connect_timeout.unwrap_or(10); if !(1..=60).contains(&connect_timeout) { return Ok(Err(invalid_request( "LDAP connect_timeout 必须在 1 到 60 秒之间", ))); } let bind_password = ldap_config .bind_password .as_deref() .map(str::trim) .map(ToOwned::to_owned); let will_have_password = bind_password .as_deref() .map(|value| !value.is_empty()) .unwrap_or_else(|| { existing .as_ref() .and_then(|config| config.bind_password_encrypted.as_deref()) .map(str::trim) .is_some_and(|value| !value.is_empty()) }); if existing.is_none() && !will_have_password { return Ok(Err(invalid_request("首次配置 LDAP 时必须设置绑定密码"))); } if ldap_config.is_exclusive && !ldap_config.is_enabled { return Ok(Err(invalid_request( "仅允许 LDAP 登录 需要先启用 LDAP 认证", ))); } if ldap_config.is_enabled && !will_have_password { return Ok(Err(invalid_request("启用 LDAP 认证 需要先设置绑定密码"))); } if ldap_config.is_enabled && ldap_config.is_exclusive { let admin_count = self .count_active_local_admin_users_with_valid_password() .await?; if admin_count < 1 { return Ok(Err(invalid_request( "启用 LDAP 独占模式前,必须至少保留 1 个有效的本地管理员账户(含有效密码)作为紧急恢复通道", ))); } } let bind_password_encrypted = match bind_password { Some(password) if password.is_empty() => None, Some(password) => Some(routed!(self .encrypt_catalog_secret_with_fallbacks(&password) .ok_or_else(|| { invalid_request("LDAP 绑定密码加密失败,请检查 Rust 数据加密配置") }))), None => existing .as_ref() .and_then(|config| config.bind_password_encrypted.clone()), }; let config = StoredLdapModuleConfig { server_url, bind_dn, bind_password_encrypted, base_dn, user_search_filter: Some(user_search_filter), username_attr: Some(username_attr), email_attr: Some(email_attr), display_name_attr: Some(display_name_attr), is_enabled: ldap_config.is_enabled, is_exclusive: ldap_config.is_exclusive, use_starttls: ldap_config.use_starttls, connect_timeout: Some(connect_timeout), }; match (existing.is_some(), merge_mode) { (true, AdminImportMergeMode::Skip) => stats.ldap.skipped += 1, (true, AdminImportMergeMode::Error) => { return Ok(Err(invalid_request("LDAP 配置已存在"))); } (true, AdminImportMergeMode::Overwrite) => { let Some(_) = self.upsert_ldap_module_config(&config).await? else { return Ok(Err(invalid_request("更新 LDAP 配置失败"))); }; stats.ldap.updated += 1; } (false, _) => { let Some(_) = self.upsert_ldap_module_config(&config).await? else { return Ok(Err(invalid_request("创建 LDAP 配置失败"))); }; stats.ldap.created += 1; } } } } if !imported_oauth_providers.is_empty() { let imported_oauth_provider_count = imported_oauth_providers.len(); let mut oauth_by_type = self .list_oauth_provider_configs() .await? .into_iter() .map(|provider| (provider.provider_type.clone(), provider)) .collect::>(); for (index, imported_oauth_item) in imported_oauth_providers.into_iter().enumerate() { let (_, oauth_provider) = imported_oauth_item.into_parts(); let provider_type = invalid!(trim_required( &oauth_provider.provider_type, "provider_type", )); let existed = oauth_by_type.contains_key(&provider_type); if existed { match merge_mode { AdminImportMergeMode::Skip => { stats.oauth.skipped += 1; continue; } AdminImportMergeMode::Error => { return Ok(Err(invalid_request(format!( "OAuth Provider '{provider_type}' 已存在" )))); } AdminImportMergeMode::Overwrite => {} } } let display_name = invalid!(trim_required(&oauth_provider.display_name, "display_name")); let client_id = invalid!(trim_required(&oauth_provider.client_id, "client_id")); let redirect_uri = invalid!(trim_required(&oauth_provider.redirect_uri, "redirect_uri")); let frontend_callback_url = invalid!(trim_required( &oauth_provider.frontend_callback_url, "frontend_callback_url", )); let client_secret_encrypted = match oauth_provider.client_secret.as_deref().map(str::trim) { Some(secret) if !secret.is_empty() => { EncryptedSecretUpdate::Set(routed!(self .encrypt_catalog_secret_with_fallbacks(secret) .ok_or_else(|| { invalid_request("gateway 未配置 OAuth provider 加密密钥") }))) } _ => EncryptedSecretUpdate::Preserve, }; let record = UpsertOAuthProviderConfigRecord { provider_type: provider_type.clone(), display_name, client_id, client_secret_encrypted, authorization_url_override: oauth_provider .authorization_url_override .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()), token_url_override: oauth_provider .token_url_override .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()), userinfo_url_override: oauth_provider .userinfo_url_override .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()), scopes: normalize_string_list(oauth_provider.scopes), redirect_uri, frontend_callback_url, attribute_mapping: invalid!(normalize_json_object( oauth_provider.attribute_mapping, "attribute_mapping", )), extra_config: invalid!(normalize_json_object( oauth_provider.extra_config, "extra_config", )), icon_url: None, is_enabled: oauth_provider.is_enabled, }; invalid!(record.validate().map_err(|err| err.to_string())); let Some(persisted) = self.upsert_oauth_provider_config(&record).await? else { stats.oauth.skipped += (imported_oauth_provider_count - index) as u64; stats.errors.push( "当前运行环境不支持 OAuth Provider 配置读写,已跳过 oauth_providers" .to_string(), ); break; }; oauth_by_type.insert(provider_type, persisted); if existed { stats.oauth.updated += 1; } else { stats.oauth.created += 1; } } } for imported_config_item in imported_system_configs { let (_, system_config) = imported_config_item.into_parts(); let ImportedSystemConfig { key, value, description, } = system_config; let normalized_key = normalize_imported_system_config_key(&key); let exists = existing_system_config_keys.contains(&normalized_key); match (exists, merge_mode) { (true, AdminImportMergeMode::Skip) => { stats.system_configs.skipped += 1; continue; } (true, AdminImportMergeMode::Error) => { return Ok(Err(invalid_request(format!( "SystemConfig '{normalized_key}' 已存在" )))); } _ => {} } let request_bytes = Bytes::from( serde_json::to_vec(&json!({ "value": value, "description": description, })) .map_err(|err| GatewayError::Internal(err.to_string()))?, ); let update_result = apply_admin_system_config_update(self, &key, &request_bytes).await?; match update_result { Ok(_) => { if exists { stats.system_configs.updated += 1; } else { stats.system_configs.created += 1; existing_system_config_keys.insert(normalized_key); } } Err((status, payload)) => return Ok(Err((status, payload))), } } Ok(Ok(json!({ "message": "配置导入成功", "stats": stats, }))) } pub(crate) async fn import_admin_system_users( &self, request_body: &Bytes, operator_id: Option<&str>, ) -> Result, GatewayError> { if !self.has_auth_user_write_capability() || !self.has_auth_wallet_write_capability() || !self.has_auth_api_key_writer() { return Ok(Err(( http::StatusCode::SERVICE_UNAVAILABLE, json!({ "detail": "Admin system data unavailable" }), ))); } let root = match serde_json::from_slice::(request_body) { Ok(Value::Object(map)) => map, _ => return Ok(Err(invalid_request("请求数据验证失败"))), }; let merge_mode = match serde_json::from_value::( root.get("merge_mode").cloned().unwrap_or(Value::Null), ) { Ok(value) => value, Err(_) => { return Ok(Err(invalid_request( "merge_mode 仅支持 skip / overwrite / error", ))); } }; let empty = Vec::new(); let users = match root.get("users") { Some(Value::Array(items)) => items, Some(_) => return Ok(Err(invalid_request("users 必须是数组"))), None => &empty, }; let standalone_keys = match root.get("standalone_keys") { Some(Value::Array(items)) => items, Some(_) => return Ok(Err(invalid_request("standalone_keys 必须是数组"))), None => &empty, }; let imported_user_groups = match root.get("user_groups") { Some(Value::Array(items)) => items, Some(_) => return Ok(Err(invalid_request("user_groups 必须是数组"))), None => &empty, }; let standalone_owner_id = match operator_id { Some(candidate) => match self.find_user_auth_by_id(candidate).await? { Some(user) if user.role.eq_ignore_ascii_case("admin") => Some(user.id), _ => None, }, None => None, }; macro_rules! invalid_value { ($expr:expr) => { match $expr { Ok(value) => value, Err(detail) => return Ok(Err(invalid_request(detail))), } }; } invalid_value!(validate_imported_system_users_export_version( root.get("version") )); let supplemental_user_usage_aggregates = invalid_value!( build_imported_user_usage_total_aggregates(users, root.get("exported_at")) ); let mut stats = AdminSystemUsersImportStats::default(); let mut imported_user_id_map = BTreeMap::::new(); let mut imported_api_key_id_map = BTreeMap::::new(); let default_group_id = self.effective_default_user_group_id().await?; let existing_groups = self.list_user_groups().await?; let mut groups_by_name = existing_groups .into_iter() .map(|group| { ( aether_data::repository::users::normalize_user_group_name(&group.name) .to_ascii_lowercase(), group, ) }) .collect::>(); let mut imported_group_id_map = BTreeMap::::new(); let mut imported_group_name_map = BTreeMap::::new(); for (index, raw_group) in imported_user_groups.iter().enumerate() { let group = match imported_object_field(raw_group, &format!("user_groups[{index}]")) { Ok(value) => value, Err(detail) => return Ok(Err(invalid_request(detail))), }; let (export_id, normalized_name, record) = invalid_value!( build_imported_user_group_record(group, &format!("user_groups[{index}]")) ); if default_group_id .as_deref() .is_some_and(|group_id| export_id.as_deref() == Some(group_id)) || normalized_name == "default" { if let Some(default_group_id) = default_group_id.as_ref() { if let Some(export_id) = export_id { imported_group_id_map.insert(export_id, default_group_id.clone()); } imported_group_name_map.insert(normalized_name, default_group_id.clone()); } stats.user_groups.skipped += 1; continue; } if let Some(existing) = groups_by_name.get(&normalized_name).cloned() { if let Some(export_id) = export_id { imported_group_id_map.insert(export_id, existing.id.clone()); } imported_group_name_map.insert(normalized_name.clone(), existing.id.clone()); match merge_mode { AdminImportMergeMode::Skip => { stats.user_groups.skipped += 1; } AdminImportMergeMode::Error => { return Ok(Err(invalid_request(format!( "用户组 '{}' 已存在", existing.name )))); } AdminImportMergeMode::Overwrite => { let Some(updated) = self.update_user_group(&existing.id, record).await? else { return Ok(Err(( http::StatusCode::SERVICE_UNAVAILABLE, json!({ "detail": "Admin system data unavailable" }), ))); }; groups_by_name.insert(normalized_name, updated); stats.user_groups.updated += 1; } } continue; } let Some(created) = self.create_user_group(record).await? else { return Ok(Err(( http::StatusCode::SERVICE_UNAVAILABLE, json!({ "detail": "Admin system data unavailable" }), ))); }; if let Some(export_id) = export_id { imported_group_id_map.insert(export_id, created.id.clone()); } imported_group_name_map.insert(normalized_name.clone(), created.id.clone()); groups_by_name.insert(normalized_name, created); stats.user_groups.created += 1; } for (index, raw_user) in users.iter().enumerate() { let user = match imported_object_field(raw_user, &format!("users[{index}]")) { Ok(value) => value, Err(detail) => return Ok(Err(invalid_request(detail))), }; let source_user_id = invalid_value!(imported_optional_string(user.get("id"))); let role = invalid_value!(imported_optional_string(user.get("role"))) .unwrap_or_else(|| "user".to_string()) .to_ascii_lowercase(); if role == "admin" { let skipped_email = invalid_value!(imported_optional_string(user.get("email"))); let skipped_username = invalid_value!(imported_optional_string(user.get("username"))); stats.users.skipped += 1; stats.errors.push(format!( "跳过管理员用户: {}", skipped_email .or(skipped_username) .unwrap_or_else(|| format!("users[{index}]")) )); continue; } let email = invalid_value!(imported_optional_string(user.get("email"))) .map(|value| value.to_ascii_lowercase()); let email_verified = invalid_value!(imported_optional_bool(user.get("email_verified"))).unwrap_or(true); let username = invalid_value!(imported_optional_string(user.get("username"))) .or_else(|| { email.as_ref().map(|value| { value .split('@') .next() .unwrap_or(value.as_str()) .to_string() }) }) .unwrap_or_else(|| format!("imported-user-{index}")); let password_hash = invalid_value!(imported_optional_string(user.get("password_hash"))); let allowed_providers = invalid_value!(normalize_imported_user_string_list( user, "allowed_providers" )); let allowed_api_formats = invalid_value!(normalize_imported_user_api_formats( user, "allowed_api_formats" )); let allowed_models = invalid_value!(normalize_imported_user_string_list(user, "allowed_models")); let rate_limit = invalid_value!(imported_optional_i32(user.get("rate_limit"), "rate_limit")); let allowed_providers_mode = invalid_value!(imported_user_list_policy_mode( user, "allowed_providers_mode", "allowed_providers", &allowed_providers, )); let allowed_api_formats_mode = invalid_value!(imported_user_list_policy_mode( user, "allowed_api_formats_mode", "allowed_api_formats", &allowed_api_formats, )); let allowed_models_mode = invalid_value!(imported_user_list_policy_mode( user, "allowed_models_mode", "allowed_models", &allowed_models, )); let rate_limit_mode = invalid_value!(imported_user_rate_limit_policy_mode( user, "rate_limit_mode", "rate_limit", rate_limit, )); let imported_user_group_ids = invalid_value!(resolve_imported_user_group_ids( user, &imported_group_id_map, &imported_group_name_map, &groups_by_name, )); let group_ids = if user.contains_key("group_ids") || user.contains_key("group_names") { let group_ids = self .include_default_user_group_ids(&imported_user_group_ids) .await?; if !group_ids.is_empty() { let existing_groups = self.list_user_groups_by_ids(&group_ids).await?; if existing_groups.len() != group_ids.len() { return Ok(Err(invalid_request(format!( "用户 '{}' 的用户组不存在", email.clone().unwrap_or(username.clone()) )))); } } Some(group_ids) } else { None }; let is_active = invalid_value!(imported_optional_bool(user.get("is_active"))).unwrap_or(true); let model_capability_settings = invalid_value!(imported_optional_json_object( user.get("model_capability_settings"), "model_capability_settings" )); let feature_settings = invalid_value!(imported_optional_json_object( user.get("feature_settings"), "feature_settings" ) .and_then(normalize_admin_feature_settings)); let wallet_payload = match user.get("wallet") { Some(Value::Object(map)) => Some(map), Some(Value::Null) | None => None, Some(_) => return Ok(Err(invalid_request("wallet 必须是对象"))), }; let wallet_target = invalid_value!(normalize_imported_wallet_target(wallet_payload, false)); let mut existing_user = if let Some(email) = email.as_deref() { self.find_user_auth_by_identifier(email).await? } else { None }; if existing_user.is_none() { existing_user = self.find_user_auth_by_identifier(&username).await?; } let user_id = if let Some(existing) = existing_user { if existing.role.eq_ignore_ascii_case("admin") { stats.users.skipped += 1; stats.errors.push(format!( "跳过管理员用户记录: {}", email.clone().unwrap_or(username.clone()) )); continue; } match merge_mode { AdminImportMergeMode::Skip => { stats.users.skipped += 1; continue; } AdminImportMergeMode::Error => { return Ok(Err(invalid_request(format!( "用户 '{}' 已存在", email.clone().unwrap_or(username.clone()) )))); } AdminImportMergeMode::Overwrite => { if let Some(email) = email.as_deref() { if self .is_other_user_auth_email_taken(email, &existing.id) .await? { return Ok(Err(invalid_request(format!("邮箱已存在: {email}")))); } } if self .is_other_user_auth_username_taken(&username, &existing.id) .await? { return Ok(Err(invalid_request(format!("用户名已存在: {username}")))); } let updated_profile = self .update_local_auth_user_profile( &existing.id, email.clone(), Some(username.clone()), ) .await?; if updated_profile.is_none() { return Ok(Err(( http::StatusCode::SERVICE_UNAVAILABLE, json!({ "detail": "Admin system data unavailable" }), ))); } if let Some(password_hash) = password_hash.as_deref().filter(|value| !value.is_empty()) { let updated_password = self .update_local_auth_user_password_hash( &existing.id, password_hash.to_string(), chrono::Utc::now(), ) .await?; if updated_password.is_none() { return Ok(Err(( http::StatusCode::SERVICE_UNAVAILABLE, json!({ "detail": "Admin system data unavailable" }), ))); } } let updated_admin_fields = self .update_local_auth_user_admin_fields( &existing.id, Some(role.clone()), user.contains_key("allowed_providers"), allowed_providers.clone(), user.contains_key("allowed_api_formats"), allowed_api_formats.clone(), user.contains_key("allowed_models"), allowed_models.clone(), user.contains_key("rate_limit"), rate_limit, Some(is_active), ) .await?; if updated_admin_fields.is_none() { return Ok(Err(( http::StatusCode::SERVICE_UNAVAILABLE, json!({ "detail": "Admin system data unavailable" }), ))); } if user.contains_key("email_verified") { stats.errors.push(format!( "用户 '{}' 的 email_verified 当前不会覆盖已有值", email.clone().unwrap_or(username.clone()) )); } if user.contains_key("model_capability_settings") { let _ = self .update_user_model_capability_settings( &existing.id, model_capability_settings.clone(), ) .await?; } if user.contains_key("feature_settings") { let _ = self .update_user_feature_settings( &existing.id, feature_settings.clone(), ) .await?; } if allowed_providers_mode.is_some() || allowed_api_formats_mode.is_some() || allowed_models_mode.is_some() || rate_limit_mode.is_some() { let updated_policy_modes = self .update_local_auth_user_policy_modes( &existing.id, allowed_providers_mode.clone(), allowed_api_formats_mode.clone(), allowed_models_mode.clone(), rate_limit_mode.clone(), ) .await?; if updated_policy_modes.is_none() { return Ok(Err(( http::StatusCode::SERVICE_UNAVAILABLE, json!({ "detail": "Admin system data unavailable" }), ))); } } if let Some(group_ids) = group_ids.as_ref() { self.replace_user_groups_for_user(&existing.id, group_ids) .await?; } self.sync_imported_user_wallet( &existing.id, &wallet_target, &email.clone().unwrap_or(username.clone()), ) .await?; stats.users.updated += 1; existing.id } } } else { let created = self .create_local_auth_user_with_settings( email.clone(), email_verified, username.clone(), password_hash.unwrap_or_default(), role.clone(), allowed_providers.clone(), allowed_api_formats.clone(), allowed_models.clone(), rate_limit, ) .await?; let Some(created) = created else { return Ok(Err(( http::StatusCode::SERVICE_UNAVAILABLE, json!({ "detail": "Admin system data unavailable" }), ))); }; if user.contains_key("model_capability_settings") { let _ = self .update_user_model_capability_settings( &created.id, model_capability_settings.clone(), ) .await?; } if user.contains_key("feature_settings") { let _ = self .update_user_feature_settings(&created.id, feature_settings.clone()) .await?; } let created = if allowed_providers_mode.is_some() || allowed_api_formats_mode.is_some() || allowed_models_mode.is_some() || rate_limit_mode.is_some() { let Some(updated_policy_modes) = self .update_local_auth_user_policy_modes( &created.id, allowed_providers_mode.clone(), allowed_api_formats_mode.clone(), allowed_models_mode.clone(), rate_limit_mode.clone(), ) .await? else { return Ok(Err(( http::StatusCode::SERVICE_UNAVAILABLE, json!({ "detail": "Admin system data unavailable" }), ))); }; updated_policy_modes } else { created }; if let Some(group_ids) = group_ids.as_ref() { self.replace_user_groups_for_user(&created.id, group_ids) .await?; } self.sync_imported_user_wallet( &created.id, &wallet_target, &email.clone().unwrap_or(username.clone()), ) .await?; stats.users.created += 1; created.id }; if let Some(source_user_id) = source_user_id { imported_user_id_map.insert(source_user_id, user_id.clone()); } let existing_api_keys = self .list_auth_api_key_export_records_by_user_ids(std::slice::from_ref(&user_id)) .await? .into_iter() .filter(|record| !record.is_standalone) .collect::>(); let imported_api_keys = match user.get("api_keys") { Some(Value::Array(items)) => items, Some(_) => return Ok(Err(invalid_request("api_keys 必须是数组"))), None => &empty, }; let mut existing_api_keys_by_hash = existing_api_keys .into_iter() .map(|record| (record.key_hash.clone(), record)) .collect::>(); for (key_index, raw_key) in imported_api_keys.iter().enumerate() { let key = match imported_object_field( raw_key, &format!("users[{index}].api_keys[{key_index}]"), ) { Ok(value) => value, Err(detail) => return Ok(Err(invalid_request(detail))), }; let Some((key_hash, key_encrypted)) = invalid_value!(self.resolve_imported_system_user_api_key_material(key)) else { stats.api_keys.skipped += 1; stats.errors.push(format!( "跳过无效 API Key: 用户 '{}'", email.clone().unwrap_or(username.clone()) )); continue; }; let source_api_key_id = invalid_value!(imported_optional_string(key.get("api_key_id"))); let name = invalid_value!(imported_optional_string(key.get("name"))); let allowed_providers = invalid_value!(normalize_imported_user_string_list( key, "allowed_providers" )); let allowed_api_formats = invalid_value!(normalize_imported_user_api_formats( key, "allowed_api_formats" )); let allowed_models = invalid_value!(normalize_imported_user_string_list(key, "allowed_models")); let ip_rules = invalid_value!(normalize_imported_user_ip_rules(key)); let rate_limit = invalid_value!(imported_optional_i32(key.get("rate_limit"), "rate_limit")) .unwrap_or(0); let concurrent_limit = invalid_value!(imported_optional_i32( key.get("concurrent_limit"), "concurrent_limit" )); if concurrent_limit.is_some_and(|value| value < 0) { return Ok(Err(invalid_request("concurrent_limit 必须是非负整数"))); } let force_capabilities = imported_optional_value(key.get("force_capabilities")); let is_active = invalid_value!(imported_optional_bool(key.get("is_active"))).unwrap_or(true); let expires_at_unix_secs = invalid_value!(imported_rfc3339_to_unix_secs( key.get("expires_at"), "expires_at" )); let auto_delete_on_expiry = invalid_value!(imported_optional_bool(key.get("auto_delete_on_expiry"))) .unwrap_or(false); let imported_total_requests = invalid_value!(imported_optional_u64( key.get("total_requests"), "total_requests" )); let total_requests = imported_total_requests.unwrap_or(0); let imported_total_tokens = invalid_value!(imported_optional_u64( key.get("total_tokens"), "total_tokens" )); let total_tokens = imported_total_tokens.unwrap_or(0); let imported_total_cost_usd = invalid_value!(imported_optional_f64( key.get("total_cost_usd"), "total_cost_usd" )); let total_cost_usd = imported_total_cost_usd.unwrap_or(0.0); let feature_settings = invalid_value!(imported_optional_json_object( key.get("feature_settings"), "feature_settings" ) .and_then(normalize_admin_feature_settings)); if let Some(existing_key) = existing_api_keys_by_hash.get(&key_hash).cloned() { match merge_mode { AdminImportMergeMode::Skip => { stats.api_keys.skipped += 1; } AdminImportMergeMode::Error => { return Ok(Err(invalid_request(format!( "用户 '{}' 的 API Key 已存在", email.clone().unwrap_or(username.clone()) )))); } AdminImportMergeMode::Overwrite => { let updated = self .update_user_api_key_basic( aether_data::repository::auth::UpdateUserApiKeyBasicRecord { user_id: user_id.clone(), api_key_id: existing_key.api_key_id.clone(), name: name.clone(), rate_limit: Some(rate_limit), concurrent_limit: if key.contains_key("concurrent_limit") { concurrent_limit } else { None }, ip_rules: imported_ip_rules_present(key) .then(|| ip_rules.clone()), }, ) .await?; if updated.is_none() { return Ok(Err(( http::StatusCode::SERVICE_UNAVAILABLE, json!({ "detail": "Admin system data unavailable" }), ))); } let _ = self .set_user_api_key_allowed_providers( &user_id, &existing_key.api_key_id, allowed_providers.clone(), ) .await?; let _ = self .set_user_api_key_force_capabilities( &user_id, &existing_key.api_key_id, force_capabilities.clone(), ) .await?; if key.contains_key("feature_settings") { let _ = self .set_user_api_key_feature_settings( &user_id, &existing_key.api_key_id, feature_settings.clone(), ) .await?; } let _ = self .set_user_api_key_active( &user_id, &existing_key.api_key_id, is_active, ) .await?; if imported_total_requests.is_some() || imported_total_tokens.is_some() || imported_total_cost_usd.is_some() { let updated_usage = self .set_api_key_usage_totals( &existing_key.api_key_id, imported_total_requests .unwrap_or(existing_key.total_requests), imported_total_tokens.unwrap_or(existing_key.total_tokens), imported_total_cost_usd .unwrap_or(existing_key.total_cost_usd), ) .await?; if updated_usage.is_none() { return Ok(Err(( http::StatusCode::SERVICE_UNAVAILABLE, json!({ "detail": "Admin system data unavailable" }), ))); } } if key.contains_key("allowed_api_formats") || key.contains_key("allowed_models") || key.contains_key("expires_at") || key.contains_key("auto_delete_on_expiry") { stats.errors.push(format!( "用户 '{}' 的现有 API Key 仅覆盖基础字段;高级导入字段保持原值", email.clone().unwrap_or(username.clone()) )); } stats.api_keys.updated += 1; if let Some(source_api_key_id) = source_api_key_id.clone() { imported_api_key_id_map .insert(source_api_key_id, existing_key.api_key_id.clone()); } } } continue; } let created = self .create_user_api_key(aether_data::repository::auth::CreateUserApiKeyRecord { user_id: user_id.clone(), api_key_id: Uuid::new_v4().to_string(), key_hash: key_hash.clone(), key_encrypted, name, allowed_providers, allowed_api_formats, allowed_models, ip_rules, rate_limit, concurrent_limit, force_capabilities, is_active, expires_at_unix_secs, auto_delete_on_expiry, total_requests, total_tokens, total_cost_usd, }) .await?; let Some(created) = created else { return Ok(Err(( http::StatusCode::SERVICE_UNAVAILABLE, json!({ "detail": "Admin system data unavailable" }), ))); }; if key.contains_key("feature_settings") { let _ = self .set_user_api_key_feature_settings( &user_id, &created.api_key_id, feature_settings.clone(), ) .await?; } let created_api_key_id = created.api_key_id.clone(); existing_api_keys_by_hash.insert(key_hash, created); if let Some(source_api_key_id) = source_api_key_id { imported_api_key_id_map.insert(source_api_key_id, created_api_key_id); } stats.api_keys.created += 1; } } if !standalone_keys.is_empty() { let Some(standalone_owner_id) = standalone_owner_id else { stats.standalone_keys.skipped += standalone_keys.len() as u64; stats .errors .push("无法导入独立余额 Key: 当前管理员用户记录不存在".to_string()); if let Some(summary) = self .import_admin_system_user_usage_aggregates( root.get("usage_aggregates"), &supplemental_user_usage_aggregates, &imported_user_id_map, &imported_api_key_id_map, merge_mode, ) .await? { stats.usage_aggregates = Some(summary); } return Ok(Ok(json!({ "message": "用户数据导入成功", "stats": stats, }))); }; let existing_standalone_keys = self .list_auth_api_key_export_standalone_records() .await? .into_iter() .collect::>(); let mut existing_standalone_by_hash = existing_standalone_keys .into_iter() .map(|record| (record.key_hash.clone(), record)) .collect::>(); for (index, raw_key) in standalone_keys.iter().enumerate() { let key = match imported_object_field(raw_key, &format!("standalone_keys[{index}]")) { Ok(value) => value, Err(detail) => return Ok(Err(invalid_request(detail))), }; let Some((key_hash, key_encrypted)) = invalid_value!(self.resolve_imported_system_user_api_key_material(key)) else { stats.standalone_keys.skipped += 1; stats .errors .push(format!("跳过无效独立余额 Key: standalone_keys[{index}]")); continue; }; let source_api_key_id = invalid_value!(imported_optional_string(key.get("api_key_id"))); let name = invalid_value!(imported_optional_string(key.get("name"))); let allowed_providers = invalid_value!(normalize_imported_user_string_list( key, "allowed_providers" )); let allowed_api_formats = invalid_value!(normalize_imported_user_api_formats( key, "allowed_api_formats" )); let allowed_models = invalid_value!(normalize_imported_user_string_list(key, "allowed_models")); let ip_rules = invalid_value!(normalize_imported_user_ip_rules(key)); let rate_limit = invalid_value!(imported_optional_i32(key.get("rate_limit"), "rate_limit")) .unwrap_or(0); let concurrent_limit = invalid_value!(imported_optional_i32( key.get("concurrent_limit"), "concurrent_limit" )); if concurrent_limit.is_some_and(|value| value < 0) { return Ok(Err(invalid_request("concurrent_limit 必须是非负整数"))); } let force_capabilities = imported_optional_value(key.get("force_capabilities")); let is_active = invalid_value!(imported_optional_bool(key.get("is_active"))).unwrap_or(true); let expires_at_unix_secs = invalid_value!(imported_rfc3339_to_unix_secs( key.get("expires_at"), "expires_at" )); let auto_delete_on_expiry = invalid_value!(imported_optional_bool(key.get("auto_delete_on_expiry"))) .unwrap_or(false); let imported_total_requests = invalid_value!(imported_optional_u64( key.get("total_requests"), "total_requests" )); let total_requests = imported_total_requests.unwrap_or(0); let imported_total_tokens = invalid_value!(imported_optional_u64( key.get("total_tokens"), "total_tokens" )); let total_tokens = imported_total_tokens.unwrap_or(0); let imported_total_cost_usd = invalid_value!(imported_optional_f64( key.get("total_cost_usd"), "total_cost_usd" )); let total_cost_usd = imported_total_cost_usd.unwrap_or(0.0); let feature_settings = invalid_value!(imported_optional_json_object( key.get("feature_settings"), "feature_settings" ) .and_then(normalize_admin_feature_settings)); let wallet_payload = match key.get("wallet") { Some(Value::Object(map)) => Some(map), Some(Value::Null) | None => None, Some(_) => return Ok(Err(invalid_request("wallet 必须是对象"))), }; let unlimited = invalid_value!(imported_optional_bool(key.get("unlimited"))).unwrap_or(false); let wallet_target = invalid_value!(normalize_imported_wallet_target(wallet_payload, unlimited)); if let Some(existing_key) = existing_standalone_by_hash.get(&key_hash).cloned() { match merge_mode { AdminImportMergeMode::Skip => { stats.standalone_keys.skipped += 1; } AdminImportMergeMode::Error => { return Ok(Err(invalid_request("独立余额 Key 已存在"))); } AdminImportMergeMode::Overwrite => { let updated = self .update_standalone_api_key_basic( aether_data::repository::auth::UpdateStandaloneApiKeyBasicRecord { api_key_id: existing_key.api_key_id.clone(), name: name.clone(), rate_limit_present: true, rate_limit: Some(rate_limit), concurrent_limit_present: key.contains_key("concurrent_limit"), concurrent_limit, allowed_providers: Some(allowed_providers.clone()), allowed_api_formats: Some(allowed_api_formats.clone()), allowed_models: Some(allowed_models.clone()), ip_rules: imported_ip_rules_present(key) .then(|| ip_rules.clone()), expires_at_present: false, expires_at_unix_secs: None, auto_delete_on_expiry_present: false, auto_delete_on_expiry: false, }, ) .await?; if updated.is_none() { return Ok(Err(( http::StatusCode::SERVICE_UNAVAILABLE, json!({ "detail": "Admin system data unavailable" }), ))); } let _ = self .set_standalone_api_key_active(&existing_key.api_key_id, is_active) .await?; if key.contains_key("feature_settings") { let _ = self .set_standalone_api_key_feature_settings( &existing_key.api_key_id, feature_settings.clone(), ) .await?; } if imported_total_requests.is_some() || imported_total_tokens.is_some() || imported_total_cost_usd.is_some() { let updated_usage = self .set_api_key_usage_totals( &existing_key.api_key_id, imported_total_requests .unwrap_or(existing_key.total_requests), imported_total_tokens.unwrap_or(existing_key.total_tokens), imported_total_cost_usd .unwrap_or(existing_key.total_cost_usd), ) .await?; if updated_usage.is_none() { return Ok(Err(( http::StatusCode::SERVICE_UNAVAILABLE, json!({ "detail": "Admin system data unavailable" }), ))); } } if key.contains_key("expires_at") || key.contains_key("auto_delete_on_expiry") || key.contains_key("force_capabilities") { stats.errors.push( "现有独立余额 Key 仅覆盖基础字段;高级导入字段保持原值" .to_string(), ); } self.sync_imported_api_key_wallet( &existing_key.api_key_id, &wallet_target, key.get("name") .and_then(Value::as_str) .unwrap_or("独立余额 Key"), ) .await?; stats.standalone_keys.updated += 1; if let Some(source_api_key_id) = source_api_key_id.clone() { imported_api_key_id_map .insert(source_api_key_id, existing_key.api_key_id.clone()); } } } continue; } let created = self .create_standalone_api_key( aether_data::repository::auth::CreateStandaloneApiKeyRecord { user_id: standalone_owner_id.clone(), api_key_id: Uuid::new_v4().to_string(), key_hash: key_hash.clone(), key_encrypted, name, allowed_providers, allowed_api_formats, allowed_models, ip_rules, rate_limit: Some(rate_limit), concurrent_limit, force_capabilities, is_active, expires_at_unix_secs, auto_delete_on_expiry, total_requests, total_tokens, total_cost_usd, }, ) .await?; let Some(created) = created else { return Ok(Err(( http::StatusCode::SERVICE_UNAVAILABLE, json!({ "detail": "Admin system data unavailable" }), ))); }; if key.contains_key("feature_settings") { let _ = self .set_standalone_api_key_feature_settings( &created.api_key_id, feature_settings.clone(), ) .await?; } self.sync_imported_api_key_wallet( &created.api_key_id, &wallet_target, created.name.as_deref().unwrap_or("独立余额 Key"), ) .await?; let created_api_key_id = created.api_key_id.clone(); existing_standalone_by_hash.insert(key_hash, created); if let Some(source_api_key_id) = source_api_key_id { imported_api_key_id_map.insert(source_api_key_id, created_api_key_id); } stats.standalone_keys.created += 1; } } if let Some(summary) = self .import_admin_system_user_usage_aggregates( root.get("usage_aggregates"), &supplemental_user_usage_aggregates, &imported_user_id_map, &imported_api_key_id_map, merge_mode, ) .await? { stats.usage_aggregates = Some(summary); } Ok(Ok(json!({ "message": "用户数据导入成功", "stats": stats, }))) } async fn import_admin_system_user_usage_aggregates( &self, value: Option<&Value>, supplemental_user_daily: &[AdminSystemStatsUserDailyAggregate], user_id_map: &BTreeMap, api_key_id_map: &BTreeMap, merge_mode: AdminImportMergeMode, ) -> Result, GatewayError> { let mut snapshot = match value { Some(value) if !value.is_null() => serde_json::from_value::< AdminSystemUsageAggregateSnapshot, >(value.clone()) .map_err(|err| GatewayError::Client { status: http::StatusCode::BAD_REQUEST, message: format!("usage_aggregates 格式无效: {err}"), })?, _ => AdminSystemUsageAggregateSnapshot::default(), }; let mut existing_user_totals = BTreeMap::::new(); for row in &snapshot.stats_user_daily { let total_tokens = row .input_tokens .saturating_add(row.output_tokens) .saturating_add(row.cache_creation_tokens) .saturating_add(row.cache_read_tokens); let entry = existing_user_totals .entry(row.user_id.clone()) .or_insert((0, 0)); entry.0 = entry.0.saturating_add(row.total_requests); entry.1 = entry.1.saturating_add(total_tokens); } for row in supplemental_user_daily { let existing = existing_user_totals .get(&row.user_id) .copied() .unwrap_or_default(); let request_delta = row.total_requests.saturating_sub(existing.0); let token_delta = row.input_tokens.saturating_sub(existing.1); if request_delta == 0 && token_delta == 0 { continue; } if let Some(existing_row) = snapshot .stats_user_daily .iter_mut() .rev() .find(|existing_row| existing_row.user_id == row.user_id) { existing_row.total_requests = existing_row.total_requests.saturating_add(request_delta); existing_row.success_requests = existing_row.success_requests.saturating_add(request_delta); existing_row.input_tokens = existing_row.input_tokens.saturating_add(token_delta); } else { let mut row = row.clone(); row.total_requests = request_delta; row.success_requests = request_delta; row.input_tokens = token_delta; snapshot.stats_user_daily.push(row); } } if snapshot.stats_daily.is_empty() && snapshot.stats_user_daily.is_empty() && snapshot.stats_daily_api_key.is_empty() { return Ok(None); } self.import_admin_system_usage_aggregates( &snapshot, user_id_map, api_key_id_map, usage_aggregate_import_mode(merge_mode), ) .await .map(Some) } async fn sync_imported_user_wallet( &self, user_id: &str, wallet_target: &ImportedWalletTarget, label: &str, ) -> Result<(), GatewayError> { if self .find_wallet(WalletLookupKey::UserId(user_id)) .await? .is_none() { let created = self .initialize_auth_user_wallet(user_id, 0.0, false) .await?; if created.is_none() { return Err(GatewayError::Internal(format!( "failed to initialize imported wallet for {label}" ))); } } self.sync_wallet_snapshot(WalletOwner::User(user_id), wallet_target, label) .await } async fn sync_imported_api_key_wallet( &self, api_key_id: &str, wallet_target: &ImportedWalletTarget, label: &str, ) -> Result<(), GatewayError> { if self .find_wallet(WalletLookupKey::ApiKeyId(api_key_id)) .await? .is_none() { let created = self .initialize_auth_api_key_wallet(api_key_id, 0.0, false) .await?; if created.is_none() { return Err(GatewayError::Internal(format!( "failed to initialize imported wallet for {label}" ))); } } self.sync_wallet_snapshot(WalletOwner::ApiKey(api_key_id), wallet_target, label) .await } async fn sync_wallet_snapshot( &self, owner: WalletOwner<'_>, wallet_target: &ImportedWalletTarget, label: &str, ) -> Result<(), GatewayError> { let updated = match owner { WalletOwner::User(user_id) => { self.update_auth_user_wallet_snapshot( user_id, wallet_target.recharge_balance, wallet_target.gift_balance, &wallet_target.limit_mode, &wallet_target.currency, &wallet_target.status, wallet_target.total_recharged, wallet_target.total_consumed, wallet_target.total_refunded, wallet_target.total_adjusted, wallet_target.updated_at_unix_secs, ) .await? } WalletOwner::ApiKey(api_key_id) => { self.update_auth_api_key_wallet_snapshot( api_key_id, wallet_target.recharge_balance, wallet_target.gift_balance, &wallet_target.limit_mode, &wallet_target.currency, &wallet_target.status, wallet_target.total_recharged, wallet_target.total_consumed, wallet_target.total_refunded, wallet_target.total_adjusted, wallet_target.updated_at_unix_secs, ) .await? } }; if updated.is_none() { return Err(GatewayError::Internal(format!( "failed to persist imported wallet snapshot for {label}" ))); } Ok(()) } fn resolve_imported_system_user_api_key_material( &self, key: &Map, ) -> Result)>, String> { let plaintext_key = imported_optional_string(key.get("key"))?; if let Some(plaintext_key) = plaintext_key.filter(|value| !value.is_empty()) { return Ok(Some(( hash_admin_user_api_key(&plaintext_key), self.encrypt_catalog_secret_with_fallbacks(&plaintext_key), ))); } let key_hash = imported_optional_string(key.get("key_hash"))?; let key_encrypted = imported_optional_string(key.get("key_encrypted"))?; Ok(key_hash.map(|key_hash| (key_hash, key_encrypted))) } } #[derive(Clone, Copy)] enum WalletOwner<'a> { User(&'a str), ApiKey(&'a str), } #[cfg(test)] mod tests { use std::sync::Arc; use aether_data::repository::pool_scores::SqlitePoolMemberScoreRepository; use aether_data::repository::provider_catalog::InMemoryProviderCatalogReadRepository; use aether_data_contracts::repository::provider_catalog::{ StoredProviderCatalogKey, StoredProviderCatalogProvider, }; use serde_json::json; use super::{ build_imported_user_usage_total_aggregates, imported_oauth_auth_config_has_credentials, imported_oauth_expiry_after_import, imported_optional_bool, imported_optional_f64, imported_optional_i32, imported_optional_u64, imported_rfc3339_to_unix_secs, imported_string_list_from_value, normalize_import_endpoint_format, normalize_import_key_formats, normalize_import_key_raw_payload, normalize_imported_wallet_target, seed_imported_oauth_pool_score, validate_imported_system_users_export_version, ImportedProviderKey, }; use crate::admin_api::AdminAppState; use crate::data::GatewayDataState; use crate::AppState; #[test] fn users_import_requires_supported_export_version() { assert!(validate_imported_system_users_export_version(Some(&json!("1.3"))).is_ok()); assert!(validate_imported_system_users_export_version(Some(&json!("1.4"))).is_ok()); assert!(validate_imported_system_users_export_version(Some(&json!("1.5"))).is_ok()); assert_eq!( validate_imported_system_users_export_version(Some(&json!("2.2"))).unwrap_err(), "不支持的用户数据版本: 2.2,支持的版本: 1.3, 1.4, 1.5" ); assert_eq!( validate_imported_system_users_export_version(Some(&json!(null))).unwrap_err(), "version 必须是 x.y 字符串" ); } #[test] fn users_import_builds_supplemental_usage_aggregates_from_summary_fields() { let users = vec![ json!({ "id": "source-user-1", "username": "alice", "request_count": 12, "total_tokens": 3456 }), json!({ "id": "source-user-zero", "username": "zero", "request_count": 0, "total_tokens": 0 }), json!({ "username": "no-source-id", "request_count": 5, "total_tokens": 6 }), ]; let rows = build_imported_user_usage_total_aggregates( &users, Some(&json!("2026-05-25T12:34:56Z")), ) .expect("supplemental usage aggregates should build"); assert_eq!(rows.len(), 1); assert_eq!(rows[0].user_id, "source-user-1"); assert_eq!(rows[0].username.as_deref(), Some("alice")); assert_eq!(rows[0].total_requests, 12); assert_eq!(rows[0].success_requests, 12); assert_eq!(rows[0].input_tokens, 3456); assert_eq!(rows[0].date_unix_secs % 86_400, 0); } #[test] fn config_import_normalizes_python_cli_api_format_aliases() { for (raw, expected) in [ ("openai:cli", "openai:responses"), ("openai:compact", "openai:responses:compact"), ("openai_image", "openai:image"), ("images", "openai:image"), ("/v1/images/generations", "openai:image"), ("/v1/images/edits", "openai:image"), ("claude:chat", "claude:messages"), ("claude:cli", "claude:messages"), ("gemini:chat", "gemini:generate_content"), ("gemini:cli", "gemini:generate_content"), ] { assert_eq!(normalize_import_endpoint_format(raw).unwrap(), expected); } } #[test] fn config_import_normalizes_key_formats_against_imported_endpoint_aliases() { let endpoint_formats = ["claude:messages", "openai:responses:compact"] .into_iter() .map(ToOwned::to_owned) .collect(); let item = ImportedProviderKey { api_key: None, auth_type: None, auth_config: None, name: None, note: None, api_formats: Some(vec!["claude:cli".to_string(), "openai:compact".to_string()]), supported_endpoints: None, rate_multipliers: None, internal_priority: None, global_priority_by_format: None, auth_type_by_format: None, allow_auth_channel_mismatch_formats: None, rpm_limit: None, allowed_models: None, capabilities: None, cache_ttl_minutes: None, max_probe_interval_minutes: None, auto_fetch_models: None, locked_models: None, model_include_patterns: None, model_exclude_patterns: None, is_active: true, proxy: None, fingerprint: None, }; let (formats, missing) = normalize_import_key_formats(&item, &endpoint_formats); assert_eq!(formats, vec!["claude:messages", "openai:responses:compact"]); assert!(missing.is_empty()); } #[test] fn config_import_filters_key_format_scoped_fields_to_selected_api_formats() { let raw_key = json!({ "name": "test-key", "api_key": "sk-test", "api_formats": ["openai:responses", "openai:video"], "auth_type_by_format": { "openai:responses": "api_key", "openai:video": "bearer" }, "allow_auth_channel_mismatch_formats": [ "openai:responses", "openai:video" ] }); let raw_key = raw_key.as_object().expect("key should be object"); let payload = normalize_import_key_raw_payload( raw_key, "api_key", &["openai:responses".to_string()], None, ); assert_eq!(payload["api_formats"], json!(["openai:responses"])); assert_eq!( payload["auth_type_by_format"], json!({ "openai:responses": "api_key" }) ); assert_eq!( payload["allow_auth_channel_mismatch_formats"], json!(["openai:responses"]) ); } #[test] fn config_import_preserves_explicit_empty_mismatch_scope_after_filtering() { let raw_key = json!({ "name": "test-key", "api_key": "sk-test", "api_formats": ["openai:responses"], "allow_auth_channel_mismatch_formats": ["openai:video"] }); let raw_key = raw_key.as_object().expect("key should be object"); let payload = normalize_import_key_raw_payload( raw_key, "api_key", &["openai:responses".to_string()], None, ); assert_eq!(payload["allow_auth_channel_mismatch_formats"], json!([])); } #[test] fn oauth_import_only_treats_non_empty_secret_fields_as_credentials() { assert!(!imported_oauth_auth_config_has_credentials(&json!({}))); assert!(!imported_oauth_auth_config_has_credentials(&json!({ "provider_type": "codex", "expires_at": 4_102_444_800u64, "account_id": "acct-1", "refresh_token": " " }))); assert!(imported_oauth_auth_config_has_credentials(&json!({ "provider_type": "codex", "refresh_token": "refresh-1" }))); assert!(imported_oauth_auth_config_has_credentials(&json!({ "session": {"sso_token": "sso-1"} }))); for field in [ "sso_rw_token", "ssoRwToken", "cf_cookies", "cfCookies", "cf_clearance", "cfClearance", "cookieHeader", ] { let mut config = serde_json::Map::new(); config.insert(field.to_string(), json!("credential-1")); assert!( imported_oauth_auth_config_has_credentials(&serde_json::Value::Object(config)), "{field} is transport credential material" ); } } #[test] fn oauth_import_expiry_tracks_the_supplied_credential_source() { let old_expiry = Some(1_700_000_000); assert_eq!( imported_oauth_expiry_after_import(old_expiry, false, None, true), None, "a new top-level api_key replaces the old session and clears its expiry" ); assert_eq!( imported_oauth_expiry_after_import(old_expiry, false, None, false), old_expiry, "metadata-only imports preserve the current OAuth expiry" ); assert_eq!( imported_oauth_expiry_after_import( old_expiry, true, Some(&json!({"expires_at": 4_102_444_800u64})), false, ), Some(4_102_444_800), "an explicit auth_config owns the replacement expiry" ); } #[tokio::test] async fn oauth_pool_score_persistence_failure_is_propagated() { let mut provider = StoredProviderCatalogProvider::new( "provider-1".to_string(), "Provider One".to_string(), None, "codex".to_string(), ) .expect("provider should build"); provider.config = Some(json!({"pool_advanced": {}})); let key = StoredProviderCatalogKey::new( "key-1".to_string(), provider.id.clone(), "OAuth Key".to_string(), "oauth".to_string(), None, true, ) .expect("key should build"); let provider_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed( vec![provider], Vec::new(), vec![key.clone()], )); let no_writer_app = AppState::new() .expect("app state should build") .with_data_state_for_tests( GatewayDataState::with_provider_catalog_repository_for_tests(Arc::clone( &provider_repository, )), ); seed_imported_oauth_pool_score(&AdminAppState::new(&no_writer_app), "provider-1", &key, 99) .await .expect("a disabled score writer remains an allowed no-op"); let pool = sqlx::sqlite::SqlitePoolOptions::new() .max_connections(1) .connect("sqlite::memory:") .await .expect("sqlite pool should connect"); let score_repository = Arc::new(SqlitePoolMemberScoreRepository::new(pool.clone())); pool.close().await; let app = AppState::new() .expect("app state should build") .with_data_state_for_tests( GatewayDataState::with_provider_catalog_repository_for_tests(provider_repository) .with_pool_score_repository_for_tests(score_repository), ); let error = seed_imported_oauth_pool_score(&AdminAppState::new(&app), "provider-1", &key, 100) .await .expect_err("closed pool must fail OAuth score recovery"); assert!(error .into_message() .contains("failed to recover OAuth pool score for key 'key-1'")); } #[test] fn import_handles_legacy_string_scalars() { assert_eq!( imported_optional_bool(Some(&json!("true"))).unwrap_err(), "字段必须是布尔值" ); assert_eq!( imported_optional_i32(Some(&json!("5")), "rate_limit").unwrap_err(), "rate_limit 必须是整数" ); assert_eq!( imported_optional_u64(Some(&json!("5")), "total_requests").unwrap_err(), "total_requests 必须是非负整数" ); assert_eq!( imported_optional_f64(Some(&json!("1.25000000")), "total_cost_usd").unwrap(), Some(1.25) ); assert_eq!( imported_optional_f64(Some(&json!("not-a-number")), "total_cost_usd").unwrap_err(), "total_cost_usd 必须是有限数值" ); } #[test] fn import_handles_python_isoformat_timestamps() { assert_eq!( imported_rfc3339_to_unix_secs(Some(&json!("2099-01-01T00:00:00+00:00")), "expires_at") .unwrap(), Some(4_070_908_800) ); assert_eq!( imported_rfc3339_to_unix_secs(Some(&json!("2099-01-01T00:00:00")), "expires_at") .unwrap(), Some(4_070_908_800) ); assert_eq!( imported_rfc3339_to_unix_secs(Some(&json!("invalid")), "expires_at").unwrap_err(), "expires_at 必须是 RFC3339 时间" ); } #[test] fn import_preserves_python_wallet_negative_recharge_balance() { let wallet = json!({ "balance": -4.5, "recharge_balance": -5.25, "gift_balance": 0.75, "limit_mode": "finite" }); let wallet = wallet.as_object().expect("wallet should be object"); let target = normalize_imported_wallet_target(Some(wallet), false).unwrap(); assert_eq!(target.recharge_balance, -5.25); assert_eq!(target.gift_balance, 0.75); assert_eq!(target.total_recharged, -5.25); } #[test] fn import_preserves_python_wallet_negative_balance_fallback() { let wallet = json!({ "balance": -4.5, "gift_balance": 0.75, "limit_mode": "finite" }); let wallet = wallet.as_object().expect("wallet should be object"); let target = normalize_imported_wallet_target(Some(wallet), false).unwrap(); assert_eq!(target.recharge_balance, -5.25); assert_eq!(target.gift_balance, 0.75); } #[test] fn import_rejects_legacy_string_lists() { assert_eq!( imported_string_list_from_value(Some(&json!("openai")), "allowed_providers") .unwrap_err(), "allowed_providers 必须是字符串列表" ); assert_eq!( imported_string_list_from_value( Some(&json!("[\"openai:chat\"]")), "allowed_api_formats" ) .unwrap_err(), "allowed_api_formats 必须是字符串列表" ); } }