mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-07 09:57:47 +08:00
refactor: 大规模模块拆分与重组,新增 aether-admin crate
- 新建独立 aether-admin crate 承载 admin 相关共享契约与纯辅助函数 - 拆分 ai_pipeline 下 kiro/private_envelope/conversion/planner 等大文件为子模块目录 - 重组 admin handlers 各业务域(billing/oauth/provider/system/users 等)为目录结构,移除 shared.rs/builders.rs 等反模式 - 移除 ai_pipeline runtime adapters 旧实现(claude/openai/gemini/kiro/vertex/antigravity 等),改由 provider transport 统一承载 - 移除 control_facade/execution_facade/auth_snapshot_facade 等冗余 facade 层 - 拆分 query/billing 与 query/monitoring 模块、state/runtime/payments 与 security 模块 - 扩展架构测试覆盖 admin_billing/admin_model/admin_users 等新模块 - 删除 docs/architecture/refactor-execution-plan.md 已完成的执行计划文档
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
use crate::handlers::admin::provider::shared::payloads::AdminProviderKeyCreateRequest;
|
||||
use crate::handlers::admin::provider::write::normalize::{
|
||||
normalize_auth_type, validate_vertex_api_formats,
|
||||
};
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use crate::handlers::admin::shared::{
|
||||
decrypt_catalog_secret_with_fallbacks, encrypt_catalog_secret_with_fallbacks,
|
||||
normalize_json_object, normalize_string_list, parse_catalog_auth_config_json,
|
||||
};
|
||||
use aether_data_contracts::repository::provider_catalog::{
|
||||
StoredProviderCatalogKey, StoredProviderCatalogProvider,
|
||||
};
|
||||
use serde_json::json;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub(crate) async fn build_admin_create_provider_key_record(
|
||||
state: &AdminAppState<'_>,
|
||||
provider: &StoredProviderCatalogProvider,
|
||||
payload: AdminProviderKeyCreateRequest,
|
||||
) -> Result<StoredProviderCatalogKey, String> {
|
||||
let state = state.as_ref();
|
||||
let name = payload.name.trim();
|
||||
if name.is_empty() {
|
||||
return Err("name 为必填字段".to_string());
|
||||
}
|
||||
|
||||
let api_formats = normalize_string_list(payload.api_formats)
|
||||
.ok_or_else(|| "api_formats 为必填字段".to_string())?;
|
||||
let auth_type = normalize_auth_type(payload.auth_type.as_deref())?;
|
||||
validate_vertex_api_formats(&provider.provider_type, &auth_type, &api_formats)?;
|
||||
|
||||
let api_key = payload.api_key.unwrap_or_default().trim().to_string();
|
||||
let auth_config = normalize_json_object(payload.auth_config, "auth_config")?;
|
||||
let auth_config_object = auth_config
|
||||
.as_ref()
|
||||
.and_then(serde_json::Value::as_object)
|
||||
.cloned();
|
||||
|
||||
match auth_type.as_str() {
|
||||
"api_key" => {
|
||||
if api_key.is_empty() {
|
||||
return Err("API Key 认证模式下 api_key 为必填字段".to_string());
|
||||
}
|
||||
}
|
||||
"service_account" => {
|
||||
if auth_config_object.is_none() {
|
||||
return Err("Service Account 认证模式下 auth_config 为必填字段".to_string());
|
||||
}
|
||||
}
|
||||
"oauth" => {
|
||||
if !api_key.is_empty() {
|
||||
return Err("OAuth 认证模式下不允许直接填写 api_key".to_string());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let existing_keys = state
|
||||
.list_provider_catalog_keys_by_provider_ids(std::slice::from_ref(&provider.id))
|
||||
.await
|
||||
.map_err(|err| format!("{err:?}"))?;
|
||||
|
||||
if auth_type == "api_key" {
|
||||
for existing in existing_keys
|
||||
.iter()
|
||||
.filter(|existing| existing.auth_type.trim().eq_ignore_ascii_case("api_key"))
|
||||
{
|
||||
let Some(decrypted) = decrypt_catalog_secret_with_fallbacks(
|
||||
state.encryption_key(),
|
||||
&existing.encrypted_api_key,
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
if decrypted != "__placeholder__" && decrypted == api_key {
|
||||
return Err(format!(
|
||||
"该 API Key 已存在于当前 Provider 中(名称: {})",
|
||||
existing.name
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if auth_type == "service_account" {
|
||||
let new_client_email = auth_config_object
|
||||
.as_ref()
|
||||
.and_then(|config| config.get("client_email"))
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned);
|
||||
if let Some(new_client_email) = new_client_email {
|
||||
for existing in existing_keys.iter().filter(|existing| {
|
||||
matches!(
|
||||
existing.auth_type.trim().to_ascii_lowercase().as_str(),
|
||||
"service_account" | "vertex_ai"
|
||||
)
|
||||
}) {
|
||||
let Some(existing_config) = parse_catalog_auth_config_json(state, existing) else {
|
||||
continue;
|
||||
};
|
||||
let Some(existing_email) = existing_config
|
||||
.get("client_email")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if existing_email == new_client_email {
|
||||
return Err(format!(
|
||||
"该 Service Account ({new_client_email}) 已存在于当前 Provider 中(名称: {})",
|
||||
existing.name
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let encrypted_api_key = match auth_type.as_str() {
|
||||
"api_key" => encrypt_catalog_secret_with_fallbacks(state, &api_key),
|
||||
_ => encrypt_catalog_secret_with_fallbacks(state, "__placeholder__"),
|
||||
}
|
||||
.ok_or_else(|| "gateway 未配置 provider key 加密密钥".to_string())?;
|
||||
|
||||
let encrypted_auth_config = auth_config
|
||||
.as_ref()
|
||||
.map(serde_json::to_string)
|
||||
.transpose()
|
||||
.map_err(|err| err.to_string())?
|
||||
.and_then(|plaintext| encrypt_catalog_secret_with_fallbacks(state, &plaintext));
|
||||
|
||||
let now_unix_secs = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.ok()
|
||||
.map(|duration| duration.as_secs())
|
||||
.unwrap_or(0);
|
||||
let mut key = StoredProviderCatalogKey::new(
|
||||
Uuid::new_v4().to_string(),
|
||||
provider.id.clone(),
|
||||
name.to_string(),
|
||||
auth_type,
|
||||
normalize_json_object(payload.capabilities, "capabilities")?,
|
||||
true,
|
||||
)
|
||||
.map_err(|err| err.to_string())?
|
||||
.with_transport_fields(
|
||||
Some(json!(api_formats)),
|
||||
encrypted_api_key,
|
||||
encrypted_auth_config,
|
||||
normalize_json_object(payload.rate_multipliers, "rate_multipliers")?,
|
||||
None,
|
||||
normalize_string_list(payload.allowed_models).map(|value| json!(value)),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.map_err(|err| err.to_string())?;
|
||||
key.note = payload
|
||||
.note
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty());
|
||||
key.internal_priority = payload.internal_priority.unwrap_or(50);
|
||||
key.rpm_limit = payload.rpm_limit;
|
||||
key.cache_ttl_minutes = payload.cache_ttl_minutes.unwrap_or(5);
|
||||
key.max_probe_interval_minutes = payload.max_probe_interval_minutes.unwrap_or(32);
|
||||
key.request_count = Some(0);
|
||||
key.success_count = Some(0);
|
||||
key.error_count = Some(0);
|
||||
key.total_response_time_ms = Some(0);
|
||||
key.auto_fetch_models = payload.auto_fetch_models.unwrap_or(false);
|
||||
key.locked_models = normalize_string_list(payload.locked_models).map(|value| json!(value));
|
||||
key.model_include_patterns =
|
||||
normalize_string_list(payload.model_include_patterns).map(|value| json!(value));
|
||||
key.model_exclude_patterns =
|
||||
normalize_string_list(payload.model_exclude_patterns).map(|value| json!(value));
|
||||
key.health_by_format = Some(json!({}));
|
||||
key.circuit_breaker_by_format = Some(json!({}));
|
||||
key.created_at_unix_secs = Some(now_unix_secs);
|
||||
key.updated_at_unix_secs = Some(now_unix_secs);
|
||||
Ok(key)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
pub(crate) use self::{
|
||||
create::build_admin_create_provider_key_record, payload::build_admin_provider_keys_payload,
|
||||
update::build_admin_update_provider_key_record,
|
||||
};
|
||||
|
||||
mod create;
|
||||
mod payload;
|
||||
mod update;
|
||||
@@ -0,0 +1,45 @@
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
pub(crate) async fn build_admin_provider_keys_payload(
|
||||
state: &AdminAppState<'_>,
|
||||
provider_id: &str,
|
||||
skip: usize,
|
||||
limit: usize,
|
||||
) -> Option<serde_json::Value> {
|
||||
if !state.has_provider_catalog_data_reader() {
|
||||
return None;
|
||||
}
|
||||
let provider = state
|
||||
.read_provider_catalog_providers_by_ids(&[provider_id.to_string()])
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|mut providers| providers.drain(..).next())?;
|
||||
let mut keys = state
|
||||
.list_provider_catalog_keys_by_provider_ids(std::slice::from_ref(&provider.id))
|
||||
.await
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
keys.sort_by(|left, right| {
|
||||
left.internal_priority
|
||||
.cmp(&right.internal_priority)
|
||||
.then_with(|| {
|
||||
left.created_at_unix_secs
|
||||
.unwrap_or_default()
|
||||
.cmp(&right.created_at_unix_secs.unwrap_or_default())
|
||||
})
|
||||
.then_with(|| left.id.cmp(&right.id))
|
||||
});
|
||||
let now_unix_secs = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.ok()
|
||||
.map(|duration| duration.as_secs())
|
||||
.unwrap_or(0);
|
||||
Some(serde_json::Value::Array(
|
||||
keys.into_iter()
|
||||
.skip(skip)
|
||||
.take(limit)
|
||||
.map(|key| state.build_admin_provider_key_response(&key, now_unix_secs))
|
||||
.collect(),
|
||||
))
|
||||
}
|
||||
+8
-218
@@ -1,193 +1,26 @@
|
||||
use super::normalize::{normalize_auth_type, validate_vertex_api_formats};
|
||||
use crate::handlers::admin::provider::shared::payloads::{
|
||||
AdminProviderKeyCreateRequest, AdminProviderKeyUpdateRequest,
|
||||
use crate::handlers::admin::provider::shared::payloads::AdminProviderKeyUpdateRequest;
|
||||
use crate::handlers::admin::provider::write::normalize::{
|
||||
normalize_auth_type, validate_vertex_api_formats,
|
||||
};
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use crate::handlers::admin::shared::{
|
||||
build_admin_provider_key_response, decrypt_catalog_secret_with_fallbacks,
|
||||
encrypt_catalog_secret_with_fallbacks, json_string_list, normalize_json_object,
|
||||
normalize_string_list, parse_catalog_auth_config_json,
|
||||
decrypt_catalog_secret_with_fallbacks, encrypt_catalog_secret_with_fallbacks, json_string_list,
|
||||
normalize_json_object, normalize_string_list, parse_catalog_auth_config_json,
|
||||
};
|
||||
use crate::AppState;
|
||||
use aether_data_contracts::repository::provider_catalog::{
|
||||
StoredProviderCatalogKey, StoredProviderCatalogProvider,
|
||||
};
|
||||
use serde_json::json;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub(crate) async fn build_admin_create_provider_key_record(
|
||||
state: &AppState,
|
||||
provider: &StoredProviderCatalogProvider,
|
||||
payload: AdminProviderKeyCreateRequest,
|
||||
) -> Result<StoredProviderCatalogKey, String> {
|
||||
let name = payload.name.trim();
|
||||
if name.is_empty() {
|
||||
return Err("name 为必填字段".to_string());
|
||||
}
|
||||
|
||||
let api_formats = normalize_string_list(payload.api_formats)
|
||||
.ok_or_else(|| "api_formats 为必填字段".to_string())?;
|
||||
let auth_type = normalize_auth_type(payload.auth_type.as_deref())?;
|
||||
validate_vertex_api_formats(&provider.provider_type, &auth_type, &api_formats)?;
|
||||
|
||||
let api_key = payload.api_key.unwrap_or_default().trim().to_string();
|
||||
let auth_config = normalize_json_object(payload.auth_config, "auth_config")?;
|
||||
let auth_config_object = auth_config
|
||||
.as_ref()
|
||||
.and_then(serde_json::Value::as_object)
|
||||
.cloned();
|
||||
|
||||
match auth_type.as_str() {
|
||||
"api_key" => {
|
||||
if api_key.is_empty() {
|
||||
return Err("API Key 认证模式下 api_key 为必填字段".to_string());
|
||||
}
|
||||
}
|
||||
"service_account" => {
|
||||
if auth_config_object.is_none() {
|
||||
return Err("Service Account 认证模式下 auth_config 为必填字段".to_string());
|
||||
}
|
||||
}
|
||||
"oauth" => {
|
||||
if !api_key.is_empty() {
|
||||
return Err("OAuth 认证模式下不允许直接填写 api_key".to_string());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let existing_keys = state
|
||||
.list_provider_catalog_keys_by_provider_ids(std::slice::from_ref(&provider.id))
|
||||
.await
|
||||
.map_err(|err| format!("{err:?}"))?;
|
||||
|
||||
if auth_type == "api_key" {
|
||||
for existing in existing_keys
|
||||
.iter()
|
||||
.filter(|existing| existing.auth_type.trim().eq_ignore_ascii_case("api_key"))
|
||||
{
|
||||
let Some(decrypted) = decrypt_catalog_secret_with_fallbacks(
|
||||
state.encryption_key(),
|
||||
&existing.encrypted_api_key,
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
if decrypted != "__placeholder__" && decrypted == api_key {
|
||||
return Err(format!(
|
||||
"该 API Key 已存在于当前 Provider 中(名称: {})",
|
||||
existing.name
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if auth_type == "service_account" {
|
||||
let new_client_email = auth_config_object
|
||||
.as_ref()
|
||||
.and_then(|config| config.get("client_email"))
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned);
|
||||
if let Some(new_client_email) = new_client_email {
|
||||
for existing in existing_keys.iter().filter(|existing| {
|
||||
matches!(
|
||||
existing.auth_type.trim().to_ascii_lowercase().as_str(),
|
||||
"service_account" | "vertex_ai"
|
||||
)
|
||||
}) {
|
||||
let Some(existing_config) = parse_catalog_auth_config_json(state, existing) else {
|
||||
continue;
|
||||
};
|
||||
let Some(existing_email) = existing_config
|
||||
.get("client_email")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if existing_email == new_client_email {
|
||||
return Err(format!(
|
||||
"该 Service Account ({new_client_email}) 已存在于当前 Provider 中(名称: {})",
|
||||
existing.name
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let encrypted_api_key = match auth_type.as_str() {
|
||||
"api_key" => encrypt_catalog_secret_with_fallbacks(state, &api_key),
|
||||
_ => encrypt_catalog_secret_with_fallbacks(state, "__placeholder__"),
|
||||
}
|
||||
.ok_or_else(|| "gateway 未配置 provider key 加密密钥".to_string())?;
|
||||
|
||||
let encrypted_auth_config = auth_config
|
||||
.as_ref()
|
||||
.map(serde_json::to_string)
|
||||
.transpose()
|
||||
.map_err(|err| err.to_string())?
|
||||
.and_then(|plaintext| encrypt_catalog_secret_with_fallbacks(state, &plaintext));
|
||||
|
||||
let now_unix_secs = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.ok()
|
||||
.map(|duration| duration.as_secs())
|
||||
.unwrap_or(0);
|
||||
let mut key = StoredProviderCatalogKey::new(
|
||||
Uuid::new_v4().to_string(),
|
||||
provider.id.clone(),
|
||||
name.to_string(),
|
||||
auth_type,
|
||||
normalize_json_object(payload.capabilities, "capabilities")?,
|
||||
true,
|
||||
)
|
||||
.map_err(|err| err.to_string())?
|
||||
.with_transport_fields(
|
||||
Some(json!(api_formats)),
|
||||
encrypted_api_key,
|
||||
encrypted_auth_config,
|
||||
normalize_json_object(payload.rate_multipliers, "rate_multipliers")?,
|
||||
None,
|
||||
normalize_string_list(payload.allowed_models).map(|value| json!(value)),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.map_err(|err| err.to_string())?;
|
||||
key.note = payload
|
||||
.note
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty());
|
||||
key.internal_priority = payload.internal_priority.unwrap_or(50);
|
||||
key.rpm_limit = payload.rpm_limit;
|
||||
key.cache_ttl_minutes = payload.cache_ttl_minutes.unwrap_or(5);
|
||||
key.max_probe_interval_minutes = payload.max_probe_interval_minutes.unwrap_or(32);
|
||||
key.request_count = Some(0);
|
||||
key.success_count = Some(0);
|
||||
key.error_count = Some(0);
|
||||
key.total_response_time_ms = Some(0);
|
||||
key.auto_fetch_models = payload.auto_fetch_models.unwrap_or(false);
|
||||
key.locked_models = normalize_string_list(payload.locked_models).map(|value| json!(value));
|
||||
key.model_include_patterns =
|
||||
normalize_string_list(payload.model_include_patterns).map(|value| json!(value));
|
||||
key.model_exclude_patterns =
|
||||
normalize_string_list(payload.model_exclude_patterns).map(|value| json!(value));
|
||||
key.health_by_format = Some(json!({}));
|
||||
key.circuit_breaker_by_format = Some(json!({}));
|
||||
key.created_at_unix_secs = Some(now_unix_secs);
|
||||
key.updated_at_unix_secs = Some(now_unix_secs);
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
pub(crate) async fn build_admin_update_provider_key_record(
|
||||
state: &AppState,
|
||||
state: &AdminAppState<'_>,
|
||||
provider: &StoredProviderCatalogProvider,
|
||||
existing: &StoredProviderCatalogKey,
|
||||
raw_payload: &serde_json::Map<String, serde_json::Value>,
|
||||
payload: AdminProviderKeyUpdateRequest,
|
||||
) -> Result<StoredProviderCatalogKey, String> {
|
||||
let state = state.as_ref();
|
||||
let mut updated = existing.clone();
|
||||
let current_auth_type = normalize_auth_type(Some(&existing.auth_type))?;
|
||||
let target_auth_type = payload
|
||||
@@ -449,46 +282,3 @@ pub(crate) async fn build_admin_update_provider_key_record(
|
||||
.map(|duration| duration.as_secs());
|
||||
Ok(updated)
|
||||
}
|
||||
|
||||
pub(crate) async fn build_admin_provider_keys_payload(
|
||||
state: &AppState,
|
||||
provider_id: &str,
|
||||
skip: usize,
|
||||
limit: usize,
|
||||
) -> Option<serde_json::Value> {
|
||||
if !state.has_provider_catalog_data_reader() {
|
||||
return None;
|
||||
}
|
||||
let provider = state
|
||||
.read_provider_catalog_providers_by_ids(&[provider_id.to_string()])
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|mut providers| providers.drain(..).next())?;
|
||||
let mut keys = state
|
||||
.list_provider_catalog_keys_by_provider_ids(std::slice::from_ref(&provider.id))
|
||||
.await
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
keys.sort_by(|left, right| {
|
||||
left.internal_priority
|
||||
.cmp(&right.internal_priority)
|
||||
.then_with(|| {
|
||||
left.created_at_unix_secs
|
||||
.unwrap_or_default()
|
||||
.cmp(&right.created_at_unix_secs.unwrap_or_default())
|
||||
})
|
||||
.then_with(|| left.id.cmp(&right.id))
|
||||
});
|
||||
let now_unix_secs = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.ok()
|
||||
.map(|duration| duration.as_secs())
|
||||
.unwrap_or(0);
|
||||
Some(serde_json::Value::Array(
|
||||
keys.into_iter()
|
||||
.skip(skip)
|
||||
.take(limit)
|
||||
.map(|key| build_admin_provider_key_response(state, &key, now_unix_secs))
|
||||
.collect(),
|
||||
))
|
||||
}
|
||||
@@ -1,511 +1,7 @@
|
||||
use super::normalize::normalize_provider_type_input;
|
||||
use crate::api::ai::{admin_default_body_rules_for_signature, admin_endpoint_signature_parts};
|
||||
use crate::handlers::admin::provider::shared::payloads::{
|
||||
AdminProviderCreateRequest, AdminProviderUpdateRequest,
|
||||
};
|
||||
use crate::handlers::admin::provider::shared::support::{
|
||||
normalize_provider_billing_type, parse_optional_rfc3339_unix_secs,
|
||||
};
|
||||
use crate::handlers::admin::shared::normalize_json_object;
|
||||
use crate::handlers::public::normalize_admin_base_url;
|
||||
use crate::provider_transport::provider_types::provider_type_enables_format_conversion_by_default;
|
||||
use crate::AppState;
|
||||
use aether_data_contracts::repository::provider_catalog::{
|
||||
StoredProviderCatalogEndpoint, StoredProviderCatalogProvider,
|
||||
};
|
||||
use serde_json::json;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use uuid::Uuid;
|
||||
mod create;
|
||||
mod endpoint;
|
||||
mod update;
|
||||
|
||||
pub(crate) async fn build_admin_update_provider_record(
|
||||
state: &AppState,
|
||||
existing: &StoredProviderCatalogProvider,
|
||||
raw_payload: &serde_json::Map<String, serde_json::Value>,
|
||||
payload: AdminProviderUpdateRequest,
|
||||
) -> Result<StoredProviderCatalogProvider, String> {
|
||||
let mut updated = existing.clone();
|
||||
|
||||
if let Some(value) = raw_payload.get("name") {
|
||||
let Some(name) = payload.name.as_deref() else {
|
||||
return Err(if value.is_null() {
|
||||
"name 不能为空".to_string()
|
||||
} else {
|
||||
"name 必须是字符串".to_string()
|
||||
});
|
||||
};
|
||||
let trimmed = name.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Err("name 不能为空".to_string());
|
||||
}
|
||||
let duplicate = state
|
||||
.list_provider_catalog_providers(false)
|
||||
.await
|
||||
.map_err(|err| format!("{err:?}"))?
|
||||
.into_iter()
|
||||
.any(|provider| provider.id != existing.id && provider.name == trimmed);
|
||||
if duplicate {
|
||||
return Err(format!("提供商名称 '{trimmed}' 已存在"));
|
||||
}
|
||||
updated.name = trimmed.to_string();
|
||||
}
|
||||
|
||||
let target_provider_type = if let Some(value) = raw_payload.get("provider_type") {
|
||||
let Some(provider_type) = payload.provider_type.as_deref() else {
|
||||
return Err(if value.is_null() {
|
||||
"provider_type 不能为空".to_string()
|
||||
} else {
|
||||
"provider_type 必须是字符串".to_string()
|
||||
});
|
||||
};
|
||||
let normalized = normalize_provider_type_input(provider_type)?;
|
||||
updated.provider_type = normalized.clone();
|
||||
normalized
|
||||
} else {
|
||||
updated.provider_type.clone()
|
||||
};
|
||||
|
||||
if raw_payload.contains_key("description") {
|
||||
updated.description = payload
|
||||
.description
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty());
|
||||
}
|
||||
|
||||
if let Some(value) = raw_payload.get("website") {
|
||||
updated.website = match payload.website {
|
||||
None => {
|
||||
if value.is_null() {
|
||||
None
|
||||
} else {
|
||||
return Err("website 必须是字符串".to_string());
|
||||
}
|
||||
}
|
||||
Some(website) => {
|
||||
let trimmed = website.trim();
|
||||
if trimmed.is_empty() {
|
||||
None
|
||||
} else if !trimmed.starts_with("http://") && !trimmed.starts_with("https://") {
|
||||
return Err("website 必须以 http:// 或 https:// 开头".to_string());
|
||||
} else {
|
||||
Some(trimmed.to_string())
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(value) = raw_payload.get("billing_type") {
|
||||
let Some(billing_type) = payload.billing_type.as_deref() else {
|
||||
return Err(if value.is_null() {
|
||||
"billing_type 不能为空".to_string()
|
||||
} else {
|
||||
"billing_type 必须是字符串".to_string()
|
||||
});
|
||||
};
|
||||
updated.billing_type = Some(normalize_provider_billing_type(billing_type)?);
|
||||
}
|
||||
|
||||
if let Some(value) = raw_payload.get("monthly_quota_usd") {
|
||||
if value.is_null() {
|
||||
updated.monthly_quota_usd = None;
|
||||
} else {
|
||||
let Some(monthly_quota_usd) = payload.monthly_quota_usd else {
|
||||
return Err("monthly_quota_usd 必须是非负数".to_string());
|
||||
};
|
||||
if !monthly_quota_usd.is_finite() || monthly_quota_usd < 0.0 {
|
||||
return Err("monthly_quota_usd 必须是非负数".to_string());
|
||||
}
|
||||
updated.monthly_quota_usd = Some(monthly_quota_usd);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(value) = raw_payload.get("quota_reset_day") {
|
||||
if value.is_null() {
|
||||
updated.quota_reset_day = None;
|
||||
} else {
|
||||
let Some(quota_reset_day) = payload.quota_reset_day else {
|
||||
return Err("quota_reset_day 必须是 1 到 365 之间的整数".to_string());
|
||||
};
|
||||
if !(1..=365).contains("a_reset_day) {
|
||||
return Err("quota_reset_day 必须是 1 到 365 之间的整数".to_string());
|
||||
}
|
||||
updated.quota_reset_day = Some(quota_reset_day);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(value) = raw_payload.get("quota_last_reset_at") {
|
||||
if value.is_null() {
|
||||
updated.quota_last_reset_at_unix_secs = None;
|
||||
} else {
|
||||
let Some(raw) = payload.quota_last_reset_at.as_deref() else {
|
||||
return Err("quota_last_reset_at 必须是字符串".to_string());
|
||||
};
|
||||
updated.quota_last_reset_at_unix_secs = Some(parse_optional_rfc3339_unix_secs(
|
||||
raw,
|
||||
"quota_last_reset_at",
|
||||
)?);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(value) = raw_payload.get("quota_expires_at") {
|
||||
if value.is_null() {
|
||||
updated.quota_expires_at_unix_secs = None;
|
||||
} else {
|
||||
let Some(raw) = payload.quota_expires_at.as_deref() else {
|
||||
return Err("quota_expires_at 必须是字符串".to_string());
|
||||
};
|
||||
updated.quota_expires_at_unix_secs =
|
||||
Some(parse_optional_rfc3339_unix_secs(raw, "quota_expires_at")?);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(value) = raw_payload.get("provider_priority") {
|
||||
let Some(provider_priority) = payload.provider_priority else {
|
||||
return Err(if value.is_null() {
|
||||
"provider_priority 不能为空".to_string()
|
||||
} else {
|
||||
"provider_priority 必须是整数".to_string()
|
||||
});
|
||||
};
|
||||
if !(0..=10_000).contains(&provider_priority) {
|
||||
return Err("provider_priority 必须在 0 到 10000 之间".to_string());
|
||||
}
|
||||
updated.provider_priority = provider_priority;
|
||||
}
|
||||
|
||||
if let Some(_value) = raw_payload.get("keep_priority_on_conversion") {
|
||||
let Some(keep_priority_on_conversion) = payload.keep_priority_on_conversion else {
|
||||
return Err("keep_priority_on_conversion 必须是布尔值".to_string());
|
||||
};
|
||||
updated.keep_priority_on_conversion = keep_priority_on_conversion;
|
||||
}
|
||||
|
||||
if let Some(_value) = raw_payload.get("is_active") {
|
||||
let Some(is_active) = payload.is_active else {
|
||||
return Err("is_active 必须是布尔值".to_string());
|
||||
};
|
||||
updated.is_active = is_active;
|
||||
}
|
||||
|
||||
if raw_payload.contains_key("concurrent_limit") {
|
||||
updated.concurrent_limit = match payload.concurrent_limit {
|
||||
Some(value) if value >= 0 => Some(value),
|
||||
Some(_) => return Err("concurrent_limit 必须是非负整数".to_string()),
|
||||
None => None,
|
||||
};
|
||||
}
|
||||
|
||||
if raw_payload.contains_key("max_retries") {
|
||||
updated.max_retries = match payload.max_retries {
|
||||
Some(value) if (0..=999).contains(&value) => Some(value),
|
||||
Some(_) => return Err("max_retries 必须是 0 到 999 之间的整数".to_string()),
|
||||
None => None,
|
||||
};
|
||||
}
|
||||
|
||||
if raw_payload.contains_key("proxy") {
|
||||
updated.proxy = normalize_json_object(payload.proxy, "proxy")?;
|
||||
}
|
||||
|
||||
if raw_payload.contains_key("stream_first_byte_timeout") {
|
||||
updated.stream_first_byte_timeout_secs = match payload.stream_first_byte_timeout {
|
||||
Some(value) if (1.0..=300.0).contains(&value) => Some(value),
|
||||
Some(_) => {
|
||||
return Err("stream_first_byte_timeout 必须是 1 到 300 之间的数字".to_string())
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
}
|
||||
|
||||
if raw_payload.contains_key("request_timeout") {
|
||||
updated.request_timeout_secs = match payload.request_timeout {
|
||||
Some(value) if (1.0..=600.0).contains(&value) => Some(value),
|
||||
Some(_) => return Err("request_timeout 必须是 1 到 600 之间的数字".to_string()),
|
||||
None => None,
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(_value) = raw_payload.get("enable_format_conversion") {
|
||||
let Some(enable_format_conversion) = payload.enable_format_conversion else {
|
||||
return Err("enable_format_conversion 必须是布尔值".to_string());
|
||||
};
|
||||
updated.enable_format_conversion = enable_format_conversion;
|
||||
}
|
||||
|
||||
let config_seed = if raw_payload.contains_key("config") {
|
||||
normalize_json_object(payload.config, "config")?
|
||||
} else {
|
||||
updated.config.clone()
|
||||
};
|
||||
let mut config_map = config_seed
|
||||
.and_then(|value| value.as_object().cloned())
|
||||
.unwrap_or_default();
|
||||
|
||||
if raw_payload.contains_key("claude_code_advanced") {
|
||||
if raw_payload
|
||||
.get("claude_code_advanced")
|
||||
.is_some_and(serde_json::Value::is_null)
|
||||
{
|
||||
config_map.remove("claude_code_advanced");
|
||||
} else {
|
||||
if target_provider_type != "claude_code" {
|
||||
return Err("claude_code_advanced 仅适用于 provider_type=claude_code".to_string());
|
||||
}
|
||||
let value =
|
||||
normalize_json_object(payload.claude_code_advanced, "claude_code_advanced")?
|
||||
.ok_or_else(|| "claude_code_advanced 必须是 JSON 对象".to_string())?;
|
||||
config_map.insert("claude_code_advanced".to_string(), value);
|
||||
}
|
||||
} else if target_provider_type != "claude_code" {
|
||||
config_map.remove("claude_code_advanced");
|
||||
}
|
||||
|
||||
if raw_payload.contains_key("pool_advanced") {
|
||||
if raw_payload
|
||||
.get("pool_advanced")
|
||||
.is_some_and(serde_json::Value::is_null)
|
||||
{
|
||||
config_map.remove("pool_advanced");
|
||||
} else {
|
||||
let value = normalize_json_object(payload.pool_advanced, "pool_advanced")?
|
||||
.ok_or_else(|| "pool_advanced 必须是 JSON 对象".to_string())?;
|
||||
config_map.insert("pool_advanced".to_string(), value);
|
||||
}
|
||||
}
|
||||
|
||||
if raw_payload.contains_key("failover_rules") {
|
||||
if raw_payload
|
||||
.get("failover_rules")
|
||||
.is_some_and(serde_json::Value::is_null)
|
||||
{
|
||||
config_map.remove("failover_rules");
|
||||
} else {
|
||||
let value = normalize_json_object(payload.failover_rules, "failover_rules")?
|
||||
.ok_or_else(|| "failover_rules 必须是 JSON 对象".to_string())?;
|
||||
config_map.insert("failover_rules".to_string(), value);
|
||||
}
|
||||
}
|
||||
|
||||
updated.config = (!config_map.is_empty()).then_some(serde_json::Value::Object(config_map));
|
||||
updated.updated_at_unix_secs = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.ok()
|
||||
.map(|duration| duration.as_secs());
|
||||
Ok(updated)
|
||||
}
|
||||
|
||||
pub(crate) async fn build_admin_create_provider_record(
|
||||
state: &AppState,
|
||||
payload: AdminProviderCreateRequest,
|
||||
) -> Result<(StoredProviderCatalogProvider, Option<i32>), String> {
|
||||
let name = payload.name.trim();
|
||||
if name.is_empty() {
|
||||
return Err("name 为必填字段".to_string());
|
||||
}
|
||||
|
||||
let existing_providers = state
|
||||
.list_provider_catalog_providers(false)
|
||||
.await
|
||||
.map_err(|err| format!("{err:?}"))?;
|
||||
if existing_providers
|
||||
.iter()
|
||||
.any(|provider| provider.name == name)
|
||||
{
|
||||
return Err(format!("提供商名称 '{name}' 已存在"));
|
||||
}
|
||||
|
||||
let provider_type =
|
||||
normalize_provider_type_input(payload.provider_type.as_deref().unwrap_or("custom"))?;
|
||||
let billing_type = normalize_provider_billing_type(
|
||||
payload.billing_type.as_deref().unwrap_or("pay_as_you_go"),
|
||||
)?;
|
||||
|
||||
let website = payload.website.and_then(|value| {
|
||||
let trimmed = value.trim().to_string();
|
||||
(!trimmed.is_empty()).then_some(trimmed)
|
||||
});
|
||||
let website = website.map(|value| {
|
||||
if value.starts_with("http://") || value.starts_with("https://") {
|
||||
value
|
||||
} else {
|
||||
format!("https://{value}")
|
||||
}
|
||||
});
|
||||
|
||||
let monthly_quota_usd = match payload.monthly_quota_usd {
|
||||
Some(value) if value.is_finite() && value >= 0.0 => Some(value),
|
||||
Some(_) => return Err("monthly_quota_usd 必须是非负数".to_string()),
|
||||
None => None,
|
||||
};
|
||||
let quota_reset_day = match payload.quota_reset_day {
|
||||
Some(value) if (1..=365).contains(&value) => Some(value),
|
||||
Some(_) => return Err("quota_reset_day 必须是 1 到 365 之间的整数".to_string()),
|
||||
None => Some(30),
|
||||
};
|
||||
let quota_last_reset_at_unix_secs = payload
|
||||
.quota_last_reset_at
|
||||
.as_deref()
|
||||
.map(|value| parse_optional_rfc3339_unix_secs(value, "quota_last_reset_at"))
|
||||
.transpose()?;
|
||||
let quota_expires_at_unix_secs = payload
|
||||
.quota_expires_at
|
||||
.as_deref()
|
||||
.map(|value| parse_optional_rfc3339_unix_secs(value, "quota_expires_at"))
|
||||
.transpose()?;
|
||||
let provider_priority = match payload.provider_priority {
|
||||
Some(value) if (0..=10_000).contains(&value) => value,
|
||||
Some(_) => return Err("provider_priority 必须在 0 到 10000 之间".to_string()),
|
||||
None => {
|
||||
let current_min_priority = existing_providers
|
||||
.iter()
|
||||
.map(|provider| provider.provider_priority)
|
||||
.min();
|
||||
match current_min_priority {
|
||||
Some(value) if value <= 0 => 0,
|
||||
Some(value) => value - 1,
|
||||
None => 100,
|
||||
}
|
||||
}
|
||||
};
|
||||
let shift_existing_priorities_from = match payload.provider_priority {
|
||||
Some(_) => Some(provider_priority),
|
||||
None => existing_providers
|
||||
.iter()
|
||||
.map(|provider| provider.provider_priority)
|
||||
.min()
|
||||
.filter(|value| *value <= 0)
|
||||
.map(|_| 0),
|
||||
};
|
||||
|
||||
let is_active = payload.is_active.unwrap_or(true);
|
||||
let concurrent_limit = match payload.concurrent_limit {
|
||||
Some(value) if value >= 0 => Some(value),
|
||||
Some(_) => return Err("concurrent_limit 必须是非负整数".to_string()),
|
||||
None => None,
|
||||
};
|
||||
let max_retries = match payload.max_retries {
|
||||
Some(value) if (0..=999).contains(&value) => Some(value),
|
||||
Some(_) => return Err("max_retries 必须是 0 到 999 之间的整数".to_string()),
|
||||
None => Some(2),
|
||||
};
|
||||
let proxy = normalize_json_object(payload.proxy, "proxy")?;
|
||||
let stream_first_byte_timeout_secs = match payload.stream_first_byte_timeout {
|
||||
Some(value) if (1.0..=300.0).contains(&value) => Some(value),
|
||||
Some(_) => return Err("stream_first_byte_timeout 必须是 1 到 300 之间的数字".to_string()),
|
||||
None => None,
|
||||
};
|
||||
let request_timeout_secs = match payload.request_timeout {
|
||||
Some(value) if (1.0..=600.0).contains(&value) => Some(value),
|
||||
Some(_) => return Err("request_timeout 必须是 1 到 600 之间的数字".to_string()),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let mut config_map = normalize_json_object(payload.config, "config")?
|
||||
.and_then(|value| value.as_object().cloned())
|
||||
.unwrap_or_default();
|
||||
if let Some(value) = normalize_json_object(payload.pool_advanced, "pool_advanced")? {
|
||||
config_map.insert("pool_advanced".to_string(), value);
|
||||
}
|
||||
if let Some(value) = normalize_json_object(payload.failover_rules, "failover_rules")? {
|
||||
config_map.insert("failover_rules".to_string(), value);
|
||||
}
|
||||
if let Some(value) =
|
||||
normalize_json_object(payload.claude_code_advanced, "claude_code_advanced")?
|
||||
{
|
||||
if provider_type != "claude_code" {
|
||||
return Err("claude_code_advanced 仅适用于 provider_type=claude_code".to_string());
|
||||
}
|
||||
config_map.insert("claude_code_advanced".to_string(), value);
|
||||
}
|
||||
let config = (!config_map.is_empty()).then_some(serde_json::Value::Object(config_map));
|
||||
|
||||
let now_unix_secs = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.ok()
|
||||
.map(|duration| duration.as_secs())
|
||||
.unwrap_or(0);
|
||||
|
||||
let record = StoredProviderCatalogProvider::new(
|
||||
Uuid::new_v4().to_string(),
|
||||
name.to_string(),
|
||||
website,
|
||||
provider_type.clone(),
|
||||
)
|
||||
.map_err(|err| err.to_string())?
|
||||
.with_description(
|
||||
payload
|
||||
.description
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty()),
|
||||
)
|
||||
.with_billing_fields(
|
||||
Some(billing_type),
|
||||
monthly_quota_usd,
|
||||
None,
|
||||
quota_reset_day,
|
||||
quota_last_reset_at_unix_secs,
|
||||
quota_expires_at_unix_secs,
|
||||
)
|
||||
.with_routing_fields(provider_priority)
|
||||
.with_transport_fields(
|
||||
is_active,
|
||||
payload.keep_priority_on_conversion.unwrap_or(false),
|
||||
provider_type_enables_format_conversion_by_default(&provider_type),
|
||||
concurrent_limit,
|
||||
max_retries,
|
||||
proxy,
|
||||
request_timeout_secs,
|
||||
stream_first_byte_timeout_secs,
|
||||
config,
|
||||
)
|
||||
.with_timestamps(Some(now_unix_secs), Some(now_unix_secs));
|
||||
|
||||
Ok((record, shift_existing_priorities_from))
|
||||
}
|
||||
|
||||
pub(crate) fn build_admin_fixed_provider_endpoint_record(
|
||||
provider: &StoredProviderCatalogProvider,
|
||||
api_format: &str,
|
||||
base_url: &str,
|
||||
) -> Result<StoredProviderCatalogEndpoint, String> {
|
||||
let (normalized_api_format, api_family, endpoint_kind) =
|
||||
admin_endpoint_signature_parts(api_format)
|
||||
.ok_or_else(|| format!("无效的 api_format: {api_format}"))?;
|
||||
let body_rules = admin_default_body_rules_for_signature(
|
||||
normalized_api_format,
|
||||
Some(provider.provider_type.as_str()),
|
||||
)
|
||||
.and_then(|(_, rules)| (!rules.is_empty()).then_some(serde_json::Value::Array(rules)));
|
||||
let endpoint_config =
|
||||
if provider.provider_type == "codex" && normalized_api_format == "openai:cli" {
|
||||
Some(json!({ "upstream_stream_policy": "force_stream" }))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let now_unix_secs = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.ok()
|
||||
.map(|duration| duration.as_secs())
|
||||
.unwrap_or(0);
|
||||
|
||||
StoredProviderCatalogEndpoint::new(
|
||||
Uuid::new_v4().to_string(),
|
||||
provider.id.clone(),
|
||||
normalized_api_format.to_string(),
|
||||
Some(api_family.to_string()),
|
||||
Some(endpoint_kind.to_string()),
|
||||
true,
|
||||
)
|
||||
.map_err(|err| err.to_string())?
|
||||
.with_timestamps(Some(now_unix_secs), Some(now_unix_secs))
|
||||
.with_transport_fields(
|
||||
normalize_admin_base_url(base_url)?,
|
||||
None,
|
||||
body_rules,
|
||||
Some(provider.max_retries.unwrap_or(2)),
|
||||
None,
|
||||
endpoint_config,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.map_err(|err| err.to_string())
|
||||
}
|
||||
pub(crate) use self::create::build_admin_create_provider_record;
|
||||
pub(crate) use self::endpoint::build_admin_fixed_provider_endpoint_record;
|
||||
pub(crate) use self::update::build_admin_update_provider_record;
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
use crate::handlers::admin::provider::shared::payloads::AdminProviderCreateRequest;
|
||||
use crate::handlers::admin::provider::shared::support::{
|
||||
normalize_provider_billing_type, parse_optional_rfc3339_unix_secs,
|
||||
};
|
||||
use crate::handlers::admin::provider::write::normalize::normalize_provider_type_input;
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use crate::handlers::admin::shared::normalize_json_object;
|
||||
use aether_data_contracts::repository::provider_catalog::StoredProviderCatalogProvider;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub(crate) async fn build_admin_create_provider_record(
|
||||
state: &AdminAppState<'_>,
|
||||
payload: AdminProviderCreateRequest,
|
||||
) -> Result<(StoredProviderCatalogProvider, Option<i32>), String> {
|
||||
let name = payload.name.trim();
|
||||
if name.is_empty() {
|
||||
return Err("name 为必填字段".to_string());
|
||||
}
|
||||
|
||||
let existing_providers = state
|
||||
.list_provider_catalog_providers(false)
|
||||
.await
|
||||
.map_err(|err| format!("{err:?}"))?;
|
||||
if existing_providers
|
||||
.iter()
|
||||
.any(|provider| provider.name == name)
|
||||
{
|
||||
return Err(format!("提供商名称 '{name}' 已存在"));
|
||||
}
|
||||
|
||||
let provider_type =
|
||||
normalize_provider_type_input(payload.provider_type.as_deref().unwrap_or("custom"))?;
|
||||
let billing_type = normalize_provider_billing_type(
|
||||
payload.billing_type.as_deref().unwrap_or("pay_as_you_go"),
|
||||
)?;
|
||||
|
||||
let website = payload.website.and_then(|value| {
|
||||
let trimmed = value.trim().to_string();
|
||||
(!trimmed.is_empty()).then_some(trimmed)
|
||||
});
|
||||
let website = website.map(|value| {
|
||||
if value.starts_with("http://") || value.starts_with("https://") {
|
||||
value
|
||||
} else {
|
||||
format!("https://{value}")
|
||||
}
|
||||
});
|
||||
|
||||
let monthly_quota_usd = match payload.monthly_quota_usd {
|
||||
Some(value) if value.is_finite() && value >= 0.0 => Some(value),
|
||||
Some(_) => return Err("monthly_quota_usd 必须是非负数".to_string()),
|
||||
None => None,
|
||||
};
|
||||
let quota_reset_day = match payload.quota_reset_day {
|
||||
Some(value) if (1..=365).contains(&value) => Some(value),
|
||||
Some(_) => return Err("quota_reset_day 必须是 1 到 365 之间的整数".to_string()),
|
||||
None => Some(30),
|
||||
};
|
||||
let quota_last_reset_at_unix_secs = payload
|
||||
.quota_last_reset_at
|
||||
.as_deref()
|
||||
.map(|value| parse_optional_rfc3339_unix_secs(value, "quota_last_reset_at"))
|
||||
.transpose()?;
|
||||
let quota_expires_at_unix_secs = payload
|
||||
.quota_expires_at
|
||||
.as_deref()
|
||||
.map(|value| parse_optional_rfc3339_unix_secs(value, "quota_expires_at"))
|
||||
.transpose()?;
|
||||
let provider_priority = match payload.provider_priority {
|
||||
Some(value) if (0..=10_000).contains(&value) => value,
|
||||
Some(_) => return Err("provider_priority 必须在 0 到 10000 之间".to_string()),
|
||||
None => {
|
||||
let current_min_priority = existing_providers
|
||||
.iter()
|
||||
.map(|provider| provider.provider_priority)
|
||||
.min();
|
||||
match current_min_priority {
|
||||
Some(value) if value <= 0 => 0,
|
||||
Some(value) => value - 1,
|
||||
None => 100,
|
||||
}
|
||||
}
|
||||
};
|
||||
let shift_existing_priorities_from = match payload.provider_priority {
|
||||
Some(_) => Some(provider_priority),
|
||||
None => existing_providers
|
||||
.iter()
|
||||
.map(|provider| provider.provider_priority)
|
||||
.min()
|
||||
.filter(|value| *value <= 0)
|
||||
.map(|_| 0),
|
||||
};
|
||||
|
||||
let is_active = payload.is_active.unwrap_or(true);
|
||||
let concurrent_limit = match payload.concurrent_limit {
|
||||
Some(value) if value >= 0 => Some(value),
|
||||
Some(_) => return Err("concurrent_limit 必须是非负整数".to_string()),
|
||||
None => None,
|
||||
};
|
||||
let max_retries = match payload.max_retries {
|
||||
Some(value) if (0..=999).contains(&value) => Some(value),
|
||||
Some(_) => return Err("max_retries 必须是 0 到 999 之间的整数".to_string()),
|
||||
None => Some(2),
|
||||
};
|
||||
let proxy = normalize_json_object(payload.proxy, "proxy")?;
|
||||
let stream_first_byte_timeout_secs = match payload.stream_first_byte_timeout {
|
||||
Some(value) if (1.0..=300.0).contains(&value) => Some(value),
|
||||
Some(_) => return Err("stream_first_byte_timeout 必须是 1 到 300 之间的数字".to_string()),
|
||||
None => None,
|
||||
};
|
||||
let request_timeout_secs = match payload.request_timeout {
|
||||
Some(value) if (1.0..=600.0).contains(&value) => Some(value),
|
||||
Some(_) => return Err("request_timeout 必须是 1 到 600 之间的数字".to_string()),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let mut config_map = normalize_json_object(payload.config, "config")?
|
||||
.and_then(|value| value.as_object().cloned())
|
||||
.unwrap_or_default();
|
||||
if let Some(value) = normalize_json_object(payload.pool_advanced, "pool_advanced")? {
|
||||
config_map.insert("pool_advanced".to_string(), value);
|
||||
}
|
||||
if let Some(value) = normalize_json_object(payload.failover_rules, "failover_rules")? {
|
||||
config_map.insert("failover_rules".to_string(), value);
|
||||
}
|
||||
if let Some(value) =
|
||||
normalize_json_object(payload.claude_code_advanced, "claude_code_advanced")?
|
||||
{
|
||||
if provider_type != "claude_code" {
|
||||
return Err("claude_code_advanced 仅适用于 provider_type=claude_code".to_string());
|
||||
}
|
||||
config_map.insert("claude_code_advanced".to_string(), value);
|
||||
}
|
||||
let config = (!config_map.is_empty()).then_some(serde_json::Value::Object(config_map));
|
||||
|
||||
let now_unix_secs = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.ok()
|
||||
.map(|duration| duration.as_secs())
|
||||
.unwrap_or(0);
|
||||
|
||||
let record = StoredProviderCatalogProvider::new(
|
||||
Uuid::new_v4().to_string(),
|
||||
name.to_string(),
|
||||
website,
|
||||
provider_type.clone(),
|
||||
)
|
||||
.map_err(|err| err.to_string())?
|
||||
.with_description(
|
||||
payload
|
||||
.description
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty()),
|
||||
)
|
||||
.with_billing_fields(
|
||||
Some(billing_type),
|
||||
monthly_quota_usd,
|
||||
None,
|
||||
quota_reset_day,
|
||||
quota_last_reset_at_unix_secs,
|
||||
quota_expires_at_unix_secs,
|
||||
)
|
||||
.with_routing_fields(provider_priority)
|
||||
.with_transport_fields(
|
||||
is_active,
|
||||
payload.keep_priority_on_conversion.unwrap_or(false),
|
||||
state.provider_type_enables_format_conversion_by_default(&provider_type),
|
||||
concurrent_limit,
|
||||
max_retries,
|
||||
proxy,
|
||||
request_timeout_secs,
|
||||
stream_first_byte_timeout_secs,
|
||||
config,
|
||||
)
|
||||
.with_timestamps(Some(now_unix_secs), Some(now_unix_secs));
|
||||
|
||||
Ok((record, shift_existing_priorities_from))
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
use crate::api::ai::{admin_default_body_rules_for_signature, admin_endpoint_signature_parts};
|
||||
use crate::handlers::public::normalize_admin_base_url;
|
||||
use aether_data_contracts::repository::provider_catalog::{
|
||||
StoredProviderCatalogEndpoint, StoredProviderCatalogProvider,
|
||||
};
|
||||
use serde_json::json;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub(crate) fn build_admin_fixed_provider_endpoint_record(
|
||||
provider: &StoredProviderCatalogProvider,
|
||||
api_format: &str,
|
||||
base_url: &str,
|
||||
) -> Result<StoredProviderCatalogEndpoint, String> {
|
||||
let (normalized_api_format, api_family, endpoint_kind) =
|
||||
admin_endpoint_signature_parts(api_format)
|
||||
.ok_or_else(|| format!("无效的 api_format: {api_format}"))?;
|
||||
let body_rules = admin_default_body_rules_for_signature(
|
||||
normalized_api_format,
|
||||
Some(provider.provider_type.as_str()),
|
||||
)
|
||||
.and_then(|(_, rules)| (!rules.is_empty()).then_some(serde_json::Value::Array(rules)));
|
||||
let endpoint_config =
|
||||
if provider.provider_type == "codex" && normalized_api_format == "openai:cli" {
|
||||
Some(json!({ "upstream_stream_policy": "force_stream" }))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let now_unix_secs = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.ok()
|
||||
.map(|duration| duration.as_secs())
|
||||
.unwrap_or(0);
|
||||
|
||||
StoredProviderCatalogEndpoint::new(
|
||||
Uuid::new_v4().to_string(),
|
||||
provider.id.clone(),
|
||||
normalized_api_format.to_string(),
|
||||
Some(api_family.to_string()),
|
||||
Some(endpoint_kind.to_string()),
|
||||
true,
|
||||
)
|
||||
.map_err(|err| err.to_string())?
|
||||
.with_timestamps(Some(now_unix_secs), Some(now_unix_secs))
|
||||
.with_transport_fields(
|
||||
normalize_admin_base_url(base_url)?,
|
||||
None,
|
||||
body_rules,
|
||||
Some(provider.max_retries.unwrap_or(2)),
|
||||
None,
|
||||
endpoint_config,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.map_err(|err| err.to_string())
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
use crate::handlers::admin::provider::shared::payloads::AdminProviderUpdateRequest;
|
||||
use crate::handlers::admin::provider::shared::support::{
|
||||
normalize_provider_billing_type, parse_optional_rfc3339_unix_secs,
|
||||
};
|
||||
use crate::handlers::admin::provider::write::normalize::normalize_provider_type_input;
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use crate::handlers::admin::shared::normalize_json_object;
|
||||
use aether_data_contracts::repository::provider_catalog::StoredProviderCatalogProvider;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
pub(crate) async fn build_admin_update_provider_record(
|
||||
state: &AdminAppState<'_>,
|
||||
existing: &StoredProviderCatalogProvider,
|
||||
raw_payload: &serde_json::Map<String, serde_json::Value>,
|
||||
payload: AdminProviderUpdateRequest,
|
||||
) -> Result<StoredProviderCatalogProvider, String> {
|
||||
let state = state.as_ref();
|
||||
let mut updated = existing.clone();
|
||||
|
||||
if let Some(value) = raw_payload.get("name") {
|
||||
let Some(name) = payload.name.as_deref() else {
|
||||
return Err(if value.is_null() {
|
||||
"name 不能为空".to_string()
|
||||
} else {
|
||||
"name 必须是字符串".to_string()
|
||||
});
|
||||
};
|
||||
let trimmed = name.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Err("name 不能为空".to_string());
|
||||
}
|
||||
let duplicate = state
|
||||
.list_provider_catalog_providers(false)
|
||||
.await
|
||||
.map_err(|err| format!("{err:?}"))?
|
||||
.into_iter()
|
||||
.any(|provider| provider.id != existing.id && provider.name == trimmed);
|
||||
if duplicate {
|
||||
return Err(format!("提供商名称 '{trimmed}' 已存在"));
|
||||
}
|
||||
updated.name = trimmed.to_string();
|
||||
}
|
||||
|
||||
let target_provider_type = if let Some(value) = raw_payload.get("provider_type") {
|
||||
let Some(provider_type) = payload.provider_type.as_deref() else {
|
||||
return Err(if value.is_null() {
|
||||
"provider_type 不能为空".to_string()
|
||||
} else {
|
||||
"provider_type 必须是字符串".to_string()
|
||||
});
|
||||
};
|
||||
let normalized = normalize_provider_type_input(provider_type)?;
|
||||
updated.provider_type = normalized.clone();
|
||||
normalized
|
||||
} else {
|
||||
updated.provider_type.clone()
|
||||
};
|
||||
|
||||
if raw_payload.contains_key("description") {
|
||||
updated.description = payload
|
||||
.description
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty());
|
||||
}
|
||||
|
||||
if let Some(value) = raw_payload.get("website") {
|
||||
updated.website = match payload.website {
|
||||
None => {
|
||||
if value.is_null() {
|
||||
None
|
||||
} else {
|
||||
return Err("website 必须是字符串".to_string());
|
||||
}
|
||||
}
|
||||
Some(website) => {
|
||||
let trimmed = website.trim();
|
||||
if trimmed.is_empty() {
|
||||
None
|
||||
} else if !trimmed.starts_with("http://") && !trimmed.starts_with("https://") {
|
||||
return Err("website 必须以 http:// 或 https:// 开头".to_string());
|
||||
} else {
|
||||
Some(trimmed.to_string())
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(value) = raw_payload.get("billing_type") {
|
||||
let Some(billing_type) = payload.billing_type.as_deref() else {
|
||||
return Err(if value.is_null() {
|
||||
"billing_type 不能为空".to_string()
|
||||
} else {
|
||||
"billing_type 必须是字符串".to_string()
|
||||
});
|
||||
};
|
||||
updated.billing_type = Some(normalize_provider_billing_type(billing_type)?);
|
||||
}
|
||||
|
||||
if let Some(value) = raw_payload.get("monthly_quota_usd") {
|
||||
if value.is_null() {
|
||||
updated.monthly_quota_usd = None;
|
||||
} else {
|
||||
let Some(monthly_quota_usd) = payload.monthly_quota_usd else {
|
||||
return Err("monthly_quota_usd 必须是非负数".to_string());
|
||||
};
|
||||
if !monthly_quota_usd.is_finite() || monthly_quota_usd < 0.0 {
|
||||
return Err("monthly_quota_usd 必须是非负数".to_string());
|
||||
}
|
||||
updated.monthly_quota_usd = Some(monthly_quota_usd);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(value) = raw_payload.get("quota_reset_day") {
|
||||
if value.is_null() {
|
||||
updated.quota_reset_day = None;
|
||||
} else {
|
||||
let Some(quota_reset_day) = payload.quota_reset_day else {
|
||||
return Err("quota_reset_day 必须是 1 到 365 之间的整数".to_string());
|
||||
};
|
||||
if !(1..=365).contains("a_reset_day) {
|
||||
return Err("quota_reset_day 必须是 1 到 365 之间的整数".to_string());
|
||||
}
|
||||
updated.quota_reset_day = Some(quota_reset_day);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(value) = raw_payload.get("quota_last_reset_at") {
|
||||
if value.is_null() {
|
||||
updated.quota_last_reset_at_unix_secs = None;
|
||||
} else {
|
||||
let Some(raw) = payload.quota_last_reset_at.as_deref() else {
|
||||
return Err("quota_last_reset_at 必须是字符串".to_string());
|
||||
};
|
||||
updated.quota_last_reset_at_unix_secs = Some(parse_optional_rfc3339_unix_secs(
|
||||
raw,
|
||||
"quota_last_reset_at",
|
||||
)?);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(value) = raw_payload.get("quota_expires_at") {
|
||||
if value.is_null() {
|
||||
updated.quota_expires_at_unix_secs = None;
|
||||
} else {
|
||||
let Some(raw) = payload.quota_expires_at.as_deref() else {
|
||||
return Err("quota_expires_at 必须是字符串".to_string());
|
||||
};
|
||||
updated.quota_expires_at_unix_secs =
|
||||
Some(parse_optional_rfc3339_unix_secs(raw, "quota_expires_at")?);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(value) = raw_payload.get("provider_priority") {
|
||||
let Some(provider_priority) = payload.provider_priority else {
|
||||
return Err(if value.is_null() {
|
||||
"provider_priority 不能为空".to_string()
|
||||
} else {
|
||||
"provider_priority 必须是整数".to_string()
|
||||
});
|
||||
};
|
||||
if !(0..=10_000).contains(&provider_priority) {
|
||||
return Err("provider_priority 必须在 0 到 10000 之间".to_string());
|
||||
}
|
||||
updated.provider_priority = provider_priority;
|
||||
}
|
||||
|
||||
if let Some(_value) = raw_payload.get("keep_priority_on_conversion") {
|
||||
let Some(keep_priority_on_conversion) = payload.keep_priority_on_conversion else {
|
||||
return Err("keep_priority_on_conversion 必须是布尔值".to_string());
|
||||
};
|
||||
updated.keep_priority_on_conversion = keep_priority_on_conversion;
|
||||
}
|
||||
|
||||
if let Some(_value) = raw_payload.get("is_active") {
|
||||
let Some(is_active) = payload.is_active else {
|
||||
return Err("is_active 必须是布尔值".to_string());
|
||||
};
|
||||
updated.is_active = is_active;
|
||||
}
|
||||
|
||||
if raw_payload.contains_key("concurrent_limit") {
|
||||
updated.concurrent_limit = match payload.concurrent_limit {
|
||||
Some(value) if value >= 0 => Some(value),
|
||||
Some(_) => return Err("concurrent_limit 必须是非负整数".to_string()),
|
||||
None => None,
|
||||
};
|
||||
}
|
||||
|
||||
if raw_payload.contains_key("max_retries") {
|
||||
updated.max_retries = match payload.max_retries {
|
||||
Some(value) if (0..=999).contains(&value) => Some(value),
|
||||
Some(_) => return Err("max_retries 必须是 0 到 999 之间的整数".to_string()),
|
||||
None => None,
|
||||
};
|
||||
}
|
||||
|
||||
if raw_payload.contains_key("proxy") {
|
||||
updated.proxy = normalize_json_object(payload.proxy, "proxy")?;
|
||||
}
|
||||
|
||||
if raw_payload.contains_key("stream_first_byte_timeout") {
|
||||
updated.stream_first_byte_timeout_secs = match payload.stream_first_byte_timeout {
|
||||
Some(value) if (1.0..=300.0).contains(&value) => Some(value),
|
||||
Some(_) => {
|
||||
return Err("stream_first_byte_timeout 必须是 1 到 300 之间的数字".to_string())
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
}
|
||||
|
||||
if raw_payload.contains_key("request_timeout") {
|
||||
updated.request_timeout_secs = match payload.request_timeout {
|
||||
Some(value) if (1.0..=600.0).contains(&value) => Some(value),
|
||||
Some(_) => return Err("request_timeout 必须是 1 到 600 之间的数字".to_string()),
|
||||
None => None,
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(_value) = raw_payload.get("enable_format_conversion") {
|
||||
let Some(enable_format_conversion) = payload.enable_format_conversion else {
|
||||
return Err("enable_format_conversion 必须是布尔值".to_string());
|
||||
};
|
||||
updated.enable_format_conversion = enable_format_conversion;
|
||||
}
|
||||
|
||||
let config_seed = if raw_payload.contains_key("config") {
|
||||
normalize_json_object(payload.config, "config")?
|
||||
} else {
|
||||
updated.config.clone()
|
||||
};
|
||||
let mut config_map = config_seed
|
||||
.and_then(|value| value.as_object().cloned())
|
||||
.unwrap_or_default();
|
||||
|
||||
if raw_payload.contains_key("claude_code_advanced") {
|
||||
if raw_payload
|
||||
.get("claude_code_advanced")
|
||||
.is_some_and(serde_json::Value::is_null)
|
||||
{
|
||||
config_map.remove("claude_code_advanced");
|
||||
} else {
|
||||
if target_provider_type != "claude_code" {
|
||||
return Err("claude_code_advanced 仅适用于 provider_type=claude_code".to_string());
|
||||
}
|
||||
let value =
|
||||
normalize_json_object(payload.claude_code_advanced, "claude_code_advanced")?
|
||||
.ok_or_else(|| "claude_code_advanced 必须是 JSON 对象".to_string())?;
|
||||
config_map.insert("claude_code_advanced".to_string(), value);
|
||||
}
|
||||
} else if target_provider_type != "claude_code" {
|
||||
config_map.remove("claude_code_advanced");
|
||||
}
|
||||
|
||||
if raw_payload.contains_key("pool_advanced") {
|
||||
if raw_payload
|
||||
.get("pool_advanced")
|
||||
.is_some_and(serde_json::Value::is_null)
|
||||
{
|
||||
config_map.remove("pool_advanced");
|
||||
} else {
|
||||
let value = normalize_json_object(payload.pool_advanced, "pool_advanced")?
|
||||
.ok_or_else(|| "pool_advanced 必须是 JSON 对象".to_string())?;
|
||||
config_map.insert("pool_advanced".to_string(), value);
|
||||
}
|
||||
}
|
||||
|
||||
if raw_payload.contains_key("failover_rules") {
|
||||
if raw_payload
|
||||
.get("failover_rules")
|
||||
.is_some_and(serde_json::Value::is_null)
|
||||
{
|
||||
config_map.remove("failover_rules");
|
||||
} else {
|
||||
let value = normalize_json_object(payload.failover_rules, "failover_rules")?
|
||||
.ok_or_else(|| "failover_rules 必须是 JSON 对象".to_string())?;
|
||||
config_map.insert("failover_rules".to_string(), value);
|
||||
}
|
||||
}
|
||||
|
||||
updated.config = (!config_map.is_empty()).then_some(serde_json::Value::Object(config_map));
|
||||
updated.updated_at_unix_secs = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.ok()
|
||||
.map(|duration| duration.as_secs());
|
||||
Ok(updated)
|
||||
}
|
||||
@@ -6,29 +6,27 @@ fn normalize_reveal_auth_type(value: &str) -> &str {
|
||||
}
|
||||
}
|
||||
|
||||
use crate::handlers::admin::shared::{
|
||||
decrypt_catalog_secret_with_fallbacks, parse_catalog_auth_config_json,
|
||||
};
|
||||
use crate::AppState;
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use crate::handlers::admin::shared::parse_catalog_auth_config_json;
|
||||
use aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKey;
|
||||
use chrono::{SecondsFormat, Utc};
|
||||
use serde_json::json;
|
||||
|
||||
pub(crate) fn build_admin_reveal_key_payload(
|
||||
state: &AppState,
|
||||
state: &AdminAppState<'_>,
|
||||
key: &StoredProviderCatalogKey,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let auth_type = normalize_reveal_auth_type(&key.auth_type);
|
||||
if matches!(auth_type, "service_account") {
|
||||
if let Some(auth_config) = parse_catalog_auth_config_json(state, key) {
|
||||
if let Some(auth_config) = state.parse_catalog_auth_config_json(key) {
|
||||
return Ok(json!({
|
||||
"auth_type": auth_type,
|
||||
"auth_config": auth_config,
|
||||
}));
|
||||
}
|
||||
let decrypted =
|
||||
decrypt_catalog_secret_with_fallbacks(state.encryption_key(), &key.encrypted_api_key)
|
||||
.ok_or_else(|| {
|
||||
let decrypted = state
|
||||
.decrypt_catalog_secret_with_fallbacks(&key.encrypted_api_key)
|
||||
.ok_or_else(|| {
|
||||
"无法解密认证配置,可能是加密密钥已更改。请重新添加该密钥。".to_string()
|
||||
})?;
|
||||
if decrypted == "__placeholder__" {
|
||||
@@ -40,11 +38,9 @@ pub(crate) fn build_admin_reveal_key_payload(
|
||||
}));
|
||||
}
|
||||
|
||||
let decrypted =
|
||||
decrypt_catalog_secret_with_fallbacks(state.encryption_key(), &key.encrypted_api_key)
|
||||
.ok_or_else(|| {
|
||||
"无法解密 API Key,可能是加密密钥已更改。请重新添加该密钥。".to_string()
|
||||
})?;
|
||||
let decrypted = state
|
||||
.decrypt_catalog_secret_with_fallbacks(&key.encrypted_api_key)
|
||||
.ok_or_else(|| "无法解密 API Key,可能是加密密钥已更改。请重新添加该密钥。".to_string())?;
|
||||
Ok(json!({
|
||||
"auth_type": auth_type,
|
||||
"api_key": decrypted,
|
||||
@@ -93,7 +89,7 @@ fn provider_oauth_export_payload(
|
||||
}
|
||||
|
||||
pub(crate) async fn build_admin_export_key_payload(
|
||||
state: &AppState,
|
||||
state: &AdminAppState<'_>,
|
||||
key: &StoredProviderCatalogKey,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let auth_type = normalize_reveal_auth_type(&key.auth_type);
|
||||
@@ -107,7 +103,8 @@ pub(crate) async fn build_admin_export_key_payload(
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(|| "缺少认证配置,无法导出".to_string())?;
|
||||
let plaintext = decrypt_catalog_secret_with_fallbacks(state.encryption_key(), ciphertext)
|
||||
let plaintext = state
|
||||
.decrypt_catalog_secret_with_fallbacks(ciphertext)
|
||||
.ok_or_else(|| "无法解密认证配置".to_string())?;
|
||||
let auth_config = serde_json::from_str::<serde_json::Value>(&plaintext)
|
||||
.ok()
|
||||
|
||||
Reference in New Issue
Block a user