2026-04-11 21:39:04 +08:00
|
|
|
use crate::handlers::admin::provider::shared::payloads::AdminProviderUpdatePatch;
|
2026-04-09 00:10:38 +08:00
|
|
|
use crate::handlers::admin::provider::shared::support::{
|
|
|
|
|
normalize_provider_billing_type, parse_optional_rfc3339_unix_secs,
|
|
|
|
|
};
|
2026-04-13 16:48:05 +08:00
|
|
|
use crate::handlers::admin::provider::write::normalize::normalize_pool_advanced_config;
|
|
|
|
|
use crate::handlers::admin::provider::write::normalize::normalize_provider_type_input;
|
2026-04-09 00:10:38 +08:00
|
|
|
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,
|
2026-04-11 21:39:04 +08:00
|
|
|
patch: AdminProviderUpdatePatch,
|
2026-04-09 00:10:38 +08:00
|
|
|
) -> Result<StoredProviderCatalogProvider, String> {
|
|
|
|
|
let state = state.as_ref();
|
|
|
|
|
let mut updated = existing.clone();
|
2026-04-11 21:39:04 +08:00
|
|
|
let (fields, payload) = patch.into_parts();
|
2026-04-09 00:10:38 +08:00
|
|
|
|
2026-04-11 21:39:04 +08:00
|
|
|
if fields.contains("name") {
|
2026-04-09 00:10:38 +08:00
|
|
|
let Some(name) = payload.name.as_deref() else {
|
2026-04-11 21:39:04 +08:00
|
|
|
return Err(if fields.is_null("name") {
|
2026-04-09 00:10:38 +08:00
|
|
|
"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();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 21:39:04 +08:00
|
|
|
let target_provider_type = if fields.contains("provider_type") {
|
2026-04-09 00:10:38 +08:00
|
|
|
let Some(provider_type) = payload.provider_type.as_deref() else {
|
2026-04-11 21:39:04 +08:00
|
|
|
return Err(if fields.is_null("provider_type") {
|
2026-04-09 00:10:38 +08:00
|
|
|
"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()
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-11 21:39:04 +08:00
|
|
|
if fields.contains("description") {
|
2026-04-09 00:10:38 +08:00
|
|
|
updated.description = payload
|
|
|
|
|
.description
|
|
|
|
|
.map(|value| value.trim().to_string())
|
|
|
|
|
.filter(|value| !value.is_empty());
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 21:39:04 +08:00
|
|
|
if fields.contains("website") {
|
2026-04-09 00:10:38 +08:00
|
|
|
updated.website = match payload.website {
|
|
|
|
|
None => {
|
2026-04-11 21:39:04 +08:00
|
|
|
if fields.is_null("website") {
|
2026-04-09 00:10:38 +08:00
|
|
|
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())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 21:39:04 +08:00
|
|
|
if fields.contains("billing_type") {
|
2026-04-09 00:10:38 +08:00
|
|
|
let Some(billing_type) = payload.billing_type.as_deref() else {
|
2026-04-11 21:39:04 +08:00
|
|
|
return Err(if fields.is_null("billing_type") {
|
2026-04-09 00:10:38 +08:00
|
|
|
"billing_type 不能为空".to_string()
|
|
|
|
|
} else {
|
|
|
|
|
"billing_type 必须是字符串".to_string()
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
updated.billing_type = Some(normalize_provider_billing_type(billing_type)?);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 21:39:04 +08:00
|
|
|
if fields.contains("monthly_quota_usd") {
|
|
|
|
|
if fields.is_null("monthly_quota_usd") {
|
2026-04-09 00:10:38 +08:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 21:39:04 +08:00
|
|
|
if fields.contains("quota_reset_day") {
|
|
|
|
|
if fields.is_null("quota_reset_day") {
|
2026-04-09 00:10:38 +08:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 21:39:04 +08:00
|
|
|
if fields.contains("quota_last_reset_at") {
|
|
|
|
|
if fields.is_null("quota_last_reset_at") {
|
2026-04-09 00:10:38 +08:00
|
|
|
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",
|
|
|
|
|
)?);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 21:39:04 +08:00
|
|
|
if fields.contains("quota_expires_at") {
|
|
|
|
|
if fields.is_null("quota_expires_at") {
|
2026-04-09 00:10:38 +08:00
|
|
|
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")?);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 21:39:04 +08:00
|
|
|
if fields.contains("provider_priority") {
|
2026-04-09 00:10:38 +08:00
|
|
|
let Some(provider_priority) = payload.provider_priority else {
|
2026-04-11 21:39:04 +08:00
|
|
|
return Err(if fields.is_null("provider_priority") {
|
2026-04-09 00:10:38 +08:00
|
|
|
"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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 21:39:04 +08:00
|
|
|
if fields.contains("keep_priority_on_conversion") {
|
2026-04-09 00:10:38 +08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 21:39:04 +08:00
|
|
|
if fields.contains("is_active") {
|
2026-04-09 00:10:38 +08:00
|
|
|
let Some(is_active) = payload.is_active else {
|
|
|
|
|
return Err("is_active 必须是布尔值".to_string());
|
|
|
|
|
};
|
|
|
|
|
updated.is_active = is_active;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 21:39:04 +08:00
|
|
|
if fields.contains("concurrent_limit") {
|
2026-04-09 00:10:38 +08:00
|
|
|
updated.concurrent_limit = match payload.concurrent_limit {
|
|
|
|
|
Some(value) if value >= 0 => Some(value),
|
|
|
|
|
Some(_) => return Err("concurrent_limit 必须是非负整数".to_string()),
|
|
|
|
|
None => None,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 21:39:04 +08:00
|
|
|
if fields.contains("max_retries") {
|
2026-04-09 00:10:38 +08:00
|
|
|
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,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 21:39:04 +08:00
|
|
|
if fields.contains("proxy") {
|
2026-04-09 00:10:38 +08:00
|
|
|
updated.proxy = normalize_json_object(payload.proxy, "proxy")?;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 21:39:04 +08:00
|
|
|
if fields.contains("stream_first_byte_timeout") {
|
2026-04-09 00:10:38 +08:00
|
|
|
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,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 21:39:04 +08:00
|
|
|
if fields.contains("request_timeout") {
|
2026-04-09 00:10:38 +08:00
|
|
|
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,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 21:39:04 +08:00
|
|
|
if fields.contains("enable_format_conversion") {
|
2026-04-09 00:10:38 +08:00
|
|
|
let Some(enable_format_conversion) = payload.enable_format_conversion else {
|
|
|
|
|
return Err("enable_format_conversion 必须是布尔值".to_string());
|
|
|
|
|
};
|
|
|
|
|
updated.enable_format_conversion = enable_format_conversion;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 21:39:04 +08:00
|
|
|
let config_seed = if fields.contains("config") {
|
2026-04-09 00:10:38 +08:00
|
|
|
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();
|
|
|
|
|
|
2026-04-11 21:39:04 +08:00
|
|
|
if fields.contains("claude_code_advanced") {
|
|
|
|
|
if fields.is_null("claude_code_advanced") {
|
2026-04-09 00:10:38 +08:00
|
|
|
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");
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 21:39:04 +08:00
|
|
|
if fields.contains("pool_advanced") {
|
|
|
|
|
if fields.is_null("pool_advanced") {
|
2026-04-09 00:10:38 +08:00
|
|
|
config_map.remove("pool_advanced");
|
|
|
|
|
} else {
|
2026-04-13 14:01:22 +08:00
|
|
|
let value = normalize_pool_advanced_config(payload.pool_advanced)?
|
2026-04-09 00:10:38 +08:00
|
|
|
.ok_or_else(|| "pool_advanced 必须是 JSON 对象".to_string())?;
|
|
|
|
|
config_map.insert("pool_advanced".to_string(), value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 21:39:04 +08:00
|
|
|
if fields.contains("failover_rules") {
|
|
|
|
|
if fields.is_null("failover_rules") {
|
2026-04-09 00:10:38 +08:00
|
|
|
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)
|
|
|
|
|
}
|