Files
Aether/crates/aether-admin/src/system.rs

2099 lines
68 KiB
Rust
Raw Normal View History

use aether_data::repository::{
auth_modules::{StoredLdapModuleConfig, StoredOAuthProviderModuleConfig},
proxy_nodes::{StoredProxyNode, StoredProxyNodeEvent},
system::StoredSystemConfigEntry,
wallet::StoredWalletSnapshot,
};
use aether_data_contracts::repository::provider_catalog::{
StoredProviderCatalogEndpoint, StoredProviderCatalogKey,
};
use axum::http;
use axum::{
body::Body,
response::{IntoResponse, Response},
Json,
};
use serde::{de, de::DeserializeOwned, Deserialize, Serialize};
use serde_json::{json, Map, Value};
use std::collections::BTreeSet;
#[derive(Debug, Clone)]
pub struct AdminSystemSettingsUpdate {
pub default_provider: Option<Option<String>>,
pub default_model: Option<Option<String>>,
pub enable_usage_tracking: Option<bool>,
pub password_policy_level: Option<String>,
}
#[derive(Debug, Clone)]
pub struct AdminSystemConfigUpdate {
pub normalized_key: String,
pub value: serde_json::Value,
pub description: Option<String>,
}
#[derive(Debug, Clone)]
pub struct AdminEmailTemplateUpdate {
pub subject: Option<String>,
pub html: Option<String>,
}
pub const ADMIN_SYSTEM_CONFIG_EXPORT_VERSION: &str = "2.2";
pub const ADMIN_SYSTEM_CONFIG_SUPPORTED_VERSIONS: &[&str] = &[ADMIN_SYSTEM_CONFIG_EXPORT_VERSION];
pub const ADMIN_SYSTEM_PROVIDER_OPS_SENSITIVE_CREDENTIAL_FIELDS: &[&str] = &[
"api_key",
"password",
"refresh_token",
"session_token",
"session_cookie",
"token_cookie",
"auth_cookie",
"cookie_string",
"cookie",
];
fn default_true() -> bool {
true
}
fn invalid_request(detail: impl Into<String>) -> (http::StatusCode, serde_json::Value) {
(
http::StatusCode::BAD_REQUEST,
json!({ "detail": detail.into() }),
)
}
fn deserialize_optional_f64_from_number<'de, D>(deserializer: D) -> Result<Option<f64>, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = Option::<Value>::deserialize(deserializer)?;
match value {
None | Some(Value::Null) => Ok(None),
Some(Value::Number(number)) => number
.as_f64()
.filter(|value| value.is_finite())
.map(Some)
.ok_or_else(|| de::Error::custom("expected a finite number")),
Some(_) => Err(de::Error::custom("expected a finite number")),
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum AdminImportMergeMode {
#[default]
Skip,
Overwrite,
Error,
}
impl AdminImportMergeMode {
fn parse_json_value(
value: Option<&serde_json::Value>,
) -> Result<Self, (http::StatusCode, serde_json::Value)> {
match value
.and_then(serde_json::Value::as_str)
.unwrap_or("skip")
.trim()
{
"" | "skip" => Ok(Self::Skip),
"overwrite" => Ok(Self::Overwrite),
"error" => Ok(Self::Error),
_ => Err(invalid_request(
"merge_mode 仅支持 skip / overwrite / error",
)),
}
}
}
impl<'de> Deserialize<'de> for AdminImportMergeMode {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = Option::<serde_json::Value>::deserialize(deserializer)?;
match value {
None | Some(serde_json::Value::Null) => Ok(Self::Skip),
Some(serde_json::Value::String(raw)) => match raw.trim() {
"" | "skip" => Ok(Self::Skip),
"overwrite" => Ok(Self::Overwrite),
"error" => Ok(Self::Error),
_ => Err(de::Error::custom(
"merge_mode 仅支持 skip / overwrite / error",
)),
},
Some(_) => Err(de::Error::custom(
"merge_mode 仅支持 skip / overwrite / error",
)),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct AdminSystemConfigImportCounter {
pub created: u64,
pub updated: u64,
pub skipped: u64,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct AdminSystemConfigImportStats {
pub global_models: AdminSystemConfigImportCounter,
pub proxy_nodes: AdminSystemConfigImportCounter,
pub providers: AdminSystemConfigImportCounter,
pub endpoints: AdminSystemConfigImportCounter,
pub keys: AdminSystemConfigImportCounter,
pub models: AdminSystemConfigImportCounter,
pub ldap: AdminSystemConfigImportCounter,
pub oauth: AdminSystemConfigImportCounter,
pub system_configs: AdminSystemConfigImportCounter,
pub errors: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AdminSystemConfigGlobalModel {
pub name: String,
pub display_name: String,
#[serde(default, deserialize_with = "deserialize_optional_f64_from_number")]
pub default_price_per_request: Option<f64>,
#[serde(default)]
pub default_tiered_pricing: Option<Value>,
#[serde(default)]
pub supported_capabilities: Option<Vec<String>>,
#[serde(default)]
pub config: Option<Value>,
#[serde(default = "default_true")]
pub is_active: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AdminSystemConfigEndpoint {
pub api_format: String,
pub base_url: String,
#[serde(default)]
pub header_rules: Option<Value>,
#[serde(default)]
pub body_rules: Option<Value>,
#[serde(default)]
pub max_retries: Option<i32>,
#[serde(default = "default_true")]
pub is_active: bool,
#[serde(default)]
pub custom_path: Option<String>,
#[serde(default)]
pub config: Option<Value>,
#[serde(default)]
pub format_acceptance_config: Option<Value>,
#[serde(default)]
pub proxy: Option<Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AdminSystemConfigProviderKey {
#[serde(default)]
pub api_key: Option<String>,
#[serde(default)]
pub auth_type: Option<String>,
#[serde(default)]
pub auth_config: Option<Value>,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub note: Option<String>,
#[serde(default)]
pub api_formats: Option<Vec<String>>,
#[serde(default)]
pub supported_endpoints: Option<Vec<String>>,
#[serde(default)]
pub rate_multipliers: Option<Value>,
#[serde(default)]
pub internal_priority: Option<i32>,
#[serde(default)]
pub global_priority_by_format: Option<Value>,
#[serde(default)]
2026-04-29 15:46:50 +08:00
pub auth_type_by_format: Option<Value>,
#[serde(default)]
pub allow_auth_channel_mismatch_formats: Option<Vec<String>>,
#[serde(default)]
pub rpm_limit: Option<u32>,
#[serde(default)]
pub allowed_models: Option<Vec<String>>,
#[serde(default)]
pub capabilities: Option<Value>,
#[serde(default)]
pub cache_ttl_minutes: Option<i32>,
#[serde(default)]
pub max_probe_interval_minutes: Option<i32>,
#[serde(default)]
pub auto_fetch_models: Option<bool>,
#[serde(default)]
pub locked_models: Option<Vec<String>>,
#[serde(default)]
pub model_include_patterns: Option<Vec<String>>,
#[serde(default)]
pub model_exclude_patterns: Option<Vec<String>>,
#[serde(default = "default_true")]
pub is_active: bool,
#[serde(default)]
pub proxy: Option<Value>,
#[serde(default)]
pub fingerprint: Option<Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AdminSystemConfigProviderModel {
#[serde(default)]
pub global_model_name: Option<String>,
pub provider_model_name: String,
#[serde(default)]
pub provider_model_mappings: Option<Value>,
#[serde(default, deserialize_with = "deserialize_optional_f64_from_number")]
pub price_per_request: Option<f64>,
#[serde(default)]
pub tiered_pricing: Option<Value>,
#[serde(default)]
pub supports_vision: Option<bool>,
#[serde(default)]
pub supports_function_calling: Option<bool>,
#[serde(default)]
pub supports_streaming: Option<bool>,
#[serde(default)]
pub supports_extended_thinking: Option<bool>,
#[serde(default)]
pub supports_image_generation: Option<bool>,
#[serde(default = "default_true")]
pub is_active: bool,
#[serde(default)]
pub config: Option<Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AdminSystemConfigProvider {
pub name: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub website: Option<String>,
#[serde(default)]
pub provider_type: Option<String>,
#[serde(default)]
pub billing_type: Option<String>,
#[serde(default, deserialize_with = "deserialize_optional_f64_from_number")]
pub monthly_quota_usd: Option<f64>,
#[serde(default)]
pub quota_reset_day: Option<u64>,
#[serde(default)]
pub provider_priority: Option<i32>,
#[serde(default)]
pub keep_priority_on_conversion: Option<bool>,
#[serde(default)]
pub enable_format_conversion: Option<bool>,
#[serde(default = "default_true")]
pub is_active: bool,
#[serde(default)]
pub concurrent_limit: Option<i32>,
#[serde(default)]
pub max_retries: Option<i32>,
#[serde(default, deserialize_with = "deserialize_optional_f64_from_number")]
pub stream_first_byte_timeout: Option<f64>,
#[serde(default, deserialize_with = "deserialize_optional_f64_from_number")]
pub request_timeout: Option<f64>,
#[serde(default)]
pub proxy: Option<Value>,
#[serde(default)]
pub config: Option<Value>,
#[serde(default)]
pub endpoints: Vec<AdminSystemConfigEndpoint>,
#[serde(default)]
pub api_keys: Vec<AdminSystemConfigProviderKey>,
#[serde(default)]
pub models: Vec<AdminSystemConfigProviderModel>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AdminSystemConfigProxyNode {
#[serde(default)]
pub id: Option<String>,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub ip: Option<String>,
#[serde(default)]
pub port: Option<i32>,
#[serde(default)]
pub region: Option<String>,
#[serde(default)]
pub is_manual: Option<bool>,
#[serde(default)]
pub proxy_url: Option<String>,
#[serde(default)]
pub proxy_username: Option<String>,
#[serde(default)]
pub proxy_password: Option<String>,
#[serde(default)]
pub tunnel_mode: Option<bool>,
#[serde(default)]
pub heartbeat_interval: Option<i32>,
#[serde(default)]
pub remote_config: Option<Value>,
#[serde(default)]
pub config_version: Option<i32>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AdminSystemConfigLdap {
pub server_url: String,
pub bind_dn: String,
#[serde(default)]
pub bind_password: Option<String>,
pub base_dn: String,
#[serde(default)]
pub user_search_filter: Option<String>,
#[serde(default)]
pub username_attr: Option<String>,
#[serde(default)]
pub email_attr: Option<String>,
#[serde(default)]
pub display_name_attr: Option<String>,
#[serde(default)]
pub is_enabled: bool,
#[serde(default)]
pub is_exclusive: bool,
#[serde(default)]
pub use_starttls: bool,
#[serde(default)]
pub connect_timeout: Option<i32>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AdminSystemConfigOAuthProvider {
pub provider_type: String,
pub display_name: String,
pub client_id: String,
#[serde(default)]
pub client_secret: Option<String>,
#[serde(default)]
pub authorization_url_override: Option<String>,
#[serde(default)]
pub token_url_override: Option<String>,
#[serde(default)]
pub userinfo_url_override: Option<String>,
#[serde(default)]
pub scopes: Option<Vec<String>>,
pub redirect_uri: String,
pub frontend_callback_url: String,
#[serde(default)]
pub attribute_mapping: Option<Value>,
#[serde(default)]
pub extra_config: Option<Value>,
#[serde(default)]
pub is_enabled: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AdminSystemConfigEntry {
pub key: String,
#[serde(default)]
pub value: Value,
#[serde(default)]
pub description: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AdminSystemConfigDocument {
pub version: String,
#[serde(default)]
pub exported_at: String,
#[serde(default)]
pub global_models: Vec<AdminSystemConfigGlobalModel>,
#[serde(default)]
pub providers: Vec<AdminSystemConfigProvider>,
#[serde(default)]
pub proxy_nodes: Vec<AdminSystemConfigProxyNode>,
#[serde(default)]
pub ldap_config: Option<AdminSystemConfigLdap>,
#[serde(default)]
pub oauth_providers: Vec<AdminSystemConfigOAuthProvider>,
#[serde(default)]
pub system_configs: Vec<AdminSystemConfigEntry>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AdminSystemConfigImportRequest {
#[serde(flatten)]
pub document: AdminSystemConfigDocument,
#[serde(default)]
pub merge_mode: AdminImportMergeMode,
}
#[derive(Debug, Clone)]
pub struct ParsedAdminSystemConfigImportRequest {
pub request: AdminSystemConfigImportRequest,
pub root: Map<String, Value>,
}
#[derive(Debug, Clone)]
pub struct ParsedAdminSystemConfigObject<T> {
pub raw: Map<String, Value>,
pub value: T,
}
impl<T> ParsedAdminSystemConfigObject<T> {
pub fn into_parts(self) -> (Map<String, Value>, T) {
(self.raw, self.value)
}
}
fn parse_admin_system_config_object<T: DeserializeOwned>(
item: Value,
field_name: &str,
) -> Result<ParsedAdminSystemConfigObject<T>, (http::StatusCode, Value)> {
let raw = item
.as_object()
.cloned()
.ok_or_else(|| invalid_request(format!("{field_name} 项必须是对象")))?;
let value = serde_json::from_value::<T>(Value::Object(raw.clone()))
.map_err(|_| invalid_request(format!("{field_name} 项格式无效")))?;
Ok(ParsedAdminSystemConfigObject { raw, value })
}
pub fn parse_admin_system_config_array<T: DeserializeOwned>(
root: &Map<String, Value>,
field_name: &str,
) -> Result<Vec<ParsedAdminSystemConfigObject<T>>, (http::StatusCode, Value)> {
let Some(value) = root.get(field_name) else {
return Ok(Vec::new());
};
let items = value
.as_array()
.ok_or_else(|| invalid_request(format!("{field_name} 必须是数组")))?;
items
.iter()
.cloned()
.map(|item| parse_admin_system_config_object(item, field_name))
.collect()
}
pub fn parse_admin_system_config_optional_object<T: DeserializeOwned>(
root: &Map<String, Value>,
field_name: &str,
) -> Result<Option<ParsedAdminSystemConfigObject<T>>, (http::StatusCode, Value)> {
let Some(value) = root.get(field_name) else {
return Ok(None);
};
if value.is_null() {
return Ok(None);
}
parse_admin_system_config_object(value.clone(), field_name).map(Some)
}
pub fn parse_admin_system_config_nested_array<T: DeserializeOwned>(
parent: &Map<String, Value>,
field_name: &str,
) -> Result<Vec<ParsedAdminSystemConfigObject<T>>, (http::StatusCode, Value)> {
let Some(value) = parent.get(field_name) else {
return Ok(Vec::new());
};
let items = value
.as_array()
.ok_or_else(|| invalid_request(format!("{field_name} 必须是数组")))?;
items
.iter()
.cloned()
.map(|item| parse_admin_system_config_object(item, field_name))
.collect()
}
#[derive(Debug, Clone, Copy)]
struct AdminApiFormatDefinition {
value: &'static str,
label: &'static str,
default_path: &'static str,
aliases: &'static [&'static str],
}
const REQUEST_RECORD_LEVEL_KEY: &str = "request_record_level";
const LEGACY_REQUEST_LOG_LEVEL_KEY: &str = "request_log_level";
const SENSITIVE_SYSTEM_CONFIG_KEYS: &[&str] = &["smtp_password"];
const ADMIN_API_FORMAT_DEFINITIONS: &[AdminApiFormatDefinition] = &[
AdminApiFormatDefinition {
value: "openai:chat",
label: "OpenAI Chat",
default_path: "/v1/chat/completions",
aliases: &[
"openai",
"openai_compatible",
"deepseek",
"grok",
"moonshot",
"zhipu",
"qwen",
"baichuan",
"minimax",
],
},
AdminApiFormatDefinition {
value: "openai:responses",
label: "OpenAI Responses",
default_path: "/v1/responses",
2026-04-29 09:25:19 +08:00
aliases: &["responses"],
},
AdminApiFormatDefinition {
value: "openai:responses:compact",
label: "OpenAI Responses Compact",
default_path: "/v1/responses/compact",
2026-04-29 09:25:19 +08:00
aliases: &["responses_compact"],
},
2026-05-03 17:32:41 +08:00
AdminApiFormatDefinition {
value: "openai:embedding",
label: "OpenAI Embedding",
default_path: "/v1/embeddings",
aliases: &["openai_embedding", "embeddings"],
},
AdminApiFormatDefinition {
value: "openai:rerank",
label: "OpenAI Rerank",
default_path: "/v1/rerank",
aliases: &["openai_rerank", "rerank"],
},
AdminApiFormatDefinition {
value: "openai:image",
label: "OpenAI Image",
default_path: "/v1/images/generations",
aliases: &["openai_image", "images"],
},
AdminApiFormatDefinition {
value: "openai:video",
label: "OpenAI Video",
default_path: "/v1/videos",
aliases: &["openai_video", "sora"],
},
AdminApiFormatDefinition {
2026-04-29 09:25:19 +08:00
value: "claude:messages",
label: "Claude Messages",
default_path: "/v1/messages",
2026-04-30 00:26:33 +08:00
aliases: &["claude", "claude_compatible"],
},
AdminApiFormatDefinition {
2026-04-29 09:25:19 +08:00
value: "gemini:generate_content",
label: "Gemini Generate Content",
default_path: "/v1beta/models/{model}:{action}",
aliases: &["gemini", "google", "vertex"],
},
2026-05-03 17:32:41 +08:00
AdminApiFormatDefinition {
value: "gemini:embedding",
label: "Gemini Embedding",
default_path: "/v1/embeddings",
aliases: &["gemini_embedding"],
},
AdminApiFormatDefinition {
value: "gemini:video",
label: "Gemini Video",
default_path: "/v1beta/models/{model}:predictLongRunning",
aliases: &["gemini_video", "veo"],
},
2026-05-03 17:32:41 +08:00
AdminApiFormatDefinition {
value: "jina:embedding",
label: "Jina Embedding",
default_path: "/v1/embeddings",
aliases: &["jina_embedding"],
},
AdminApiFormatDefinition {
value: "jina:rerank",
label: "Jina Rerank",
default_path: "/v1/rerank",
aliases: &["jina_rerank"],
},
AdminApiFormatDefinition {
value: "doubao:embedding",
label: "Doubao Embedding",
default_path: "/v1/embeddings",
aliases: &["doubao_embedding"],
},
];
pub fn build_admin_system_check_update_payload(current_version: String) -> serde_json::Value {
json!({
"current_version": current_version,
"latest_version": serde_json::Value::Null,
"has_update": false,
"release_url": serde_json::Value::Null,
"release_notes": serde_json::Value::Null,
"published_at": serde_json::Value::Null,
"error": "检查更新需要 Rust 管理后端",
})
}
pub fn build_admin_system_stats_payload(
total_users: u64,
active_users: u64,
total_providers: u64,
active_providers: u64,
total_api_keys: u64,
total_requests: u64,
) -> serde_json::Value {
json!({
"users": {
"total": total_users,
"active": active_users,
},
"providers": {
"total": total_providers,
"active": active_providers,
},
"api_keys": total_api_keys,
"requests": total_requests,
})
}
pub fn build_admin_system_settings_payload(
default_provider: Option<String>,
default_model: Option<String>,
enable_usage_tracking: bool,
password_policy_level: String,
) -> serde_json::Value {
json!({
"default_provider": default_provider,
"default_model": default_model,
"enable_usage_tracking": enable_usage_tracking,
"password_policy_level": password_policy_level,
})
}
pub fn parse_admin_system_settings_update(
request_body: &[u8],
) -> Result<AdminSystemSettingsUpdate, (http::StatusCode, serde_json::Value)> {
let payload = match serde_json::from_slice::<serde_json::Value>(request_body) {
Ok(serde_json::Value::Object(payload)) => payload,
Ok(_) | Err(_) => {
return Err((
http::StatusCode::BAD_REQUEST,
json!({ "detail": "请求数据验证失败" }),
));
}
};
let default_provider = match payload.get("default_provider") {
Some(serde_json::Value::String(value)) => {
let value = value.trim();
if value.is_empty() {
Some(None)
} else {
Some(Some(value.to_string()))
}
}
Some(serde_json::Value::Null) => Some(None),
Some(_) => {
return Err((
http::StatusCode::BAD_REQUEST,
json!({ "detail": "请求数据验证失败" }),
));
}
None => None,
};
let default_model = match payload.get("default_model") {
Some(serde_json::Value::String(value)) => {
let value = value.trim();
if value.is_empty() {
Some(None)
} else {
Some(Some(value.to_string()))
}
}
Some(serde_json::Value::Null) => Some(None),
Some(_) => {
return Err((
http::StatusCode::BAD_REQUEST,
json!({ "detail": "请求数据验证失败" }),
));
}
None => None,
};
let enable_usage_tracking = match payload.get("enable_usage_tracking") {
Some(serde_json::Value::Bool(value)) => Some(*value),
Some(serde_json::Value::Null) => {
return Err((
http::StatusCode::BAD_REQUEST,
json!({ "detail": "请求数据验证失败" }),
));
}
Some(_) => {
return Err((
http::StatusCode::BAD_REQUEST,
json!({ "detail": "请求数据验证失败" }),
));
}
None => None,
};
let password_policy_level = match payload.get("password_policy_level") {
Some(serde_json::Value::String(value)) => {
let value = value.trim();
if matches!(value, "weak" | "medium" | "strong") {
Some(value.to_string())
} else {
return Err((
http::StatusCode::BAD_REQUEST,
json!({ "detail": "请求数据验证失败" }),
));
}
}
Some(_) => {
return Err((
http::StatusCode::BAD_REQUEST,
json!({ "detail": "请求数据验证失败" }),
));
}
None => None,
};
Ok(AdminSystemSettingsUpdate {
default_provider,
default_model,
enable_usage_tracking,
password_policy_level,
})
}
pub fn build_admin_system_settings_updated_payload() -> serde_json::Value {
json!({ "message": "系统设置更新成功" })
}
pub fn build_admin_email_templates_payload(templates: Vec<serde_json::Value>) -> serde_json::Value {
json!({ "templates": templates })
}
pub fn admin_email_template_not_found_error(
template_type: &str,
) -> (http::StatusCode, serde_json::Value) {
(
http::StatusCode::NOT_FOUND,
json!({ "detail": format!("模板类型 '{template_type}' 不存在") }),
)
}
pub fn parse_admin_email_template_update(
request_body: &[u8],
) -> Result<AdminEmailTemplateUpdate, (http::StatusCode, serde_json::Value)> {
let payload = match serde_json::from_slice::<serde_json::Value>(request_body) {
Ok(serde_json::Value::Object(payload)) => payload,
_ => {
return Err((
http::StatusCode::BAD_REQUEST,
json!({ "detail": "请求数据验证失败" }),
));
}
};
let subject = match payload.get("subject") {
Some(serde_json::Value::String(value)) => Some(value.clone()),
Some(serde_json::Value::Null) | None => None,
Some(_) => {
return Err((
http::StatusCode::BAD_REQUEST,
json!({ "detail": "请求数据验证失败" }),
));
}
};
let html = match payload.get("html") {
Some(serde_json::Value::String(value)) => Some(value.clone()),
Some(serde_json::Value::Null) | None => None,
Some(_) => {
return Err((
http::StatusCode::BAD_REQUEST,
json!({ "detail": "请求数据验证失败" }),
));
}
};
if subject.is_none() && html.is_none() {
return Err((
http::StatusCode::BAD_REQUEST,
json!({ "detail": "请提供 subject 或 html" }),
));
}
Ok(AdminEmailTemplateUpdate { subject, html })
}
pub fn parse_admin_email_template_preview_payload(
request_body: Option<&[u8]>,
) -> Result<serde_json::Map<String, serde_json::Value>, (http::StatusCode, serde_json::Value)> {
match request_body {
Some(bytes) => match serde_json::from_slice::<serde_json::Value>(bytes) {
Ok(serde_json::Value::Object(payload)) => Ok(payload),
Ok(serde_json::Value::Null) => Ok(serde_json::Map::new()),
_ => Err((
http::StatusCode::BAD_REQUEST,
json!({ "detail": "请求数据验证失败" }),
)),
},
None => Ok(serde_json::Map::new()),
}
}
pub fn build_admin_email_template_saved_payload() -> serde_json::Value {
json!({ "message": "模板保存成功" })
}
pub fn build_admin_email_template_preview_payload(
rendered_html: String,
preview_variables: std::collections::BTreeMap<String, String>,
) -> serde_json::Value {
json!({
"html": rendered_html,
"variables": preview_variables,
})
}
pub fn build_admin_email_template_reset_payload(
template_type: &str,
name: &str,
default_subject: &str,
default_html: &str,
) -> serde_json::Value {
json!({
"message": "模板已重置为默认值",
"template": {
"type": template_type,
"name": name,
"subject": default_subject,
"html": default_html,
}
})
}
pub fn build_admin_api_formats_payload() -> serde_json::Value {
json!({
"formats": ADMIN_API_FORMAT_DEFINITIONS
.iter()
.map(|definition| json!({
"value": definition.value,
"label": definition.label,
"default_path": definition.default_path,
"aliases": definition.aliases,
}))
.collect::<Vec<_>>(),
})
}
pub fn admin_module_name_from_status_path(request_path: &str) -> Option<String> {
request_path
.strip_prefix("/api/admin/modules/status/")
.map(str::trim)
.filter(|value| !value.is_empty() && !value.contains('/'))
.map(ToOwned::to_owned)
}
pub fn admin_module_name_from_enabled_path(request_path: &str) -> Option<String> {
request_path
.strip_prefix("/api/admin/modules/status/")
.and_then(|value| value.strip_suffix("/enabled"))
.map(str::trim)
.filter(|value| !value.is_empty() && !value.contains('/'))
.map(ToOwned::to_owned)
}
pub fn oauth_module_config_is_valid(providers: &[StoredOAuthProviderModuleConfig]) -> bool {
!providers.is_empty()
&& providers.iter().all(|provider| {
!provider.client_id.trim().is_empty()
&& provider
.client_secret_encrypted
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.is_some()
&& !provider.redirect_uri.trim().is_empty()
})
}
pub fn ldap_module_config_is_valid(config: Option<&StoredLdapModuleConfig>) -> bool {
let Some(config) = config else {
return false;
};
!config.server_url.trim().is_empty()
&& !config.bind_dn.trim().is_empty()
&& !config.base_dn.trim().is_empty()
&& config
.bind_password_encrypted
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.is_some()
}
pub fn build_admin_module_validation_result(
module_name: &str,
oauth_providers: &[StoredOAuthProviderModuleConfig],
ldap_config: Option<&StoredLdapModuleConfig>,
gemini_files_has_capable_key: bool,
smtp_configured: bool,
) -> (bool, Option<String>) {
match module_name {
"oauth" => {
if oauth_providers.is_empty() {
return (
false,
Some("请先配置并启用至少一个 OAuth Provider".to_string()),
);
}
for provider in oauth_providers {
if provider.client_id.trim().is_empty() {
return (
false,
Some(format!(
"Provider [{}] 未配置 Client ID",
provider.display_name
)),
);
}
if provider
.client_secret_encrypted
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.is_none()
{
return (
false,
Some(format!(
"Provider [{}] 未配置 Client Secret",
provider.display_name
)),
);
}
if provider.redirect_uri.trim().is_empty() {
return (
false,
Some(format!(
"Provider [{}] 未配置回调地址",
provider.display_name
)),
);
}
}
(true, None)
}
"ldap" => {
let Some(config) = ldap_config else {
return (false, Some("请先配置 LDAP 连接信息".to_string()));
};
if config.server_url.trim().is_empty() {
return (false, Some("请配置 LDAP 服务器地址".to_string()));
}
if config.bind_dn.trim().is_empty() {
return (false, Some("请配置绑定 DN".to_string()));
}
if config.base_dn.trim().is_empty() {
return (false, Some("请配置搜索基准 DN".to_string()));
}
if config
.bind_password_encrypted
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.is_none()
{
return (false, Some("请配置绑定密码".to_string()));
}
(true, None)
}
"notification_email" => {
if smtp_configured {
(true, None)
} else {
(false, Some("请先完成邮件配置SMTP".to_string()))
}
}
"gemini_files" => {
if gemini_files_has_capable_key {
(true, None)
} else {
(
false,
Some("至少启用一个具有「Gemini 文件 API」能力的 Key".to_string()),
)
}
}
2026-05-03 14:48:25 +08:00
"management_tokens" | "model_directives" | "proxy_nodes" => (true, None),
_ => (true, None),
}
}
pub fn build_admin_module_health(
module_name: &str,
gemini_files_has_capable_key: bool,
) -> &'static str {
match module_name {
2026-05-03 14:48:25 +08:00
"management_tokens" | "model_directives" | "proxy_nodes" => "healthy",
"gemini_files" => {
if gemini_files_has_capable_key {
"healthy"
} else {
"degraded"
}
}
_ => "unknown",
}
}
#[allow(clippy::too_many_arguments)]
pub fn build_admin_module_status_payload(
name: &str,
display_name: &str,
description: &str,
category: &str,
admin_route: Option<&str>,
admin_menu_icon: Option<&str>,
admin_menu_group: Option<&str>,
admin_menu_order: i32,
available: bool,
enabled: bool,
config_validated: bool,
config_error: Option<String>,
health: &str,
) -> serde_json::Value {
let active = available && enabled && config_validated;
json!({
"name": name,
"available": available,
"enabled": enabled,
"active": active,
"config_validated": config_validated,
"config_error": if config_validated { serde_json::Value::Null } else { json!(config_error) },
"display_name": display_name,
"description": description,
"category": category,
"admin_route": if available { json!(admin_route) } else { serde_json::Value::Null },
"admin_menu_icon": admin_menu_icon,
"admin_menu_group": admin_menu_group,
"admin_menu_order": admin_menu_order,
"health": health,
})
}
pub fn normalize_admin_system_export_api_formats(
raw_formats: Option<&serde_json::Value>,
mut signature_for: impl FnMut(&str) -> Option<String>,
) -> Vec<String> {
let Some(raw_formats) = raw_formats.and_then(serde_json::Value::as_array) else {
return Vec::new();
};
let mut normalized = Vec::new();
let mut seen = BTreeSet::new();
for raw in raw_formats {
let Some(value) = raw
.as_str()
.map(str::trim)
.filter(|value| !value.is_empty())
else {
continue;
};
let Some(signature) = signature_for(value) else {
continue;
};
if seen.insert(signature.clone()) {
normalized.push(signature);
}
}
normalized
}
pub fn resolve_admin_system_export_key_api_formats(
raw_formats: Option<&serde_json::Value>,
provider_endpoint_formats: &[String],
signature_for: impl FnMut(&str) -> Option<String>,
) -> Vec<String> {
let normalized = normalize_admin_system_export_api_formats(raw_formats, signature_for);
if !normalized.is_empty() {
return normalized;
}
if raw_formats.is_none() {
return provider_endpoint_formats.to_vec();
}
Vec::new()
}
pub fn collect_admin_system_export_provider_endpoint_formats(
endpoints: &[StoredProviderCatalogEndpoint],
mut signature_for: impl FnMut(&str) -> Option<String>,
) -> Vec<String> {
endpoints
.iter()
.filter_map(|endpoint| signature_for(&endpoint.api_format))
.collect::<BTreeSet<_>>()
.into_iter()
.collect()
}
pub fn serialize_admin_system_users_export_wallet(
wallet: Option<&StoredWalletSnapshot>,
) -> Option<serde_json::Value> {
let wallet = wallet?;
let recharge_balance = wallet.balance;
let gift_balance = wallet.gift_balance;
let spendable_balance = recharge_balance + gift_balance;
let unlimited = wallet.limit_mode.eq_ignore_ascii_case("unlimited");
Some(json!({
"id": wallet.id.clone(),
"balance": spendable_balance,
"recharge_balance": recharge_balance,
"gift_balance": gift_balance,
"refundable_balance": recharge_balance,
"currency": wallet.currency.clone(),
"status": wallet.status.clone(),
"limit_mode": wallet.limit_mode.clone(),
"unlimited": unlimited,
"total_recharged": wallet.total_recharged,
"total_consumed": wallet.total_consumed,
"total_refunded": wallet.total_refunded,
"total_adjusted": wallet.total_adjusted,
"updated_at": unix_secs_to_rfc3339(wallet.updated_at_unix_secs),
}))
}
pub fn parse_admin_system_config_import_request(
request_body: &[u8],
) -> Result<ParsedAdminSystemConfigImportRequest, (http::StatusCode, serde_json::Value)> {
let root = match serde_json::from_slice::<serde_json::Value>(request_body) {
Ok(serde_json::Value::Object(root)) => root,
_ => return Err(invalid_request("请求数据验证失败")),
};
let version = root
.get("version")
.and_then(serde_json::Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or_else(|| invalid_request("version 为必填字段"))?;
if !ADMIN_SYSTEM_CONFIG_SUPPORTED_VERSIONS.contains(&version) {
return Err(invalid_request(format!(
"不支持的配置版本: {version},支持的版本: {}",
ADMIN_SYSTEM_CONFIG_SUPPORTED_VERSIONS.join(", ")
)));
}
let merge_mode = AdminImportMergeMode::parse_json_value(root.get("merge_mode"))?;
let document = serde_path_to_error::deserialize::<_, AdminSystemConfigDocument>(
serde_json::Value::Object(root.clone()),
)
.map_err(|err| {
let path = err.path().to_string();
let inner = err.into_inner();
let detail = if path.is_empty() {
format!("配置文件格式无效: {inner}")
} else {
format!("配置文件格式无效: {path}: {inner}")
};
invalid_request(detail)
})?;
Ok(ParsedAdminSystemConfigImportRequest {
request: AdminSystemConfigImportRequest {
document,
merge_mode,
},
root,
})
}
pub fn normalize_admin_system_config_key(requested_key: &str) -> String {
let trimmed = requested_key.trim();
if trimmed.eq_ignore_ascii_case(LEGACY_REQUEST_LOG_LEVEL_KEY) {
REQUEST_RECORD_LEVEL_KEY.to_string()
} else {
trimmed.to_string()
}
}
pub fn admin_system_config_delete_keys(requested_key: &str) -> Vec<String> {
let normalized = normalize_admin_system_config_key(requested_key);
if normalized == REQUEST_RECORD_LEVEL_KEY {
vec![
REQUEST_RECORD_LEVEL_KEY.to_string(),
LEGACY_REQUEST_LOG_LEVEL_KEY.to_string(),
]
} else {
vec![normalized]
}
}
pub fn is_sensitive_admin_system_config_key(key: &str) -> bool {
SENSITIVE_SYSTEM_CONFIG_KEYS
.iter()
.any(|candidate| candidate.eq_ignore_ascii_case(key))
}
pub fn admin_system_config_default_value(key: &str) -> Option<serde_json::Value> {
match key {
"site_name" => Some(json!("Aether")),
"site_subtitle" => Some(json!("AI Gateway")),
"default_user_initial_gift_usd" => Some(json!(10.0)),
"password_policy_level" => Some(json!("weak")),
REQUEST_RECORD_LEVEL_KEY => Some(json!("full")),
"max_request_body_size" => Some(json!(5_242_880)),
"max_response_body_size" => Some(json!(5_242_880)),
"sensitive_headers" => Some(json!([
"authorization",
"x-api-key",
"api-key",
"cookie",
"set-cookie"
])),
"detail_log_retention_days" => Some(json!(7)),
"compressed_log_retention_days" => Some(json!(30)),
"header_retention_days" => Some(json!(90)),
"log_retention_days" => Some(json!(365)),
"enable_auto_cleanup" => Some(json!(true)),
"cleanup_batch_size" => Some(json!(1000)),
"request_candidates_retention_days" => Some(json!(30)),
"request_candidates_cleanup_batch_size" => Some(json!(5000)),
"enable_provider_checkin" => Some(json!(true)),
"provider_checkin_time" => Some(json!("01:05")),
"provider_priority_mode" => Some(json!("provider")),
"scheduling_mode" => Some(json!("cache_affinity")),
"auto_delete_expired_keys" => Some(json!(false)),
"email_suffix_mode" => Some(json!("none")),
"email_suffix_list" => Some(json!([])),
"enable_format_conversion" => Some(json!(false)),
2026-05-03 14:48:25 +08:00
"enable_model_directives" => Some(json!(false)),
"model_directives" => Some(json!({
"reasoning_effort": {
"enabled": true,
"api_formats": {
"openai:chat": {
"enabled": true,
"mappings": {
"low": { "reasoning_effort": "low" },
"medium": { "reasoning_effort": "medium" },
"high": { "reasoning_effort": "high" },
"xhigh": { "reasoning_effort": "xhigh" },
"max": { "reasoning_effort": "xhigh" }
}
},
"openai:responses": {
"enabled": true,
"mappings": {
"low": { "reasoning": { "effort": "low" } },
"medium": { "reasoning": { "effort": "medium" } },
"high": { "reasoning": { "effort": "high" } },
"xhigh": { "reasoning": { "effort": "xhigh" } },
"max": { "reasoning": { "effort": "xhigh" } }
}
},
"openai:responses:compact": {
"enabled": true,
"mappings": {
"low": { "reasoning": { "effort": "low" } },
"medium": { "reasoning": { "effort": "medium" } },
"high": { "reasoning": { "effort": "high" } },
"xhigh": { "reasoning": { "effort": "xhigh" } },
"max": { "reasoning": { "effort": "xhigh" } }
}
},
"claude:messages": {
"enabled": true,
"mappings": {
"low": { "thinking": { "type": "enabled", "budget_tokens": 1024 } },
"medium": { "thinking": { "type": "enabled", "budget_tokens": 4096 } },
"high": { "thinking": { "type": "enabled", "budget_tokens": 8192 } },
"xhigh": { "thinking": { "type": "enabled", "budget_tokens": 16384 } },
"max": { "thinking": { "type": "enabled", "budget_tokens": 32768 } }
}
},
"gemini:generate_content": {
"enabled": true,
"mappings": {
"low": { "generationConfig": { "thinkingConfig": { "thinkingBudget": 1024 } } },
"medium": { "generationConfig": { "thinkingConfig": { "thinkingBudget": 4096 } } },
"high": { "generationConfig": { "thinkingConfig": { "thinkingBudget": 8192 } } },
"xhigh": { "generationConfig": { "thinkingConfig": { "thinkingBudget": 16384 } } },
"max": { "generationConfig": { "thinkingConfig": { "thinkingBudget": -1 } } }
}
}
}
}
})),
"keep_priority_on_conversion" => Some(json!(false)),
"audit_log_retention_days" => Some(json!(30)),
"enable_db_maintenance" => Some(json!(true)),
"system_proxy_node_id" => Some(serde_json::Value::Null),
"smtp_host" => Some(serde_json::Value::Null),
"smtp_port" => Some(json!(587)),
"smtp_user" => Some(serde_json::Value::Null),
"smtp_password" => Some(serde_json::Value::Null),
"smtp_use_tls" => Some(json!(true)),
"smtp_use_ssl" => Some(json!(false)),
"smtp_from_email" => Some(serde_json::Value::Null),
"smtp_from_name" => Some(json!("Aether")),
"enable_oauth_token_refresh" => Some(json!(true)),
_ => None,
}
}
pub fn build_admin_system_configs_payload(
entries: &[StoredSystemConfigEntry],
) -> serde_json::Value {
let has_request_record_level = entries
.iter()
.any(|entry| entry.key == REQUEST_RECORD_LEVEL_KEY);
json!(entries
.iter()
.filter_map(|entry| {
if entry.key == LEGACY_REQUEST_LOG_LEVEL_KEY && has_request_record_level {
return None;
}
let key = if entry.key == LEGACY_REQUEST_LOG_LEVEL_KEY {
REQUEST_RECORD_LEVEL_KEY
} else {
entry.key.as_str()
};
Some(build_admin_system_config_list_item(
key,
&entry.value,
entry.description.as_deref(),
entry.updated_at_unix_secs,
))
})
.collect::<Vec<_>>())
}
pub fn build_admin_system_config_detail_payload(
requested_key: &str,
value: Option<serde_json::Value>,
) -> Result<serde_json::Value, (http::StatusCode, serde_json::Value)> {
let normalized_key = normalize_admin_system_config_key(requested_key);
let value = value.or_else(|| admin_system_config_default_value(&normalized_key));
let Some(value) = value else {
return Err((
http::StatusCode::NOT_FOUND,
json!({ "detail": format!("配置项 '{requested_key}' 不存在") }),
));
};
if is_sensitive_admin_system_config_key(&normalized_key) {
return Ok(json!({
"key": requested_key,
"value": serde_json::Value::Null,
"is_set": system_config_is_set(&value),
}));
}
Ok(json!({
"key": requested_key,
"value": value,
}))
}
pub fn parse_admin_system_config_update(
requested_key: &str,
request_body: &[u8],
) -> Result<AdminSystemConfigUpdate, (http::StatusCode, serde_json::Value)> {
let payload = match serde_json::from_slice::<serde_json::Value>(request_body) {
Ok(serde_json::Value::Object(payload)) => payload,
_ => {
return Err((
http::StatusCode::BAD_REQUEST,
json!({ "detail": "请求数据验证失败" }),
));
}
};
let normalized_key = normalize_admin_system_config_key(requested_key);
let mut value = payload
.get("value")
.cloned()
.unwrap_or(serde_json::Value::Null);
let description = match payload.get("description") {
Some(serde_json::Value::String(value)) => Some(value.trim().to_string()),
Some(serde_json::Value::Null) | None => None,
Some(_) => {
return Err((
http::StatusCode::BAD_REQUEST,
json!({ "detail": "请求数据验证失败" }),
));
}
};
if normalized_key == "password_policy_level" {
match value.as_str().map(str::trim) {
Some("weak" | "medium" | "strong") => {
value = json!(value.as_str().unwrap().trim());
}
Some(_) => {
return Err((
http::StatusCode::BAD_REQUEST,
json!({ "detail": "请求数据验证失败" }),
));
}
None if value.is_null() => {
value = json!("weak");
}
None => {
return Err((
http::StatusCode::BAD_REQUEST,
json!({ "detail": "请求数据验证失败" }),
));
}
}
}
Ok(AdminSystemConfigUpdate {
normalized_key,
value,
description,
})
}
pub fn build_admin_system_config_updated_payload(
key: String,
value: serde_json::Value,
description: Option<String>,
updated_at_unix_secs: Option<u64>,
) -> serde_json::Value {
json!({
"key": key,
"value": value,
"description": description,
"updated_at": updated_at_unix_secs.and_then(unix_secs_to_rfc3339),
})
}
pub fn build_admin_system_config_deleted_payload(requested_key: &str) -> serde_json::Value {
json!({
"message": format!("配置项 '{}' 已删除", requested_key.trim()),
})
}
pub fn is_admin_management_tokens_root(request_path: &str) -> bool {
matches!(
request_path,
"/api/admin/management-tokens" | "/api/admin/management-tokens/"
)
}
pub fn is_admin_system_configs_root(request_path: &str) -> bool {
matches!(
request_path,
"/api/admin/system/configs" | "/api/admin/system/configs/"
)
}
pub fn is_admin_system_email_templates_root(request_path: &str) -> bool {
matches!(
request_path,
"/api/admin/system/email/templates" | "/api/admin/system/email/templates/"
)
}
pub fn admin_system_config_key_from_path(request_path: &str) -> Option<String> {
path_identifier_from_path(request_path, "/api/admin/system/configs/")
}
pub fn admin_system_email_template_type_from_path(request_path: &str) -> Option<String> {
path_identifier_from_path(request_path, "/api/admin/system/email/templates/")
}
pub fn admin_system_email_template_preview_type_from_path(request_path: &str) -> Option<String> {
suffixed_path_identifier_from_path(
request_path,
"/api/admin/system/email/templates/",
"/preview",
)
}
pub fn admin_system_email_template_reset_type_from_path(request_path: &str) -> Option<String> {
suffixed_path_identifier_from_path(request_path, "/api/admin/system/email/templates/", "/reset")
}
pub fn admin_management_token_id_from_path(request_path: &str) -> Option<String> {
path_identifier_from_path(request_path, "/api/admin/management-tokens/")
}
pub fn admin_management_token_status_id_from_path(request_path: &str) -> Option<String> {
suffixed_path_identifier_from_path(request_path, "/api/admin/management-tokens/", "/status")
}
pub fn admin_adaptive_effective_limit(key: &StoredProviderCatalogKey) -> Option<u32> {
if key.rpm_limit.is_none() {
key.learned_rpm_limit
} else {
key.rpm_limit
}
}
pub fn admin_adaptive_adjustment_items(
value: Option<&serde_json::Value>,
) -> Vec<serde_json::Map<String, serde_json::Value>> {
value
.and_then(serde_json::Value::as_array)
.into_iter()
.flatten()
.filter_map(serde_json::Value::as_object)
.cloned()
.collect()
}
pub fn admin_adaptive_key_payload(key: &StoredProviderCatalogKey) -> serde_json::Value {
json!({
"id": key.id,
"name": key.name,
"provider_id": key.provider_id,
"api_formats": key
.api_formats
.as_ref()
.and_then(serde_json::Value::as_array)
.map(|items| {
items
.iter()
.filter_map(serde_json::Value::as_str)
.map(ToOwned::to_owned)
.collect::<Vec<_>>()
})
.unwrap_or_default(),
"is_adaptive": key.rpm_limit.is_none(),
"rpm_limit": key.rpm_limit,
"effective_limit": admin_adaptive_effective_limit(key),
"learned_rpm_limit": key.learned_rpm_limit,
"concurrent_429_count": key.concurrent_429_count.unwrap_or(0),
"rpm_429_count": key.rpm_429_count.unwrap_or(0),
})
}
pub fn build_admin_adaptive_summary_payload(
keys: &[StoredProviderCatalogKey],
) -> serde_json::Value {
let adaptive_keys = keys
.iter()
.filter(|key| key.rpm_limit.is_none())
.collect::<Vec<_>>();
let total_keys = adaptive_keys.len() as u64;
let total_concurrent_429 = adaptive_keys
.iter()
.map(|key| u64::from(key.concurrent_429_count.unwrap_or(0)))
.sum::<u64>();
let total_rpm_429 = adaptive_keys
.iter()
.map(|key| u64::from(key.rpm_429_count.unwrap_or(0)))
.sum::<u64>();
let mut recent_adjustments = Vec::new();
let mut total_adjustments = 0usize;
for key in adaptive_keys {
let history = admin_adaptive_adjustment_items(key.adjustment_history.as_ref());
total_adjustments += history.len();
for adjustment in history.into_iter().rev().take(3) {
let mut payload = adjustment;
payload.insert("key_id".to_string(), json!(key.id));
payload.insert("key_name".to_string(), json!(key.name));
recent_adjustments.push(serde_json::Value::Object(payload));
}
}
recent_adjustments.sort_by(|left, right| {
let lhs = left
.get("timestamp")
.and_then(serde_json::Value::as_str)
.unwrap_or_default();
let rhs = right
.get("timestamp")
.and_then(serde_json::Value::as_str)
.unwrap_or_default();
rhs.cmp(lhs)
});
json!({
"total_adaptive_keys": total_keys,
"total_concurrent_429_errors": total_concurrent_429,
"total_rpm_429_errors": total_rpm_429,
"total_adjustments": total_adjustments,
"recent_adjustments": recent_adjustments.into_iter().take(10).collect::<Vec<_>>(),
})
}
pub fn build_admin_adaptive_stats_payload(key: &StoredProviderCatalogKey) -> serde_json::Value {
let status_snapshot = key
.status_snapshot
.as_ref()
.and_then(serde_json::Value::as_object);
let adjustments = admin_adaptive_adjustment_items(key.adjustment_history.as_ref());
let adjustment_count = adjustments.len();
let recent_adjustments = adjustments
.into_iter()
.rev()
.take(10)
.map(serde_json::Value::Object)
.collect::<Vec<_>>();
json!({
"adaptive_mode": key.rpm_limit.is_none(),
"rpm_limit": key.rpm_limit,
"effective_limit": admin_adaptive_effective_limit(key),
"learned_limit": key.learned_rpm_limit,
"concurrent_429_count": key.concurrent_429_count.unwrap_or(0),
"rpm_429_count": key.rpm_429_count.unwrap_or(0),
"last_429_at": key.last_429_at_unix_secs.and_then(unix_secs_to_rfc3339),
"last_429_type": key.last_429_type,
"adjustment_count": adjustment_count,
"recent_adjustments": recent_adjustments,
"learning_confidence": status_snapshot.and_then(|value| value.get("learning_confidence")).cloned(),
"enforcement_active": status_snapshot.and_then(|value| value.get("enforcement_active")).cloned(),
"observation_count": status_snapshot
.and_then(|value| value.get("observation_count"))
.and_then(serde_json::Value::as_u64)
.unwrap_or(0),
"header_observation_count": status_snapshot
.and_then(|value| value.get("header_observation_count"))
.and_then(serde_json::Value::as_u64)
.unwrap_or(0),
"latest_upstream_limit": status_snapshot
.and_then(|value| value.get("latest_upstream_limit"))
.and_then(serde_json::Value::as_u64),
})
}
pub fn build_admin_adaptive_toggle_mode_payload(
updated: &StoredProviderCatalogKey,
message: String,
) -> serde_json::Value {
json!({
"message": message,
"key_id": updated.id,
"is_adaptive": updated.rpm_limit.is_none(),
"rpm_limit": updated.rpm_limit,
"effective_limit": admin_adaptive_effective_limit(updated),
})
}
pub fn build_admin_adaptive_set_limit_payload(
updated: &StoredProviderCatalogKey,
was_adaptive: bool,
limit: u32,
) -> serde_json::Value {
json!({
"message": format!("已设置为固定限制模式RPM 限制为 {limit}"),
"key_id": updated.id,
"is_adaptive": false,
"rpm_limit": updated.rpm_limit,
"previous_mode": if was_adaptive { "adaptive" } else { "fixed" },
})
}
pub fn build_admin_adaptive_reset_learning_payload(key_id: &str) -> serde_json::Value {
json!({
"message": "学习状态已重置",
"key_id": key_id,
})
}
pub fn admin_adaptive_key_not_found_response(key_id: &str) -> Response<Body> {
(
http::StatusCode::NOT_FOUND,
Json(json!({ "detail": format!("Key {key_id} 不存在") })),
)
.into_response()
}
pub fn admin_adaptive_dispatcher_not_found_response() -> Response<Body> {
(
http::StatusCode::NOT_FOUND,
Json(json!({ "detail": "Adaptive route not found" })),
)
.into_response()
}
pub fn admin_adaptive_key_id_from_path(path: &str) -> Option<String> {
let normalized = path.trim_end_matches('/');
let mut segments = normalized.split('/').filter(|segment| !segment.is_empty());
match (
segments.next(),
segments.next(),
segments.next(),
segments.next(),
segments.next(),
) {
(Some("api"), Some("admin"), Some("adaptive"), Some("keys"), Some(key_id))
if !key_id.is_empty() =>
{
Some(key_id.to_string())
}
_ => None,
}
}
pub const ADMIN_PROXY_NODES_DATA_UNAVAILABLE_DETAIL: &str = "Admin proxy nodes data unavailable";
pub fn build_admin_proxy_nodes_data_unavailable_response() -> Response<Body> {
(
http::StatusCode::SERVICE_UNAVAILABLE,
Json(json!({ "detail": ADMIN_PROXY_NODES_DATA_UNAVAILABLE_DETAIL })),
)
.into_response()
}
pub fn build_admin_proxy_nodes_invalid_status_response() -> Response<Body> {
(
http::StatusCode::BAD_REQUEST,
Json(json!({
"detail": "status 必须是以下之一: ['offline', 'online']"
})),
)
.into_response()
}
pub fn build_admin_proxy_nodes_not_found_response() -> Response<Body> {
(
http::StatusCode::NOT_FOUND,
Json(json!({ "detail": "Proxy node 不存在" })),
)
.into_response()
}
pub fn build_admin_proxy_node_payload(node: &StoredProxyNode) -> serde_json::Value {
let mut payload = serde_json::Map::from_iter([
("id".to_string(), json!(node.id)),
("name".to_string(), json!(node.name)),
("ip".to_string(), json!(node.ip)),
("port".to_string(), json!(node.port)),
("region".to_string(), json!(node.region)),
("status".to_string(), json!(node.status)),
("is_manual".to_string(), json!(node.is_manual)),
("tunnel_mode".to_string(), json!(node.tunnel_mode)),
("tunnel_connected".to_string(), json!(node.tunnel_connected)),
(
"tunnel_connected_at".to_string(),
json!(node
.tunnel_connected_at_unix_secs
.and_then(unix_secs_to_rfc3339)),
),
("registered_by".to_string(), json!(node.registered_by)),
(
"last_heartbeat_at".to_string(),
json!(node
.last_heartbeat_at_unix_secs
.and_then(unix_secs_to_rfc3339)),
),
(
"heartbeat_interval".to_string(),
json!(node.heartbeat_interval),
),
(
"active_connections".to_string(),
json!(node.active_connections),
),
("total_requests".to_string(), json!(node.total_requests)),
("avg_latency_ms".to_string(), json!(node.avg_latency_ms)),
("failed_requests".to_string(), json!(node.failed_requests)),
("dns_failures".to_string(), json!(node.dns_failures)),
("stream_errors".to_string(), json!(node.stream_errors)),
("proxy_metadata".to_string(), json!(node.proxy_metadata)),
("hardware_info".to_string(), json!(node.hardware_info)),
(
"estimated_max_concurrency".to_string(),
json!(node.estimated_max_concurrency),
),
("remote_config".to_string(), json!(node.remote_config)),
("config_version".to_string(), json!(node.config_version)),
(
"created_at".to_string(),
json!(node.created_at_unix_ms.and_then(unix_secs_to_rfc3339)),
),
(
"updated_at".to_string(),
json!(node.updated_at_unix_secs.and_then(unix_secs_to_rfc3339)),
),
]);
if node.is_manual {
payload.insert("proxy_url".to_string(), json!(node.proxy_url));
payload.insert("proxy_username".to_string(), json!(node.proxy_username));
payload.insert(
"proxy_password".to_string(),
json!(mask_admin_proxy_node_password(
node.proxy_password.as_deref()
)),
);
}
serde_json::Value::Object(payload)
}
pub fn build_admin_proxy_node_event_payload(event: &StoredProxyNodeEvent) -> serde_json::Value {
json!({
"id": event.id,
"event_type": event.event_type,
"detail": event.detail,
"created_at": event.created_at_unix_ms.and_then(unix_secs_to_rfc3339),
})
}
pub fn admin_proxy_node_event_node_id_from_path(request_path: &str) -> Option<&str> {
let node_id = request_path.strip_prefix("/api/admin/proxy-nodes/")?;
let node_id = node_id.strip_suffix("/events")?;
if node_id.is_empty() || node_id.contains('/') {
None
} else {
Some(node_id)
}
}
pub fn build_admin_proxy_nodes_list_payload_response(
items: Vec<serde_json::Value>,
total: usize,
skip: usize,
limit: usize,
rollout: Option<serde_json::Value>,
) -> Response<Body> {
Json(json!({
"items": items,
"total": total,
"skip": skip,
"limit": limit,
"rollout": rollout,
}))
.into_response()
}
pub fn build_admin_proxy_node_events_payload_response(
items: Vec<serde_json::Value>,
) -> Response<Body> {
Json(json!({ "items": items })).into_response()
}
fn system_config_is_set(value: &serde_json::Value) -> bool {
match value {
serde_json::Value::Null => false,
serde_json::Value::Bool(value) => *value,
serde_json::Value::Number(value) => value
.as_i64()
.map(|value| value != 0)
.or_else(|| value.as_u64().map(|value| value != 0))
.or_else(|| value.as_f64().map(|value| value != 0.0))
.unwrap_or(false),
serde_json::Value::String(value) => !value.trim().is_empty(),
serde_json::Value::Array(value) => !value.is_empty(),
serde_json::Value::Object(value) => !value.is_empty(),
}
}
fn build_admin_system_config_list_item(
key: &str,
value: &serde_json::Value,
description: Option<&str>,
updated_at_unix_secs: Option<u64>,
) -> serde_json::Value {
let masked_value = if is_sensitive_admin_system_config_key(key) {
serde_json::Value::Null
} else {
value.clone()
};
let is_set = is_sensitive_admin_system_config_key(key).then(|| system_config_is_set(value));
let mut payload = json!({
"key": key,
"description": description,
"updated_at": updated_at_unix_secs.and_then(unix_secs_to_rfc3339),
"value": masked_value,
});
if let Some(is_set) = is_set {
payload["is_set"] = json!(is_set);
}
payload
}
fn unix_secs_to_rfc3339(unix_secs: u64) -> Option<String> {
chrono::DateTime::<chrono::Utc>::from_timestamp(unix_secs as i64, 0)
.map(|value| value.to_rfc3339_opts(chrono::SecondsFormat::Secs, true))
}
fn path_identifier_from_path(request_path: &str, prefix: &str) -> Option<String> {
let value = request_path
.strip_prefix(prefix)?
.trim()
.trim_matches('/')
.to_string();
if value.is_empty() || value.contains('/') {
None
} else {
Some(value)
}
}
fn suffixed_path_identifier_from_path(
request_path: &str,
prefix: &str,
suffix: &str,
) -> Option<String> {
request_path
.strip_prefix(prefix)?
.strip_suffix(suffix)
.map(|value| value.trim().trim_matches('/').to_string())
.filter(|value| !value.is_empty() && !value.contains('/'))
}
fn mask_admin_proxy_node_password(password: Option<&str>) -> Option<String> {
let password = password?;
if password.is_empty() {
return None;
}
if password.len() < 8 {
return Some("****".to_string());
}
Some(format!(
"{}****{}",
&password[..2],
&password[password.len() - 2..]
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_admin_system_config_import_request_accepts_supported_versions() {
let parsed = parse_admin_system_config_import_request(
json!({
"version": ADMIN_SYSTEM_CONFIG_EXPORT_VERSION,
"global_models": [],
"providers": [],
})
.to_string()
.as_bytes(),
)
.expect("current version should parse");
assert_eq!(
parsed.request.document.version,
ADMIN_SYSTEM_CONFIG_EXPORT_VERSION
);
assert_eq!(parsed.request.merge_mode, AdminImportMergeMode::Skip);
assert!(parsed.request.document.oauth_providers.is_empty());
assert!(parsed.request.document.system_configs.is_empty());
assert!(parsed.request.document.ldap_config.is_none());
}
#[test]
fn parse_admin_system_config_import_request_rejects_removed_versions() {
for version in ["2.0", "2.1"] {
let err = parse_admin_system_config_import_request(
json!({
"version": version,
"global_models": [],
"providers": [],
})
.to_string()
.as_bytes(),
)
.expect_err("removed versions should fail");
assert_eq!(err.0, http::StatusCode::BAD_REQUEST);
assert_eq!(
err.1["detail"],
format!(
"不支持的配置版本: {version},支持的版本: {}",
ADMIN_SYSTEM_CONFIG_SUPPORTED_VERSIONS.join(", ")
)
);
}
}
#[test]
fn parse_admin_system_config_import_request_rejects_invalid_merge_mode() {
let err = parse_admin_system_config_import_request(
json!({
"version": "2.2",
"merge_mode": "replace_all",
})
.to_string()
.as_bytes(),
)
.expect_err("invalid merge mode should fail");
assert_eq!(err.0, http::StatusCode::BAD_REQUEST);
assert_eq!(
err.1["detail"],
"merge_mode 仅支持 skip / overwrite / error"
);
}
#[test]
fn parse_admin_system_config_import_request_reports_field_path_for_shape_errors() {
let err = parse_admin_system_config_import_request(
json!({
"version": "2.2",
"global_models": [],
"providers": [{
"name": "import-openai",
"endpoints": [{
"api_format": "openai:chat",
"base_url": "https://api.example.com",
"is_active": "yes"
}]
}],
})
.to_string()
.as_bytes(),
)
.expect_err("invalid endpoint shape should fail");
assert_eq!(err.0, http::StatusCode::BAD_REQUEST);
let detail = err.1["detail"].as_str().expect("detail should be a string");
assert!(detail.contains("配置文件格式无效"));
assert!(detail.contains("providers[0].endpoints[0].is_active"));
}
#[test]
fn parse_admin_system_config_import_request_rejects_numeric_string_fields() {
let err = parse_admin_system_config_import_request(
json!({
"version": "2.2",
"global_models": [{
"name": "veo3.1",
"display_name": "Veo 3.1",
"default_price_per_request": "1.80000000",
}],
"providers": [{
"name": "undyapi",
"monthly_quota_usd": "12.50",
"stream_first_byte_timeout": "60",
"request_timeout": "120",
"models": [{
"global_model_name": "veo3.1",
"provider_model_name": "veo3.1",
"price_per_request": "0.70000000",
}]
}],
})
.to_string()
.as_bytes(),
)
.expect_err("numeric string fields should fail");
assert_eq!(err.0, http::StatusCode::BAD_REQUEST);
let detail = err.1["detail"].as_str().expect("detail should be a string");
assert!(detail.contains("配置文件格式无效"));
assert!(detail.contains("default_price_per_request"));
}
#[test]
fn resolve_admin_system_export_key_api_formats_uses_endpoint_fallback() {
2026-04-29 09:25:19 +08:00
let provider_formats = vec!["openai:chat".to_string(), "claude:messages".to_string()];
let resolved =
resolve_admin_system_export_key_api_formats(None, &provider_formats, |value| {
Some(value.to_string())
});
assert_eq!(resolved, provider_formats);
}
#[test]
fn sensitive_admin_system_config_keys_are_case_insensitive() {
assert!(is_sensitive_admin_system_config_key("smtp_password"));
assert!(is_sensitive_admin_system_config_key("SMTP_PASSWORD"));
assert!(!is_sensitive_admin_system_config_key("site_name"));
}
}