refactor: 大规模模块拆分与代码精简,新增 ai-pipeline/data-contracts 独立 crate

- 新增 aether-ai-pipeline 和 aether-data-contracts crate,将 pipeline 逻辑与数据契约从 gateway 中解耦
- 重构 admin handlers:拆分单体模块为 auth/billing/endpoint/features/model/observability/provider/system 等独立子模块
- 合并 chat/cli 重复代码路径:精简 conversion、finalize、planner 中的 sync/chat/cli 分支
- 重构 scheduler/executor/data 层,引入 facade 模式降低模块间耦合
- 移除冗余的 intent 模块,将 plan_fallback/policy/stream_path/sync_path 迁移至 executor
- 前端适配:调整 admin API 调用和 provider 模型测试对话框
This commit is contained in:
fawney19
2026-04-07 02:50:19 +08:00
parent 763ff03a7b
commit 5d96d6673b
732 changed files with 28589 additions and 20662 deletions
@@ -0,0 +1,355 @@
use crate::api::ai::{admin_default_body_rules_for_signature, admin_endpoint_signature_parts};
use crate::handlers::public::{admin_requested_force_stream, normalize_admin_base_url};
use crate::provider_transport::provider_types::provider_type_is_fixed;
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;
use super::payloads::{build_admin_provider_endpoint_response, endpoint_key_counts_by_format};
use super::payloads::{AdminProviderEndpointCreateRequest, AdminProviderEndpointUpdateRequest};
pub(crate) async fn build_admin_provider_endpoints_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 endpoints = state
.list_provider_catalog_endpoints_by_provider_ids(std::slice::from_ref(&provider.id))
.await
.ok()
.unwrap_or_default();
endpoints.sort_by(|left, right| {
right
.created_at_unix_secs
.unwrap_or_default()
.cmp(&left.created_at_unix_secs.unwrap_or_default())
.then_with(|| left.id.cmp(&right.id))
});
let keys = state
.list_provider_catalog_keys_by_provider_ids(std::slice::from_ref(&provider.id))
.await
.ok()
.unwrap_or_default();
let (total_keys_by_format, active_keys_by_format) = endpoint_key_counts_by_format(&keys);
let now_unix_secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.ok()
.map(|duration| duration.as_secs())
.unwrap_or(0);
Some(serde_json::Value::Array(
endpoints
.into_iter()
.skip(skip)
.take(limit)
.map(|endpoint| {
build_admin_provider_endpoint_response(
&endpoint,
&provider.name,
total_keys_by_format
.get(endpoint.api_format.as_str())
.copied()
.unwrap_or(0),
active_keys_by_format
.get(endpoint.api_format.as_str())
.copied()
.unwrap_or(0),
now_unix_secs,
)
})
.collect(),
))
}
pub(crate) async fn build_admin_endpoint_payload(
state: &AppState,
endpoint_id: &str,
) -> Option<serde_json::Value> {
if !state.has_provider_catalog_data_reader() {
return None;
}
let endpoint = state
.read_provider_catalog_endpoints_by_ids(&[endpoint_id.to_string()])
.await
.ok()
.and_then(|mut endpoints| endpoints.drain(..).next())?;
let provider = state
.read_provider_catalog_providers_by_ids(std::slice::from_ref(&endpoint.provider_id))
.await
.ok()
.and_then(|mut providers| providers.drain(..).next())?;
let keys = state
.list_provider_catalog_keys_by_provider_ids(std::slice::from_ref(&endpoint.provider_id))
.await
.ok()
.unwrap_or_default();
let (total_keys_by_format, active_keys_by_format) = endpoint_key_counts_by_format(&keys);
let now_unix_secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.ok()
.map(|duration| duration.as_secs())
.unwrap_or(0);
Some(build_admin_provider_endpoint_response(
&endpoint,
&provider.name,
total_keys_by_format
.get(endpoint.api_format.as_str())
.copied()
.unwrap_or(0),
active_keys_by_format
.get(endpoint.api_format.as_str())
.copied()
.unwrap_or(0),
now_unix_secs,
))
}
pub(crate) async fn build_admin_create_provider_endpoint_record(
state: &AppState,
provider: &StoredProviderCatalogProvider,
payload: AdminProviderEndpointCreateRequest,
) -> Result<StoredProviderCatalogEndpoint, String> {
if payload.provider_id.trim() != provider.id {
return Err("provider_id 不匹配".to_string());
}
if provider_type_is_fixed(&provider.provider_type) {
return Err("固定类型 Provider 不允许手动新增 Endpoint".to_string());
}
if !(0..=999).contains(&payload.max_retries) {
return Err("max_retries 必须在 0 到 999 之间".to_string());
}
let (normalized_api_format, api_family, endpoint_kind) =
admin_endpoint_signature_parts(&payload.api_format)
.ok_or_else(|| format!("无效的 api_format: {}", payload.api_format))?;
let base_url = normalize_admin_base_url(&payload.base_url)?;
let existing_endpoints = state
.list_provider_catalog_endpoints_by_provider_ids(std::slice::from_ref(&provider.id))
.await
.map_err(|err| format!("{err:?}"))?;
if existing_endpoints
.iter()
.any(|endpoint| endpoint.api_format == normalized_api_format)
{
return Err(format!(
"Provider {} 已存在 {} 格式的 Endpoint",
provider.name, normalized_api_format
));
}
let body_rules = match payload.body_rules {
Some(value) => Some(value),
None => 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 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(
base_url,
payload.header_rules,
body_rules,
Some(payload.max_retries),
payload.custom_path.and_then(|value| {
let trimmed = value.trim().to_string();
(!trimmed.is_empty()).then_some(trimmed)
}),
payload.config,
payload.format_acceptance_config,
payload.proxy,
)
.map_err(|err| err.to_string())
}
pub(crate) async fn build_admin_update_provider_endpoint_record(
state: &AppState,
provider: &StoredProviderCatalogProvider,
existing_endpoint: &StoredProviderCatalogEndpoint,
raw_payload: &serde_json::Map<String, serde_json::Value>,
payload: AdminProviderEndpointUpdateRequest,
) -> Result<StoredProviderCatalogEndpoint, String> {
let mut updated = existing_endpoint.clone();
if provider_type_is_fixed(&provider.provider_type)
&& (raw_payload.contains_key("base_url") || raw_payload.contains_key("custom_path"))
{
return Err("固定类型 Provider 的 Endpoint 不允许修改 base_url/custom_path".to_string());
}
if let Some(value) = raw_payload.get("base_url") {
let Some(base_url) = payload.base_url.as_deref() else {
return Err(if value.is_null() {
"base_url 不能为空".to_string()
} else {
"base_url 必须是字符串".to_string()
});
};
updated.base_url = normalize_admin_base_url(base_url)?;
}
if raw_payload.contains_key("custom_path") {
updated.custom_path = payload.custom_path;
}
if let Some(value) = raw_payload.get("header_rules") {
if !value.is_null() && !value.is_array() {
return Err("header_rules 必须是数组或 null".to_string());
}
updated.header_rules = if value.is_null() {
None
} else {
payload.header_rules
};
}
if let Some(value) = raw_payload.get("body_rules") {
if !value.is_null() && !value.is_array() {
return Err("body_rules 必须是数组或 null".to_string());
}
updated.body_rules = if value.is_null() {
None
} else {
payload.body_rules
};
}
if let Some(value) = raw_payload.get("max_retries") {
let Some(max_retries) = payload.max_retries else {
return Err(if value.is_null() {
"max_retries 必须是 0 到 999 之间的整数".to_string()
} else {
"max_retries 必须是整数".to_string()
});
};
if !(0..=999).contains(&max_retries) {
return Err("max_retries 必须在 0 到 999 之间".to_string());
}
updated.max_retries = Some(max_retries);
}
if raw_payload.contains_key("is_active") {
let Some(is_active) = payload.is_active else {
return Err("is_active 必须是布尔值".to_string());
};
updated.is_active = is_active;
}
if let Some(value) = raw_payload.get("config") {
if !value.is_null() && !value.is_object() {
return Err("config 必须是对象或 null".to_string());
}
updated.config = if value.is_null() {
None
} else {
payload.config
};
}
if let Some(value) = raw_payload.get("proxy") {
if value.is_null() {
updated.proxy = None;
} else {
let Some(mut proxy) = payload.proxy.and_then(|value| value.as_object().cloned()) else {
return Err("proxy 必须是对象或 null".to_string());
};
if !proxy.contains_key("password") {
if let Some(old_password) = existing_endpoint
.proxy
.as_ref()
.and_then(serde_json::Value::as_object)
.and_then(|proxy| proxy.get("password"))
.and_then(serde_json::Value::as_str)
.filter(|value| !value.is_empty())
{
proxy.insert("password".to_string(), json!(old_password));
}
}
updated.proxy = Some(serde_json::Value::Object(proxy));
}
}
if let Some(value) = raw_payload.get("format_acceptance_config") {
if !value.is_null() && !value.is_object() {
return Err("format_acceptance_config 必须是对象或 null".to_string());
}
updated.format_acceptance_config = if value.is_null() {
None
} else {
payload.format_acceptance_config
};
}
let provider_type = provider.provider_type.trim().to_ascii_lowercase();
if provider_type == "codex" && existing_endpoint.api_format == "openai:cli" {
let has_config_in_payload = raw_payload.contains_key("config");
let config_payload = if has_config_in_payload {
updated.config.clone().unwrap_or_else(|| json!({}))
} else {
existing_endpoint
.config
.clone()
.unwrap_or_else(|| json!({}))
};
let mut config = config_payload.as_object().cloned().unwrap_or_default();
let requested = config
.get("upstream_stream_policy")
.or_else(|| config.get("upstreamStreamPolicy"))
.or_else(|| config.get("upstream_stream"));
if has_config_in_payload
&& requested.is_some()
&& !admin_requested_force_stream(requested.expect("checked above"))
{
return Err("Codex OpenAI CLI 端点固定为强制流式,不允许修改".to_string());
}
config.remove("upstreamStreamPolicy");
config.remove("upstream_stream");
config.insert("upstream_stream_policy".to_string(), json!("force_stream"));
updated.config = Some(serde_json::Value::Object(config));
}
let (_, api_family, endpoint_kind) = admin_endpoint_signature_parts(&updated.api_format)
.ok_or_else(|| format!("无效的 api_format: {}", updated.api_format))?;
updated.api_family = Some(api_family.to_string());
updated.endpoint_kind = Some(endpoint_kind.to_string());
updated.updated_at_unix_secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.ok()
.map(|duration| duration.as_secs());
let _ = state;
Ok(updated)
}
@@ -0,0 +1,31 @@
pub(super) fn admin_provider_id_for_endpoints(request_path: &str) -> Option<String> {
let raw = request_path.strip_prefix("/api/admin/endpoints/providers/")?;
let raw = raw.strip_suffix("/endpoints")?;
let normalized = raw.trim().trim_matches('/');
if normalized.is_empty() {
None
} else {
Some(normalized.to_string())
}
}
pub(super) fn admin_endpoint_id(request_path: &str) -> Option<String> {
let raw = request_path.strip_prefix("/api/admin/endpoints/")?;
let normalized = raw.trim().trim_matches('/');
if normalized.is_empty() || normalized.contains('/') {
None
} else {
Some(normalized.to_string())
}
}
pub(super) fn admin_default_body_rules_api_format(request_path: &str) -> Option<String> {
let raw = request_path.strip_prefix("/api/admin/endpoints/defaults/")?;
let raw = raw.strip_suffix("/body-rules")?;
let normalized = raw.trim().trim_matches('/');
if normalized.is_empty() {
None
} else {
Some(normalized.to_string())
}
}
@@ -0,0 +1,478 @@
mod builders;
mod extractors;
mod payloads;
use self::builders::{
build_admin_create_provider_endpoint_record, build_admin_endpoint_payload,
build_admin_provider_endpoints_payload, build_admin_update_provider_endpoint_record,
};
use self::extractors::{
admin_default_body_rules_api_format, admin_endpoint_id, admin_provider_id_for_endpoints,
};
use self::payloads::endpoint_key_counts_by_format;
use self::payloads::{
build_admin_provider_endpoint_response, key_api_formats_without_entry,
AdminProviderEndpointCreateRequest, AdminProviderEndpointUpdateRequest,
};
use crate::api::ai::admin_default_body_rules_for_signature;
use crate::control::GatewayPublicRequestContext;
use crate::handlers::admin::shared::query_param_value;
use crate::{AppState, GatewayError};
use axum::{
body::{Body, Bytes},
http,
response::{IntoResponse, Response},
Json,
};
use serde_json::json;
use std::time::{SystemTime, UNIX_EPOCH};
const ADMIN_ENDPOINTS_DATA_UNAVAILABLE_DETAIL: &str = "Admin endpoint data unavailable";
fn build_admin_endpoints_data_unavailable_response() -> Response<Body> {
(
http::StatusCode::SERVICE_UNAVAILABLE,
Json(json!({ "detail": ADMIN_ENDPOINTS_DATA_UNAVAILABLE_DETAIL })),
)
.into_response()
}
pub(crate) async fn maybe_build_local_admin_endpoints_routes_response(
state: &AppState,
request_context: &GatewayPublicRequestContext,
request_body: Option<&Bytes>,
) -> Result<Option<Response<Body>>, GatewayError> {
let Some(decision) = request_context.control_decision.as_ref() else {
return Ok(None);
};
if decision.route_family.as_deref() == Some("endpoints_manage")
&& decision.route_kind.as_deref() == Some("list_provider_endpoints")
&& request_context
.request_path
.starts_with("/api/admin/endpoints/providers/")
&& request_context.request_path.ends_with("/endpoints")
{
if !state.has_provider_catalog_data_reader() {
return Ok(Some(build_admin_endpoints_data_unavailable_response()));
}
let Some(provider_id) = admin_provider_id_for_endpoints(&request_context.request_path)
else {
return Ok(Some(
(
http::StatusCode::NOT_FOUND,
Json(json!({ "detail": "Provider 不存在" })),
)
.into_response(),
));
};
let skip = query_param_value(request_context.request_query_string.as_deref(), "skip")
.and_then(|value| value.parse::<usize>().ok())
.unwrap_or(0);
let limit = query_param_value(request_context.request_query_string.as_deref(), "limit")
.and_then(|value| value.parse::<usize>().ok())
.filter(|value| *value > 0)
.unwrap_or(100);
return Ok(Some(
match build_admin_provider_endpoints_payload(state, &provider_id, skip, limit).await {
Some(payload) => Json(payload).into_response(),
None => (
http::StatusCode::NOT_FOUND,
Json(json!({ "detail": format!("Provider {provider_id} 不存在") })),
)
.into_response(),
},
));
}
if decision.route_family.as_deref() == Some("endpoints_manage")
&& decision.route_kind.as_deref() == Some("create_endpoint")
&& request_context.request_method == http::Method::POST
&& request_context
.request_path
.starts_with("/api/admin/endpoints/providers/")
&& request_context.request_path.ends_with("/endpoints")
{
if !state.has_provider_catalog_data_reader() || !state.has_provider_catalog_data_writer() {
return Ok(Some(build_admin_endpoints_data_unavailable_response()));
}
let Some(provider_id) = admin_provider_id_for_endpoints(&request_context.request_path)
else {
return Ok(Some(
(
http::StatusCode::NOT_FOUND,
Json(json!({ "detail": "Provider 不存在" })),
)
.into_response(),
));
};
let Some(request_body) = request_body else {
return Ok(Some(
(
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": "请求体不能为空" })),
)
.into_response(),
));
};
let payload =
match serde_json::from_slice::<AdminProviderEndpointCreateRequest>(request_body) {
Ok(payload) => payload,
Err(_) => {
return Ok(Some(
(
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": "请求体必须是合法的 JSON 对象" })),
)
.into_response(),
));
}
};
let Some(provider) = state
.read_provider_catalog_providers_by_ids(std::slice::from_ref(&provider_id))
.await?
.into_iter()
.next()
else {
return Ok(Some(
(
http::StatusCode::NOT_FOUND,
Json(json!({ "detail": format!("Provider {provider_id} 不存在") })),
)
.into_response(),
));
};
let record =
match build_admin_create_provider_endpoint_record(state, &provider, payload).await {
Ok(record) => record,
Err(detail) => {
return Ok(Some(
(
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": detail })),
)
.into_response(),
));
}
};
let Some(created) = state.create_provider_catalog_endpoint(&record).await? else {
return Ok(Some(build_admin_endpoints_data_unavailable_response()));
};
let now_unix_secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.ok()
.map(|duration| duration.as_secs())
.unwrap_or(0);
return Ok(Some(
Json(build_admin_provider_endpoint_response(
&created,
&provider.name,
0,
0,
now_unix_secs,
))
.into_response(),
));
}
if decision.route_family.as_deref() == Some("endpoints_manage")
&& decision.route_kind.as_deref() == Some("update_endpoint")
&& request_context.request_method == http::Method::PUT
&& request_context
.request_path
.starts_with("/api/admin/endpoints/")
{
if !state.has_provider_catalog_data_reader() || !state.has_provider_catalog_data_writer() {
return Ok(Some(build_admin_endpoints_data_unavailable_response()));
}
let Some(endpoint_id) = admin_endpoint_id(&request_context.request_path) else {
return Ok(Some(
(
http::StatusCode::NOT_FOUND,
Json(json!({ "detail": "Endpoint 不存在" })),
)
.into_response(),
));
};
let Some(request_body) = request_body else {
return Ok(Some(
(
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": "请求体不能为空" })),
)
.into_response(),
));
};
let raw_value = match serde_json::from_slice::<serde_json::Value>(request_body) {
Ok(value) => value,
Err(_) => {
return Ok(Some(
(
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": "请求体必须是合法的 JSON 对象" })),
)
.into_response(),
));
}
};
let Some(raw_payload) = raw_value.as_object().cloned() else {
return Ok(Some(
(
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": "请求体必须是合法的 JSON 对象" })),
)
.into_response(),
));
};
let payload = match serde_json::from_value::<AdminProviderEndpointUpdateRequest>(raw_value)
{
Ok(payload) => payload,
Err(_) => {
return Ok(Some(
(
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": "请求体必须是合法的 JSON 对象" })),
)
.into_response(),
));
}
};
let Some(existing_endpoint) = state
.read_provider_catalog_endpoints_by_ids(std::slice::from_ref(&endpoint_id))
.await?
.into_iter()
.next()
else {
return Ok(Some(
(
http::StatusCode::NOT_FOUND,
Json(json!({ "detail": format!("Endpoint {endpoint_id} 不存在") })),
)
.into_response(),
));
};
let Some(provider) = state
.read_provider_catalog_providers_by_ids(std::slice::from_ref(
&existing_endpoint.provider_id,
))
.await?
.into_iter()
.next()
else {
return Ok(Some(
(
http::StatusCode::NOT_FOUND,
Json(json!({ "detail": format!("Provider {} 不存在", existing_endpoint.provider_id) })),
)
.into_response(),
));
};
let updated_record = match build_admin_update_provider_endpoint_record(
state,
&provider,
&existing_endpoint,
&raw_payload,
payload,
)
.await
{
Ok(record) => record,
Err(detail) => {
return Ok(Some(
(
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": detail })),
)
.into_response(),
));
}
};
let Some(updated) = state
.update_provider_catalog_endpoint(&updated_record)
.await?
else {
return Ok(Some(build_admin_endpoints_data_unavailable_response()));
};
let now_unix_secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.ok()
.map(|duration| duration.as_secs())
.unwrap_or(0);
let keys = state
.list_provider_catalog_keys_by_provider_ids(std::slice::from_ref(&provider.id))
.await
.unwrap_or_default();
let (total_keys_by_format, active_keys_by_format) = endpoint_key_counts_by_format(&keys);
return Ok(Some(
Json(build_admin_provider_endpoint_response(
&updated,
&provider.name,
total_keys_by_format
.get(updated.api_format.as_str())
.copied()
.unwrap_or(0),
active_keys_by_format
.get(updated.api_format.as_str())
.copied()
.unwrap_or(0),
now_unix_secs,
))
.into_response(),
));
}
if decision.route_family.as_deref() == Some("endpoints_manage")
&& decision.route_kind.as_deref() == Some("delete_endpoint")
&& request_context.request_method == http::Method::DELETE
&& request_context
.request_path
.starts_with("/api/admin/endpoints/")
{
if !state.has_provider_catalog_data_reader() || !state.has_provider_catalog_data_writer() {
return Ok(Some(build_admin_endpoints_data_unavailable_response()));
}
let Some(endpoint_id) = admin_endpoint_id(&request_context.request_path) else {
return Ok(Some(
(
http::StatusCode::NOT_FOUND,
Json(json!({ "detail": "Endpoint 不存在" })),
)
.into_response(),
));
};
let Some(existing_endpoint) = state
.read_provider_catalog_endpoints_by_ids(std::slice::from_ref(&endpoint_id))
.await?
.into_iter()
.next()
else {
return Ok(Some(
(
http::StatusCode::NOT_FOUND,
Json(json!({ "detail": format!("Endpoint {endpoint_id} 不存在") })),
)
.into_response(),
));
};
let now_unix_secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.ok()
.map(|duration| duration.as_secs())
.unwrap_or(0);
let keys = state
.list_provider_catalog_keys_by_provider_ids(std::slice::from_ref(
&existing_endpoint.provider_id,
))
.await
.unwrap_or_default();
let mut affected_keys_count = 0usize;
for key in keys {
let Some(updated_formats) =
key_api_formats_without_entry(&key, existing_endpoint.api_format.as_str())
else {
continue;
};
let mut updated_key = key.clone();
updated_key.api_formats = Some(serde_json::Value::Array(
updated_formats
.into_iter()
.map(serde_json::Value::String)
.collect(),
));
updated_key.updated_at_unix_secs = Some(now_unix_secs);
if state
.update_provider_catalog_key(&updated_key)
.await?
.is_none()
{
return Ok(Some(build_admin_endpoints_data_unavailable_response()));
}
affected_keys_count += 1;
}
if !state.delete_provider_catalog_endpoint(&endpoint_id).await? {
return Ok(Some(
(
http::StatusCode::NOT_FOUND,
Json(json!({ "detail": format!("Endpoint {endpoint_id} 不存在") })),
)
.into_response(),
));
}
return Ok(Some(
Json(json!({
"message": format!("Endpoint {endpoint_id} 已删除"),
"affected_keys_count": affected_keys_count,
}))
.into_response(),
));
}
if decision.route_family.as_deref() == Some("endpoints_manage")
&& decision.route_kind.as_deref() == Some("get_endpoint")
&& request_context
.request_path
.starts_with("/api/admin/endpoints/")
{
if !state.has_provider_catalog_data_reader() {
return Ok(Some(build_admin_endpoints_data_unavailable_response()));
}
let Some(endpoint_id) = admin_endpoint_id(&request_context.request_path) else {
return Ok(Some(
(
http::StatusCode::NOT_FOUND,
Json(json!({ "detail": "Endpoint 不存在" })),
)
.into_response(),
));
};
return Ok(Some(
match build_admin_endpoint_payload(state, &endpoint_id).await {
Some(payload) => Json(payload).into_response(),
None => (
http::StatusCode::NOT_FOUND,
Json(json!({ "detail": format!("Endpoint {endpoint_id} 不存在") })),
)
.into_response(),
},
));
}
if decision.route_family.as_deref() == Some("endpoints_manage")
&& decision.route_kind.as_deref() == Some("default_body_rules")
&& request_context
.request_path
.starts_with("/api/admin/endpoints/defaults/")
&& request_context.request_path.ends_with("/body-rules")
{
let Some(api_format) = admin_default_body_rules_api_format(&request_context.request_path)
else {
return Ok(Some(
(
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": "无效的 api_format" })),
)
.into_response(),
));
};
let provider_type = query_param_value(
request_context.request_query_string.as_deref(),
"provider_type",
);
return Ok(Some(
match admin_default_body_rules_for_signature(&api_format, provider_type.as_deref()) {
Some((normalized_api_format, body_rules)) => Json(json!({
"api_format": normalized_api_format,
"body_rules": body_rules,
}))
.into_response(),
None => (
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": format!("无效的 api_format: {api_format}") })),
)
.into_response(),
},
));
}
Ok(None)
}
@@ -0,0 +1,146 @@
use crate::handlers::admin::shared::unix_secs_to_rfc3339;
use aether_data_contracts::repository::provider_catalog::{
StoredProviderCatalogEndpoint, StoredProviderCatalogKey,
};
use serde::Deserialize;
use serde_json::json;
use std::collections::BTreeMap;
pub(super) fn key_api_formats_without_entry(
key: &StoredProviderCatalogKey,
api_format: &str,
) -> Option<Vec<String>> {
let current_formats =
crate::handlers::admin::shared::json_string_list(key.api_formats.as_ref());
if !current_formats
.iter()
.any(|candidate| candidate == api_format)
{
return None;
}
Some(
current_formats
.into_iter()
.filter(|candidate| candidate != api_format)
.collect(),
)
}
pub(super) fn endpoint_key_counts_by_format(
keys: &[StoredProviderCatalogKey],
) -> (BTreeMap<String, usize>, BTreeMap<String, usize>) {
let mut total = BTreeMap::new();
let mut active = BTreeMap::new();
for key in keys {
let Some(formats) = key
.api_formats
.as_ref()
.and_then(serde_json::Value::as_array)
else {
continue;
};
for api_format in formats.iter().filter_map(serde_json::Value::as_str) {
*total.entry(api_format.to_string()).or_insert(0) += 1;
if key.is_active {
*active.entry(api_format.to_string()).or_insert(0) += 1;
}
}
}
(total, active)
}
pub(super) fn build_admin_provider_endpoint_response(
endpoint: &StoredProviderCatalogEndpoint,
provider_name: &str,
total_keys: usize,
active_keys: usize,
now_unix_secs: u64,
) -> serde_json::Value {
json!({
"id": endpoint.id,
"provider_id": endpoint.provider_id,
"provider_name": provider_name,
"api_format": endpoint.api_format,
"base_url": endpoint.base_url,
"custom_path": endpoint.custom_path,
"header_rules": endpoint.header_rules,
"body_rules": endpoint.body_rules,
"max_retries": endpoint.max_retries.unwrap_or(2),
"is_active": endpoint.is_active,
"config": endpoint.config,
"proxy": masked_proxy_value(endpoint.proxy.as_ref()),
"format_acceptance_config": endpoint.format_acceptance_config,
"total_keys": total_keys,
"active_keys": active_keys,
"created_at": endpoint_timestamp_or_now(endpoint.created_at_unix_secs, now_unix_secs),
"updated_at": endpoint_timestamp_or_now(endpoint.updated_at_unix_secs, now_unix_secs),
})
}
fn masked_proxy_value(proxy: Option<&serde_json::Value>) -> serde_json::Value {
let Some(proxy) = proxy.and_then(serde_json::Value::as_object) else {
return serde_json::Value::Null;
};
let mut masked = proxy.clone();
if masked
.get("password")
.and_then(serde_json::Value::as_str)
.is_some_and(|value| !value.trim().is_empty())
{
masked.insert("password".to_string(), json!("***"));
}
serde_json::Value::Object(masked)
}
fn endpoint_timestamp_or_now(value: Option<u64>, now_unix_secs: u64) -> serde_json::Value {
unix_secs_to_rfc3339(value.unwrap_or(now_unix_secs))
.map(serde_json::Value::String)
.unwrap_or(serde_json::Value::Null)
}
fn default_admin_endpoint_max_retries() -> i32 {
2
}
#[derive(Debug, Deserialize)]
pub(super) struct AdminProviderEndpointCreateRequest {
pub(super) provider_id: String,
pub(super) api_format: String,
pub(super) base_url: String,
#[serde(default)]
pub(super) custom_path: Option<String>,
#[serde(default)]
pub(super) header_rules: Option<serde_json::Value>,
#[serde(default)]
pub(super) body_rules: Option<serde_json::Value>,
#[serde(default = "default_admin_endpoint_max_retries")]
pub(super) max_retries: i32,
#[serde(default)]
pub(super) config: Option<serde_json::Value>,
#[serde(default)]
pub(super) proxy: Option<serde_json::Value>,
#[serde(default)]
pub(super) format_acceptance_config: Option<serde_json::Value>,
}
#[derive(Debug, Deserialize)]
pub(super) struct AdminProviderEndpointUpdateRequest {
#[serde(default)]
pub(super) base_url: Option<String>,
#[serde(default)]
pub(super) custom_path: Option<String>,
#[serde(default)]
pub(super) header_rules: Option<serde_json::Value>,
#[serde(default)]
pub(super) body_rules: Option<serde_json::Value>,
#[serde(default)]
pub(super) max_retries: Option<i32>,
#[serde(default)]
pub(super) is_active: Option<bool>,
#[serde(default)]
pub(super) config: Option<serde_json::Value>,
#[serde(default)]
pub(super) proxy: Option<serde_json::Value>,
#[serde(default)]
pub(super) format_acceptance_config: Option<serde_json::Value>,
}