mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-10 19:29:50 +08:00
feat(admin): 实现系统数据导入导出功能,支持提供商和模型批量配置
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user