feat(admin): 实现系统数据导入导出功能,支持提供商和模型批量配置

This commit is contained in:
fawney19
2026-04-11 21:39:04 +08:00
parent 801e16c988
commit a9f610fa69
36 changed files with 3247 additions and 409 deletions
@@ -1,7 +1,7 @@
use super::AdminAppState;
use crate::handlers::admin::provider::shared::payloads::{
AdminImportProviderModelsRequest, AdminProviderModelCreateRequest,
AdminProviderModelUpdateRequest,
AdminProviderModelUpdatePatch,
};
use crate::handlers::admin::shared::{normalize_json_array, normalize_json_object};
use crate::GatewayError;
@@ -128,12 +128,12 @@ impl<'a> AdminAppState<'a> {
pub(crate) async fn build_admin_provider_model_update_record(
&self,
existing: &StoredAdminProviderModel,
raw_payload: &serde_json::Map<String, serde_json::Value>,
payload: AdminProviderModelUpdateRequest,
patch: AdminProviderModelUpdatePatch,
) -> Result<UpsertAdminProviderModelRecord, String> {
let provider_model_name = if let Some(value) = raw_payload.get("provider_model_name") {
let (fields, payload) = patch.into_parts();
let provider_model_name = if fields.contains("provider_model_name") {
let Some(name) = payload.provider_model_name.as_deref() else {
return Err(if value.is_null() {
return Err(if fields.is_null("provider_model_name") {
"provider_model_name 不能为空".to_string()
} else {
"provider_model_name 必须是字符串".to_string()
@@ -155,9 +155,9 @@ impl<'a> AdminAppState<'a> {
existing.provider_model_name.clone()
};
let global_model_id = if let Some(value) = raw_payload.get("global_model_id") {
let global_model_id = if fields.contains("global_model_id") {
let Some(global_model_id) = payload.global_model_id.as_deref() else {
return Err(if value.is_null() {
return Err(if fields.is_null("global_model_id") {
"global_model_id 不能为空".to_string()
} else {
"global_model_id 必须是字符串".to_string()
@@ -175,7 +175,7 @@ impl<'a> AdminAppState<'a> {
existing.global_model_id.clone()
};
let price_per_request = if raw_payload.contains_key("price_per_request") {
let price_per_request = if fields.contains("price_per_request") {
admin_provider_models_write_pure::normalize_optional_price(
payload.price_per_request,
"price_per_request",
@@ -183,17 +183,17 @@ impl<'a> AdminAppState<'a> {
} else {
existing.price_per_request
};
let tiered_pricing = if raw_payload.contains_key("tiered_pricing") {
let tiered_pricing = if fields.contains("tiered_pricing") {
normalize_json_object(payload.tiered_pricing, "tiered_pricing")?
} else {
existing.tiered_pricing.clone()
};
let provider_model_mappings = if raw_payload.contains_key("provider_model_mappings") {
let provider_model_mappings = if fields.contains("provider_model_mappings") {
normalize_json_array(payload.provider_model_mappings, "provider_model_mappings")?
} else {
existing.provider_model_mappings.clone()
};
let config = if raw_payload.contains_key("config") {
let config = if fields.contains("config") {
normalize_json_object(payload.config, "config")?
} else {
existing.config.clone()
@@ -206,22 +206,22 @@ impl<'a> AdminAppState<'a> {
provider_model_mappings,
price_per_request,
tiered_pricing,
if raw_payload.contains_key("supports_vision") {
if fields.contains("supports_vision") {
payload.supports_vision
} else {
existing.supports_vision
},
if raw_payload.contains_key("supports_function_calling") {
if fields.contains("supports_function_calling") {
payload.supports_function_calling
} else {
existing.supports_function_calling
},
if raw_payload.contains_key("supports_streaming") {
if fields.contains("supports_streaming") {
payload.supports_streaming
} else {
existing.supports_streaming
},
if raw_payload.contains_key("supports_extended_thinking") {
if fields.contains("supports_extended_thinking") {
payload.supports_extended_thinking
} else {
existing.supports_extended_thinking
@@ -23,16 +23,11 @@ impl<'a> AdminAppState<'a> {
&self,
provider: &aether_data_contracts::repository::provider_catalog::StoredProviderCatalogProvider,
existing: &aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKey,
raw_payload: &serde_json::Map<String, serde_json::Value>,
payload: crate::handlers::admin::provider::shared::payloads::AdminProviderKeyUpdateRequest,
patch: crate::handlers::admin::provider::shared::payloads::AdminProviderKeyUpdatePatch,
) -> Result<aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKey, String>
{
crate::handlers::admin::provider::write::keys::build_admin_update_provider_key_record(
self,
provider,
existing,
raw_payload,
payload,
self, provider, existing, patch,
)
.await
}
@@ -130,17 +125,13 @@ impl<'a> AdminAppState<'a> {
pub(crate) async fn build_admin_update_provider_record(
&self,
existing: &aether_data_contracts::repository::provider_catalog::StoredProviderCatalogProvider,
raw_payload: &serde_json::Map<String, serde_json::Value>,
payload: crate::handlers::admin::provider::shared::payloads::AdminProviderUpdateRequest,
patch: crate::handlers::admin::provider::shared::payloads::AdminProviderUpdatePatch,
) -> Result<
aether_data_contracts::repository::provider_catalog::StoredProviderCatalogProvider,
String,
> {
crate::handlers::admin::provider::write::provider::build_admin_update_provider_record(
self,
existing,
raw_payload,
payload,
self, existing, patch,
)
.await
}
@@ -277,8 +268,7 @@ impl<'a> AdminAppState<'a> {
&self,
provider: &aether_data_contracts::repository::provider_catalog::StoredProviderCatalogProvider,
existing_endpoint: &aether_data_contracts::repository::provider_catalog::StoredProviderCatalogEndpoint,
raw_payload: &serde_json::Map<String, serde_json::Value>,
payload: crate::handlers::admin::provider::endpoints_admin::payloads::AdminProviderEndpointUpdateRequest,
patch: crate::handlers::admin::provider::endpoints_admin::payloads::AdminProviderEndpointUpdatePatch,
) -> Result<
aether_data_contracts::repository::provider_catalog::StoredProviderCatalogEndpoint,
String,
@@ -286,9 +276,10 @@ impl<'a> AdminAppState<'a> {
use crate::api::ai::admin_endpoint_signature_parts;
use crate::handlers::public::{admin_requested_force_stream, normalize_admin_base_url};
use aether_admin::provider::endpoints as admin_provider_endpoints_pure;
let (fields, payload) = patch.into_parts();
if self.provider_type_is_fixed(&provider.provider_type)
&& (raw_payload.contains_key("base_url") || raw_payload.contains_key("custom_path"))
&& (fields.contains("base_url") || fields.contains("custom_path"))
{
return Err(
"固定类型 Provider 的 Endpoint 不允许修改 base_url/custom_path".to_string(),
@@ -312,13 +303,14 @@ impl<'a> AdminAppState<'a> {
let mut updated =
admin_provider_endpoints_pure::apply_admin_provider_endpoint_update_fields(
existing_endpoint,
raw_payload,
|field| fields.contains(field),
|field| fields.is_null(field),
&update_fields,
)?;
let provider_type = provider.provider_type.trim().to_ascii_lowercase();
if provider_type == "codex" && existing_endpoint.api_format == "openai:cli" {
let has_config_in_payload = raw_payload.contains_key("config");
let has_config_in_payload = fields.contains("config");
let config_payload = if has_config_in_payload {
updated
.config
@@ -2,11 +2,15 @@ use crate::handlers::admin::request::AdminAppState;
use crate::handlers::admin::system::shared::configs::is_sensitive_admin_system_config_key;
use crate::handlers::admin::system::shared::export::{
build_admin_system_export_providers_payload, decrypt_admin_system_export_secret,
ADMIN_SYSTEM_CONFIG_EXPORT_VERSION, ADMIN_SYSTEM_EXPORT_PAGE_LIMIT,
ADMIN_SYSTEM_EXPORT_PAGE_LIMIT,
};
use crate::handlers::shared::{system_config_string, unix_secs_to_rfc3339};
use crate::GatewayError;
use aether_admin::system::serialize_admin_system_users_export_wallet;
use aether_admin::system::{
serialize_admin_system_users_export_wallet, AdminSystemConfigDocument, AdminSystemConfigEntry,
AdminSystemConfigGlobalModel, AdminSystemConfigLdap, AdminSystemConfigOAuthProvider,
AdminSystemConfigProxyNode, ADMIN_SYSTEM_CONFIG_EXPORT_VERSION,
};
use aether_data_contracts::repository::global_models::AdminGlobalModelListQuery;
use chrono::Utc;
use serde_json::json;
@@ -31,42 +35,50 @@ impl<'a> AdminAppState<'a> {
.collect::<BTreeMap<_, _>>();
let global_models_data = global_models
.iter()
.map(|model| {
json!({
"name": model.name,
"display_name": model.display_name,
"default_price_per_request": model.default_price_per_request,
"default_tiered_pricing": model.default_tiered_pricing,
"supported_capabilities": model.supported_capabilities,
"config": model.config,
"is_active": model.is_active,
})
.map(|model| AdminSystemConfigGlobalModel {
name: model.name.clone(),
display_name: model.display_name.clone(),
default_price_per_request: model.default_price_per_request,
default_tiered_pricing: model.default_tiered_pricing.clone(),
supported_capabilities: model.supported_capabilities.as_ref().and_then(|value| {
value.as_array().map(|items| {
items
.iter()
.filter_map(serde_json::Value::as_str)
.map(ToOwned::to_owned)
.collect::<Vec<_>>()
})
}),
config: model.config.clone(),
is_active: model.is_active,
})
.collect::<Vec<_>>();
let providers_data =
build_admin_system_export_providers_payload(self, &global_model_name_by_id).await?;
let ldap_data = self.get_ldap_module_config().await?.map(|config| {
let bind_password = config
.bind_password_encrypted
.as_deref()
.and_then(|ciphertext| decrypt_admin_system_export_secret(self, ciphertext))
.unwrap_or_default();
json!({
"server_url": config.server_url,
"bind_dn": config.bind_dn,
"bind_password": bind_password,
"base_dn": config.base_dn,
"user_search_filter": config.user_search_filter,
"username_attr": config.username_attr,
"email_attr": config.email_attr,
"display_name_attr": config.display_name_attr,
"is_enabled": config.is_enabled,
"is_exclusive": config.is_exclusive,
"use_starttls": config.use_starttls,
"connect_timeout": config.connect_timeout,
})
});
let ldap_data = self
.get_ldap_module_config()
.await?
.map(|config| AdminSystemConfigLdap {
server_url: config.server_url,
bind_dn: config.bind_dn,
bind_password: Some(
config
.bind_password_encrypted
.as_deref()
.and_then(|ciphertext| decrypt_admin_system_export_secret(self, ciphertext))
.unwrap_or_default(),
),
base_dn: config.base_dn,
user_search_filter: config.user_search_filter,
username_attr: config.username_attr,
email_attr: config.email_attr,
display_name_attr: config.display_name_attr,
is_enabled: config.is_enabled,
is_exclusive: config.is_exclusive,
use_starttls: config.use_starttls,
connect_timeout: config.connect_timeout,
});
let system_configs = self.list_system_config_entries().await?;
let system_configs_data = system_configs
@@ -82,73 +94,72 @@ impl<'a> AdminAppState<'a> {
} else {
entry.value.clone()
};
json!({
"key": entry.key,
"value": value,
"description": entry.description,
})
AdminSystemConfigEntry {
key: entry.key.clone(),
value,
description: entry.description.clone(),
}
})
.collect::<Vec<_>>();
let oauth_providers = self.list_oauth_provider_configs().await?;
let oauth_data = oauth_providers
.iter()
.map(|provider| {
let client_secret = provider
.client_secret_encrypted
.as_deref()
.and_then(|ciphertext| decrypt_admin_system_export_secret(self, ciphertext))
.unwrap_or_default();
json!({
"provider_type": provider.provider_type,
"display_name": provider.display_name,
"client_id": provider.client_id,
"client_secret": client_secret,
"authorization_url_override": provider.authorization_url_override,
"token_url_override": provider.token_url_override,
"userinfo_url_override": provider.userinfo_url_override,
"scopes": provider.scopes,
"redirect_uri": provider.redirect_uri,
"frontend_callback_url": provider.frontend_callback_url,
"attribute_mapping": provider.attribute_mapping,
"extra_config": provider.extra_config,
"is_enabled": provider.is_enabled,
})
.map(|provider| AdminSystemConfigOAuthProvider {
provider_type: provider.provider_type.clone(),
display_name: provider.display_name.clone(),
client_id: provider.client_id.clone(),
client_secret: Some(
provider
.client_secret_encrypted
.as_deref()
.and_then(|ciphertext| decrypt_admin_system_export_secret(self, ciphertext))
.unwrap_or_default(),
),
authorization_url_override: provider.authorization_url_override.clone(),
token_url_override: provider.token_url_override.clone(),
userinfo_url_override: provider.userinfo_url_override.clone(),
scopes: provider.scopes.clone(),
redirect_uri: provider.redirect_uri.clone(),
frontend_callback_url: provider.frontend_callback_url.clone(),
attribute_mapping: provider.attribute_mapping.clone(),
extra_config: provider.extra_config.clone(),
is_enabled: provider.is_enabled,
})
.collect::<Vec<_>>();
let proxy_nodes = self.list_proxy_nodes().await?;
let proxy_nodes_data = proxy_nodes
.iter()
.map(|node| {
json!({
"id": node.id,
"name": node.name,
"ip": node.ip,
"port": node.port,
"region": node.region,
"is_manual": node.is_manual,
"proxy_url": node.proxy_url,
"proxy_username": node.proxy_username,
"proxy_password": node.proxy_password,
"tunnel_mode": node.tunnel_mode,
"heartbeat_interval": node.heartbeat_interval,
"remote_config": node.remote_config,
"config_version": node.config_version,
})
.map(|node| AdminSystemConfigProxyNode {
id: Some(node.id.clone()),
name: Some(node.name.clone()),
ip: Some(node.ip.clone()),
port: Some(node.port),
region: node.region.clone(),
is_manual: Some(node.is_manual),
proxy_url: node.proxy_url.clone(),
proxy_username: node.proxy_username.clone(),
proxy_password: node.proxy_password.clone(),
tunnel_mode: Some(node.tunnel_mode),
heartbeat_interval: Some(node.heartbeat_interval),
remote_config: node.remote_config.clone(),
config_version: Some(node.config_version),
})
.collect::<Vec<_>>();
Ok(json!({
"version": ADMIN_SYSTEM_CONFIG_EXPORT_VERSION,
"exported_at": Utc::now().to_rfc3339(),
"global_models": global_models_data,
"providers": providers_data,
"proxy_nodes": proxy_nodes_data,
"ldap_config": ldap_data,
"oauth_providers": oauth_data,
"system_configs": system_configs_data,
}))
let document = AdminSystemConfigDocument {
version: ADMIN_SYSTEM_CONFIG_EXPORT_VERSION.to_string(),
exported_at: Utc::now().to_rfc3339(),
global_models: global_models_data,
providers: providers_data,
proxy_nodes: proxy_nodes_data,
ldap_config: ldap_data,
oauth_providers: oauth_data,
system_configs: system_configs_data,
};
serde_json::to_value(document).map_err(|err| GatewayError::Internal(err.to_string()))
}
pub(crate) async fn build_admin_system_users_export_payload(
File diff suppressed because it is too large Load Diff
@@ -3,6 +3,7 @@ use crate::GatewayError;
mod adaptive;
mod export;
mod import;
mod modules;
mod proxy_nodes;
mod templates;